import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,21 @@
/* 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.util.AttributeSet;
import android.widget.TextView;
public class AllCapsTextView extends TextView {
public AllCapsTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void setText(CharSequence text, BufferType type) {
super.setText(text.toString().toUpperCase(), type);
}
}

View file

@ -0,0 +1,130 @@
/* -*- 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 org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.R;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import org.mozilla.gecko.util.HardwareUtils;
/**
* AnchoredPopup is the base class for doorhanger notifications, and is anchored to the urlbar.
*/
public abstract class AnchoredPopup extends PopupWindow {
public interface OnVisibilityChangeListener {
public void onDoorHangerShow();
public void onDoorHangerHide();
}
private View mAnchor;
private OnVisibilityChangeListener onVisibilityChangeListener;
protected RoundedCornerLayout mContent;
protected boolean mInflated;
protected final Context mContext;
public AnchoredPopup(Context context) {
super(context);
mContext = context;
setAnimationStyle(R.style.PopupAnimation);
}
protected void init() {
// Hide the default window background. Passing null prevents the below setOutTouchable()
// call from working, so use an empty BitmapDrawable instead.
setBackgroundDrawable(new BitmapDrawable(mContext.getResources()));
// Allow the popup to be dismissed when touching outside.
setOutsideTouchable(true);
// PopupWindow has a default width and height of 0, so set the width here.
int width = (int) mContext.getResources().getDimension(R.dimen.doorhanger_width);
setWindowLayoutMode(0, ViewGroup.LayoutParams.WRAP_CONTENT);
setWidth(width);
final LayoutInflater inflater = LayoutInflater.from(mContext);
final View layout = inflater.inflate(R.layout.anchored_popup, null);
setContentView(layout);
mContent = (RoundedCornerLayout) layout.findViewById(R.id.content);
mInflated = true;
}
/**
* Sets the anchor for this popup.
*
* @param anchor Anchor view for positioning the arrow.
*/
public void setAnchor(View anchor) {
mAnchor = anchor;
}
public void setOnVisibilityChangeListener(OnVisibilityChangeListener listener) {
onVisibilityChangeListener = listener;
}
/**
* Shows the popup with the arrow pointing to the center of the anchor view. If the anchor
* isn't visible, the popup will just be shown at the top of the root view.
*/
public void show() {
if (!mInflated) {
throw new IllegalStateException("ArrowPopup#init() must be called before ArrowPopup#show()");
}
if (onVisibilityChangeListener != null) {
onVisibilityChangeListener.onDoorHangerShow();
}
final int[] anchorLocation = new int[2];
if (mAnchor != null) {
mAnchor.getLocationInWindow(anchorLocation);
}
// The doorhanger should overlap the bottom of the urlbar.
int offsetY = mContext.getResources().getDimensionPixelOffset(R.dimen.doorhanger_offsetY);
final View decorView = ((Activity) mContext).getWindow().getDecorView();
final boolean validAnchor = (mAnchor != null) && (anchorLocation[1] > 0);
if (HardwareUtils.isTablet()) {
if (validAnchor) {
showAsDropDown(mAnchor, 0, 0);
} else {
// The anchor will be offscreen if the dynamic toolbar is hidden, so anticipate the re-shown position
// of the toolbar.
final int offsetX = mContext.getResources().getDimensionPixelOffset(R.dimen.doorhanger_offsetX);
showAtLocation(decorView, Gravity.TOP | Gravity.LEFT, offsetX, offsetY);
}
} else {
// If the anchor is null or out of the window bounds, just show the popup at the top of the
// root view.
final View anchor = validAnchor ? mAnchor : decorView;
showAtLocation(anchor, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, offsetY);
}
}
@Override
public void dismiss() {
super.dismiss();
if (onVisibilityChangeListener != null) {
onVisibilityChangeListener.onDoorHangerHide();
}
}
}

View file

@ -0,0 +1,77 @@
/* 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 org.mozilla.gecko.animation.HeightChangeAnimation;
import android.content.Context;
import android.util.AttributeSet;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.widget.RelativeLayout;
public class AnimatedHeightLayout extends RelativeLayout {
private static final String LOGTAG = "GeckoAnimatedHeightLayout";
private static final int ANIMATION_DURATION = 100;
private boolean mAnimating;
public AnimatedHeightLayout(Context context) {
super(context, null);
}
public AnimatedHeightLayout(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public AnimatedHeightLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int oldHeight = getMeasuredHeight();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int newHeight = getMeasuredHeight();
if (!mAnimating && oldHeight != 0 && oldHeight != newHeight) {
mAnimating = true;
setMeasuredDimension(getMeasuredWidth(), oldHeight);
// Animate the difference of suggestion row height
Animation anim = new HeightChangeAnimation(this, oldHeight, newHeight);
anim.setDuration(ANIMATION_DURATION);
anim.setInterpolator(new DecelerateInterpolator());
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationRepeat(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
post(new Runnable() {
@Override
public void run() {
finishAnimation();
}
});
}
});
startAnimation(anim);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
finishAnimation();
}
void finishAnimation() {
if (mAnimating) {
getLayoutParams().height = LayoutParams.WRAP_CONTENT;
mAnimating = false;
}
}
}

View file

@ -0,0 +1,140 @@
/* -*- 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 org.mozilla.gecko.R;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ArrayAdapter;
import android.widget.AdapterView;
import android.widget.CheckedTextView;
import android.widget.ListView;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
public class BasicColorPicker extends ListView {
private final static String LOGTAG = "GeckoBasicColorPicker";
private final static List<Integer> DEFAULT_COLORS = Arrays.asList(Color.rgb(215, 57, 32),
Color.rgb(255, 134, 5),
Color.rgb(255, 203, 19),
Color.rgb(95, 173, 71),
Color.rgb(84, 201, 168),
Color.rgb(33, 161, 222),
Color.rgb(16, 36, 87),
Color.rgb(91, 32, 103),
Color.rgb(212, 221, 228),
Color.BLACK);
private static Drawable mCheckDrawable;
int mSelected;
final ColorPickerListAdapter mAdapter;
public BasicColorPicker(Context context) {
this(context, null);
}
public BasicColorPicker(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BasicColorPicker(Context context, AttributeSet attrs, int style) {
this(context, attrs, style, DEFAULT_COLORS);
}
public BasicColorPicker(Context context, AttributeSet attrs, int style, List<Integer> colors) {
super(context, attrs, style);
mAdapter = new ColorPickerListAdapter(context, new ArrayList<Integer>(colors));
setAdapter(mAdapter);
setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mSelected = position;
mAdapter.notifyDataSetChanged();
}
});
}
public int getColor() {
return mAdapter.getItem(mSelected);
}
public void setColor(int color) {
if (!DEFAULT_COLORS.contains(color)) {
mSelected = mAdapter.getCount();
mAdapter.add(color);
} else {
mSelected = DEFAULT_COLORS.indexOf(color);
}
setSelection(mSelected);
mAdapter.notifyDataSetChanged();
}
Drawable getCheckDrawable() {
if (mCheckDrawable == null) {
Resources res = getContext().getResources();
TypedValue typedValue = new TypedValue();
getContext().getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, typedValue, true);
DisplayMetrics metrics = new android.util.DisplayMetrics();
((WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(metrics);
int height = (int) typedValue.getDimension(metrics);
Drawable background = res.getDrawable(R.drawable.color_picker_row_bg);
Rect r = new Rect();
background.getPadding(r);
height -= r.top + r.bottom;
mCheckDrawable = res.getDrawable(R.drawable.color_picker_checkmark);
mCheckDrawable.setBounds(0, 0, height, height);
}
return mCheckDrawable;
}
private class ColorPickerListAdapter extends ArrayAdapter<Integer> {
private final List<Integer> mColors;
public ColorPickerListAdapter(Context context, List<Integer> colors) {
super(context, R.layout.color_picker_row, colors);
mColors = colors;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = super.getView(position, convertView, parent);
Drawable d = v.getBackground();
d.setColorFilter(getItem(position), PorterDuff.Mode.MULTIPLY);
v.setBackgroundDrawable(d);
Drawable check = null;
CheckedTextView checked = ((CheckedTextView) v);
if (mSelected == position) {
check = getCheckDrawable();
}
checked.setCompoundDrawables(check, null, null, null);
checked.setText("");
return v;
}
}
}

View file

@ -0,0 +1,52 @@
/* 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 org.mozilla.gecko.R;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.CheckBox;
import android.widget.Checkable;
import android.widget.LinearLayout;
public class CheckableLinearLayout extends LinearLayout implements Checkable {
private CheckBox mCheckBox;
public CheckableLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean isChecked() {
return mCheckBox != null && mCheckBox.isChecked();
}
@Override
public void setChecked(boolean isChecked) {
if (mCheckBox != null) {
mCheckBox.setChecked(isChecked);
}
}
@Override
public void toggle() {
if (mCheckBox != null) {
mCheckBox.toggle();
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mCheckBox = (CheckBox) findViewById(R.id.checkbox);
mCheckBox.setClickable(false);
}
}

View file

@ -0,0 +1,25 @@
/* -*- 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.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.EditText;
public class ClickableWhenDisabledEditText extends EditText {
public ClickableWhenDisabledEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled() && event.getAction() == MotionEvent.ACTION_UP) {
return performClick();
}
return super.onTouchEvent(event);
}
}

View file

@ -0,0 +1,127 @@
/* -*- 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.support.v4.content.ContextCompat;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;
import org.mozilla.gecko.R;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.view.View;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.toolbar.SiteIdentityPopup;
import java.util.Locale;
public class ContentSecurityDoorHanger extends DoorHanger {
private static final String LOGTAG = "GeckoSecurityDoorHanger";
private final TextView mTitle;
private final TextView mSecurityState;
private final TextView mMessage;
public ContentSecurityDoorHanger(Context context, DoorhangerConfig config, Type type) {
super(context, config, type);
mTitle = (TextView) findViewById(R.id.security_title);
mSecurityState = (TextView) findViewById(R.id.security_state);
mMessage = (TextView) findViewById(R.id.security_message);
loadConfig(config);
}
@Override
protected void loadConfig(DoorhangerConfig config) {
final String message = config.getMessage();
if (message != null) {
mMessage.setText(message);
}
final JSONObject options = config.getOptions();
if (options != null) {
setOptions(options);
}
final DoorhangerConfig.Link link = config.getLink();
if (link != null) {
addLink(link.label, link.url);
}
addButtonsToLayout(config);
}
@Override
protected int getContentResource() {
return R.layout.doorhanger_security;
}
@Override
public void setOptions(final JSONObject options) {
super.setOptions(options);
final JSONObject link = options.optJSONObject("link");
if (link != null) {
try {
final String linkLabel = link.getString("label");
final String linkUrl = link.getString("url");
addLink(linkLabel, linkUrl);
} catch (JSONException e) { }
}
final JSONObject trackingProtection = options.optJSONObject("tracking_protection");
if (trackingProtection != null) {
mTitle.setVisibility(VISIBLE);
mTitle.setText(R.string.doorhanger_tracking_title);
try {
final boolean enabled = trackingProtection.getBoolean("enabled");
if (enabled) {
mMessage.setText(R.string.doorhanger_tracking_message_enabled);
mSecurityState.setText(R.string.doorhanger_tracking_state_enabled);
mSecurityState.setTextColor(ContextCompat.getColor(getContext(), R.color.affirmative_green));
} else {
mMessage.setText(R.string.doorhanger_tracking_message_disabled);
mSecurityState.setText(R.string.doorhanger_tracking_state_disabled);
mSecurityState.setTextColor(ContextCompat.getColor(getContext(), R.color.rejection_red));
}
mMessage.setVisibility(VISIBLE);
mSecurityState.setVisibility(VISIBLE);
} catch (JSONException e) { }
}
}
@Override
protected OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra) {
return new Button.OnClickListener() {
@Override
public void onClick(View v) {
final String expandedExtra = mType.toString().toLowerCase(Locale.US) + "-" + telemetryExtra;
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DOORHANGER, expandedExtra);
final JSONObject response = new JSONObject();
try {
switch (mType) {
case TRACKING:
response.put("allowContent", (id == SiteIdentityPopup.ButtonType.DISABLE.ordinal()));
response.put("contentType", ("tracking"));
break;
default:
Log.w(LOGTAG, "Unknown doorhanger type " + mType.toString());
}
} catch (JSONException e) {
Log.e(LOGTAG, "Error creating onClick response", e);
}
mOnButtonClickListener.onButtonClick(response, ContentSecurityDoorHanger.this);
}
};
}
}

View file

@ -0,0 +1,143 @@
/* -*- 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.Matrix;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;
import org.mozilla.gecko.widget.themed.ThemedImageView;
/**
* An ImageView which will always display at the given width and calculated height (based on the width and
* the supplied aspect ratio), drawn starting from the top left hand corner. A supplied drawable will be resized to fit
* the width of the view; if the resized drawable is too tall for the view then the drawable will be cropped at the
* bottom, however if the resized drawable is too short for the view to display whilst honouring it's given width and
* height then the drawable will be displayed at full height with the right hand side cropped.
*/
public abstract class CropImageView extends ThemedImageView {
public static final String LOGTAG = "Gecko" + CropImageView.class.getSimpleName();
private int viewWidth;
private int viewHeight;
private int drawableWidth;
private int drawableHeight;
private boolean resize = true;
private Matrix layoutCurrentMatrix = new Matrix();
private Matrix layoutNextMatrix = new Matrix();
public CropImageView(final Context context) {
this(context, null);
}
public CropImageView(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
public CropImageView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
protected abstract float getAspectRatio();
protected void init() {
// Setting the pivots means that the image will be drawn from the top left hand corner. There are
// issues in Android 4.1 (16) which mean setting these values to 0 may not work.
// http://stackoverflow.com/questions/26658124/setpivotx-doesnt-work-on-android-4-1-1-nineoldandroids
setPivotX(1);
setPivotY(1);
}
/**
* Measure the view to determine the measured width and height.
* The height is constrained by the measured width.
*
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
* @param heightMeasureSpec vertical space requirements as imposed by the parent, but ignored.
*/
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
// Default measuring.
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Force the height based on the aspect ratio.
viewWidth = getMeasuredWidth();
viewHeight = (int) (viewWidth * getAspectRatio());
setMeasuredDimension(viewWidth, viewHeight);
updateImageMatrix();
}
protected void updateImageMatrix() {
if (!resize || getDrawable() == null) {
return;
}
setScaleType(ImageView.ScaleType.MATRIX);
getDrawable().setBounds(0, 0, viewWidth, viewHeight);
final float horizontalScaleValue = (float) viewWidth / (float) drawableWidth;
final float verticalScaleValue = (float) viewHeight / (float) drawableHeight;
final float scale = Math.max(verticalScaleValue, horizontalScaleValue);
layoutNextMatrix.reset();
layoutNextMatrix.setScale(scale, scale);
setImageMatrix(layoutNextMatrix);
// You can't modify the matrix in place and we want to avoid allocation, so let's keep two references to two
// different matrix objects that we can swap when the values need to change
final Matrix swapReferenceMatrix = layoutCurrentMatrix;
layoutCurrentMatrix = layoutNextMatrix;
layoutNextMatrix = swapReferenceMatrix;
}
public void setImageBitmap(final Bitmap bm, final boolean resize) {
super.setImageBitmap(bm);
this.resize = resize;
updateImageMatrix();
}
@Override
public void setImageResource(final int resId) {
super.setImageResource(resId);
setImageMatrix(null);
resize = false;
}
@Override
public void setImageDrawable(final Drawable drawable) {
this.setImageDrawable(drawable, false);
}
public void setImageDrawable(final Drawable drawable, final boolean resize) {
super.setImageDrawable(drawable);
if (drawable != null) {
// Reset the matrix to ensure that any previous changes aren't carried through.
setImageMatrix(null);
drawableWidth = drawable.getIntrinsicWidth();
drawableHeight = drawable.getIntrinsicHeight();
} else {
drawableWidth = -1;
drawableHeight = -1;
}
this.resize = resize;
updateImageMatrix();
}
}

View file

@ -0,0 +1,665 @@
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mozilla.gecko.widget;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Locale;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.R;
import android.content.Context;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.InputMethodManager;
import android.widget.CalendarView;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.NumberPicker;
public class DateTimePicker extends FrameLayout {
private static final boolean DEBUG = true;
private static final String LOGTAG = "GeckoDateTimePicker";
private static final int DEFAULT_START_YEAR = 1;
private static final int DEFAULT_END_YEAR = 9999;
private static final char DATE_FORMAT_DAY = 'd';
private static final char DATE_FORMAT_MONTH = 'M';
private static final char DATE_FORMAT_YEAR = 'y';
boolean mYearEnabled = true;
boolean mMonthEnabled = true;
boolean mWeekEnabled;
boolean mDayEnabled = true;
boolean mHourEnabled = true;
boolean mMinuteEnabled = true;
boolean mIs12HourMode;
private boolean mCalendarEnabled;
// Size of the screen in inches;
private final int mScreenWidth;
private final int mScreenHeight;
private final OnValueChangeListener mOnChangeListener;
private final LinearLayout mPickers;
private final LinearLayout mDateSpinners;
private final LinearLayout mTimeSpinners;
final NumberPicker mDaySpinner;
final NumberPicker mMonthSpinner;
final NumberPicker mWeekSpinner;
final NumberPicker mYearSpinner;
final NumberPicker mHourSpinner;
final NumberPicker mMinuteSpinner;
final NumberPicker mAMPMSpinner;
private final CalendarView mCalendar;
private final EditText mDaySpinnerInput;
private final EditText mMonthSpinnerInput;
private final EditText mWeekSpinnerInput;
private final EditText mYearSpinnerInput;
private final EditText mHourSpinnerInput;
private final EditText mMinuteSpinnerInput;
private final EditText mAMPMSpinnerInput;
private Locale mCurrentLocale;
private String[] mShortMonths;
private String[] mShortAMPMs;
private int mNumberOfMonths;
Calendar mTempDate;
Calendar mCurrentDate;
private Calendar mMinDate;
private Calendar mMaxDate;
private final PickersState mState;
public static enum PickersState { DATE, MONTH, WEEK, TIME, DATETIME };
public class OnValueChangeListener implements NumberPicker.OnValueChangeListener {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
updateInputState();
mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
if (DEBUG) {
Log.d(LOGTAG, "SDK version > 10, using new behavior");
}
// The native date picker widget on these SDKs increments
// the next field when one field reaches the maximum.
if (picker == mDaySpinner && mDayEnabled) {
int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH);
int old = mTempDate.get(Calendar.DAY_OF_MONTH);
setTempDate(Calendar.DAY_OF_MONTH, old, newVal, 1, maxDayOfMonth);
} else if (picker == mMonthSpinner && mMonthEnabled) {
int old = mTempDate.get(Calendar.MONTH);
setTempDate(Calendar.MONTH, old, newVal, Calendar.JANUARY, Calendar.DECEMBER);
} else if (picker == mWeekSpinner) {
int old = mTempDate.get(Calendar.WEEK_OF_YEAR);
int maxWeekOfYear = mTempDate.getActualMaximum(Calendar.WEEK_OF_YEAR);
setTempDate(Calendar.WEEK_OF_YEAR, old, newVal, 0, maxWeekOfYear);
} else if (picker == mYearSpinner && mYearEnabled) {
int month = mTempDate.get(Calendar.MONTH);
mTempDate.set(Calendar.YEAR, newVal);
// Changing the year shouldn't change the month. (in case of non-leap year a Feb 29)
// change the day instead;
if (month != mTempDate.get(Calendar.MONTH)) {
mTempDate.set(Calendar.MONTH, month);
mTempDate.set(Calendar.DAY_OF_MONTH,
mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH));
}
} else if (picker == mHourSpinner && mHourEnabled) {
if (mIs12HourMode) {
setTempDate(Calendar.HOUR, oldVal, newVal, 1, 12);
} else {
setTempDate(Calendar.HOUR_OF_DAY, oldVal, newVal, 0, 23);
}
} else if (picker == mMinuteSpinner && mMinuteEnabled) {
setTempDate(Calendar.MINUTE, oldVal, newVal, 0, 59);
} else if (picker == mAMPMSpinner && mHourEnabled) {
mTempDate.set(Calendar.AM_PM, newVal);
} else {
throw new IllegalArgumentException();
}
setDate(mTempDate);
if (mDayEnabled) {
mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
}
if (mWeekEnabled) {
mWeekSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.WEEK_OF_YEAR));
}
updateCalendar();
updateSpinners();
notifyDateChanged();
}
private void setTempDate(int field, int oldVal, int newVal, int min, int max) {
if (oldVal == max && newVal == min) {
mTempDate.add(field, 1);
} else if (oldVal == min && newVal == max) {
mTempDate.add(field, -1);
} else {
mTempDate.add(field, newVal - oldVal);
}
}
}
private static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() {
final StringBuilder mBuilder = new StringBuilder();
final java.util.Formatter mFmt = new java.util.Formatter(mBuilder, java.util.Locale.US);
final Object[] mArgs = new Object[1];
@Override
public String format(int value) {
mArgs[0] = value;
mBuilder.delete(0, mBuilder.length());
mFmt.format("%02d", mArgs);
return mFmt.toString();
}
};
private void displayPickers() {
setWeekShown(false);
set12HourShown(mIs12HourMode);
if (mState == PickersState.DATETIME) {
return;
}
setHourShown(false);
setMinuteShown(false);
if (mState == PickersState.WEEK) {
setDayShown(false);
setMonthShown(false);
setWeekShown(true);
} else if (mState == PickersState.MONTH) {
setDayShown(false);
}
}
public DateTimePicker(Context context) {
this(context, "", "", PickersState.DATE, null, null);
}
public DateTimePicker(Context context, String dateFormat, String dateTimeValue, PickersState state, String minDateValue, String maxDateValue) {
super(context);
setCurrentLocale(Locale.getDefault());
mState = state;
LayoutInflater inflater = LayoutInflater.from(context);
inflater.inflate(R.layout.datetime_picker, this, true);
mOnChangeListener = new OnValueChangeListener();
mDateSpinners = (LinearLayout)findViewById(R.id.date_spinners);
mTimeSpinners = (LinearLayout)findViewById(R.id.time_spinners);
mPickers = (LinearLayout)findViewById(R.id.datetime_picker);
// We will display differently according to the screen size width.
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
DisplayMetrics dm = new DisplayMetrics();
display.getMetrics(dm);
mScreenWidth = display.getWidth() / dm.densityDpi;
mScreenHeight = display.getHeight() / dm.densityDpi;
if (DEBUG) {
Log.d(LOGTAG, "screen width: " + mScreenWidth + " screen height: " + mScreenHeight);
}
// Set the min / max attribute.
try {
if (minDateValue != null && !minDateValue.equals("")) {
mMinDate.setTime(new SimpleDateFormat(dateFormat).parse(minDateValue));
} else {
mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
}
} catch (Exception ex) {
Log.e(LOGTAG, "Error parsing format sting: " + ex);
mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
}
try {
if (maxDateValue != null && !maxDateValue.equals("")) {
mMaxDate.setTime(new SimpleDateFormat(dateFormat).parse(maxDateValue));
} else {
mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
}
} catch (Exception ex) {
Log.e(LOGTAG, "Error parsing format string: " + ex);
mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
}
// Find the initial date from the constructor arguments.
try {
if (!dateTimeValue.equals("")) {
mTempDate.setTime(new SimpleDateFormat(dateFormat).parse(dateTimeValue));
} else {
mTempDate.setTimeInMillis(System.currentTimeMillis());
}
} catch (Exception ex) {
Log.e(LOGTAG, "Error parsing format string: " + ex);
mTempDate.setTimeInMillis(System.currentTimeMillis());
}
if (mMaxDate.before(mMinDate)) {
// If the input date range is illogical/garbage, we should not restrict the input range (i.e. allow the
// user to select any date). If we try to make any assumptions based on the illogical min/max date we could
// potentially prevent the user from selecting dates that are in the developers intended range, so it's best
// to allow anything.
mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
}
// mTempDate will either be a site-supplied value, or today's date otherwise. CalendarView implementations can
// crash if they're supplied an invalid date (i.e. a date not in the specified range), hence we need to set
// a sensible default date here.
if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
mTempDate.setTimeInMillis(mMinDate.getTimeInMillis());
}
// If we're displaying a date, the screen is wide enough
// (and if we're using an SDK where the calendar view exists)
// then display a calendar.
if (mState == PickersState.DATE || mState == PickersState.DATETIME) {
mCalendar = new CalendarView(context);
mCalendar.setVisibility(GONE);
mCalendar.setFocusable(true);
mCalendar.setFocusableInTouchMode(true);
mCalendar.setMaxDate(mMaxDate.getTimeInMillis());
mCalendar.setMinDate(mMinDate.getTimeInMillis());
mCalendar.setDate(mTempDate.getTimeInMillis(), false, false);
mCalendar.setOnDateChangeListener(new CalendarView.OnDateChangeListener() {
@Override
public void onSelectedDayChange(
CalendarView view, int year, int month, int monthDay) {
mTempDate.set(year, month, monthDay);
setDate(mTempDate);
notifyDateChanged();
}
});
final int height;
if (Versions.preLollipop) {
// The 4.X version of CalendarView doesn't request any height, resulting in
// the whole dialog not appearing unless we manually request height.
height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200, getResources().getDisplayMetrics());;
} else {
height = LayoutParams.WRAP_CONTENT;
}
mPickers.addView(mCalendar, LayoutParams.MATCH_PARENT, height);
} else {
// If the screen is more wide than high, we are displaying day and
// time spinners, and if there is no calendar displayed, we should
// display the fields in one row.
if (mScreenWidth > mScreenHeight && mState == PickersState.DATETIME) {
mPickers.setOrientation(LinearLayout.HORIZONTAL);
}
mCalendar = null;
}
// Initialize all spinners.
mDaySpinner = setupSpinner(R.id.day, 1,
mTempDate.get(Calendar.DAY_OF_MONTH));
mDaySpinner.setFormatter(TWO_DIGIT_FORMATTER);
mDaySpinnerInput = (EditText) mDaySpinner.getChildAt(1);
mMonthSpinner = setupSpinner(R.id.month, 1,
mTempDate.get(Calendar.MONTH) + 1); // Month is 0-based
mMonthSpinner.setFormatter(TWO_DIGIT_FORMATTER);
mMonthSpinner.setDisplayedValues(mShortMonths);
mMonthSpinnerInput = (EditText) mMonthSpinner.getChildAt(1);
mWeekSpinner = setupSpinner(R.id.week, 1,
mTempDate.get(Calendar.WEEK_OF_YEAR));
mWeekSpinner.setFormatter(TWO_DIGIT_FORMATTER);
mWeekSpinnerInput = (EditText) mWeekSpinner.getChildAt(1);
mYearSpinner = setupSpinner(R.id.year, DEFAULT_START_YEAR,
DEFAULT_END_YEAR);
mYearSpinnerInput = (EditText) mYearSpinner.getChildAt(1);
mAMPMSpinner = setupSpinner(R.id.ampm, 0, 1);
mAMPMSpinner.setFormatter(TWO_DIGIT_FORMATTER);
if (mIs12HourMode) {
mHourSpinner = setupSpinner(R.id.hour, 1, 12);
mAMPMSpinnerInput = (EditText) mAMPMSpinner.getChildAt(1);
mAMPMSpinner.setDisplayedValues(mShortAMPMs);
} else {
mHourSpinner = setupSpinner(R.id.hour, 0, 23);
mAMPMSpinnerInput = null;
}
mHourSpinner.setFormatter(TWO_DIGIT_FORMATTER);
mHourSpinnerInput = (EditText) mHourSpinner.getChildAt(1);
mMinuteSpinner = setupSpinner(R.id.minute, 0, 59);
mMinuteSpinner.setFormatter(TWO_DIGIT_FORMATTER);
mMinuteSpinnerInput = (EditText) mMinuteSpinner.getChildAt(1);
// The order in which the spinners are displayed are locale-dependent
reorderDateSpinners();
// Set the date to the initial date. Since this date can come from the user,
// it can fire an exception (out-of-bound date)
try {
updateDate(mTempDate);
} catch (Exception ex) {
}
// Display only the pickers needed for the current state.
displayPickers();
}
public NumberPicker setupSpinner(int id, int min, int max) {
NumberPicker mSpinner = (NumberPicker) findViewById(id);
mSpinner.setMinValue(min);
mSpinner.setMaxValue(max);
mSpinner.setOnValueChangedListener(mOnChangeListener);
mSpinner.setOnLongPressUpdateInterval(100);
return mSpinner;
}
public long getTimeInMillis() {
return mCurrentDate.getTimeInMillis();
}
private void reorderDateSpinners() {
mDateSpinners.removeAllViews();
char[] order = DateFormat.getDateFormatOrder(getContext());
final int spinnerCount = order.length;
for (int i = 0; i < spinnerCount; i++) {
switch (order[i]) {
case DATE_FORMAT_DAY:
mDateSpinners.addView(mDaySpinner);
break;
case DATE_FORMAT_MONTH:
mDateSpinners.addView(mMonthSpinner);
break;
case DATE_FORMAT_YEAR:
mDateSpinners.addView(mYearSpinner);
break;
default:
throw new IllegalArgumentException();
}
}
mDateSpinners.addView(mWeekSpinner);
}
void setDate(Calendar calendar) {
mCurrentDate = mTempDate;
if (mCurrentDate.before(mMinDate)) {
mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
} else if (mCurrentDate.after(mMaxDate)) {
mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
}
}
void updateInputState() {
InputMethodManager inputMethodManager = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (mYearEnabled && inputMethodManager.isActive(mYearSpinnerInput)) {
mYearSpinnerInput.clearFocus();
inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
} else if (mMonthEnabled && inputMethodManager.isActive(mMonthSpinnerInput)) {
mMonthSpinnerInput.clearFocus();
inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
} else if (mDayEnabled && inputMethodManager.isActive(mDaySpinnerInput)) {
mDaySpinnerInput.clearFocus();
inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
} else if (mHourEnabled && inputMethodManager.isActive(mHourSpinnerInput)) {
mHourSpinnerInput.clearFocus();
inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
} else if (mMinuteEnabled && inputMethodManager.isActive(mMinuteSpinnerInput)) {
mMinuteSpinnerInput.clearFocus();
inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
}
}
void updateSpinners() {
if (mDayEnabled) {
if (mCurrentDate.equals(mMinDate)) {
mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
} else if (mCurrentDate.equals(mMaxDate)) {
mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
} else {
mDaySpinner.setMinValue(1);
mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
}
mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
}
if (mWeekEnabled) {
mWeekSpinner.setMinValue(1);
mWeekSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.WEEK_OF_YEAR));
mWeekSpinner.setValue(mCurrentDate.get(Calendar.WEEK_OF_YEAR));
}
if (mMonthEnabled) {
mMonthSpinner.setDisplayedValues(null);
if (mCurrentDate.equals(mMinDate)) {
mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH));
mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
} else if (mCurrentDate.equals(mMaxDate)) {
mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH));
} else {
mMonthSpinner.setMinValue(Calendar.JANUARY);
mMonthSpinner.setMaxValue(Calendar.DECEMBER);
}
String[] displayedValues = Arrays.copyOfRange(mShortMonths,
mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1);
mMonthSpinner.setDisplayedValues(displayedValues);
mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH));
}
if (mYearEnabled) {
mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR));
mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR));
mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR));
}
if (mHourEnabled) {
if (mIs12HourMode) {
mHourSpinner.setValue(mCurrentDate.get(Calendar.HOUR));
mAMPMSpinner.setValue(mCurrentDate.get(Calendar.AM_PM));
mAMPMSpinner.setDisplayedValues(mShortAMPMs);
} else {
mHourSpinner.setValue(mCurrentDate.get(Calendar.HOUR_OF_DAY));
}
}
if (mMinuteEnabled) {
mMinuteSpinner.setValue(mCurrentDate.get(Calendar.MINUTE));
}
}
void updateCalendar() {
if (mCalendarEnabled) {
mCalendar.setDate(mCurrentDate.getTimeInMillis(), false, false);
}
}
void notifyDateChanged() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
}
public void toggleCalendar(boolean shown) {
if ((mState != PickersState.DATE && mState != PickersState.DATETIME)) {
return;
}
if (shown) {
mCalendarEnabled = true;
mCalendar.setVisibility(VISIBLE);
setYearShown(false);
setWeekShown(false);
setMonthShown(false);
setDayShown(false);
} else {
mCalendar.setVisibility(GONE);
setYearShown(true);
setMonthShown(true);
setDayShown(true);
mPickers.setOrientation(LinearLayout.HORIZONTAL);
mCalendarEnabled = false;
}
}
private void setYearShown(boolean shown) {
if (shown) {
toggleCalendar(false);
mYearSpinner.setVisibility(VISIBLE);
mYearEnabled = true;
} else {
mYearSpinner.setVisibility(GONE);
mYearEnabled = false;
}
}
private void setWeekShown(boolean shown) {
if (shown) {
toggleCalendar(false);
mWeekSpinner.setVisibility(VISIBLE);
mWeekEnabled = true;
} else {
mWeekSpinner.setVisibility(GONE);
mWeekEnabled = false;
}
}
private void setMonthShown(boolean shown) {
if (shown) {
toggleCalendar(false);
mMonthSpinner.setVisibility(VISIBLE);
mMonthEnabled = true;
} else {
mMonthSpinner.setVisibility(GONE);
mMonthEnabled = false;
}
}
private void setDayShown(boolean shown) {
if (shown) {
toggleCalendar(false);
mDaySpinner.setVisibility(VISIBLE);
mDayEnabled = true;
} else {
mDaySpinner.setVisibility(GONE);
mDayEnabled = false;
}
}
private void set12HourShown(boolean shown) {
if (shown) {
mAMPMSpinner.setVisibility(VISIBLE);
} else {
mAMPMSpinner.setVisibility(GONE);
}
}
private void setHourShown(boolean shown) {
if (shown) {
mHourSpinner.setVisibility(VISIBLE);
mHourEnabled = true;
} else {
mHourSpinner.setVisibility(GONE);
mAMPMSpinner.setVisibility(GONE);
mTimeSpinners.setVisibility(GONE);
mHourEnabled = false;
}
}
private void setMinuteShown(boolean shown) {
if (shown) {
mMinuteSpinner.setVisibility(VISIBLE);
mTimeSpinners.findViewById(R.id.mincolon).setVisibility(VISIBLE);
mMinuteEnabled = true;
} else {
mMinuteSpinner.setVisibility(GONE);
mTimeSpinners.findViewById(R.id.mincolon).setVisibility(GONE);
mMinuteEnabled = false;
}
}
private void setCurrentLocale(Locale locale) {
if (locale.equals(mCurrentLocale)) {
return;
}
mCurrentLocale = locale;
mIs12HourMode = !DateFormat.is24HourFormat(getContext());
mTempDate = getCalendarForLocale(mTempDate, locale);
mMinDate = getCalendarForLocale(mMinDate, locale);
mMaxDate = getCalendarForLocale(mMaxDate, locale);
mCurrentDate = getCalendarForLocale(mCurrentDate, locale);
mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1;
mShortAMPMs = new String[2];
mShortAMPMs[0] = DateUtils.getAMPMString(Calendar.AM);
mShortAMPMs[1] = DateUtils.getAMPMString(Calendar.PM);
mShortMonths = new String[mNumberOfMonths];
for (int i = 0; i < mNumberOfMonths; i++) {
mShortMonths[i] = DateUtils.getMonthString(Calendar.JANUARY + i,
DateUtils.LENGTH_MEDIUM);
}
}
private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
if (oldCalendar == null) {
return Calendar.getInstance(locale);
}
final long currentTimeMillis = oldCalendar.getTimeInMillis();
Calendar newCalendar = Calendar.getInstance(locale);
newCalendar.setTimeInMillis(currentTimeMillis);
return newCalendar;
}
public void updateDate(Calendar calendar) {
if (mCurrentDate.equals(calendar)) {
return;
}
mCurrentDate.setTimeInMillis(calendar.getTimeInMillis());
if (mCurrentDate.before(mMinDate)) {
mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
} else if (mCurrentDate.after(mMaxDate)) {
mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
}
updateSpinners();
notifyDateChanged();
}
}

View file

@ -0,0 +1,190 @@
/* -*- 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.support.v4.content.ContextCompat;
import android.text.Html;
import android.text.Spanned;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.prompts.PromptInput;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class DefaultDoorHanger extends DoorHanger {
private static final String LOGTAG = "GeckoDefaultDoorHanger";
private static int sSpinnerTextColor = -1;
private final TextView mMessage;
private List<PromptInput> mInputs;
private CheckBox mCheckBox;
public DefaultDoorHanger(Context context, DoorhangerConfig config, Type type) {
super(context, config, type);
mMessage = (TextView) findViewById(R.id.doorhanger_message);
if (sSpinnerTextColor == -1) {
sSpinnerTextColor = ContextCompat.getColor(context, R.color.text_color_primary_disable_only);
}
switch (mType) {
case GEOLOCATION:
mIcon.setImageResource(R.drawable.location);
mIcon.setVisibility(VISIBLE);
break;
case DESKTOPNOTIFICATION2:
mIcon.setImageResource(R.drawable.push_notification);
mIcon.setVisibility(VISIBLE);
break;
}
loadConfig(config);
}
@Override
protected void loadConfig(DoorhangerConfig config) {
final String message = config.getMessage();
if (message != null) {
setMessage(message);
}
final JSONObject options = config.getOptions();
if (options != null) {
setOptions(options);
}
final DoorhangerConfig.Link link = config.getLink();
if (link != null) {
addLink(link.label, link.url);
}
addButtonsToLayout(config);
}
@Override
protected int getContentResource() {
return R.layout.default_doorhanger;
}
private List<PromptInput> getInputs() {
return mInputs;
}
private CheckBox getCheckBox() {
return mCheckBox;
}
@Override
public void setOptions(final JSONObject options) {
super.setOptions(options);
final JSONArray inputs = options.optJSONArray("inputs");
if (inputs != null) {
mInputs = new ArrayList<PromptInput>();
final ViewGroup group = (ViewGroup) findViewById(R.id.doorhanger_inputs);
group.setVisibility(VISIBLE);
for (int i = 0; i < inputs.length(); i++) {
try {
PromptInput input = PromptInput.getInput(inputs.getJSONObject(i));
mInputs.add(input);
final int padding = mResources.getDimensionPixelSize(R.dimen.doorhanger_section_padding_medium);
View v = input.getView(getContext());
styleInput(input, v);
v.setPadding(0, 0, 0, padding);
group.addView(v);
} catch (JSONException ex) { }
}
}
final String checkBoxText = options.optString("checkbox");
if (!TextUtils.isEmpty(checkBoxText)) {
mCheckBox = (CheckBox) findViewById(R.id.doorhanger_checkbox);
mCheckBox.setText(checkBoxText);
if (options.has("checkboxState")) {
final boolean checkBoxState = options.optBoolean("checkboxState");
mCheckBox.setChecked(checkBoxState);
}
mCheckBox.setVisibility(VISIBLE);
}
}
@Override
protected OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra) {
return new Button.OnClickListener() {
@Override
public void onClick(View v) {
final String expandedExtra = mType.toString().toLowerCase(Locale.US) + "-" + telemetryExtra;
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DOORHANGER, expandedExtra);
final JSONObject response = new JSONObject();
try {
response.put("callback", id);
CheckBox checkBox = getCheckBox();
// If the checkbox is being used, pass its value
if (checkBox != null) {
response.put("checked", checkBox.isChecked());
}
List<PromptInput> doorHangerInputs = getInputs();
if (doorHangerInputs != null) {
JSONObject inputs = new JSONObject();
for (PromptInput input : doorHangerInputs) {
inputs.put(input.getId(), input.getValue());
}
response.put("inputs", inputs);
}
} catch (JSONException e) {
Log.e(LOGTAG, "Error creating onClick response", e);
}
mOnButtonClickListener.onButtonClick(response, DefaultDoorHanger.this);
}
};
}
private void setMessage(String message) {
Spanned markupMessage = Html.fromHtml(message);
mMessage.setText(markupMessage);
}
private void styleInput(PromptInput input, View view) {
if (input instanceof PromptInput.MenulistInput) {
styleDropdownInputs(input, view);
}
view.setPadding(0, 0, 0, mResources.getDimensionPixelSize(R.dimen.doorhanger_subsection_padding));
}
private void styleDropdownInputs(PromptInput input, View view) {
PromptInput.MenulistInput spinInput = (PromptInput.MenulistInput) input;
if (spinInput.textView != null) {
spinInput.textView.setTextColor(sSpinnerTextColor);
}
}
}

View file

@ -0,0 +1,685 @@
/* -*- 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.support.annotation.NonNull;
import android.support.v4.animation.AnimatorCompatHelper;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPropertyAnimatorCompat;
import android.support.v4.view.ViewPropertyAnimatorListener;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SimpleItemAnimator;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
/**
* This basically follows the approach taken by Wasabeef:
* <a href="https://github.com/wasabeef/recyclerview-animators">https://github.com/wasabeef/recyclerview-animators</a>
* based off of Android's DefaultItemAnimator from October 2016:
* <a href="https://github.com/android/platform_frameworks_support/blob/432f3317f8a9b8cf98277938ea5df4021e983055/v7/recyclerview/src/android/support/v7/widget/DefaultItemAnimator.java">
* https://github.com/android/platform_frameworks_support/blob/432f3317f8a9b8cf98277938ea5df4021e983055/v7/recyclerview/src/android/support/v7/widget/DefaultItemAnimator.java
* </a>
* <p>
* Usage Notes:
* </p>
* <ul>
* <li>You <strong>must</strong> add a Default*VpaListener to your animate*Impl animation - the
* listener takes care of animation bookkeeping.</li>
* <li>You should call {@link #resetAnimation(RecyclerView.ViewHolder)} at some point in
* preAnimate*Impl if you choose to proceed with the animation. Some animations will want to
* know some or all of the current animation values for initializing their own animation
* values before resetting the current animation, so this class does not provide the reset
* service itself.</li>
* <li>{@link #resetViewProperties(View)} is used to reset a view any time an animation ends or
* gets canceled - you should redefine resetViewProperties if the version here doesn't reset
* all of the properties you're animating.</li>
* </ul>
*/
public class DefaultItemAnimatorBase extends SimpleItemAnimator {
private List<RecyclerView.ViewHolder> pendingRemovals = new ArrayList<>();
private List<RecyclerView.ViewHolder> pendingAdditions = new ArrayList<>();
private List<MoveInfo> pendingMoves = new ArrayList<>();
private List<ChangeInfo> pendingChanges = new ArrayList<>();
private List<List<RecyclerView.ViewHolder>> additionsList = new ArrayList<>();
private List<List<MoveInfo>> movesList = new ArrayList<>();
private List<List<ChangeInfo>> changesList = new ArrayList<>();
private List<RecyclerView.ViewHolder> addAnimations = new ArrayList<>();
private List<RecyclerView.ViewHolder> moveAnimations = new ArrayList<>();
private List<RecyclerView.ViewHolder> removeAnimations = new ArrayList<>();
private List<RecyclerView.ViewHolder> changeAnimations = new ArrayList<>();
protected static class MoveInfo {
public RecyclerView.ViewHolder holder;
public int fromX, fromY, toX, toY;
public MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
this.holder = holder;
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
}
protected static class ChangeInfo {
public RecyclerView.ViewHolder oldHolder, newHolder;
public int fromX, fromY, toX, toY;
public ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) {
this.oldHolder = oldHolder;
this.newHolder = newHolder;
}
public ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
this(oldHolder, newHolder);
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
@Override
public String toString() {
return "ChangeInfo{" +
"oldHolder=" + oldHolder +
", newHolder=" + newHolder +
", fromX=" + fromX +
", fromY=" + fromY +
", toX=" + toX +
", toY=" + toY +
'}';
}
}
@Override
public void runPendingAnimations() {
final boolean removalsPending = !pendingRemovals.isEmpty();
final boolean movesPending = !pendingMoves.isEmpty();
final boolean changesPending = !pendingChanges.isEmpty();
final boolean additionsPending = !pendingAdditions.isEmpty();
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
return;
}
// First, remove stuff.
for (final RecyclerView.ViewHolder holder : pendingRemovals) {
animateRemoveImpl(holder);
}
pendingRemovals.clear();
// Next, move stuff.
if (movesPending) {
final List<MoveInfo> moves = new ArrayList<>();
moves.addAll(pendingMoves);
movesList.add(moves);
pendingMoves.clear();
final Runnable mover = new Runnable() {
@Override
public void run() {
for (final MoveInfo moveInfo : moves) {
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
moveInfo.toX, moveInfo.toY);
}
moves.clear();
movesList.remove(moves);
}
};
if (removalsPending) {
final View view = moves.get(0).holder.itemView;
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
} else {
mover.run();
}
}
// Next, change stuff, to run in parallel with move animations.
if (changesPending) {
final List<ChangeInfo> changes = new ArrayList<>();
changes.addAll(pendingChanges);
changesList.add(changes);
pendingChanges.clear();
final Runnable changer = new Runnable() {
@Override
public void run() {
for (final ChangeInfo change : changes) {
animateChangeImpl(change);
}
changes.clear();
changesList.remove(changes);
}
};
if (removalsPending) {
RecyclerView.ViewHolder holder = changes.get(0).oldHolder;
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
} else {
changer.run();
}
}
// Next, add stuff.
if (additionsPending) {
final List<RecyclerView.ViewHolder> additions = new ArrayList<>();
additions.addAll(pendingAdditions);
additionsList.add(additions);
pendingAdditions.clear();
final Runnable adder = new Runnable() {
public void run() {
for (final RecyclerView.ViewHolder holder : additions) {
animateAddImpl(holder);
}
additions.clear();
additionsList.remove(additions);
}
};
if (removalsPending || movesPending || changesPending) {
final long removeDuration = removalsPending ? getRemoveDuration() : 0;
final long moveDuration = movesPending ? getMoveDuration() : 0;
final long changeDuration = changesPending ? getChangeDuration() : 0;
final long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
final View view = additions.get(0).itemView;
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
} else {
adder.run();
}
}
}
@Override
public boolean animateRemove(final RecyclerView.ViewHolder holder) {
if (!preAnimateRemoveImpl(holder)) {
dispatchRemoveFinished(holder);
return false;
}
pendingRemovals.add(holder);
return true;
}
protected boolean preAnimateRemoveImpl(final RecyclerView.ViewHolder holder) {
resetAnimation(holder);
return true;
}
protected void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
ViewCompat.animate(holder.itemView)
.setDuration(getRemoveDuration())
.alpha(0)
.setListener(new DefaultRemoveVpaListener(holder))
.start();
}
@Override
public boolean animateAdd(final RecyclerView.ViewHolder holder) {
if (!preAnimateAddImpl(holder)) {
dispatchAddFinished(holder);
return false;
}
pendingAdditions.add(holder);
return true;
}
protected boolean preAnimateAddImpl(RecyclerView.ViewHolder holder) {
resetAnimation(holder);
holder.itemView.setAlpha(0);
return true;
}
protected void animateAddImpl(final RecyclerView.ViewHolder holder) {
ViewCompat.animate(holder.itemView)
.setDuration(getAddDuration())
.alpha(1)
.setListener(new DefaultAddVpaListener(holder))
.start();
}
@Override
public boolean animateMove(final RecyclerView.ViewHolder holder,
int fromX, int fromY, int toX, int toY) {
final View view = holder.itemView;
fromX += ViewCompat.getTranslationX(holder.itemView);
fromY += ViewCompat.getTranslationY(holder.itemView);
final int deltaX = toX - fromX;
final int deltaY = toY - fromY;
if (deltaX == 0 && deltaY == 0) {
dispatchMoveFinished(holder);
return false;
}
resetAnimation(holder);
if (deltaX != 0) {
ViewCompat.setTranslationX(view, -deltaX);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, -deltaY);
}
pendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
return true;
}
protected void animateMoveImpl(final RecyclerView.ViewHolder holder,
int fromX, int fromY, int toX, int toY) {
final View view = holder.itemView;
final int deltaX = toX - fromX;
final int deltaY = toY - fromY;
if (deltaX != 0) {
ViewCompat.animate(view).translationX(0);
}
if (deltaY != 0) {
ViewCompat.animate(view).translationY(0);
}
// TODO: make EndActions end listeners instead, since end actions aren't called when
// vpas are canceled (and can't end them. why?)
// need listener functionality in VPACompat for this. Ick.
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
moveAnimations.add(holder);
animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchMoveStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
resetViewProperties(view);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchMoveFinished(holder);
moveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
if (oldHolder == newHolder) {
// Don't know how to run change animations when the same view holder is re-used.
// Run a move animation to handle position changes (if there are any).
if (fromX != toX || fromY != toY) {
// *Don't* call dispatchChangeFinished here, it leads to unbalanced isRecyclable calls.
return animateMove(oldHolder, fromX, fromY, toX, toY);
}
dispatchChangeFinished(oldHolder, true);
return false;
}
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
resetAnimation(oldHolder);
final int deltaX = (int) (toX - fromX - prevTranslationX);
final int deltaY = (int) (toY - fromY - prevTranslationY);
// Recover previous translation state after ending animation.
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
if (newHolder != null) {
// Carry over translation values.
resetAnimation(newHolder);
ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
ViewCompat.setAlpha(newHolder.itemView, 0);
}
pendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
return true;
}
protected void animateChangeImpl(final ChangeInfo changeInfo) {
final RecyclerView.ViewHolder holder = changeInfo.oldHolder;
final View view = holder == null ? null : holder.itemView;
final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
final View newView = newHolder != null ? newHolder.itemView : null;
if (view != null) {
final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
getChangeDuration());
changeAnimations.add(changeInfo.oldHolder);
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.oldHolder, true);
}
@Override
public void onAnimationEnd(View view) {
oldViewAnim.setListener(null);
resetViewProperties(view);
dispatchChangeFinished(changeInfo.oldHolder, true);
changeAnimations.remove(changeInfo.oldHolder);
dispatchFinishedWhenDone();
}
}).start();
}
if (newView != null) {
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
changeAnimations.add(changeInfo.newHolder);
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
alpha(1).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.newHolder, false);
}
@Override
public void onAnimationEnd(View view) {
newViewAnimation.setListener(null);
resetViewProperties(view);
dispatchChangeFinished(changeInfo.newHolder, false);
changeAnimations.remove(changeInfo.newHolder);
dispatchFinishedWhenDone();
}
}).start();
}
}
private void endChangeAnimation(List<ChangeInfo> infoList, RecyclerView.ViewHolder item) {
for (int i = infoList.size() - 1; i >= 0; i--) {
final ChangeInfo changeInfo = infoList.get(i);
if (endChangeAnimationIfNecessary(changeInfo, item)) {
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
infoList.remove(changeInfo);
}
}
}
}
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
if (changeInfo.oldHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
}
if (changeInfo.newHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
}
}
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) {
boolean oldItem = false;
if (changeInfo.newHolder == item) {
changeInfo.newHolder = null;
} else if (changeInfo.oldHolder == item) {
changeInfo.oldHolder = null;
oldItem = true;
} else {
return false;
}
resetViewProperties(item.itemView);
dispatchChangeFinished(item, oldItem);
return true;
}
/**
* Called to reset all properties possibly animated by any and all defined animations.
*/
protected void resetViewProperties(View view) {
view.setTranslationX(0);
view.setTranslationY(0);
view.setAlpha(1);
}
@Override
public void endAnimation(RecyclerView.ViewHolder item) {
final View view = item.itemView;
// This calls dispatch*Finished, resets view properties, and removes item from current
// animations list if the view is currently being animated.
ViewCompat.animate(view).cancel();
// TODO if some other animations are chained to end, how do we cancel them as well?
for (int i = pendingMoves.size() - 1; i >= 0; i--) {
final MoveInfo moveInfo = pendingMoves.get(i);
if (moveInfo.holder == item) {
resetViewProperties(view);
dispatchMoveFinished(item);
pendingMoves.remove(i);
}
}
endChangeAnimation(pendingChanges, item);
if (pendingRemovals.remove(item)) {
resetViewProperties(view);
dispatchRemoveFinished(item);
}
if (pendingAdditions.remove(item)) {
resetViewProperties(view);
dispatchAddFinished(item);
}
for (int i = changesList.size() - 1; i >= 0; i--) {
final List<ChangeInfo> changes = changesList.get(i);
endChangeAnimation(changes, item);
if (changes.isEmpty()) {
changesList.remove(i);
}
}
for (int i = movesList.size() - 1; i >= 0; i--) {
final List<MoveInfo> moves = movesList.get(i);
for (int j = moves.size() - 1; j >= 0; j--) {
final MoveInfo moveInfo = moves.get(j);
if (moveInfo.holder == item) {
resetViewProperties(view);
dispatchMoveFinished(item);
moves.remove(j);
if (moves.isEmpty()) {
movesList.remove(i);
}
break;
}
}
}
for (int i = additionsList.size() - 1; i >= 0; i--) {
final List<RecyclerView.ViewHolder> additions = additionsList.get(i);
if (additions.remove(item)) {
resetViewProperties(view);
dispatchAddFinished(item);
if (additions.isEmpty()) {
additionsList.remove(i);
}
}
}
dispatchFinishedWhenDone();
}
protected void resetAnimation(RecyclerView.ViewHolder holder) {
AnimatorCompatHelper.clearInterpolator(holder.itemView);
endAnimation(holder);
}
@Override
public boolean isRunning() {
return (!pendingAdditions.isEmpty() ||
!pendingChanges.isEmpty() ||
!pendingMoves.isEmpty() ||
!pendingRemovals.isEmpty() ||
!moveAnimations.isEmpty() ||
!removeAnimations.isEmpty() ||
!addAnimations.isEmpty() ||
!changeAnimations.isEmpty() ||
!movesList.isEmpty() ||
!additionsList.isEmpty() ||
!changesList.isEmpty());
}
/**
* Check the state of currently pending and running animations. If there are none
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
* listeners.
*/
protected void dispatchFinishedWhenDone() {
if (!isRunning()) {
dispatchAnimationsFinished();
}
}
@Override
public void endAnimations() {
int count = pendingMoves.size();
for (int i = count - 1; i >= 0; i--) {
final MoveInfo item = pendingMoves.get(i);
resetViewProperties(item.holder.itemView);
dispatchMoveFinished(item.holder);
pendingMoves.remove(i);
}
count = pendingRemovals.size();
for (int i = count - 1; i >= 0; i--) {
final RecyclerView.ViewHolder item = pendingRemovals.get(i);
resetViewProperties(item.itemView);
dispatchRemoveFinished(item);
pendingRemovals.remove(i);
}
count = pendingAdditions.size();
for (int i = count - 1; i >= 0; i--) {
final RecyclerView.ViewHolder item = pendingAdditions.get(i);
resetViewProperties(item.itemView);
dispatchAddFinished(item);
pendingAdditions.remove(i);
}
count = pendingChanges.size();
for (int i = count - 1; i >= 0; i--) {
endChangeAnimationIfNecessary(pendingChanges.get(i));
}
pendingChanges.clear();
if (!isRunning()) {
return;
}
int listCount = movesList.size();
for (int i = listCount - 1; i >= 0; i--) {
final List<MoveInfo> moves = movesList.get(i);
count = moves.size();
for (int j = count - 1; j >= 0; j--) {
final MoveInfo moveInfo = moves.get(j);
final RecyclerView.ViewHolder item = moveInfo.holder;
resetViewProperties(item.itemView);
dispatchMoveFinished(item);
moves.remove(j);
if (moves.isEmpty()) {
movesList.remove(moves);
}
}
}
listCount = additionsList.size();
for (int i = listCount - 1; i >= 0; i--) {
final List<RecyclerView.ViewHolder> additions = additionsList.get(i);
count = additions.size();
for (int j = count - 1; j >= 0; j--) {
final RecyclerView.ViewHolder item = additions.get(j);
resetViewProperties(item.itemView);
dispatchAddFinished(item);
additions.remove(j);
if (additions.isEmpty()) {
additionsList.remove(additions);
}
}
}
listCount = changesList.size();
for (int i = listCount - 1; i >= 0; i--) {
final List<ChangeInfo> changes = changesList.get(i);
count = changes.size();
for (int j = count - 1; j >= 0; j--) {
endChangeAnimationIfNecessary(changes.get(j));
if (changes.isEmpty()) {
changesList.remove(changes);
}
}
}
cancelAll(removeAnimations);
cancelAll(moveAnimations);
cancelAll(addAnimations);
cancelAll(changeAnimations);
dispatchAnimationsFinished();
}
public void cancelAll(List<RecyclerView.ViewHolder> viewHolders) {
for (int i = viewHolders.size() - 1; i >= 0; i--) {
ViewCompat.animate(viewHolders.get(i).itemView).cancel();
}
}
/**
* {@inheritDoc}
* <p>
* If the payload list is not empty, DefaultItemAnimator returns <code>true</code>.
* When this is the case:
* <ul>
* <li>If you override
* {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)},
* both ViewHolder arguments will be the same instance.
* </li>
* <li>
* If you are not overriding
* {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)},
* then DefaultItemAnimator will call
* {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int)} and run a move animation
* instead.
* </li>
* </ul>
*/
@Override
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull List<Object> payloads) {
return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
}
private class VpaListenerAdapter implements ViewPropertyAnimatorListener {
@Override
public void onAnimationStart(View view) {}
// Note that onAnimationEnd is called (in addition to OnAnimationCancel) whenever an
// animation is canceled.
@Override
public void onAnimationEnd(View view) {
resetViewProperties(view);
view.animate().setListener(null);
}
@Override
public void onAnimationCancel(View view) {}
}
protected class DefaultRemoveVpaListener extends VpaListenerAdapter {
private final RecyclerView.ViewHolder viewHolder;
public DefaultRemoveVpaListener(final RecyclerView.ViewHolder holder) {
viewHolder = holder;
}
@Override
public void onAnimationStart(View view) {
removeAnimations.add(viewHolder);
dispatchRemoveStarting(viewHolder);
}
@Override
public void onAnimationEnd(View view) {
removeAnimations.remove(viewHolder);
dispatchRemoveFinished(viewHolder);
dispatchFinishedWhenDone();
super.onAnimationEnd(view);
}
}
protected class DefaultAddVpaListener extends VpaListenerAdapter {
private final RecyclerView.ViewHolder viewHolder;
public DefaultAddVpaListener(final RecyclerView.ViewHolder holder) {
viewHolder = holder;
}
@Override
public void onAnimationStart(View view) {
addAnimations.add(viewHolder);
dispatchAddStarting(viewHolder);
}
@Override
public void onAnimationEnd(View view) {
addAnimations.remove(viewHolder);
dispatchAddFinished(viewHolder);
dispatchFinishedWhenDone();
super.onAnimationEnd(view);
}
}
}

View file

@ -0,0 +1,220 @@
/* -*- 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.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.support.v4.content.ContextCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewStub;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.json.JSONObject;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import java.util.Locale;
public abstract class DoorHanger extends LinearLayout {
public static DoorHanger Get(Context context, DoorhangerConfig config) {
final Type type = config.getType();
switch (type) {
case LOGIN:
return new LoginDoorHanger(context, config);
case TRACKING:
return new ContentSecurityDoorHanger(context, config, type);
}
return new DefaultDoorHanger(context, config, type);
}
// Doorhanger types created from Gecko are checked against enum strings to determine type.
public static enum Type { DEFAULT, LOGIN, TRACKING, GEOLOCATION, DESKTOPNOTIFICATION2, WEBRTC, VIBRATION }
public interface OnButtonClickListener {
public void onButtonClick(JSONObject response, DoorHanger doorhanger);
}
private static final String LOGTAG = "GeckoDoorHanger";
// Divider between doorhangers.
private final View mDivider;
private final Button mNegativeButton;
private final Button mPositiveButton;
protected final OnButtonClickListener mOnButtonClickListener;
// The tab this doorhanger is associated with.
private final int mTabId;
// DoorHanger identifier.
private final String mIdentifier;
protected final Type mType;
protected final ImageView mIcon;
protected final TextView mLink;
protected final TextView mDoorhangerTitle;
protected final Context mContext;
protected final Resources mResources;
protected int mDividerColor;
protected boolean mPersistWhileVisible;
protected int mPersistenceCount;
protected long mTimeout;
protected DoorHanger(Context context, DoorhangerConfig config, Type type) {
super(context);
mContext = context;
mResources = context.getResources();
mTabId = config.getTabId();
mIdentifier = config.getId();
mType = type;
LayoutInflater.from(context).inflate(R.layout.doorhanger, this);
setOrientation(VERTICAL);
mDivider = findViewById(R.id.divider_doorhanger);
mIcon = (ImageView) findViewById(R.id.doorhanger_icon);
mLink = (TextView) findViewById(R.id.doorhanger_link);
mDoorhangerTitle = (TextView) findViewById(R.id.doorhanger_title);
mNegativeButton = (Button) findViewById(R.id.doorhanger_button_negative);
mPositiveButton = (Button) findViewById(R.id.doorhanger_button_positive);
mOnButtonClickListener = config.getButtonClickListener();
mDividerColor = ContextCompat.getColor(context, R.color.toolbar_divider_grey);
final ViewStub contentStub = (ViewStub) findViewById(R.id.content);
contentStub.setLayoutResource(getContentResource());
contentStub.inflate();
final String typeExtra = mType.toString().toLowerCase(Locale.US);
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.DOORHANGER, typeExtra);
}
protected abstract int getContentResource();
protected abstract void loadConfig(DoorhangerConfig config);
protected void setOptions(final JSONObject options) {
final int persistence = options.optInt("persistence");
if (persistence > 0) {
mPersistenceCount = persistence;
}
mPersistWhileVisible = options.optBoolean("persistWhileVisible");
final long timeout = options.optLong("timeout");
if (timeout > 0) {
mTimeout = timeout;
}
}
protected void addButtonsToLayout(DoorhangerConfig config) {
final DoorhangerConfig.ButtonConfig negativeButtonConfig = config.getNegativeButtonConfig();
final DoorhangerConfig.ButtonConfig positiveButtonConfig = config.getPositiveButtonConfig();
if (negativeButtonConfig != null) {
mNegativeButton.setText(negativeButtonConfig.label);
mNegativeButton.setOnClickListener(makeOnButtonClickListener(negativeButtonConfig.callback, "negative"));
mNegativeButton.setVisibility(VISIBLE);
}
if (positiveButtonConfig != null) {
mPositiveButton.setText(positiveButtonConfig.label);
mPositiveButton.setOnClickListener(makeOnButtonClickListener(positiveButtonConfig.callback, "positive"));
mPositiveButton.setVisibility(VISIBLE);
}
}
public int getTabId() {
return mTabId;
}
public String getIdentifier() {
return mIdentifier;
}
public void showDivider() {
mDivider.setVisibility(View.VISIBLE);
}
public void hideDivider() {
mDivider.setVisibility(View.GONE);
}
public void setIcon(int resId) {
mIcon.setImageResource(resId);
mIcon.setVisibility(View.VISIBLE);
}
protected void addLink(String label, final String url) {
mLink.setText(label);
mLink.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
final String typeExtra = mType.toString().toLowerCase(Locale.US);
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.DOORHANGER, typeExtra);
Tabs.getInstance().loadUrlInTab(url);
}
});
mLink.setVisibility(VISIBLE);
}
protected abstract OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra);
/*
* Checks with persistence and timeout options to see if it's okay to remove a doorhanger.
*
* @param isShowing Whether or not this doorhanger is currently visible to the user.
* (e.g. the DoorHanger view might be VISIBLE, but its parent could be hidden)
*/
public boolean shouldRemove(boolean isShowing) {
if (mPersistWhileVisible && isShowing) {
// We still want to decrement mPersistence, even if the popup is showing
if (mPersistenceCount != 0)
mPersistenceCount--;
return false;
}
// If persistence is set to -1, the doorhanger will never be
// automatically removed.
if (mPersistenceCount != 0) {
mPersistenceCount--;
return false;
}
if (System.currentTimeMillis() <= mTimeout) {
return false;
}
return true;
}
public void showTitle(Bitmap favicon, String title) {
mDoorhangerTitle.setText(title);
mDoorhangerTitle.setCompoundDrawablesWithIntrinsicBounds(new BitmapDrawable(getResources(), favicon), null, null, null);
if (favicon != null) {
mDoorhangerTitle.setCompoundDrawablePadding((int) mContext.getResources().getDimension(R.dimen.doorhanger_drawable_padding));
}
mDoorhangerTitle.setVisibility(VISIBLE);
}
public void hideTitle() {
mDoorhangerTitle.setVisibility(GONE);
}
}

View file

@ -0,0 +1,127 @@
/* -*- 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.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.widget.DoorHanger.Type;
public class DoorhangerConfig {
public static class Link {
public final String label;
public final String url;
private Link(String label, String url) {
this.label = label;
this.url = url;
}
}
public static class ButtonConfig {
public final String label;
public final int callback;
public ButtonConfig(String label, int callback) {
this.label = label;
this.callback = callback;
}
}
private static final String LOGTAG = "DoorhangerConfig";
private final int tabId;
private final String id;
private final DoorHanger.OnButtonClickListener buttonClickListener;
private final DoorHanger.Type type;
private String message;
private JSONObject options;
private Link link;
private ButtonConfig positiveButtonConfig;
private ButtonConfig negativeButtonConfig;
public DoorhangerConfig(Type type, DoorHanger.OnButtonClickListener listener) {
// XXX: This should only be used by SiteIdentityPopup doorhangers which
// don't need tab or id references, until bug 1141904 unifies doorhangers.
this(-1, null, type, listener);
}
public DoorhangerConfig(int tabId, String id, DoorHanger.Type type, DoorHanger.OnButtonClickListener buttonClickListener) {
this.tabId = tabId;
this.id = id;
this.type = type;
this.buttonClickListener = buttonClickListener;
}
public int getTabId() {
return tabId;
}
public String getId() {
return id;
}
public Type getType() {
return type;
}
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setOptions(JSONObject options) {
this.options = options;
// Set link if there is a link provided in options.
final JSONObject linkObj = options.optJSONObject("link");
if (linkObj != null) {
try {
setLink(linkObj.getString("label"), linkObj.getString("url"));
} catch (JSONException e) {
Log.e(LOGTAG, "Malformed link object in options");
}
}
}
public JSONObject getOptions() {
return options;
}
public void setButton(String label, int callbackId, boolean isPositive) {
final ButtonConfig buttonConfig = new ButtonConfig(label, callbackId);
if (isPositive) {
positiveButtonConfig = buttonConfig;
} else {
negativeButtonConfig = buttonConfig;
}
}
public ButtonConfig getPositiveButtonConfig() {
return positiveButtonConfig;
}
public ButtonConfig getNegativeButtonConfig() {
return negativeButtonConfig;
}
public DoorHanger.OnButtonClickListener getButtonClickListener() {
return this.buttonClickListener;
}
public void setLink(String label, String url) {
this.link = new Link(label, url);
}
public Link getLink() {
return link;
}
}

View file

@ -0,0 +1,65 @@
/* -*- 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 org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.widget.TextView;
/**
* Text view that correctly handles maxLines and ellipsizing for Android < 2.3.
*/
public class EllipsisTextView extends TextView {
private final String ellipsis;
private final int maxLines;
private CharSequence originalText;
public EllipsisTextView(Context context) {
this(context, null);
}
public EllipsisTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public EllipsisTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
ellipsis = getResources().getString(R.string.ellipsis);
TypedArray a = context.getTheme()
.obtainStyledAttributes(attrs, R.styleable.EllipsisTextView, 0, 0);
maxLines = a.getInteger(R.styleable.EllipsisTextView_ellipsizeAtLine, 1);
a.recycle();
}
public void setOriginalText(CharSequence text) {
originalText = text;
setText(text);
}
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// There is extra space, start over with the original text
if (getLineCount() < maxLines) {
setText(originalText);
}
// If we are over the max line attribute, ellipsize
if (getLineCount() > maxLines) {
final int endIndex = getLayout().getLineEnd(maxLines - 1) - 1 - ellipsis.length();
final String text = getText().subSequence(0, endIndex) + ellipsis;
// Make sure that we don't change originalText
setText(text);
}
}
}

View file

@ -0,0 +1,106 @@
// 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 org.mozilla.gecko.ActivityHandlerHelper;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import java.util.List;
/**
* A DialogFragment to contain a dialog that appears when the user clicks an Intent:// URI during private browsing. The
* dialog appears to notify the user that a clicked link will open in an external application, potentially leaking their
* browsing history.
*/
public class ExternalIntentDuringPrivateBrowsingPromptFragment extends DialogFragment {
private static final String LOGTAG = ExternalIntentDuringPrivateBrowsingPromptFragment.class.getSimpleName();
private static final String FRAGMENT_TAG = "ExternalIntentPB";
private static final String KEY_APPLICATION_NAME = "matchingApplicationName";
private static final String KEY_INTENT = "intent";
@Override
public Dialog onCreateDialog(final Bundle savedInstanceState) {
final Bundle args = getArguments();
final CharSequence matchingApplicationName = args.getCharSequence(KEY_APPLICATION_NAME);
final Intent intent = args.getParcelable(KEY_INTENT);
final Context context = getActivity();
final String promptMessage = context.getString(R.string.intent_uri_private_browsing_prompt, matchingApplicationName);
final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage(promptMessage)
.setTitle(intent.getDataString())
.setPositiveButton(R.string.button_yes, new DialogInterface.OnClickListener() {
public void onClick(final DialogInterface dialog, final int id) {
context.startActivity(intent);
}
})
.setNegativeButton(R.string.button_no, null /* we do nothing if the user rejects */ );
return builder.create();
}
@Override
public void onDestroy() {
super.onDestroy();
GeckoApplication.watchReference(getActivity(), this);
}
/**
* @return true if the Activity is started or a dialog is shown. false if the Activity fails to start.
*/
public static boolean showDialogOrAndroidChooser(final Context context, final FragmentManager fragmentManager,
final Intent intent) {
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
if (selectedTab == null || !selectedTab.isPrivate()) {
return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, intent);
}
final PackageManager pm = context.getPackageManager();
final List<ResolveInfo> matchingActivities = pm.queryIntentActivities(intent, 0);
if (matchingActivities.size() == 1) {
final ExternalIntentDuringPrivateBrowsingPromptFragment fragment = new ExternalIntentDuringPrivateBrowsingPromptFragment();
final Bundle args = new Bundle(2);
args.putCharSequence(KEY_APPLICATION_NAME, matchingActivities.get(0).loadLabel(pm));
args.putParcelable(KEY_INTENT, intent);
fragment.setArguments(args);
fragment.show(fragmentManager, FRAGMENT_TAG);
// We don't know the results of the user interaction with the fragment so just return true.
return true;
} else if (matchingActivities.size() > 1) {
// We want to show the Android Intent Chooser. However, we have no way of distinguishing regular tabs from
// private tabs to the chooser. Thus, if a user chooses "Always" in regular browsing mode, the chooser will
// not be shown and the URL will be opened. Therefore we explicitly show the chooser (which notably does not
// have an "Always" option).
final String androidChooserTitle =
context.getResources().getString(R.string.intent_uri_private_browsing_multiple_match_title);
final Intent chooserIntent = Intent.createChooser(intent, androidChooserTitle);
return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, chooserIntent);
} else {
// Normally, we show about:neterror when an Intent does not resolve
// but we don't have the references here to do that so log instead.
Log.w(LOGTAG, "showDialogOrAndroidChooser unexpectedly called with Intent that does not resolve");
return false;
}
}
}

View file

@ -0,0 +1,108 @@
/* -*- 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 org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
/**
* Fades the end of the text by gecko:fadeWidth amount,
* if the text is too long and requires an ellipsis.
*
* This implementation is an improvement over Android's built-in fadingEdge
* but potentially slower than the {@link org.mozilla.gecko.widget.FadedSingleColorTextView}.
* It works for text of multiple colors but only one background color. It works by
* drawing a gradient rectangle with the background color over the text, fading it out.
*/
public class FadedMultiColorTextView extends FadedTextView {
private final ColorStateList fadeBackgroundColorList;
private final Paint fadePaint;
private FadedTextGradient backgroundGradient;
public FadedMultiColorTextView(Context context, AttributeSet attrs) {
super(context, attrs);
fadePaint = new Paint();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadedMultiColorTextView);
fadeBackgroundColorList =
a.getColorStateList(R.styleable.FadedMultiColorTextView_fadeBackgroundColor);
a.recycle();
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
final boolean needsEllipsis = needsEllipsis();
if (needsEllipsis) {
final int right = getWidth() - getCompoundPaddingRight();
final float left = right - fadeWidth;
updateGradientShader(needsEllipsis, right);
// Shrink height of gradient to prevent it overlaying parent view border.
// The shrunk size just nee to cover the text itself.
final float density = getResources().getDisplayMetrics().density;
final float h = Math.abs(fadePaint.getFontMetrics().top) + 1;
final float l = fadePaint.getFontMetrics().bottom + 1;
final float top = getBaseline() - h * density;
final float bottom = getBaseline() + l * density;
canvas.drawRect(left, top, right, bottom, fadePaint);
}
}
private void updateGradientShader(final boolean needsEllipsis, final int gradientEndRight) {
final int backgroundColor =
fadeBackgroundColorList.getColorForState(getDrawableState(), Color.RED);
final boolean needsNewGradient = (backgroundGradient == null ||
backgroundGradient.getBackgroundColor() != backgroundColor ||
backgroundGradient.getEndRight() != gradientEndRight);
if (needsEllipsis && needsNewGradient) {
backgroundGradient = new FadedTextGradient(gradientEndRight, fadeWidth, backgroundColor);
fadePaint.setShader(backgroundGradient);
}
}
private static class FadedTextGradient extends LinearGradient {
private final int endRight;
private final int backgroundColor;
public FadedTextGradient(final int gradientEndRight, final int fadeWidth,
final int backgroundColor) {
super(gradientEndRight - fadeWidth, 0, gradientEndRight, 0,
getColorWithZeroedAlpha(backgroundColor), backgroundColor, Shader.TileMode.CLAMP);
this.endRight = gradientEndRight;
this.backgroundColor = backgroundColor;
}
private static int getColorWithZeroedAlpha(final int color) {
return Color.argb(0, Color.red(color), Color.green(color), Color.blue(color));
}
public int getEndRight() {
return endRight;
}
public int getBackgroundColor() {
return backgroundColor;
}
}
}

View file

@ -0,0 +1,74 @@
/* -*- 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.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Shader;
import android.util.AttributeSet;
/**
* Fades the end of the text by gecko:fadeWidth amount,
* if the text is too long and requires an ellipsis.
*
* This implementation is an improvement over Android's built-in fadingEdge
* and the fastest of Fennec's implementations. However, it only works for
* text of one color. It works by applying a linear gradient directly to the text.
*/
public class FadedSingleColorTextView extends FadedTextView {
// Shader for the fading edge.
private FadedTextGradient mTextGradient;
public FadedSingleColorTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
private void updateGradientShader() {
final int color = getCurrentTextColor();
final int width = getAvailableWidth();
final boolean needsNewGradient = (mTextGradient == null ||
mTextGradient.getColor() != color ||
mTextGradient.getWidth() != width);
final boolean needsEllipsis = needsEllipsis();
if (needsEllipsis && needsNewGradient) {
mTextGradient = new FadedTextGradient(width, fadeWidth, color);
}
getPaint().setShader(needsEllipsis ? mTextGradient : null);
}
@Override
public void onDraw(Canvas canvas) {
updateGradientShader();
super.onDraw(canvas);
}
private static class FadedTextGradient extends LinearGradient {
private final int mWidth;
private final int mColor;
public FadedTextGradient(int width, int fadeWidth, int color) {
super(0, 0, width, 0,
new int[] { color, color, 0x0 },
new float[] { 0, ((float) (width - fadeWidth) / width), 1.0f },
Shader.TileMode.CLAMP);
mWidth = width;
mColor = color;
}
public int getWidth() {
return mWidth;
}
public int getColor() {
return mColor;
}
}
}

View file

@ -0,0 +1,48 @@
/* -*- 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.content.res.TypedArray;
import android.text.Layout;
import android.util.AttributeSet;
import org.mozilla.gecko.R;
import org.mozilla.gecko.widget.themed.ThemedTextView;
/**
* An implementation of FadedTextView should fade the end of the text
* by gecko:fadeWidth amount, if the text is too long and requires an ellipsis.
*/
public abstract class FadedTextView extends ThemedTextView {
// Width of the fade effect from end of the view.
protected final int fadeWidth;
public FadedTextView(final Context context, final AttributeSet attrs) {
super(context, attrs);
setSingleLine(true);
setEllipsize(null);
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadedTextView);
fadeWidth = a.getDimensionPixelSize(R.styleable.FadedTextView_fadeWidth, 0);
a.recycle();
}
protected int getAvailableWidth() {
return getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
}
protected boolean needsEllipsis() {
final int width = getAvailableWidth();
if (width <= 0) {
return false;
}
final Layout layout = getLayout();
return (layout != null && layout.getLineWidth(0) > width);
}
}

View file

@ -0,0 +1,268 @@
/* -*- 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 org.mozilla.gecko.R;
import org.mozilla.gecko.icons.IconCallback;
import org.mozilla.gecko.icons.IconResponse;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.widget.ImageView;
import java.lang.ref.WeakReference;
/**
* Special version of ImageView for favicons.
* Displays solid colour background around Favicon to fill space not occupied by the icon. Colour
* selected is the dominant colour of the provided Favicon.
*/
public class FaviconView extends ImageView {
private static final String LOGTAG = "GeckoFaviconView";
private static String DEFAULT_FAVICON_KEY = FaviconView.class.getSimpleName() + "DefaultFavicon";
// Default x/y-radius of the oval used to round the corners of the background (dp)
private static final int DEFAULT_CORNER_RADIUS_DP = 4;
private Bitmap mIconBitmap;
// Reference to the unscaled bitmap, if any, to prevent repeated assignments of the same bitmap
// to the view from causing repeated rescalings (Some of the callers do this)
private Bitmap mUnscaledBitmap;
private int mActualWidth;
private int mActualHeight;
// Flag indicating if the most recently assigned image is considered likely to need scaling.
private boolean mScalingExpected;
// Dominant color of the favicon.
private int mDominantColor;
// Paint for drawing the background.
private static final Paint sBackgroundPaint;
// Size of the background rectangle.
private final RectF mBackgroundRect;
// The x/y-radius of the oval used to round the corners of the background (pixels)
private final float mBackgroundCornerRadius;
// Type of the border whose value is defined in attrs.xml .
private final boolean isDominantBorderEnabled;
// boolean switch for overriding scaletype, whose value is defined in attrs.xml .
private final boolean isOverrideScaleTypeEnabled;
// boolean switch for disabling rounded corners, value defined in attrs.xml .
private final boolean areRoundCornersEnabled;
// Initializing the static paints.
static {
sBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
sBackgroundPaint.setStyle(Paint.Style.FILL);
}
public FaviconView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.FaviconView, 0, 0);
try {
isDominantBorderEnabled = a.getBoolean(R.styleable.FaviconView_dominantBorderEnabled, true);
isOverrideScaleTypeEnabled = a.getBoolean(R.styleable.FaviconView_overrideScaleType, true);
areRoundCornersEnabled = a.getBoolean(R.styleable.FaviconView_enableRoundCorners, true);
} finally {
a.recycle();
}
if (isOverrideScaleTypeEnabled) {
setScaleType(ImageView.ScaleType.CENTER);
}
final DisplayMetrics metrics = getResources().getDisplayMetrics();
mBackgroundRect = new RectF(0, 0, 0, 0);
mBackgroundCornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_CORNER_RADIUS_DP, metrics);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// No point rechecking the image if there hasn't really been any change.
if (w == mActualWidth && h == mActualHeight) {
return;
}
mActualWidth = w;
mActualHeight = h;
mBackgroundRect.right = w;
mBackgroundRect.bottom = h;
formatImage();
}
@Override
public void onDraw(Canvas canvas) {
if (isDominantBorderEnabled) {
sBackgroundPaint.setColor(mDominantColor & 0x7FFFFFFF);
if (areRoundCornersEnabled) {
canvas.drawRoundRect(mBackgroundRect, mBackgroundCornerRadius, mBackgroundCornerRadius, sBackgroundPaint);
} else {
canvas.drawRect(mBackgroundRect, sBackgroundPaint);
}
}
super.onDraw(canvas);
}
/**
* Formats the image for display, if the prerequisite data are available. Upscales tiny Favicons to
* normal sized ones, replaces null bitmaps with the default Favicon, and fills all remaining space
* in this view with the coloured background.
*/
private void formatImage() {
// We're waiting for both onSizeChanged and updateImage to be called before scaling.
if (mIconBitmap == null || mActualWidth == 0 || mActualHeight == 0) {
showNoImage();
return;
}
if (mScalingExpected && mActualWidth != mIconBitmap.getWidth()) {
scaleBitmap();
// Don't scale the image every time something changes.
mScalingExpected = false;
}
setImageBitmap(mIconBitmap);
// After scaling, determine if we have empty space around the scaled image which we need to
// fill with the coloured background. If applicable, show it.
// We assume Favicons are still squares and only bother with the background if more than 3px
// of it would be displayed.
if (Math.abs(mIconBitmap.getWidth() - mActualWidth) < 3) {
mDominantColor = 0;
}
}
private void scaleBitmap() {
// If the Favicon can be resized to fill the view exactly without an enlargment of more than
// a factor of two, do so.
int doubledSize = mIconBitmap.getWidth() * 2;
if (mActualWidth > doubledSize) {
// If the view is more than twice the size of the image, just double the image size
// and do the rest with padding.
mIconBitmap = Bitmap.createScaledBitmap(mIconBitmap, doubledSize, doubledSize, true);
} else {
// Otherwise, scale the image to fill the view.
mIconBitmap = Bitmap.createScaledBitmap(mIconBitmap, mActualWidth, mActualWidth, true);
}
}
/**
* Sets the icon displayed in this Favicon view to the bitmap provided. If the size of the view
* has been set, the display will be updated right away, otherwise the update will be deferred
* until then. The key provided is used to cache the result of the calculation of the dominant
* colour of the provided image - this value is used to draw the coloured background in this view
* if the icon is not large enough to fill it.
*
* @param allowScaling If true, allows the provided bitmap to be scaled by this FaviconView.
* Typically, you should prefer using Favicons obtained via the caching system
* (Favicons class), so as to exploit caching.
*/
private void updateImageInternal(IconResponse response, boolean allowScaling) {
// Reassigning the same bitmap? Don't bother.
if (mUnscaledBitmap == response.getBitmap()) {
return;
}
mUnscaledBitmap = response.getBitmap();
mIconBitmap = response.getBitmap();
mDominantColor = response.getColor();
mScalingExpected = allowScaling;
// Possibly update the display.
formatImage();
}
private void showNoImage() {
setImageDrawable(null);
mDominantColor = 0;
}
/**
* Clear image and background shown by this view.
*/
public void clearImage() {
showNoImage();
mUnscaledBitmap = null;
mIconBitmap = null;
mDominantColor = 0;
mScalingExpected = false;
}
/**
* Update the displayed image and apply the scaling logic.
* The scaling logic will attempt to resize the image to fit correctly inside the view in a way
* that avoids unreasonable levels of loss of quality.
* Scaling is necessary only when the icon being provided is not drawn from the Favicon cache
* introduced in Bug 914296.
*
* Due to Bug 913746, icons bundled for search engines are not available to the cache, so must
* always have the scaling logic applied here. At the time of writing, this is the only case in
* which the scaling logic here is applied.
*/
public void updateAndScaleImage(IconResponse response) {
updateImageInternal(response, true);
}
/**
* Update the image displayed in the Favicon view without scaling. Images larger than the view
* will be centrally cropped. Images smaller than the view will be placed centrally and the
* extra space filled with the dominant colour of the provided image.
*/
public void updateImage(IconResponse response) {
updateImageInternal(response, false);
}
public Bitmap getBitmap() {
return mIconBitmap;
}
/**
* Create an IconCallback implementation that will update this view after an icon has been loaded.
*/
public IconCallback createIconCallback() {
return new Callback(this);
}
private static class Callback implements IconCallback {
private final WeakReference<FaviconView> viewReference;
private Callback(FaviconView view) {
this.viewReference = new WeakReference<FaviconView>(view);
}
@Override
public void onIconResponse(IconResponse response) {
final FaviconView view = viewReference.get();
if (view == null) {
return;
}
view.updateImage(response);
}
}
}

View file

@ -0,0 +1,39 @@
/* -*- 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.support.v7.widget.CardView;
import android.util.AttributeSet;
import org.mozilla.gecko.AppConstants;
/**
* CardView that ensures its content can fill the entire card. Use this instead of CardView
* if you want to fill the card with e.g. images, backgrounds, etc.
*
* On API < 21, CardView content isn't clipped for performance reasons. We work around this by disabling
* rounded corners on those devices.
*/
public class FilledCardView extends CardView {
public FilledCardView(Context context, AttributeSet attrs) {
super(context, attrs);
// Disable corners on < lollipop:
// CardView only supports clipping content on API >= 21 (for performance reasons). Without
// content clipping, any cards that provide their own content that fills the card will look
// ugly: by default there is a 2px white edge along the top and sides (i.e. an inset corresponding
// to the corner radius), if we disable the inset then the corners overlap.
// It's possible to implement custom clipping, however given that the support library
// chose not to support this for performance reasons, we too have chosen to just disable
// corners on < 21, see Bug 1271428.
if (AppConstants.Versions.preLollipop) {
setRadius(0);
}
setUseCompatPadding(true);
}
}

View file

@ -0,0 +1,91 @@
/* 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 org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
public class FlowLayout extends ViewGroup {
private int mSpacing;
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, org.mozilla.gecko.R.styleable.FlowLayout);
mSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_spacing, (int) context.getResources().getDimension(R.dimen.flow_layout_spacing));
a.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
final int childCount = getChildCount();
int rowWidth = 0;
int totalWidth = 0;
int totalHeight = 0;
boolean firstChild = true;
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE)
continue;
measureChild(child, widthMeasureSpec, heightMeasureSpec);
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
if (firstChild || (rowWidth + childWidth > parentWidth)) {
rowWidth = 0;
totalHeight += childHeight;
if (!firstChild)
totalHeight += mSpacing;
firstChild = false;
}
rowWidth += childWidth;
if (rowWidth > totalWidth)
totalWidth = rowWidth;
rowWidth += mSpacing;
}
setMeasuredDimension(totalWidth, totalHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int childCount = getChildCount();
final int totalWidth = r - l;
int x = 0;
int y = 0;
int prevChildHeight = 0;
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE)
continue;
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
if (x + childWidth > totalWidth) {
x = 0;
y += prevChildHeight + mSpacing;
}
prevChildHeight = childHeight;
child.layout(x, y, x + childWidth, y + childHeight);
x += childWidth + mSpacing;
}
}
}

View file

@ -0,0 +1,360 @@
/* -*- 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.app.Activity;
import android.net.Uri;
import android.support.design.widget.Snackbar;
import android.util.Base64;
import android.view.Menu;
import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.R;
import org.mozilla.gecko.SnackbarBuilder;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.overlays.ui.ShareDialog;
import org.mozilla.gecko.menu.MenuItemSwitcherLayout;
import org.mozilla.gecko.util.IOUtils;
import org.mozilla.gecko.util.IntentUtils;
import org.mozilla.gecko.util.ThreadUtils;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.SubMenu;
import android.view.View;
import android.view.View.OnClickListener;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import android.webkit.URLUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
public class GeckoActionProvider {
private static final int MAX_HISTORY_SIZE_DEFAULT = 2;
/**
* A listener to know when a target was selected.
* When setting a provider, the activity can listen to this,
* to close the menu.
*/
public interface OnTargetSelectedListener {
public void onTargetSelected();
}
final Context mContext;
public final static String DEFAULT_MIME_TYPE = "text/plain";
public static final String DEFAULT_HISTORY_FILE_NAME = "history.xml";
// History file.
String mHistoryFileName = DEFAULT_HISTORY_FILE_NAME;
OnTargetSelectedListener mOnTargetListener;
private final Callbacks mCallbacks = new Callbacks();
private static final HashMap<String, GeckoActionProvider> mProviders = new HashMap<String, GeckoActionProvider>();
private static String getFilenameFromMimeType(String mimeType) {
String[] mime = mimeType.split("/");
// All text mimetypes use the default provider
if ("text".equals(mime[0])) {
return DEFAULT_HISTORY_FILE_NAME;
}
return "history-" + mime[0] + ".xml";
}
// Gets the action provider for a particular mimetype
public static GeckoActionProvider getForType(String mimeType, Context context) {
if (!mProviders.keySet().contains(mimeType)) {
GeckoActionProvider provider = new GeckoActionProvider(context);
// For empty types, we just return a default provider
if (TextUtils.isEmpty(mimeType)) {
return provider;
}
provider.setHistoryFileName(getFilenameFromMimeType(mimeType));
mProviders.put(mimeType, provider);
}
return mProviders.get(mimeType);
}
public GeckoActionProvider(Context context) {
mContext = context;
}
/**
* Creates the action view using the default history size.
*/
public View onCreateActionView(final ActionViewType viewType) {
return onCreateActionView(MAX_HISTORY_SIZE_DEFAULT, viewType);
}
public View onCreateActionView(final int maxHistorySize, final ActionViewType viewType) {
// Create the view and set its data model.
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
final MenuItemSwitcherLayout view;
switch (viewType) {
case DEFAULT:
view = new MenuItemSwitcherLayout(mContext, null);
break;
case CONTEXT_MENU:
view = new MenuItemSwitcherLayout(mContext, null);
view.initContextMenuStyles();
break;
default:
throw new IllegalArgumentException(
"Unknown " + ActionViewType.class.getSimpleName() + ": " + viewType);
}
view.addActionButtonClickListener(mCallbacks);
final PackageManager packageManager = mContext.getPackageManager();
int historySize = dataModel.getDistinctActivityCountInHistory();
if (historySize > maxHistorySize) {
historySize = maxHistorySize;
}
// Historical data is dependent on past selection of activities.
// Activity count is determined by the number of activities that can handle
// the particular intent. When no intent is set, the activity count is 0,
// while the history count can be a valid number.
if (historySize > dataModel.getActivityCount()) {
return view;
}
for (int i = 0; i < historySize; i++) {
view.addActionButton(dataModel.getActivity(i).loadIcon(packageManager),
dataModel.getActivity(i).loadLabel(packageManager));
}
return view;
}
public boolean hasSubMenu() {
return true;
}
public void onPrepareSubMenu(SubMenu subMenu) {
// Clear since the order of items may change.
subMenu.clear();
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
PackageManager packageManager = mContext.getPackageManager();
// Populate the sub-menu with a sub set of the activities.
final String shareDialogClassName = ShareDialog.class.getCanonicalName();
final String sendTabLabel = mContext.getResources().getString(R.string.overlay_share_send_other);
final int count = dataModel.getActivityCount();
for (int i = 0; i < count; i++) {
ResolveInfo activity = dataModel.getActivity(i);
final CharSequence activityLabel = activity.loadLabel(packageManager);
// Pin internal actions to the top. Note:
// the order here does not affect quick share.
final int order;
if (shareDialogClassName.equals(activity.activityInfo.name) &&
sendTabLabel.equals(activityLabel)) {
order = Menu.FIRST + i;
} else {
order = Menu.FIRST + (i | Menu.CATEGORY_SECONDARY);
}
subMenu.add(0, i, order, activityLabel)
.setIcon(activity.loadIcon(packageManager))
.setOnMenuItemClickListener(mCallbacks);
}
}
public void setHistoryFileName(String historyFile) {
mHistoryFileName = historyFile;
}
public Intent getIntent() {
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
return dataModel.getIntent();
}
public void setIntent(Intent intent) {
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
dataModel.setIntent(intent);
// Inform the target listener to refresh it's UI, if needed.
if (mOnTargetListener != null) {
mOnTargetListener.onTargetSelected();
}
}
public void setOnTargetSelectedListener(OnTargetSelectedListener listener) {
mOnTargetListener = listener;
}
public ArrayList<ResolveInfo> getSortedActivities() {
ArrayList<ResolveInfo> infos = new ArrayList<ResolveInfo>();
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
// Populate the sub-menu with a sub set of the activities.
final int count = dataModel.getActivityCount();
for (int i = 0; i < count; i++) {
infos.add(dataModel.getActivity(i));
}
return infos;
}
public void chooseActivity(int position) {
mCallbacks.chooseActivity(position);
}
/**
* Listener for handling default activity / menu item clicks.
*/
private class Callbacks implements OnMenuItemClickListener,
OnClickListener {
void chooseActivity(int index) {
final ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
final Intent launchIntent = dataModel.chooseActivity(index);
if (launchIntent != null) {
// This may cause a download to happen. Make sure we're on the background thread.
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
// Share image downloads the image before sharing it.
String type = launchIntent.getType();
if (Intent.ACTION_SEND.equals(launchIntent.getAction()) && type != null && type.startsWith("image/")) {
downloadImageForIntent(launchIntent);
}
launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
mContext.startActivity(launchIntent);
}
});
}
if (mOnTargetListener != null) {
mOnTargetListener.onTargetSelected();
}
}
@Override
public boolean onMenuItemClick(MenuItem item) {
chooseActivity(item.getItemId());
// Context: Sharing via chrome mainmenu list (no explicit session is active)
Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "actionprovider");
return true;
}
@Override
public void onClick(View view) {
Integer index = (Integer) view.getTag();
chooseActivity(index);
// Context: Sharing via chrome mainmenu and content contextmenu quickshare (no explicit session is active)
Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.BUTTON, "actionprovider");
}
}
public enum ActionViewType {
DEFAULT,
CONTEXT_MENU,
}
/**
* Downloads the URI pointed to by a share intent, and alters the intent to point to the
* locally stored file.
*
* @param intent share intent to alter in place.
*/
public void downloadImageForIntent(final Intent intent) {
final String src = IntentUtils.getStringExtraSafe(intent, Intent.EXTRA_TEXT);
final File dir = GeckoApp.getTempDirectory();
if (src == null || dir == null) {
// We should be, but currently aren't, statically guaranteed an Activity context.
// Try our best.
if (mContext instanceof Activity) {
SnackbarBuilder.builder((Activity) mContext)
.message(mContext.getApplicationContext().getString(R.string.share_image_failed))
.duration(Snackbar.LENGTH_LONG)
.buildAndShow();
}
return;
}
GeckoApp.deleteTempFiles();
String type = intent.getType();
OutputStream os = null;
try {
// Create a temporary file for the image
if (src.startsWith("data:")) {
final int dataStart = src.indexOf(",");
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
// If we weren't given an explicit mimetype, try to dig one out of the data uri.
if (TextUtils.isEmpty(extension) && dataStart > 5) {
type = src.substring(5, dataStart).replace(";base64", "");
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
}
final File imageFile = File.createTempFile("image", "." + extension, dir);
os = new FileOutputStream(imageFile);
byte[] buf = Base64.decode(src.substring(dataStart + 1), Base64.DEFAULT);
os.write(buf);
// Only alter the intent when we're sure everything has worked
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile));
} else {
InputStream is = null;
try {
final byte[] buf = new byte[2048];
final URL url = new URL(src);
final String filename = URLUtil.guessFileName(src, null, type);
is = url.openStream();
final File imageFile = new File(dir, filename);
os = new FileOutputStream(imageFile);
int length;
while ((length = is.read(buf)) != -1) {
os.write(buf, 0, length);
}
// Only alter the intent when we're sure everything has worked
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile));
} finally {
IOUtils.safeStreamClose(is);
}
}
} catch (IOException ex) {
// If something went wrong, we'll just leave the intent un-changed
} finally {
IOUtils.safeStreamClose(os);
}
}
}

View file

@ -0,0 +1,189 @@
/* -*- 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 org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.GeckoMenuInflater;
import org.mozilla.gecko.menu.MenuPanel;
import org.mozilla.gecko.menu.MenuPopup;
import android.content.Context;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
/**
* A PopupMenu that uses the custom GeckoMenu. This menu is
* usually tied to an anchor, and show as a dropdrown from the anchor.
*/
public class GeckoPopupMenu implements GeckoMenu.Callback,
GeckoMenu.MenuPresenter {
// An interface for listeners for dismissal.
public static interface OnDismissListener {
public boolean onDismiss(GeckoMenu menu);
}
// An interface for listeners for menu item click events.
public static interface OnMenuItemClickListener {
public boolean onMenuItemClick(MenuItem item);
}
// An interface for listeners for menu item long click events.
public static interface OnMenuItemLongClickListener {
public boolean onMenuItemLongClick(MenuItem item);
}
private View mAnchor;
private MenuPopup mMenuPopup;
private MenuPanel mMenuPanel;
private GeckoMenu mMenu;
private GeckoMenuInflater mMenuInflater;
private OnDismissListener mDismissListener;
private OnMenuItemClickListener mClickListener;
private OnMenuItemLongClickListener mLongClickListener;
public GeckoPopupMenu(Context context) {
initialize(context, null);
}
public GeckoPopupMenu(Context context, View anchor) {
initialize(context, anchor);
}
/**
* This method creates an empty menu and attaches the necessary listeners.
* If an anchor is supplied, it is stored as well.
*/
private void initialize(Context context, View anchor) {
mMenu = new GeckoMenu(context, null);
mMenu.setCallback(this);
mMenu.setMenuPresenter(this);
mMenuInflater = new GeckoMenuInflater(context);
mMenuPopup = new MenuPopup(context);
mMenuPanel = new MenuPanel(context, null);
mMenuPanel.addView(mMenu);
mMenuPopup.setPanelView(mMenuPanel);
setAnchor(anchor);
}
/**
* Returns the menu that is current being shown.
*
* @return The menu being shown.
*/
public GeckoMenu getMenu() {
return mMenu;
}
/**
* Returns the menu inflater that was used to create the menu.
*
* @return The menu inflater used.
*/
public MenuInflater getMenuInflater() {
return mMenuInflater;
}
/**
* Inflates a menu resource to the menu using the menu inflater.
*
* @param menuRes The menu resource to be inflated.
*/
public void inflate(int menuRes) {
if (menuRes > 0) {
mMenuInflater.inflate(menuRes, mMenu);
}
}
/**
* Set a different anchor after the menu is inflated.
*
* @param anchor The new anchor for the popup.
*/
public void setAnchor(View anchor) {
mAnchor = anchor;
// Reposition the popup if the anchor changes while it's showing.
if (mMenuPopup.isShowing()) {
mMenuPopup.dismiss();
mMenuPopup.showAsDropDown(mAnchor);
}
}
public void setOnDismissListener(OnDismissListener listener) {
mDismissListener = listener;
}
public void setOnMenuItemClickListener(OnMenuItemClickListener listener) {
mClickListener = listener;
}
public void setOnMenuItemLongClickListener(OnMenuItemLongClickListener listener) {
mLongClickListener = listener;
}
/**
* Show the inflated menu.
*/
public void show() {
if (!mMenuPopup.isShowing())
mMenuPopup.showAsDropDown(mAnchor);
}
/**
* Hide the inflated menu.
*/
public void dismiss() {
if (mMenuPopup.isShowing()) {
mMenuPopup.dismiss();
if (mDismissListener != null)
mDismissListener.onDismiss(mMenu);
}
}
@Override
public boolean onMenuItemClick(MenuItem item) {
if (mClickListener != null) {
return mClickListener.onMenuItemClick(item);
}
return false;
}
@Override
public boolean onMenuItemLongClick(MenuItem item) {
if (mLongClickListener != null) {
return mLongClickListener.onMenuItemLongClick(item);
}
return false;
}
@Override
public void openMenu() {
show();
}
@Override
public void showMenu(View menu) {
mMenuPanel.removeAllViews();
mMenuPanel.addView(menu);
openMenu();
}
@Override
public void closeMenu() {
dismiss();
}
}

View file

@ -0,0 +1,66 @@
/* -*- 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.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import org.mozilla.gecko.R;
import org.mozilla.gecko.home.CombinedHistoryItem;
public class HistoryDividerItemDecoration extends RecyclerView.ItemDecoration {
private final int mDividerHeight;
private final Paint mDividerPaint;
public HistoryDividerItemDecoration(Context context) {
mDividerHeight = (int) context.getResources().getDimension(R.dimen.page_row_divider_height);
mDividerPaint = new Paint();
mDividerPaint.setColor(ContextCompat.getColor(context, R.color.toolbar_divider_grey));
mDividerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
final int position = parent.getChildAdapterPosition(view);
if (position == RecyclerView.NO_POSITION) {
// This view is no longer corresponds to an adapter position (pending changes).
return;
}
if (parent.getAdapter().getItemViewType(position) !=
CombinedHistoryItem.ItemType.itemTypeToViewType(CombinedHistoryItem.ItemType.SECTION_HEADER)) {
outRect.set(0, 0, 0, mDividerHeight);
}
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getChildCount() == 0) {
return;
}
for (int i = 0; i < parent.getChildCount(); i++) {
final View child = parent.getChildAt(i);
final int position = parent.getChildAdapterPosition(child);
if (position == RecyclerView.NO_POSITION) {
// This view is no longer corresponds to an adapter position (pending changes).
continue;
}
if (parent.getAdapter().getItemViewType(position) !=
CombinedHistoryItem.ItemType.itemTypeToViewType(CombinedHistoryItem.ItemType.SECTION_HEADER)) {
final float bottom = child.getBottom() + child.getTranslationY();
c.drawRect(0, bottom, parent.getWidth(), bottom + mDividerHeight, mDividerPaint);
}
}
}
}

View file

@ -0,0 +1,111 @@
/* 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 org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageButton;
import android.widget.TabWidget;
import android.widget.TextView;
public class IconTabWidget extends TabWidget {
OnTabChangedListener mListener;
private final int mButtonLayoutId;
private final boolean mIsIcon;
public static interface OnTabChangedListener {
public void onTabChanged(int tabIndex);
}
public IconTabWidget(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.IconTabWidget);
mButtonLayoutId = a.getResourceId(R.styleable.IconTabWidget_android_layout, 0);
mIsIcon = (a.getInt(R.styleable.IconTabWidget_display, 0x00) == 0x00);
a.recycle();
if (mButtonLayoutId == 0) {
throw new RuntimeException("You must supply layout attribute");
}
}
public View addTab(final int imageResId, final int stringResId) {
View button = LayoutInflater.from(getContext()).inflate(mButtonLayoutId, this, false);
if (mIsIcon) {
((ImageButton) button).setImageResource(imageResId);
button.setContentDescription(getContext().getString(stringResId));
} else {
((TextView) button).setText(getContext().getString(stringResId));
}
addView(button);
button.setOnClickListener(new TabClickListener(getTabCount() - 1));
button.setOnFocusChangeListener(this);
return button;
}
public void setTabSelectionListener(OnTabChangedListener listener) {
mListener = listener;
}
@Override
public void onFocusChange(View view, boolean hasFocus) {
}
private class TabClickListener implements OnClickListener {
private final int mIndex;
public TabClickListener(int index) {
mIndex = index;
}
@Override
public void onClick(View view) {
if (mListener != null)
mListener.onTabChanged(mIndex);
}
}
/**
* Fetch the Drawable icon corresponding to the given panel.
* @param panel to fetch icon for.
* @return Drawable instance, or null if no icon is being displayed, or the icon does not exist.
*/
public Drawable getIconDrawable(int index) {
if (!mIsIcon) {
return null;
}
// We can have multiple views in the tabs panel for each child. This finds the
// first view corresponding to the given tab. This varies by Android
// version. The first view should always be our ImageButton, but let's
// be safe.
final View view = getChildTabViewAt(index);
if (view instanceof ImageButton) {
return ((ImageButton) view).getDrawable();
}
return null;
}
public void setIconDrawable(int index, int resource) {
if (!mIsIcon) {
return;
}
// We can have multiple views in the tabs panel for each child. This finds the
// first view corresponding to the given tab. This varies by Android
// version. The first view should always be our ImageButton, but let's
// be safe.
final View view = getChildTabViewAt(index);
if (view instanceof ImageButton) {
((ImageButton) view).setImageResource(resource);
}
}
}

View file

@ -0,0 +1,228 @@
/* -*- 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.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.PasswordTransformationMethod;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import java.util.Locale;
public class LoginDoorHanger extends DoorHanger {
private static final String LOGTAG = "LoginDoorHanger";
private enum ActionType { EDIT, SELECT }
private final TextView mMessage;
private final DoorhangerConfig.ButtonConfig mButtonConfig;
public LoginDoorHanger(Context context, DoorhangerConfig config) {
super(context, config, Type.LOGIN);
mMessage = (TextView) findViewById(R.id.doorhanger_message);
mIcon.setImageResource(R.drawable.icon_key);
mIcon.setVisibility(View.VISIBLE);
mButtonConfig = config.getPositiveButtonConfig();
loadConfig(config);
}
private void setMessage(String message) {
Spanned markupMessage = Html.fromHtml(message);
mMessage.setText(markupMessage);
}
@Override
protected void loadConfig(DoorhangerConfig config) {
setOptions(config.getOptions());
setMessage(config.getMessage());
// Store the positive callback id for nested dialogs that need the same callback id.
addButtonsToLayout(config);
}
@Override
protected int getContentResource() {
return R.layout.login_doorhanger;
}
@Override
protected void setOptions(final JSONObject options) {
super.setOptions(options);
final JSONObject actionText = options.optJSONObject("actionText");
addActionText(actionText);
}
@Override
protected OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra) {
return new Button.OnClickListener() {
@Override
public void onClick(View v) {
final String expandedExtra = mType.toString().toLowerCase(Locale.US) + "-" + telemetryExtra;
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DOORHANGER, expandedExtra);
final JSONObject response = new JSONObject();
try {
response.put("callback", id);
} catch (JSONException e) {
Log.e(LOGTAG, "Error making doorhanger response message", e);
}
mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this);
}
};
}
/**
* Add sub-text to the doorhanger and add the click action.
*
* If the parsing the action from the JSON throws, the text is left visible, but there is no
* click action.
* @param actionTextObj JSONObject containing blob for making an action.
*/
private void addActionText(JSONObject actionTextObj) {
if (actionTextObj == null) {
mLink.setVisibility(View.GONE);
return;
}
// Make action.
try {
final JSONObject bundle = actionTextObj.getJSONObject("bundle");
final ActionType type = ActionType.valueOf(actionTextObj.getString("type"));
final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
switch (type) {
case EDIT:
builder.setTitle(mResources.getString(R.string.doorhanger_login_edit_title));
final View view = LayoutInflater.from(mContext).inflate(R.layout.login_edit_dialog, null);
final EditText username = (EditText) view.findViewById(R.id.username_edit);
username.setText(bundle.getString("username"));
final EditText password = (EditText) view.findViewById(R.id.password_edit);
password.setText(bundle.getString("password"));
final CheckBox passwordCheckbox = (CheckBox) view.findViewById(R.id.checkbox_toggle_password);
passwordCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
password.setTransformationMethod(null);
} else {
password.setTransformationMethod(PasswordTransformationMethod.getInstance());
}
}
});
builder.setView(view);
builder.setPositiveButton(mButtonConfig.label, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
JSONObject response = new JSONObject();
try {
response.put("callback", mButtonConfig.callback);
final JSONObject inputs = new JSONObject();
inputs.put("username", username.getText());
inputs.put("password", password.getText());
response.put("inputs", inputs);
} catch (JSONException e) {
Log.e(LOGTAG, "Error creating doorhanger reply message");
response = null;
Toast.makeText(mContext, mResources.getString(R.string.doorhanger_login_edit_toast_error), Toast.LENGTH_SHORT).show();
}
mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this);
}
});
builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
String text = actionTextObj.optString("text");
if (TextUtils.isEmpty(text)) {
text = mResources.getString(R.string.doorhanger_login_no_username);
}
mLink.setText(text);
mLink.setVisibility(View.VISIBLE);
break;
case SELECT:
try {
builder.setTitle(mResources.getString(R.string.doorhanger_login_select_title));
final JSONArray logins = bundle.getJSONArray("logins");
final int numLogins = logins.length();
final CharSequence[] usernames = new CharSequence[numLogins];
final String[] passwords = new String[numLogins];
final String noUser = mResources.getString(R.string.doorhanger_login_no_username);
for (int i = 0; i < numLogins; i++) {
final JSONObject login = (JSONObject) logins.get(i);
String user = login.getString("username");
if (TextUtils.isEmpty(user)) {
user = noUser;
}
usernames[i] = user;
passwords[i] = login.getString("password");
}
builder.setItems(usernames, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
final JSONObject response = new JSONObject();
try {
response.put("callback", mButtonConfig.callback);
response.put("password", passwords[which]);
} catch (JSONException e) {
Log.e(LOGTAG, "Error making login select dialog JSON", e);
}
mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this);
}
});
builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
mLink.setText(R.string.doorhanger_login_select_action_text);
mLink.setVisibility(View.VISIBLE);
} catch (JSONException e) {
Log.e(LOGTAG, "Problem creating list of logins");
}
break;
}
final Dialog dialog = builder.create();
mLink.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
dialog.show();
}
});
} catch (JSONException e) {
Log.e(LOGTAG, "Error fetching actionText from JSON", e);
}
}
}

View file

@ -0,0 +1,105 @@
/* -*- 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.support.v7.widget.RecyclerView;
import android.view.View;
import org.mozilla.gecko.R;
/**
* {@link RecyclerViewClickSupport} implementation that will notify an OnClickListener about clicks and long clicks
* on items displayed by the RecyclerView.
* @see <a href="http://www.littlerobots.nl/blog/Handle-Android-RecyclerView-Clicks/">littlerobots.nl</a>
*/
public class RecyclerViewClickSupport {
private final RecyclerView mRecyclerView;
private OnItemClickListener mOnItemClickListener;
private OnItemLongClickListener mOnItemLongClickListener;
private View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mOnItemClickListener != null) {
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
}
}
};
private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if (mOnItemLongClickListener != null) {
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
}
return false;
}
};
private RecyclerView.OnChildAttachStateChangeListener mAttachListener
= new RecyclerView.OnChildAttachStateChangeListener() {
@Override
public void onChildViewAttachedToWindow(View view) {
if (mOnItemClickListener != null) {
view.setOnClickListener(mOnClickListener);
}
if (mOnItemLongClickListener != null) {
view.setOnLongClickListener(mOnLongClickListener);
}
}
@Override
public void onChildViewDetachedFromWindow(View view) {
}
};
private RecyclerViewClickSupport(RecyclerView recyclerView) {
mRecyclerView = recyclerView;
mRecyclerView.setTag(R.id.recycler_view_click_support, this);
mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
}
public static RecyclerViewClickSupport addTo(RecyclerView view) {
RecyclerViewClickSupport support = (RecyclerViewClickSupport) view.getTag(R.id.recycler_view_click_support);
if (support == null) {
support = new RecyclerViewClickSupport(view);
}
return support;
}
public static RecyclerViewClickSupport removeFrom(RecyclerView view) {
RecyclerViewClickSupport support = (RecyclerViewClickSupport) view.getTag(R.id.recycler_view_click_support);
if (support != null) {
support.detach(view);
}
return support;
}
public RecyclerViewClickSupport setOnItemClickListener(OnItemClickListener listener) {
mOnItemClickListener = listener;
return this;
}
public RecyclerViewClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
mOnItemLongClickListener = listener;
return this;
}
private void detach(RecyclerView view) {
view.removeOnChildAttachStateChangeListener(mAttachListener);
view.setTag(R.id.recycler_view_click_support, null);
}
public interface OnItemClickListener {
void onItemClicked(RecyclerView recyclerView, int position, View v);
}
public interface OnItemLongClickListener {
boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
}
}

View file

@ -0,0 +1,117 @@
/* -*- 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.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.Shape;
public class ResizablePathDrawable extends ShapeDrawable {
// An attribute mirroring the super class' value. getAlpha() is only
// available in API 19+ so to use that alpha value, we have to mirror it.
private int alpha = 255;
private final ColorStateList colorStateList;
private int currentColor;
public ResizablePathDrawable(NonScaledPathShape shape, int color) {
this(shape, ColorStateList.valueOf(color));
}
public ResizablePathDrawable(NonScaledPathShape shape, ColorStateList colorStateList) {
super(shape);
this.colorStateList = colorStateList;
updateColor(getState());
}
private boolean updateColor(int[] stateSet) {
int newColor = colorStateList.getColorForState(stateSet, Color.WHITE);
if (newColor != currentColor) {
currentColor = newColor;
alpha = Color.alpha(currentColor);
invalidateSelf();
return true;
}
return false;
}
public Path getPath() {
final NonScaledPathShape shape = (NonScaledPathShape) getShape();
return shape.path;
}
@Override
public boolean isStateful() {
return true;
}
@Override
protected void onDraw(Shape shape, Canvas canvas, Paint paint) {
paint.setColor(currentColor);
// setAlpha overrides the alpha value in set color. Since we just set the color,
// the alpha value is reset: override the alpha value with the old value. We don't
// set alpha if the color is transparent.
//
// Note: We *should* be able to call Shape.setAlpha, rather than Paint.setAlpha, but
// then the opacity doesn't change - dunno why but probably not worth the time.
if (currentColor != Color.TRANSPARENT) {
paint.setAlpha(alpha);
}
super.onDraw(shape, canvas, paint);
}
@Override
public void setAlpha(final int alpha) {
super.setAlpha(alpha);
this.alpha = alpha;
}
@Override
protected boolean onStateChange(int[] stateSet) {
return updateColor(stateSet);
}
/**
* Path-based shape implementation that re-creates the path
* when it gets resized as opposed to PathShape's scaling
* behaviour.
*/
public static class NonScaledPathShape extends Shape {
private Path path;
public NonScaledPathShape() {
path = new Path();
}
@Override
public void draw(Canvas canvas, Paint paint) {
// No point in drawing the shape if it's not
// going to be visible.
if (paint.getColor() == Color.TRANSPARENT) {
return;
}
canvas.drawPath(path, paint);
}
protected Path getPath() {
return path;
}
@Override
public NonScaledPathShape clone() throws CloneNotSupportedException {
final NonScaledPathShape clonedShape = (NonScaledPathShape) super.clone();
clonedShape.path = new Path(path);
return clonedShape;
}
}
}

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 org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.R;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.widget.LinearLayout;
public class RoundedCornerLayout extends LinearLayout {
private static final String LOGTAG = "Gecko" + RoundedCornerLayout.class.getSimpleName();
private float cornerRadius;
private Path path;
boolean cannotClipPath;
public RoundedCornerLayout(Context context) {
super(context);
init(context);
}
public RoundedCornerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public RoundedCornerLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
// Bug 1201081 - clipPath with hardware acceleration crashes on r11-18.
cannotClipPath = !AppConstants.Versions.feature19Plus;
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
cornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
getResources().getDimensionPixelSize(R.dimen.doorhanger_rounded_corner_radius), metrics);
setWillNotDraw(false);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (cannotClipPath) {
return;
}
final RectF r = new RectF(0, 0, w, h);
path = new Path();
path.addRoundRect(r, cornerRadius, cornerRadius, Path.Direction.CW);
path.close();
}
@Override
public void draw(Canvas canvas) {
if (cannotClipPath) {
super.draw(canvas);
return;
}
canvas.save();
canvas.clipPath(path);
super.draw(canvas);
canvas.restore();
}
}

View file

@ -0,0 +1,16 @@
package org.mozilla.gecko.widget;
import org.json.JSONArray;
import org.json.JSONObject;
public class SiteLogins {
private final JSONArray logins;
public SiteLogins(JSONArray logins) {
this.logins = logins;
}
public JSONArray getLogins() {
return logins;
}
}

View file

@ -0,0 +1,21 @@
package org.mozilla.gecko.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.ImageView;
final class SquaredImageView extends ImageView {
public SquaredImageView(Context context) {
super(context);
}
public SquaredImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth());
}
}

View file

@ -0,0 +1,33 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.util.AttributeSet;
import android.widget.RelativeLayout;
public class SquaredRelativeLayout extends RelativeLayout {
public SquaredRelativeLayout(Context context) {
super(context);
}
public SquaredRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SquaredRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int squareMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
super.onMeasure(squareMeasureSpec, squareMeasureSpec);
}
}

View file

@ -0,0 +1,356 @@
/*
* Copyright 2012 Roman Nurik
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mozilla.gecko.widget;
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.RecyclerListener;
import android.widget.ListView;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.view.ViewPropertyAnimator;
import org.mozilla.gecko.R;
/**
* This code is based off of Jake Wharton's NOA port (https://github.com/JakeWharton/SwipeToDismissNOA)
* of Roman Nurik's SwipeToDismiss library. It has been modified for better support with async
* adapters.
*
* A {@link android.view.View.OnTouchListener} that makes the list items in a {@link ListView}
* dismissable. {@link ListView} is given special treatment because by default it handles touches
* for its list items... i.e. it's in charge of drawing the pressed state (the list selector),
* handling list item clicks, etc.
*
* <p>After creating the listener, the caller should also call
* {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}, passing
* in the scroll listener returned by {@link #makeScrollListener()}. If a scroll listener is
* already assigned, the caller should still pass scroll changes through to this listener. This will
* ensure that this {@link SwipeDismissListViewTouchListener} is paused during list view
* scrolling.</p>
*
* <p>Example usage:</p>
*
* <pre>
* SwipeDismissListViewTouchListener touchListener =
* new SwipeDismissListViewTouchListener(
* listView,
* new SwipeDismissListViewTouchListener.OnDismissCallback() {
* public void onDismiss(ListView listView, int[] reverseSortedPositions) {
* for (int position : reverseSortedPositions) {
* adapter.remove(adapter.getItem(position));
* }
* adapter.notifyDataSetChanged();
* }
* });
* listView.setOnTouchListener(touchListener);
* listView.setOnScrollListener(touchListener.makeScrollListener());
* </pre>
*
* <p>For a generalized {@link android.view.View.OnTouchListener} that makes any view dismissable,
* see {@link SwipeDismissTouchListener}.</p>
*
* @see SwipeDismissTouchListener
*/
public class SwipeDismissListViewTouchListener implements View.OnTouchListener {
// Cached ViewConfiguration and system-wide constant values
private final int mSlop;
private final int mMinFlingVelocity;
private final int mMaxFlingVelocity;
private final long mAnimationTime;
// Fixed properties
private final ListView mListView;
private final OnDismissCallback mCallback;
private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero
// Transient properties
private float mDownX;
private boolean mSwiping;
private VelocityTracker mVelocityTracker;
private int mDownPosition;
private View mDownView;
private boolean mPaused;
private boolean mDismissing;
/**
* The callback interface used by {@link SwipeDismissListViewTouchListener} to inform its client
* about a successful dismissal of a list item.
*/
public interface OnDismissCallback {
/**
* Called when the user has indicated they she would like to dismiss one or more list item
* positions.
*
* @param listView The originating {@link ListView}.
* @param position The position being dismissed.
*/
void onDismiss(ListView listView, int position);
}
/**
* Constructs a new swipe-to-dismiss touch listener for the given list view.
*
* @param listView The list view whose items should be dismissable.
* @param callback The callback to trigger when the user has indicated that she would like to
* dismiss one or more list items.
*/
public SwipeDismissListViewTouchListener(ListView listView, OnDismissCallback callback) {
ViewConfiguration vc = ViewConfiguration.get(listView.getContext());
mSlop = vc.getScaledTouchSlop();
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
mAnimationTime = listView.getContext().getResources().getInteger(
android.R.integer.config_shortAnimTime);
mListView = listView;
mCallback = callback;
}
/**
* Enables or disables (pauses or resumes) watching for swipe-to-dismiss gestures.
*
* @param enabled Whether or not to watch for gestures.
*/
public void setEnabled(boolean enabled) {
mPaused = !enabled;
}
/**
* Returns an {@link android.widget.AbsListView.OnScrollListener} to be added to the
* {@link ListView} using
* {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}.
* If a scroll listener is already assigned, the caller should still pass scroll changes
* through to this listener. This will ensure that this
* {@link SwipeDismissListViewTouchListener} is paused during list view scrolling.</p>
*
* @see {@link SwipeDismissListViewTouchListener}
*/
public AbsListView.OnScrollListener makeScrollListener() {
return new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView absListView, int scrollState) {
setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
}
@Override
public void onScroll(AbsListView absListView, int i, int i1, int i2) {
}
};
}
/**
* Returns a {@link android.widget.AbsListView.RecyclerListener} to be added to the
* {@link ListView} using {@link ListView#setRecyclerListener(RecyclerListener)}.
*/
public AbsListView.RecyclerListener makeRecyclerListener() {
return new AbsListView.RecyclerListener() {
@Override
public void onMovedToScrapHeap(View view) {
final Object tag = view.getTag(R.id.original_height);
// To reset the view to the correct height after its animation, the view's height
// is stored in its tag. Reset the view here.
if (tag instanceof Integer) {
view.setAlpha(1f);
view.setTranslationX(0);
final ViewGroup.LayoutParams lp = view.getLayoutParams();
lp.height = (int) tag;
view.setLayoutParams(lp);
view.setTag(R.id.original_height, null);
}
}
};
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (mViewWidth < 2) {
mViewWidth = mListView.getWidth();
}
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
if (mPaused) {
return false;
}
if (mDismissing) {
return true;
}
// TODO: ensure this is a finger, and set a flag
// Find the child view that was touched (perform a hit test)
Rect rect = new Rect();
int childCount = mListView.getChildCount();
int[] listViewCoords = new int[2];
mListView.getLocationOnScreen(listViewCoords);
int x = (int) motionEvent.getRawX() - listViewCoords[0];
int y = (int) motionEvent.getRawY() - listViewCoords[1];
View child;
for (int i = 0; i < childCount; i++) {
child = mListView.getChildAt(i);
child.getHitRect(rect);
if (rect.contains(x, y)) {
mDownView = child;
break;
}
}
if (mDownView != null) {
mDownX = motionEvent.getRawX();
mDownPosition = mListView.getPositionForView(mDownView);
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(motionEvent);
}
view.onTouchEvent(motionEvent);
return true;
}
case MotionEvent.ACTION_UP: {
if (mVelocityTracker == null) {
break;
}
float deltaX = motionEvent.getRawX() - mDownX;
mVelocityTracker.addMovement(motionEvent);
mVelocityTracker.computeCurrentVelocity(1000);
float velocityX = Math.abs(mVelocityTracker.getXVelocity());
float velocityY = Math.abs(mVelocityTracker.getYVelocity());
boolean dismiss = false;
boolean dismissRight = false;
if (Math.abs(deltaX) > mViewWidth / 2) {
dismiss = true;
dismissRight = deltaX > 0;
} else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
&& velocityY < velocityX) {
dismiss = true;
dismissRight = mVelocityTracker.getXVelocity() > 0;
}
if (dismiss) {
// dismiss
mDismissing = true;
final View downView = mDownView; // mDownView gets null'd before animation ends
final int downPosition = mDownPosition;
mDownView.animate()
.translationX(dismissRight ? mViewWidth : -mViewWidth)
.alpha(0)
.setDuration(mAnimationTime)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
performDismiss(downView, downPosition);
}
});
} else {
// cancel
mDownView.animate()
.translationX(0)
.alpha(1)
.setDuration(mAnimationTime)
.setListener(null);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mDownX = 0;
mDownView = null;
mDownPosition = ListView.INVALID_POSITION;
mSwiping = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (mVelocityTracker == null || mPaused) {
break;
}
mVelocityTracker.addMovement(motionEvent);
float deltaX = motionEvent.getRawX() - mDownX;
if (Math.abs(deltaX) > mSlop) {
mSwiping = true;
mListView.requestDisallowInterceptTouchEvent(true);
// Cancel ListView's touch (un-highlighting the item)
MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
(motionEvent.getActionIndex()
<< MotionEvent.ACTION_POINTER_INDEX_SHIFT));
mListView.onTouchEvent(cancelEvent);
cancelEvent.recycle();
}
if (mSwiping) {
mDownView.setTranslationX(deltaX);
mDownView.setAlpha(Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX) / mViewWidth)));
return true;
}
break;
}
}
return false;
}
/**
* Animate the dismissed list item to zero-height and fire the dismiss callback when it finishes.
*
* @param dismissView ListView item to dismiss
* @param dismissPosition Position of dismissed item
*/
private void performDismiss(final View dismissView, final int dismissPosition) {
final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();
final int originalHeight = lp.height;
ValueAnimator animator = ValueAnimator.ofInt(dismissView.getHeight(), 1).setDuration(mAnimationTime);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Since the view is still a part of the ListView, we can't reset the animated
// properties yet; otherwise, the view would briefly reappear. Store the original
// height in the view's tag to flag it for the recycler. This is racy since the user
// could scroll the dismissed view off the screen, then back on the screen, before
// it's removed from the adapter, causing the dismissed view to briefly reappear.
dismissView.setTag(R.id.original_height, originalHeight);
mCallback.onDismiss(mListView, dismissPosition);
mDismissing = false;
}
});
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
lp.height = (Integer) valueAnimator.getAnimatedValue();
dismissView.setLayoutParams(lp);
}
});
animator.start();
}
}

View file

@ -0,0 +1,38 @@
package org.mozilla.gecko.widget;
import android.content.Context;
import android.util.AttributeSet;
import org.mozilla.gecko.R;
import org.mozilla.gecko.widget.themed.ThemedRelativeLayout;
public class TabThumbnailWrapper extends ThemedRelativeLayout {
private boolean mRecording;
private static final int[] STATE_RECORDING = { R.attr.state_recording };
public TabThumbnailWrapper(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public TabThumbnailWrapper(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (mRecording) {
mergeDrawableStates(drawableState, STATE_RECORDING);
}
return drawableState;
}
public void setRecording(boolean recording) {
if (mRecording != recording) {
mRecording = recording;
refreshDrawableState();
}
}
}

View file

@ -0,0 +1,86 @@
/* -*- 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 org.mozilla.gecko.R;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.widget.themed.ThemedImageView;
/* Special version of ImageView for thumbnails. Scales a thumbnail so that it maintains its aspect
* ratio and so that the images width and height are the same size or greater than the view size
*/
public class ThumbnailView extends ThemedImageView {
private static final String LOGTAG = "GeckoThumbnailView";
final private Matrix mMatrix;
private int mWidthSpec = -1;
private int mHeightSpec = -1;
private boolean mLayoutChanged;
private boolean mScale = false;
public ThumbnailView(Context context, AttributeSet attrs) {
super(context, attrs);
mMatrix = new Matrix();
mLayoutChanged = true;
}
@Override
public void onDraw(Canvas canvas) {
if (!mScale) {
super.onDraw(canvas);
return;
}
Drawable d = getDrawable();
if (mLayoutChanged) {
int w1 = d.getIntrinsicWidth();
int h1 = d.getIntrinsicHeight();
int w2 = getWidth();
int h2 = getHeight();
float scale = ((w2 / h2) < (w1 / h1)) ? (float) h2 / h1 : (float) w2 / w1;
mMatrix.setScale(scale, scale);
}
int saveCount = canvas.save();
canvas.concat(mMatrix);
d.draw(canvas);
canvas.restoreToCount(saveCount);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// OnLayout.changed isn't a reliable measure of whether or not the size of this view has changed
// neither is onSizeChanged called often enough. Instead, we track changes in size ourselves, and
// only invalidate this matrix if we have a new width/height spec
if (widthMeasureSpec != mWidthSpec || heightMeasureSpec != mHeightSpec) {
mWidthSpec = widthMeasureSpec;
mHeightSpec = heightMeasureSpec;
mLayoutChanged = true;
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public void setImageDrawable(Drawable drawable) {
if (drawable == null) {
drawable = ContextCompat.getDrawable(getContext(), R.drawable.tab_panel_tab_background);
setScaleType(ScaleType.FIT_XY);
mScale = false;
} else {
mScale = true;
setScaleType(ScaleType.FIT_CENTER);
}
super.setImageDrawable(drawable);
}
}

View file

@ -0,0 +1,134 @@
/* 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.graphics.Rect;
import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewConfiguration;
/**
* This is a copy of TouchDelegate from
* https://github.com/android/platform_frameworks_base/blob/4b1a8f46d6ec55796bf77fd8921a5a242a219278/core/java/android/view/TouchDelegate.java
* with a fix to reset mDelegateTargeted on each new gesture - the sole substantive change is a new
* else leg in the ACTION_DOWN case of onTouchEvent marked by "START|END BUG FIX" comments.
*/
/**
* Helper class to handle situations where you want a view to have a larger touch area than its
* actual view bounds. The view whose touch area is changed is called the delegate view. This
* class should be used by an ancestor of the delegate. To use a TouchDelegate, first create an
* instance that specifies the bounds that should be mapped to the delegate and the delegate
* view itself.
* <p>
* The ancestor should then forward all of its touch events received in its
* {@link android.view.View#onTouchEvent(MotionEvent)} to {@link #onTouchEvent(MotionEvent)}.
* </p>
*/
public class TouchDelegateWithReset extends TouchDelegate {
/**
* View that should receive forwarded touch events
*/
private View mDelegateView;
/**
* Bounds in local coordinates of the containing view that should be mapped to the delegate
* view. This rect is used for initial hit testing.
*/
private Rect mBounds;
/**
* mBounds inflated to include some slop. This rect is to track whether the motion events
* should be considered to be be within the delegate view.
*/
private Rect mSlopBounds;
/**
* True if the delegate had been targeted on a down event (intersected mBounds).
*/
private boolean mDelegateTargeted;
private int mSlop;
/**
* Constructor
*
* @param bounds Bounds in local coordinates of the containing view that should be mapped to
* the delegate view
* @param delegateView The view that should receive motion events
*/
public TouchDelegateWithReset(Rect bounds, View delegateView) {
super(bounds, delegateView);
mBounds = bounds;
mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
mSlopBounds = new Rect(bounds);
mSlopBounds.inset(-mSlop, -mSlop);
mDelegateView = delegateView;
}
/**
* Will forward touch events to the delegate view if the event is within the bounds
* specified in the constructor.
*
* @param event The touch event to forward
* @return True if the event was forwarded to the delegate, false otherwise.
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int)event.getX();
int y = (int)event.getY();
boolean sendToDelegate = false;
boolean hit = true;
boolean handled = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Rect bounds = mBounds;
if (bounds.contains(x, y)) {
mDelegateTargeted = true;
sendToDelegate = true;
} /* START BUG FIX */
else {
mDelegateTargeted = false;
}
/* END BUG FIX */
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_MOVE:
sendToDelegate = mDelegateTargeted;
if (sendToDelegate) {
Rect slopBounds = mSlopBounds;
if (!slopBounds.contains(x, y)) {
hit = false;
}
}
break;
case MotionEvent.ACTION_CANCEL:
sendToDelegate = mDelegateTargeted;
mDelegateTargeted = false;
break;
}
if (sendToDelegate) {
final View delegateView = mDelegateView;
if (hit) {
// Offset event coordinates to be inside the target view
event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2);
} else {
// Offset event coordinates to be outside the target view (in case it does
// something like tracking pressed state)
int slop = mSlop;
event.setLocation(-(slop * 2), -(slop * 2));
}
handled = delegateView.dispatchTouchEvent(event);
}
return handled;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,172 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.themed;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.DrawableUtil;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
public class ThemedEditText extends android.widget.EditText
implements LightweightTheme.OnChangeListener {
private LightweightTheme theme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean isPrivate;
private boolean isLight;
private boolean isDark;
private boolean autoUpdateTheme; // always false if there's no theme.
private ColorStateList drawableColors;
public ThemedEditText(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs, 0);
}
public ThemedEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(context, attrs, defStyle);
}
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
// The theme can be null, particularly if we might be instantiating this
// View in an IDE, with no ambient GeckoApplication.
final Context applicationContext = context.getApplicationContext();
if (applicationContext instanceof GeckoApplication) {
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
}
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (autoUpdateTheme)
theme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (autoUpdateTheme)
theme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (isLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (isDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (autoUpdateTheme && theme.isEnabled())
setTheme(theme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (autoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return isPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (this.isPrivate != isPrivate) {
this.isPrivate = isPrivate;
refreshDrawableState();
invalidate();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && this.isLight != isLight) ||
(!isLight && this.isDark == isLight)) {
if (isLight) {
this.isLight = true;
this.isDark = false;
} else {
this.isLight = false;
this.isDark = true;
}
refreshDrawableState();
invalidate();
}
}
public void resetTheme() {
if (isLight || isDark) {
isLight = false;
isDark = false;
refreshDrawableState();
invalidate();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (theme == null) {
return;
}
if (this.autoUpdateTheme != autoUpdateTheme) {
this.autoUpdateTheme = autoUpdateTheme;
if (autoUpdateTheme)
theme.addListener(this);
else
theme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
}
protected LightweightTheme getTheme() {
return theme;
}
}

View file

@ -0,0 +1,172 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.themed;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.DrawableUtil;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
public class ThemedFrameLayout extends android.widget.FrameLayout
implements LightweightTheme.OnChangeListener {
private LightweightTheme theme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean isPrivate;
private boolean isLight;
private boolean isDark;
private boolean autoUpdateTheme; // always false if there's no theme.
private ColorStateList drawableColors;
public ThemedFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs, 0);
}
public ThemedFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(context, attrs, defStyle);
}
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
// The theme can be null, particularly if we might be instantiating this
// View in an IDE, with no ambient GeckoApplication.
final Context applicationContext = context.getApplicationContext();
if (applicationContext instanceof GeckoApplication) {
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
}
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (autoUpdateTheme)
theme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (autoUpdateTheme)
theme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (isLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (isDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (autoUpdateTheme && theme.isEnabled())
setTheme(theme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (autoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return isPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (this.isPrivate != isPrivate) {
this.isPrivate = isPrivate;
refreshDrawableState();
invalidate();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && this.isLight != isLight) ||
(!isLight && this.isDark == isLight)) {
if (isLight) {
this.isLight = true;
this.isDark = false;
} else {
this.isLight = false;
this.isDark = true;
}
refreshDrawableState();
invalidate();
}
}
public void resetTheme() {
if (isLight || isDark) {
isLight = false;
isDark = false;
refreshDrawableState();
invalidate();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (theme == null) {
return;
}
if (this.autoUpdateTheme != autoUpdateTheme) {
this.autoUpdateTheme = autoUpdateTheme;
if (autoUpdateTheme)
theme.addListener(this);
else
theme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
}
protected LightweightTheme getTheme() {
return theme;
}
}

View file

@ -0,0 +1,200 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.themed;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.DrawableUtil;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
public class ThemedImageButton extends android.widget.ImageButton
implements LightweightTheme.OnChangeListener {
private LightweightTheme theme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean isPrivate;
private boolean isLight;
private boolean isDark;
private boolean autoUpdateTheme; // always false if there's no theme.
private ColorStateList drawableColors;
public ThemedImageButton(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs, 0);
}
public ThemedImageButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(context, attrs, defStyle);
}
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
// The theme can be null, particularly if we might be instantiating this
// View in an IDE, with no ambient GeckoApplication.
final Context applicationContext = context.getApplicationContext();
if (applicationContext instanceof GeckoApplication) {
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
}
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
final TypedArray themedA = context.obtainStyledAttributes(attrs, R.styleable.ThemedView, defStyle, 0);
drawableColors = themedA.getColorStateList(R.styleable.ThemedView_drawableTintList);
themedA.recycle();
// Apply the tint initially - the Drawable is
// initially set by XML via super's constructor.
setTintedImageDrawable(getDrawable());
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (autoUpdateTheme)
theme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (autoUpdateTheme)
theme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (isLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (isDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (autoUpdateTheme && theme.isEnabled())
setTheme(theme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (autoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return isPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (this.isPrivate != isPrivate) {
this.isPrivate = isPrivate;
refreshDrawableState();
invalidate();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && this.isLight != isLight) ||
(!isLight && this.isDark == isLight)) {
if (isLight) {
this.isLight = true;
this.isDark = false;
} else {
this.isLight = false;
this.isDark = true;
}
refreshDrawableState();
invalidate();
}
}
public void resetTheme() {
if (isLight || isDark) {
isLight = false;
isDark = false;
refreshDrawableState();
invalidate();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (theme == null) {
return;
}
if (this.autoUpdateTheme != autoUpdateTheme) {
this.autoUpdateTheme = autoUpdateTheme;
if (autoUpdateTheme)
theme.addListener(this);
else
theme.removeListener(this);
}
}
@Override
public void setImageDrawable(final Drawable drawable) {
setTintedImageDrawable(drawable);
}
private void setTintedImageDrawable(final Drawable drawable) {
final Drawable tintedDrawable;
if (drawableColors == null || R.id.bookmark == getId()) {
// NB: The bookmarked state uses a blue star, so this is a hack to keep it untinted.
// NB: If we tint a drawable with a null ColorStateList, it will override
// any existing colorFilters and tint... so don't!
tintedDrawable = drawable;
} else if (drawable == null) {
tintedDrawable = null;
} else {
tintedDrawable = DrawableUtil.tintDrawableWithStateList(drawable, drawableColors);
}
super.setImageDrawable(tintedDrawable);
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
}
protected LightweightTheme getTheme() {
return theme;
}
}

View file

@ -0,0 +1,199 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.themed;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.DrawableUtil;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
public class ThemedImageView extends android.widget.ImageView
implements LightweightTheme.OnChangeListener {
private LightweightTheme theme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean isPrivate;
private boolean isLight;
private boolean isDark;
private boolean autoUpdateTheme; // always false if there's no theme.
private ColorStateList drawableColors;
public ThemedImageView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs, 0);
}
public ThemedImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(context, attrs, defStyle);
}
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
// The theme can be null, particularly if we might be instantiating this
// View in an IDE, with no ambient GeckoApplication.
final Context applicationContext = context.getApplicationContext();
if (applicationContext instanceof GeckoApplication) {
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
}
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
final TypedArray themedA = context.obtainStyledAttributes(attrs, R.styleable.ThemedView, defStyle, 0);
drawableColors = themedA.getColorStateList(R.styleable.ThemedView_drawableTintList);
themedA.recycle();
// Apply the tint initially - the Drawable is
// initially set by XML via super's constructor.
setTintedImageDrawable(getDrawable());
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (autoUpdateTheme)
theme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (autoUpdateTheme)
theme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (isLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (isDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (autoUpdateTheme && theme.isEnabled())
setTheme(theme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (autoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return isPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (this.isPrivate != isPrivate) {
this.isPrivate = isPrivate;
refreshDrawableState();
invalidate();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && this.isLight != isLight) ||
(!isLight && this.isDark == isLight)) {
if (isLight) {
this.isLight = true;
this.isDark = false;
} else {
this.isLight = false;
this.isDark = true;
}
refreshDrawableState();
invalidate();
}
}
public void resetTheme() {
if (isLight || isDark) {
isLight = false;
isDark = false;
refreshDrawableState();
invalidate();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (theme == null) {
return;
}
if (this.autoUpdateTheme != autoUpdateTheme) {
this.autoUpdateTheme = autoUpdateTheme;
if (autoUpdateTheme)
theme.addListener(this);
else
theme.removeListener(this);
}
}
@Override
public void setImageDrawable(final Drawable drawable) {
setTintedImageDrawable(drawable);
}
private void setTintedImageDrawable(final Drawable drawable) {
final Drawable tintedDrawable;
if (drawableColors == null) {
// NB: If we tint a drawable with a null ColorStateList, it will override
// any existing colorFilters and tint... so don't!
tintedDrawable = drawable;
} else if (drawable == null) {
tintedDrawable = null;
} else {
tintedDrawable = DrawableUtil.tintDrawableWithStateList(drawable, drawableColors);
}
super.setImageDrawable(tintedDrawable);
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
}
protected LightweightTheme getTheme() {
return theme;
}
}

View file

@ -0,0 +1,167 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.themed;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.DrawableUtil;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
public class ThemedLinearLayout extends android.widget.LinearLayout
implements LightweightTheme.OnChangeListener {
private LightweightTheme theme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean isPrivate;
private boolean isLight;
private boolean isDark;
private boolean autoUpdateTheme; // always false if there's no theme.
private ColorStateList drawableColors;
public ThemedLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs, 0);
}
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
// The theme can be null, particularly if we might be instantiating this
// View in an IDE, with no ambient GeckoApplication.
final Context applicationContext = context.getApplicationContext();
if (applicationContext instanceof GeckoApplication) {
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
}
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (autoUpdateTheme)
theme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (autoUpdateTheme)
theme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (isLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (isDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (autoUpdateTheme && theme.isEnabled())
setTheme(theme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (autoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return isPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (this.isPrivate != isPrivate) {
this.isPrivate = isPrivate;
refreshDrawableState();
invalidate();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && this.isLight != isLight) ||
(!isLight && this.isDark == isLight)) {
if (isLight) {
this.isLight = true;
this.isDark = false;
} else {
this.isLight = false;
this.isDark = true;
}
refreshDrawableState();
invalidate();
}
}
public void resetTheme() {
if (isLight || isDark) {
isLight = false;
isDark = false;
refreshDrawableState();
invalidate();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (theme == null) {
return;
}
if (this.autoUpdateTheme != autoUpdateTheme) {
this.autoUpdateTheme = autoUpdateTheme;
if (autoUpdateTheme)
theme.addListener(this);
else
theme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
}
protected LightweightTheme getTheme() {
return theme;
}
}

View file

@ -0,0 +1,172 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.themed;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.DrawableUtil;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
public class ThemedRelativeLayout extends android.widget.RelativeLayout
implements LightweightTheme.OnChangeListener {
private LightweightTheme theme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean isPrivate;
private boolean isLight;
private boolean isDark;
private boolean autoUpdateTheme; // always false if there's no theme.
private ColorStateList drawableColors;
public ThemedRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs, 0);
}
public ThemedRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(context, attrs, defStyle);
}
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
// The theme can be null, particularly if we might be instantiating this
// View in an IDE, with no ambient GeckoApplication.
final Context applicationContext = context.getApplicationContext();
if (applicationContext instanceof GeckoApplication) {
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
}
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (autoUpdateTheme)
theme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (autoUpdateTheme)
theme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (isLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (isDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (autoUpdateTheme && theme.isEnabled())
setTheme(theme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (autoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return isPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (this.isPrivate != isPrivate) {
this.isPrivate = isPrivate;
refreshDrawableState();
invalidate();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && this.isLight != isLight) ||
(!isLight && this.isDark == isLight)) {
if (isLight) {
this.isLight = true;
this.isDark = false;
} else {
this.isLight = false;
this.isDark = true;
}
refreshDrawableState();
invalidate();
}
}
public void resetTheme() {
if (isLight || isDark) {
isLight = false;
isDark = false;
refreshDrawableState();
invalidate();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (theme == null) {
return;
}
if (this.autoUpdateTheme != autoUpdateTheme) {
this.autoUpdateTheme = autoUpdateTheme;
if (autoUpdateTheme)
theme.addListener(this);
else
theme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
}
protected LightweightTheme getTheme() {
return theme;
}
}

View file

@ -0,0 +1,167 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.themed;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.DrawableUtil;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
public class ThemedTextSwitcher extends android.widget.TextSwitcher
implements LightweightTheme.OnChangeListener {
private LightweightTheme theme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean isPrivate;
private boolean isLight;
private boolean isDark;
private boolean autoUpdateTheme; // always false if there's no theme.
private ColorStateList drawableColors;
public ThemedTextSwitcher(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs, 0);
}
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
// The theme can be null, particularly if we might be instantiating this
// View in an IDE, with no ambient GeckoApplication.
final Context applicationContext = context.getApplicationContext();
if (applicationContext instanceof GeckoApplication) {
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
}
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (autoUpdateTheme)
theme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (autoUpdateTheme)
theme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (isLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (isDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (autoUpdateTheme && theme.isEnabled())
setTheme(theme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (autoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return isPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (this.isPrivate != isPrivate) {
this.isPrivate = isPrivate;
refreshDrawableState();
invalidate();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && this.isLight != isLight) ||
(!isLight && this.isDark == isLight)) {
if (isLight) {
this.isLight = true;
this.isDark = false;
} else {
this.isLight = false;
this.isDark = true;
}
refreshDrawableState();
invalidate();
}
}
public void resetTheme() {
if (isLight || isDark) {
isLight = false;
isDark = false;
refreshDrawableState();
invalidate();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (theme == null) {
return;
}
if (this.autoUpdateTheme != autoUpdateTheme) {
this.autoUpdateTheme = autoUpdateTheme;
if (autoUpdateTheme)
theme.addListener(this);
else
theme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
}
protected LightweightTheme getTheme() {
return theme;
}
}

View file

@ -0,0 +1,172 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.themed;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.DrawableUtil;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
public class ThemedTextView extends android.widget.TextView
implements LightweightTheme.OnChangeListener {
private LightweightTheme theme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean isPrivate;
private boolean isLight;
private boolean isDark;
private boolean autoUpdateTheme; // always false if there's no theme.
private ColorStateList drawableColors;
public ThemedTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs, 0);
}
public ThemedTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(context, attrs, defStyle);
}
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
// The theme can be null, particularly if we might be instantiating this
// View in an IDE, with no ambient GeckoApplication.
final Context applicationContext = context.getApplicationContext();
if (applicationContext instanceof GeckoApplication) {
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
}
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (autoUpdateTheme)
theme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (autoUpdateTheme)
theme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (isLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (isDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (autoUpdateTheme && theme.isEnabled())
setTheme(theme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (autoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return isPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (this.isPrivate != isPrivate) {
this.isPrivate = isPrivate;
refreshDrawableState();
invalidate();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && this.isLight != isLight) ||
(!isLight && this.isDark == isLight)) {
if (isLight) {
this.isLight = true;
this.isDark = false;
} else {
this.isLight = false;
this.isDark = true;
}
refreshDrawableState();
invalidate();
}
}
public void resetTheme() {
if (isLight || isDark) {
isLight = false;
isDark = false;
refreshDrawableState();
invalidate();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (theme == null) {
return;
}
if (this.autoUpdateTheme != autoUpdateTheme) {
this.autoUpdateTheme = autoUpdateTheme;
if (autoUpdateTheme)
theme.addListener(this);
else
theme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
}
protected LightweightTheme getTheme() {
return theme;
}
}

View file

@ -0,0 +1,172 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.themed;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.DrawableUtil;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
public class ThemedView extends android.view.View
implements LightweightTheme.OnChangeListener {
private LightweightTheme theme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean isPrivate;
private boolean isLight;
private boolean isDark;
private boolean autoUpdateTheme; // always false if there's no theme.
private ColorStateList drawableColors;
public ThemedView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs, 0);
}
public ThemedView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(context, attrs, defStyle);
}
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
// The theme can be null, particularly if we might be instantiating this
// View in an IDE, with no ambient GeckoApplication.
final Context applicationContext = context.getApplicationContext();
if (applicationContext instanceof GeckoApplication) {
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
}
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (autoUpdateTheme)
theme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (autoUpdateTheme)
theme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (isLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (isDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (autoUpdateTheme && theme.isEnabled())
setTheme(theme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (autoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return isPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (this.isPrivate != isPrivate) {
this.isPrivate = isPrivate;
refreshDrawableState();
invalidate();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && this.isLight != isLight) ||
(!isLight && this.isDark == isLight)) {
if (isLight) {
this.isLight = true;
this.isDark = false;
} else {
this.isLight = false;
this.isDark = true;
}
refreshDrawableState();
invalidate();
}
}
public void resetTheme() {
if (isLight || isDark) {
isLight = false;
isDark = false;
refreshDrawableState();
invalidate();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (theme == null) {
return;
}
if (this.autoUpdateTheme != autoUpdateTheme) {
this.autoUpdateTheme = autoUpdateTheme;
if (autoUpdateTheme)
theme.addListener(this);
else
theme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
}
protected LightweightTheme getTheme() {
return theme;
}
}

View file

@ -0,0 +1,211 @@
//#filter substitution
// This file is generated by generate_themed_views.py; do not edit.
/* 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.themed;
import android.support.v4.content.ContextCompat;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.DrawableUtil;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
public class Themed@VIEW_NAME_SUFFIX@ extends @BASE_TYPE@
implements LightweightTheme.OnChangeListener {
private LightweightTheme theme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean isPrivate;
private boolean isLight;
private boolean isDark;
private boolean autoUpdateTheme; // always false if there's no theme.
private ColorStateList drawableColors;
public Themed@VIEW_NAME_SUFFIX@(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs, 0);
}
//#ifdef STYLE_CONSTRUCTOR
public Themed@VIEW_NAME_SUFFIX@(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(context, attrs, defStyle);
}
//#endif
private void initialize(final Context context, final AttributeSet attrs, final int defStyle) {
// The theme can be null, particularly if we might be instantiating this
// View in an IDE, with no ambient GeckoApplication.
final Context applicationContext = context.getApplicationContext();
if (applicationContext instanceof GeckoApplication) {
theme = ((GeckoApplication) applicationContext).getLightweightTheme();
}
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
autoUpdateTheme = theme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
//#if TINT_FOREGROUND_DRAWABLE
final TypedArray themedA = context.obtainStyledAttributes(attrs, R.styleable.ThemedView, defStyle, 0);
drawableColors = themedA.getColorStateList(R.styleable.ThemedView_drawableTintList);
themedA.recycle();
// Apply the tint initially - the Drawable is
// initially set by XML via super's constructor.
setTintedImageDrawable(getDrawable());
//#endif
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (autoUpdateTheme)
theme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (autoUpdateTheme)
theme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (isLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (isDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (autoUpdateTheme && theme.isEnabled())
setTheme(theme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (autoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return isPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (this.isPrivate != isPrivate) {
this.isPrivate = isPrivate;
refreshDrawableState();
invalidate();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && this.isLight != isLight) ||
(!isLight && this.isDark == isLight)) {
if (isLight) {
this.isLight = true;
this.isDark = false;
} else {
this.isLight = false;
this.isDark = true;
}
refreshDrawableState();
invalidate();
}
}
public void resetTheme() {
if (isLight || isDark) {
isLight = false;
isDark = false;
refreshDrawableState();
invalidate();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (theme == null) {
return;
}
if (this.autoUpdateTheme != autoUpdateTheme) {
this.autoUpdateTheme = autoUpdateTheme;
if (autoUpdateTheme)
theme.addListener(this);
else
theme.removeListener(this);
}
}
//#ifdef TINT_FOREGROUND_DRAWABLE
@Override
public void setImageDrawable(final Drawable drawable) {
setTintedImageDrawable(drawable);
}
private void setTintedImageDrawable(final Drawable drawable) {
final Drawable tintedDrawable;
//#ifdef BOOKMARK_NO_TINT
if (drawableColors == null || R.id.bookmark == getId()) {
// NB: The bookmarked state uses a blue star, so this is a hack to keep it untinted.
//#else
if (drawableColors == null) {
//#endif
// NB: If we tint a drawable with a null ColorStateList, it will override
// any existing colorFilters and tint... so don't!
tintedDrawable = drawable;
} else if (drawable == null) {
tintedDrawable = null;
} else {
tintedDrawable = DrawableUtil.tintDrawableWithStateList(drawable, drawableColors);
}
super.setImageDrawable(tintedDrawable);
}
//#endif
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(ContextCompat.getColor(getContext(), id));
}
protected LightweightTheme getTheme() {
return theme;
}
}

View file

@ -0,0 +1,72 @@
#!/bin/python
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# 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/.
'''
Script to generate Themed*.java source files for Fennec.
This script runs the preprocessor on a input template and writes
updated files into the source directory.
To update the themed views, update the input template
(ThemedView.java.frag) and run the script using 'mach python <script.py>'. Use version control to
examine the differences, and don't forget to commit the changes to the
template and the outputs.
'''
from __future__ import (
print_function,
unicode_literals,
)
import os
from mozbuild.preprocessor import Preprocessor
__DIR__ = os.path.dirname(os.path.abspath(__file__))
template = os.path.join(__DIR__, 'ThemedView.java.frag')
dest_format_string = 'Themed%(VIEW_NAME_SUFFIX)s.java'
views = [
dict(VIEW_NAME_SUFFIX='EditText',
BASE_TYPE='android.widget.EditText',
STYLE_CONSTRUCTOR=1),
dict(VIEW_NAME_SUFFIX='FrameLayout',
BASE_TYPE='android.widget.FrameLayout',
STYLE_CONSTRUCTOR=1),
dict(VIEW_NAME_SUFFIX='ImageButton',
BASE_TYPE='android.widget.ImageButton',
STYLE_CONSTRUCTOR=1,
TINT_FOREGROUND_DRAWABLE=1,
BOOKMARK_NO_TINT=1),
dict(VIEW_NAME_SUFFIX='ImageView',
BASE_TYPE='android.widget.ImageView',
STYLE_CONSTRUCTOR=1,
TINT_FOREGROUND_DRAWABLE=1),
dict(VIEW_NAME_SUFFIX='LinearLayout',
BASE_TYPE='android.widget.LinearLayout'),
dict(VIEW_NAME_SUFFIX='RelativeLayout',
BASE_TYPE='android.widget.RelativeLayout',
STYLE_CONSTRUCTOR=1),
dict(VIEW_NAME_SUFFIX='TextSwitcher',
BASE_TYPE='android.widget.TextSwitcher'),
dict(VIEW_NAME_SUFFIX='TextView',
BASE_TYPE='android.widget.TextView',
STYLE_CONSTRUCTOR=1),
dict(VIEW_NAME_SUFFIX='View',
BASE_TYPE='android.view.View',
STYLE_CONSTRUCTOR=1),
]
for view in views:
pp = Preprocessor(defines=view, marker='//#')
dest = os.path.join(__DIR__, dest_format_string % view)
with open(template, 'rU') as input:
with open(dest, 'wt') as output:
pp.processFile(input=input, output=output)
print('%s' % dest)