Merge Android Pie into master

Bug: 112104996
Change-Id: I127e4e586242f7bb6011cf96a81ea87c2de6acdf
This commit is contained in:
Xin Li 2018-08-06 16:50:57 -07:00
commit da231a3b6a
88 changed files with 2115 additions and 787 deletions

4
OWNERS Normal file
View file

@ -0,0 +1,4 @@
russellbrenner@google.com
ajayns@google.com
iofir@google.com
yukl@google.com

View file

@ -15,6 +15,7 @@ LOCAL_RESOURCE_DIR := \
$(LOCAL_PATH)/platform/res
LOCAL_SDK_VERSION := current
LOCAL_SRC_FILES := $(call all-java-files-under, main/src platform/src)
LOCAL_MIN_SDK_VERSION := 23
include $(BUILD_STATIC_JAVA_LIBRARY)
@ -48,4 +49,6 @@ LOCAL_SHARED_ANDROID_LIBRARIES := \
android-support-v7-appcompat \
android-support-v7-recyclerview
LOCAL_MIN_SDK_VERSION := 14
include $(BUILD_STATIC_JAVA_LIBRARY)

View file

@ -1,3 +1,4 @@
# DEPRECATED: This variant is no longer maintained. Use common-gingerbread instead
#
# Include this make file to build your application against this module.
#
@ -9,7 +10,7 @@
# LOCAL_RESOURCE_DIR := \
# $(LOCAL_PATH)/res
#
# include frameworks/opt/setupwizard/library/common.mk
# include frameworks/opt/setupwizard/library/common-platform-deprecated.mk
#
# Path to directory of setup wizard lib (e.g. frameworks/opt/setupwizard/library)

View file

@ -20,7 +20,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:descendantFocusability="blocksDescendants"
android:orientation="horizontal"
android:tag="noBackground">

View file

@ -20,7 +20,7 @@
style="@style/SuwItemContainer.Verbose"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants"
android:clipToPadding="false"
android:orientation="horizontal">
<FrameLayout

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 Google Inc.
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.
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Not needed for dark theme, as default nav bar bg color is black. We need a separate style
override here since windowLightNavigationBar is new in v27, and these two styles need to be
applied together as a unit. -->
<style name="SuwThemeGlifV3.Light" parent="SuwBaseThemeGlifV3.Light">
<item name="android:navigationBarColor">@color/suw_glif_v3_nav_bar_color_light</item>
<!-- Ignore NewApi: For some reason lint seems to think this API is new in v28 (b/73514594) -->
<item name="android:navigationBarDividerColor" tools:ignore="NewApi">@color/suw_glif_v3_nav_bar_divider_color_light</item>
<!-- Ignore NewApi: For some reason lint seems to think this API is new in v28 (b/73514594) -->
<item name="android:windowLightNavigationBar" tools:ignore="NewApi">true</item>
</style>
</resources>

View file

@ -20,6 +20,7 @@
<!-- General styles -->
<style name="SuwThemeMaterial" parent="Theme.AppCompat.NoActionBar">
<item name="android:colorBackground">@color/suw_color_background_dark</item>
<item name="android:indeterminateTint" tools:ignore="NewApi">@color/suw_progress_bar_color_dark</item>
<!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
<item name="android:indeterminateTintMode" tools:ignore="NewApi">src_in</item>
@ -38,6 +39,8 @@
<item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
<item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
<item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
<item name="suwButtonAllCaps">true</item>
<item name="suwButtonFontFamily">sans-serif</item>
<item name="suwCardBackground">@drawable/suw_card_bg_dark</item>
<item name="suwDividerInsetEnd">0dp</item>
<item name="suwDividerInsetStart">@dimen/suw_items_icon_divider_inset</item>
@ -51,6 +54,7 @@
</style>
<style name="SuwThemeMaterial.Light" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:colorBackground">@color/suw_color_background_light</item>
<item name="android:indeterminateTint" tools:ignore="NewApi">@color/suw_progress_bar_color_light</item>
<!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
<item name="android:indeterminateTintMode" tools:ignore="NewApi">src_in</item>
@ -69,6 +73,8 @@
<item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
<item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
<item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
<item name="suwButtonAllCaps">true</item>
<item name="suwButtonFontFamily">sans-serif</item>
<item name="suwCardBackground">@drawable/suw_card_bg_light</item>
<item name="suwDividerInsetEnd">0dp</item>
<item name="suwDividerInsetStart">@dimen/suw_items_icon_divider_inset</item>
@ -98,15 +104,19 @@
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="colorAccent">@color/suw_color_accent_glif_dark</item>
<item name="colorPrimary">@color/suw_color_accent_glif_dark</item>
<item name="colorPrimary">?attr/colorAccent</item>
<item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
<item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
<item name="suwButtonAllCaps">true</item>
<item name="suwButtonCornerRadius">@dimen/suw_glif_button_corner_radius</item>
<item name="suwButtonFontFamily">sans-serif</item>
<item name="suwColorPrimary">?attr/colorPrimary</item>
<item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
<item name="suwDividerInsetEnd">0dp</item>
<item name="suwDividerInsetStart">@dimen/suw_items_glif_icon_divider_inset</item>
<item name="suwDividerInsetStartNoIcon">@dimen/suw_items_glif_text_divider_inset</item>
<item name="suwGlifHeaderGravity">start</item>
<item name="suwGlifIconStyle">@style/SuwGlifIcon</item>
<item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
<item name="suwItemDescriptionTitleStyle">@style/SuwItemTitle.GlifDescription</item>
<item name="suwListItemIconColor">@color/suw_list_item_icon_color_dark</item>
@ -133,15 +143,19 @@
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="colorAccent">@color/suw_color_accent_glif_light</item>
<item name="colorPrimary">@color/suw_color_accent_glif_light</item>
<item name="colorPrimary">?attr/colorAccent</item>
<item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
<item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
<item name="suwButtonAllCaps">true</item>
<item name="suwButtonCornerRadius">@dimen/suw_glif_button_corner_radius</item>
<item name="suwButtonFontFamily">sans-serif</item>
<item name="suwColorPrimary">?attr/colorPrimary</item>
<item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
<item name="suwDividerInsetEnd">0dp</item>
<item name="suwDividerInsetStart">@dimen/suw_items_glif_icon_divider_inset</item>
<item name="suwDividerInsetStartNoIcon">@dimen/suw_items_glif_text_divider_inset</item>
<item name="suwGlifHeaderGravity">start</item>
<item name="suwGlifIconStyle">@style/SuwGlifIcon</item>
<item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
<item name="suwItemDescriptionTitleStyle">@style/SuwItemTitle.GlifDescription</item>
<item name="suwListItemIconColor">@color/suw_list_item_icon_color_light</item>
@ -151,6 +165,21 @@
<item name="textAppearanceListItemSmall">@style/TextAppearance.SuwGlifItemSummary</item>
</style>
<style name="SuwThemeGlifV3" parent="SuwThemeGlifV2">
<item name="colorAccent">@color/suw_color_accent_glif_v3</item>
<item name="suwButtonAllCaps">false</item>
<item name="suwButtonCornerRadius">@dimen/suw_glif_v3_button_corner_radius</item>
<item name="suwButtonFontFamily">@string/suwFontSecondaryMedium</item>
</style>
<style name="SuwBaseThemeGlifV3.Light" parent="SuwThemeGlifV2.Light">
<item name="colorAccent">@color/suw_color_accent_glif_v3</item>
<item name="suwButtonAllCaps">false</item>
<item name="suwButtonCornerRadius">@dimen/suw_glif_v3_button_corner_radius</item>
<item name="suwButtonFontFamily">@string/suwFontSecondaryMedium</item>
</style>
<style name="SuwThemeGlifV3.Light" parent="SuwBaseThemeGlifV3.Light" />
<!-- Content styles -->
<style name="TextAppearance.SuwDescription" parent="TextAppearance.AppCompat.Medium">
@ -197,11 +226,18 @@
ContextThemeWrapper. These self-referencing attributes make sure this is applied as
both to the button. -->
<item name="android:buttonStyle">@style/SuwGlifButton.Primary</item>
<item name="android:theme">@style/SuwGlifButton.Primary</item>
<item name="buttonStyle">@style/SuwGlifButton.Primary</item>
<!-- Values used in styles -->
<item name="android:fontFamily" tools:targetApi="jelly_bean">?attr/suwButtonFontFamily</item>
<item name="android:paddingLeft">@dimen/suw_glif_button_padding</item>
<item name="android:paddingRight">@dimen/suw_glif_button_padding</item>
<item name="android:textAllCaps" tools:targetApi="ice_cream_sandwich">?attr/suwButtonAllCaps</item>
<item name="textAllCaps">?attr/suwButtonAllCaps</item>
<!-- Values used in themes -->
<item name="android:buttonCornerRadius" tools:ignore="NewApi">?attr/suwButtonCornerRadius</item>
</style>
<style name="SuwGlifButton.Secondary" parent="Widget.AppCompat.Button.Borderless.Colored">
@ -213,11 +249,15 @@
<item name="buttonStyle">@style/SuwGlifButton.Secondary</item>
<!-- Values used in styles -->
<item name="android:fontFamily" tools:targetApi="jelly_bean">?attr/suwButtonFontFamily</item>
<item name="android:minWidth">0dp</item>
<item name="android:paddingLeft">@dimen/suw_glif_button_padding</item>
<item name="android:paddingRight">@dimen/suw_glif_button_padding</item>
<item name="android:textAllCaps" tools:targetApi="ice_cream_sandwich">?attr/suwButtonAllCaps</item>
<item name="textAllCaps">?attr/suwButtonAllCaps</item>
<!-- Values used in themes -->
<item name="android:buttonCornerRadius" tools:ignore="NewApi">?attr/suwButtonCornerRadius</item>
<item name="android:colorControlHighlight" tools:targetApi="lollipop">@color/suw_flat_button_highlight</item>
<item name="colorControlHighlight">@color/suw_flat_button_highlight</item>
</style>
@ -234,6 +274,13 @@
<item name="android:background">?attr/colorPrimary</item>
</style>
<style name="SuwBase.ProgressBarLarge" parent="@android:style/Widget.ProgressBar.Large" />
<style name="SuwFourColorIndeterminateProgressBar" parent="SuwBase.ProgressBarLarge">
<item name="android:layout_gravity">center</item>
<item name="android:indeterminate">true</item>
</style>
<!-- Navigation bar styles -->
<style name="SuwNavBarButtonStyle" parent="@android:style/Widget.Button">
@ -258,4 +305,8 @@
<item name="suwNavBarButtonBackground">@drawable/suw_navbar_btn_bg_light</item>
</style>
<style name="SuwAlertDialogTheme" parent="Theme.AppCompat.Dialog.Alert" />
<style name="SuwAlertDialogTheme.Light" parent="Theme.AppCompat.Light.Dialog.Alert" />
</resources>

View file

@ -138,6 +138,10 @@ public class ExpandableSwitchItem extends SwitchItem
}
tintCompoundDrawables(view);
// Expandable switch item has focusability on the expandable layout on the left, and the
// switch on the right, but not the item itself.
view.setFocusable(false);
}
@Override

View file

@ -19,6 +19,8 @@ package com.android.setupwizardlib.util;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
@ -38,12 +40,11 @@ import java.util.List;
/**
* An accessibility delegate that allows {@link android.text.style.ClickableSpan} to be focused and
* clicked by accessibility services.
* <p>
* <strong>Note: </strong> From Android O on, there is native support for ClickableSpan
* accessibility, so this class is not needed (and indeed has no effect.)
* </p>
*
* <p />Sample usage:
* <p><strong>Note:</strong> This class is a no-op on Android O or above since there is native
* support for ClickableSpan accessibility.
*
* <p>Sample usage:
* <pre>
* LinkAccessibilityHelper mAccessibilityHelper;
*
@ -68,294 +69,260 @@ public class LinkAccessibilityHelper extends AccessibilityDelegateCompat {
private static final String TAG = "LinkAccessibilityHelper";
private final TextView mView;
private final Rect mTempRect = new Rect();
private final ExploreByTouchHelper mExploreByTouchHelper;
private final AccessibilityDelegateCompat mDelegate;
public LinkAccessibilityHelper(TextView view) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
// Pre-O, we essentially extend ExploreByTouchHelper to expose a virtual view hierarchy
mExploreByTouchHelper = new ExploreByTouchHelper(view) {
@Override
protected int getVirtualViewAt(float x, float y) {
return LinkAccessibilityHelper.this.getVirtualViewAt(x, y);
}
this(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
// Platform support was added in O. This helper will be no-op
? new AccessibilityDelegateCompat()
// Pre-O, we extend ExploreByTouchHelper to expose a virtual view hierarchy
: new PreOLinkAccessibilityHelper(view));
}
@Override
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
LinkAccessibilityHelper.this.getVisibleVirtualViews(virtualViewIds);
}
@Override
protected void onPopulateEventForVirtualView(int virtualViewId,
AccessibilityEvent event) {
LinkAccessibilityHelper
.this.onPopulateEventForVirtualView(virtualViewId, event);
}
@Override
protected void onPopulateNodeForVirtualView(int virtualViewId,
AccessibilityNodeInfoCompat infoCompat) {
LinkAccessibilityHelper
.this.onPopulateNodeForVirtualView(virtualViewId, infoCompat);
}
@Override
protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
Bundle arguments) {
return LinkAccessibilityHelper.this
.onPerformActionForVirtualView(virtualViewId, action, arguments);
}
};
} else {
mExploreByTouchHelper = null;
}
mView = view;
@VisibleForTesting
LinkAccessibilityHelper(@NonNull AccessibilityDelegateCompat delegate) {
mDelegate = delegate;
}
@Override
public void sendAccessibilityEvent(View host, int eventType) {
if (mExploreByTouchHelper != null) {
mExploreByTouchHelper.sendAccessibilityEvent(host, eventType);
} else {
super.sendAccessibilityEvent(host, eventType);
}
mDelegate.sendAccessibilityEvent(host, eventType);
}
@Override
public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) {
if (mExploreByTouchHelper != null) {
mExploreByTouchHelper.sendAccessibilityEventUnchecked(host, event);
} else {
super.sendAccessibilityEventUnchecked(host, event);
}
mDelegate.sendAccessibilityEventUnchecked(host, event);
}
@Override
public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
return (mExploreByTouchHelper != null)
? mExploreByTouchHelper.dispatchPopulateAccessibilityEvent(host, event)
: super.dispatchPopulateAccessibilityEvent(host, event);
return mDelegate.dispatchPopulateAccessibilityEvent(host, event);
}
@Override
public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
if (mExploreByTouchHelper != null) {
mExploreByTouchHelper.onPopulateAccessibilityEvent(host, event);
} else {
super.onPopulateAccessibilityEvent(host, event);
}
mDelegate.onPopulateAccessibilityEvent(host, event);
}
@Override
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
if (mExploreByTouchHelper != null) {
mExploreByTouchHelper.onInitializeAccessibilityEvent(host, event);
} else {
super.onInitializeAccessibilityEvent(host, event);
}
mDelegate.onInitializeAccessibilityEvent(host, event);
}
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
if (mExploreByTouchHelper != null) {
mExploreByTouchHelper.onInitializeAccessibilityNodeInfo(host, info);
} else {
super.onInitializeAccessibilityNodeInfo(host, info);
}
mDelegate.onInitializeAccessibilityNodeInfo(host, info);
}
@Override
public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
AccessibilityEvent event) {
return (mExploreByTouchHelper != null)
? mExploreByTouchHelper.onRequestSendAccessibilityEvent(host, child, event)
: super.onRequestSendAccessibilityEvent(host, child, event);
return mDelegate.onRequestSendAccessibilityEvent(host, child, event);
}
@Override
public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) {
return (mExploreByTouchHelper != null)
? mExploreByTouchHelper.getAccessibilityNodeProvider(host)
: super.getAccessibilityNodeProvider(host);
return mDelegate.getAccessibilityNodeProvider(host);
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
return (mExploreByTouchHelper != null)
? mExploreByTouchHelper.performAccessibilityAction(host, action, args)
: super.performAccessibilityAction(host, action, args);
return mDelegate.performAccessibilityAction(host, action, args);
}
/**
* Delegated to {@link ExploreByTouchHelper}
* Dispatches hover event to the virtual view hierarchy. This method should be called in
* {@link View#dispatchHoverEvent(MotionEvent)}.
*
* @see ExploreByTouchHelper#dispatchHoverEvent(MotionEvent)
*/
public final boolean dispatchHoverEvent(MotionEvent event) {
return (mExploreByTouchHelper != null) ? mExploreByTouchHelper.dispatchHoverEvent(event)
: false;
return mDelegate instanceof ExploreByTouchHelper
&& ((ExploreByTouchHelper) mDelegate).dispatchHoverEvent(event);
}
protected int getVirtualViewAt(float x, float y) {
final CharSequence text = mView.getText();
if (text instanceof Spanned) {
final Spanned spannedText = (Spanned) text;
final int offset = getOffsetForPosition(mView, x, y);
ClickableSpan[] linkSpans = spannedText.getSpans(offset, offset, ClickableSpan.class);
if (linkSpans.length == 1) {
ClickableSpan linkSpan = linkSpans[0];
return spannedText.getSpanStart(linkSpan);
@VisibleForTesting
static class PreOLinkAccessibilityHelper extends ExploreByTouchHelper {
private final Rect mTempRect = new Rect();
private final TextView mView;
PreOLinkAccessibilityHelper(TextView view) {
super(view);
mView = view;
}
@Override
protected int getVirtualViewAt(float x, float y) {
final CharSequence text = mView.getText();
if (text instanceof Spanned) {
final Spanned spannedText = (Spanned) text;
final int offset = getOffsetForPosition(mView, x, y);
ClickableSpan[] linkSpans =
spannedText.getSpans(offset, offset, ClickableSpan.class);
if (linkSpans.length == 1) {
ClickableSpan linkSpan = linkSpans[0];
return spannedText.getSpanStart(linkSpan);
}
}
return ExploreByTouchHelper.INVALID_ID;
}
@Override
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
final CharSequence text = mView.getText();
if (text instanceof Spanned) {
final Spanned spannedText = (Spanned) text;
ClickableSpan[] linkSpans = spannedText.getSpans(0, spannedText.length(),
ClickableSpan.class);
for (ClickableSpan span : linkSpans) {
virtualViewIds.add(spannedText.getSpanStart(span));
}
}
}
return ExploreByTouchHelper.INVALID_ID;
}
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
final CharSequence text = mView.getText();
if (text instanceof Spanned) {
final Spanned spannedText = (Spanned) text;
ClickableSpan[] linkSpans = spannedText.getSpans(0, spannedText.length(),
ClickableSpan.class);
for (ClickableSpan span : linkSpans) {
virtualViewIds.add(spannedText.getSpanStart(span));
}
}
}
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
final ClickableSpan span = getSpanForOffset(virtualViewId);
if (span != null) {
event.setContentDescription(getTextForSpan(span));
} else {
Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
event.setContentDescription(mView.getText());
}
}
protected void onPopulateNodeForVirtualView(int virtualViewId,
AccessibilityNodeInfoCompat info) {
final ClickableSpan span = getSpanForOffset(virtualViewId);
if (span != null) {
info.setContentDescription(getTextForSpan(span));
} else {
Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
info.setContentDescription(mView.getText());
}
info.setFocusable(true);
info.setClickable(true);
getBoundsForSpan(span, mTempRect);
if (mTempRect.isEmpty()) {
Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId);
mTempRect.set(0, 0, 1, 1);
}
info.setBoundsInParent(mTempRect);
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
}
protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
Bundle arguments) {
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
ClickableSpan span = getSpanForOffset(virtualViewId);
@Override
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
final ClickableSpan span = getSpanForOffset(virtualViewId);
if (span != null) {
span.onClick(mView);
return true;
event.setContentDescription(getTextForSpan(span));
} else {
Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
event.setContentDescription(mView.getText());
}
}
return false;
}
private ClickableSpan getSpanForOffset(int offset) {
CharSequence text = mView.getText();
if (text instanceof Spanned) {
Spanned spannedText = (Spanned) text;
ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class);
if (spans.length == 1) {
return spans[0];
@Override
protected void onPopulateNodeForVirtualView(
int virtualViewId,
AccessibilityNodeInfoCompat info) {
final ClickableSpan span = getSpanForOffset(virtualViewId);
if (span != null) {
info.setContentDescription(getTextForSpan(span));
} else {
Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
info.setContentDescription(mView.getText());
}
info.setFocusable(true);
info.setClickable(true);
getBoundsForSpan(span, mTempRect);
if (mTempRect.isEmpty()) {
Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId);
mTempRect.set(0, 0, 1, 1);
}
info.setBoundsInParent(mTempRect);
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
}
return null;
}
private CharSequence getTextForSpan(ClickableSpan span) {
CharSequence text = mView.getText();
if (text instanceof Spanned) {
Spanned spannedText = (Spanned) text;
return spannedText.subSequence(spannedText.getSpanStart(span),
spannedText.getSpanEnd(span));
}
return text;
}
// Find the bounds of a span. If it spans multiple lines, it will only return the bounds for the
// section on the first line.
private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) {
CharSequence text = mView.getText();
outRect.setEmpty();
if (text instanceof Spanned) {
final Layout layout = mView.getLayout();
if (layout != null) {
Spanned spannedText = (Spanned) text;
final int spanStart = spannedText.getSpanStart(span);
final int spanEnd = spannedText.getSpanEnd(span);
final float xStart = layout.getPrimaryHorizontal(spanStart);
final float xEnd = layout.getPrimaryHorizontal(spanEnd);
final int lineStart = layout.getLineForOffset(spanStart);
final int lineEnd = layout.getLineForOffset(spanEnd);
layout.getLineBounds(lineStart, outRect);
if (lineEnd == lineStart) {
// If the span is on a single line, adjust both the left and right bounds
// so outrect is exactly bounding the span.
outRect.left = (int) Math.min(xStart, xEnd);
outRect.right = (int) Math.max(xStart, xEnd);
@Override
protected boolean onPerformActionForVirtualView(
int virtualViewId,
int action,
Bundle arguments) {
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
ClickableSpan span = getSpanForOffset(virtualViewId);
if (span != null) {
span.onClick(mView);
return true;
} else {
// If the span wraps across multiple lines, only use the first line (as returned
// by layout.getLineBounds above), and adjust the "start" of outrect to where
// the span starts, leaving the "end" of outrect at the end of the line.
// ("start" being left for LTR, and right for RTL)
if (layout.getParagraphDirection(lineStart) == Layout.DIR_RIGHT_TO_LEFT) {
outRect.right = (int) xStart;
} else {
outRect.left = (int) xStart;
}
Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
}
// Offset for padding
outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop());
}
return false;
}
return outRect;
}
// Compat implementation of TextView#getOffsetForPosition().
private ClickableSpan getSpanForOffset(int offset) {
CharSequence text = mView.getText();
if (text instanceof Spanned) {
Spanned spannedText = (Spanned) text;
ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class);
if (spans.length == 1) {
return spans[0];
}
}
return null;
}
private static int getOffsetForPosition(TextView view, float x, float y) {
if (view.getLayout() == null) return -1;
final int line = getLineAtCoordinate(view, y);
return getOffsetAtCoordinate(view, line, x);
}
private CharSequence getTextForSpan(ClickableSpan span) {
CharSequence text = mView.getText();
if (text instanceof Spanned) {
Spanned spannedText = (Spanned) text;
return spannedText.subSequence(
spannedText.getSpanStart(span),
spannedText.getSpanEnd(span));
}
return text;
}
private static float convertToLocalHorizontalCoordinate(TextView view, float x) {
x -= view.getTotalPaddingLeft();
// Clamp the position to inside of the view.
x = Math.max(0.0f, x);
x = Math.min(view.getWidth() - view.getTotalPaddingRight() - 1, x);
x += view.getScrollX();
return x;
}
// Find the bounds of a span. If it spans multiple lines, it will only return the bounds for
// the section on the first line.
private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) {
CharSequence text = mView.getText();
outRect.setEmpty();
if (text instanceof Spanned) {
final Layout layout = mView.getLayout();
if (layout != null) {
Spanned spannedText = (Spanned) text;
final int spanStart = spannedText.getSpanStart(span);
final int spanEnd = spannedText.getSpanEnd(span);
final float xStart = layout.getPrimaryHorizontal(spanStart);
final float xEnd = layout.getPrimaryHorizontal(spanEnd);
final int lineStart = layout.getLineForOffset(spanStart);
final int lineEnd = layout.getLineForOffset(spanEnd);
layout.getLineBounds(lineStart, outRect);
if (lineEnd == lineStart) {
// If the span is on a single line, adjust both the left and right bounds
// so outrect is exactly bounding the span.
outRect.left = (int) Math.min(xStart, xEnd);
outRect.right = (int) Math.max(xStart, xEnd);
} else {
// If the span wraps across multiple lines, only use the first line (as
// returned by layout.getLineBounds above), and adjust the "start" of
// outrect to where the span starts, leaving the "end" of outrect at the end
// of the line. ("start" being left for LTR, and right for RTL)
if (layout.getParagraphDirection(lineStart) == Layout.DIR_RIGHT_TO_LEFT) {
outRect.right = (int) xStart;
} else {
outRect.left = (int) xStart;
}
}
private static int getLineAtCoordinate(TextView view, float y) {
y -= view.getTotalPaddingTop();
// Clamp the position to inside of the view.
y = Math.max(0.0f, y);
y = Math.min(view.getHeight() - view.getTotalPaddingBottom() - 1, y);
y += view.getScrollY();
return view.getLayout().getLineForVertical((int) y);
}
// Offset for padding
outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop());
}
}
return outRect;
}
private static int getOffsetAtCoordinate(TextView view, int line, float x) {
x = convertToLocalHorizontalCoordinate(view, x);
return view.getLayout().getOffsetForHorizontal(line, x);
// Compat implementation of TextView#getOffsetForPosition().
private static int getOffsetForPosition(TextView view, float x, float y) {
if (view.getLayout() == null) return -1;
final int line = getLineAtCoordinate(view, y);
return getOffsetAtCoordinate(view, line, x);
}
private static float convertToLocalHorizontalCoordinate(TextView view, float x) {
x -= view.getTotalPaddingLeft();
// Clamp the position to inside of the view.
x = Math.max(0.0f, x);
x = Math.min(view.getWidth() - view.getTotalPaddingRight() - 1, x);
x += view.getScrollX();
return x;
}
private static int getLineAtCoordinate(TextView view, float y) {
y -= view.getTotalPaddingTop();
// Clamp the position to inside of the view.
y = Math.max(0.0f, y);
y = Math.min(view.getHeight() - view.getTotalPaddingBottom() - 1, y);
y += view.getScrollY();
return view.getLayout().getLineForVertical((int) y);
}
private static int getOffsetAtCoordinate(TextView view, int line, float x) {
x = convertToLocalHorizontalCoordinate(view, x);
return view.getLayout().getOffsetForHorizontal(line, x);
}
}
}

View file

@ -16,6 +16,7 @@
package com.android.setupwizardlib.view;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
@ -30,6 +31,7 @@ import android.widget.Button;
* Button for navigation bar, which includes tinting of its compound drawables to be used for dark
* and light themes.
*/
@SuppressLint("AppCompatCustomView")
public class NavigationBarButton extends Button {
public NavigationBarButton(Context context) {

View file

@ -25,7 +25,7 @@ import android.support.v7.widget.AppCompatTextView;
import android.text.Annotation;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.TextAppearanceSpan;
import android.util.AttributeSet;
@ -36,6 +36,7 @@ import com.android.setupwizardlib.span.LinkSpan;
import com.android.setupwizardlib.span.LinkSpan.OnLinkClickListener;
import com.android.setupwizardlib.span.SpanHelper;
import com.android.setupwizardlib.util.LinkAccessibilityHelper;
import com.android.setupwizardlib.view.TouchableMovementMethod.TouchableLinkMovementMethod;
/**
* An extension of TextView that automatically replaces the annotation tags as specified in
@ -121,7 +122,7 @@ public class RichTextView extends AppCompatTextView implements OnLinkClickListen
// nullifying any return values of MovementMethod.onTouchEvent.
// To still allow propagating touch events to the parent when this view doesn't have
// links, we only set the movement method here if the text contains links.
setMovementMethod(LinkMovementMethod.getInstance());
setMovementMethod(TouchableLinkMovementMethod.getInstance());
} else {
setMovementMethod(null);
}
@ -130,6 +131,17 @@ public class RichTextView extends AppCompatTextView implements OnLinkClickListen
// as individual TextViews consume touch events and thereby reducing the focus window
// shown by Talkback. Disable focus if there are no links
setFocusable(hasLinks);
// Do not "reveal" (i.e. scroll to) this view when this view is focused. Since this view is
// focusable in touch mode, we may be focused when the screen is first shown, and starting
// a screen halfway scrolled down is confusing to the user.
if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
setRevealOnFocusHint(false);
// setRevealOnFocusHint is a new API added in SDK 25. For lower SDK versions, do not
// call setFocusableInTouchMode. We won't get touch effect on those earlier versions,
// but the link will still work, and will prevent the scroll view from starting halfway
// down the page.
setFocusableInTouchMode(hasLinks);
}
}
private boolean hasLinks(CharSequence text) {
@ -141,6 +153,25 @@ public class RichTextView extends AppCompatTextView implements OnLinkClickListen
return false;
}
@Override
@SuppressWarnings("ClickableViewAccessibility") // super.onTouchEvent is called
public boolean onTouchEvent(MotionEvent event) {
// Since View#onTouchEvent always return true if the view is clickable (which is the case
// when a TextView has a movement method), override the implementation to allow the movement
// method, if it implements TouchableMovementMethod, to say that the touch is not handled,
// allowing the event to bubble up to the parent view.
boolean superResult = super.onTouchEvent(event);
MovementMethod movementMethod = getMovementMethod();
if (movementMethod instanceof TouchableMovementMethod) {
TouchableMovementMethod touchableMovementMethod =
(TouchableMovementMethod) movementMethod;
if (touchableMovementMethod.getLastTouchEvent() == event) {
return touchableMovementMethod.isLastTouchEventHandled();
}
}
return superResult;
}
@Override
protected boolean dispatchHoverEvent(MotionEvent event) {
if (mAccessibilityHelper != null && mAccessibilityHelper.dispatchHoverEvent(event)) {

View file

@ -18,6 +18,7 @@ package com.android.setupwizardlib.items;
import static org.junit.Assert.assertTrue;
import android.support.annotation.StyleRes;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.SmallTest;
import android.support.test.rule.UiThreadTestRule;
@ -29,7 +30,6 @@ import android.widget.LinearLayout;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.test.util.DrawingTestHelper;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -38,40 +38,61 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class ButtonItemDrawingTest {
private static final int GOOGLE_BLUE = 0xff4285f4;
private static final int GLIF_ACCENT_COLOR = 0xff4285f4;
private static final int GLIF_V3_ACCENT_COLOR = 0xff1a73e8;
// These tests need to be run on UI thread because button uses ValueAnimator
@Rule
public UiThreadTestRule mUiThreadTestRule = new UiThreadTestRule();
private ViewGroup mParent;
@Before
public void setUp() throws Exception {
mParent = new LinearLayout(
DrawingTestHelper.createCanvasActivity(R.style.SuwThemeGlif_Light));
}
@Test
@UiThreadTest
public void testColoredButtonTheme() {
TestButtonItem item = new TestButtonItem();
item.setTheme(R.style.SuwButtonItem_Colored);
item.setText("foobar");
final Button button = item.createButton(mParent);
public void drawButton_glif_shouldHaveAccentColoredButton()
throws InstantiationException, IllegalAccessException {
Button button = createButton(R.style.SuwThemeGlif_Light);
DrawingTestHelper drawingTestHelper = new DrawingTestHelper(50, 50);
drawingTestHelper.drawView(button);
int googleBluePixelCount = 0;
for (int pixel : drawingTestHelper.getPixels()) {
if (pixel == GOOGLE_BLUE) {
googleBluePixelCount++;
int accentPixelCount =
countPixelsWithColor(drawingTestHelper.getPixels(), GLIF_ACCENT_COLOR);
assertTrue("> 10 pixels should be #4285f4. Found " + accentPixelCount,
accentPixelCount > 10);
}
@Test
@UiThreadTest
public void drawButton_glifV3_shouldHaveAccentColoredButton()
throws InstantiationException, IllegalAccessException {
Button button = createButton(R.style.SuwThemeGlifV3_Light);
DrawingTestHelper drawingTestHelper = new DrawingTestHelper(50, 50);
drawingTestHelper.drawView(button);
int accentPixelCount =
countPixelsWithColor(drawingTestHelper.getPixels(), GLIF_V3_ACCENT_COLOR);
assertTrue("> 10 pixels should be #1a73e8. Found " + accentPixelCount,
accentPixelCount > 10);
}
private Button createButton(@StyleRes int theme)
throws InstantiationException, IllegalAccessException {
final ViewGroup parent = new LinearLayout(DrawingTestHelper.createCanvasActivity(theme));
TestButtonItem item = new TestButtonItem();
item.setTheme(R.style.SuwButtonItem_Colored);
item.setText("foobar");
return item.createButton(parent);
}
private int countPixelsWithColor(int[] pixels, int color) {
int count = 0;
for (int pixel : pixels) {
if (pixel == color) {
count++;
}
}
assertTrue("> 10 pixels should be Google blue. Found " + googleBluePixelCount,
googleBluePixelCount > 10);
return count;
}
private static class TestButtonItem extends ButtonItem {

View file

@ -14,29 +14,35 @@
* limitations under the License.
*/
package com.android.setupwizardlib.test;
package com.android.setupwizardlib.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.text.BidiFormatter;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import android.support.v4.widget.ExploreByTouchHelper;
import android.text.SpannableStringBuilder;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.android.setupwizardlib.span.LinkSpan;
import com.android.setupwizardlib.util.LinkAccessibilityHelper;
import com.android.setupwizardlib.util.LinkAccessibilityHelper.PreOLinkAccessibilityHelper;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -52,13 +58,12 @@ public class LinkAccessibilityHelperTest {
private static final LinkSpan LINK_SPAN = new LinkSpan("foobar");
private TextView mTextView;
private TestLinkAccessibilityHelper mHelper;
private TestPreOLinkAccessibilityHelper mHelper;
private DisplayMetrics mDisplayMetrics;
@Test
public void testGetVirtualViewAt() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
initTextView();
final int virtualViewId = mHelper.getVirtualViewAt(dp2Px(15), dp2Px(10));
assertEquals("Virtual view ID should be 1", 1, virtualViewId);
@ -66,7 +71,6 @@ public class LinkAccessibilityHelperTest {
@Test
public void testGetVirtualViewAtHost() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
initTextView();
final int virtualViewId = mHelper.getVirtualViewAt(dp2Px(100), dp2Px(100));
assertEquals("Virtual view ID should be INVALID_ID",
@ -75,7 +79,6 @@ public class LinkAccessibilityHelperTest {
@Test
public void testGetVisibleVirtualViews() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
initTextView();
List<Integer> virtualViewIds = new ArrayList<>();
mHelper.getVisibleVirtualViews(virtualViewIds);
@ -86,7 +89,6 @@ public class LinkAccessibilityHelperTest {
@Test
public void testOnPopulateEventForVirtualView() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
initTextView();
AccessibilityEvent event = AccessibilityEvent.obtain();
mHelper.onPopulateEventForVirtualView(1, event);
@ -100,7 +102,6 @@ public class LinkAccessibilityHelperTest {
@Test
public void testOnPopulateEventForVirtualViewHost() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
initTextView();
AccessibilityEvent event = AccessibilityEvent.obtain();
mHelper.onPopulateEventForVirtualView(ExploreByTouchHelper.INVALID_ID, event);
@ -113,7 +114,6 @@ public class LinkAccessibilityHelperTest {
@Test
public void testOnPopulateNodeForVirtualView() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
initTextView();
AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
mHelper.onPopulateNodeForVirtualView(1, info);
@ -132,7 +132,6 @@ public class LinkAccessibilityHelperTest {
@Test
public void testNullLayout() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
initTextView();
// Setting the padding will cause the layout to be null-ed out.
mTextView.setPadding(1, 1, 1, 1);
@ -150,7 +149,6 @@ public class LinkAccessibilityHelperTest {
@Test
public void testRtlLayout() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
SpannableStringBuilder ssb = new SpannableStringBuilder("מכונה בתרגום");
ssb.setSpan(LINK_SPAN, 1, 2, 0 /* flags */);
initTextView(ssb);
@ -170,7 +168,6 @@ public class LinkAccessibilityHelperTest {
@Test
public void testMultilineLink() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
SpannableStringBuilder ssb = new SpannableStringBuilder(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
+ "Praesent accumsan efficitur eros eu porttitor.");
@ -192,7 +189,6 @@ public class LinkAccessibilityHelperTest {
@Test
public void testRtlMultilineLink() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
String iwLoremIpsum = "אחר על רביעי אקטואליה. לוח דת אחרות המקובל רומנית, מיזמים מועמדים "
+ "האנציקלופדיה בה צ'ט. מתן מה שנורו לערוך ייִדיש, בקר או החול אנתרופולוגיה, עוד "
+ "דפים המחשב מיזמים ב.";
@ -216,7 +212,6 @@ public class LinkAccessibilityHelperTest {
@Test
public void testBidiMultilineLink() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
String iwLoremIpsum = "אחר על רביעי אקטואליה. לוח דת אחרות המקובל רומנית, מיזמים מועמדים "
+ "האנציקלופדיה בה צ'ט. מתן מה שנורו לערוך ייִדיש, בקר או החול אנתרופולוגיה, עוד "
+ "דפים המחשב מיזמים ב.";
@ -243,6 +238,70 @@ public class LinkAccessibilityHelperTest {
info.recycle();
}
@Test
public void testMethodDelegation() {
initTextView();
ExploreByTouchHelper delegate = mock(TestPreOLinkAccessibilityHelper.class);
LinkAccessibilityHelper helper = new LinkAccessibilityHelper(delegate);
AccessibilityEvent accessibilityEvent =
AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_CLICKED);
helper.sendAccessibilityEvent(mTextView, AccessibilityEvent.TYPE_VIEW_CLICKED);
verify(delegate).sendAccessibilityEvent(
same(mTextView),
eq(AccessibilityEvent.TYPE_VIEW_CLICKED));
helper.sendAccessibilityEventUnchecked(mTextView, accessibilityEvent);
verify(delegate).sendAccessibilityEventUnchecked(same(mTextView), same(accessibilityEvent));
helper.performAccessibilityAction(
mTextView,
AccessibilityActionCompat.ACTION_CLICK.getId(),
Bundle.EMPTY);
verify(delegate).performAccessibilityAction(
same(mTextView),
eq(AccessibilityActionCompat.ACTION_CLICK.getId()),
eq(Bundle.EMPTY));
helper.dispatchPopulateAccessibilityEvent(
mTextView,
accessibilityEvent);
verify(delegate).dispatchPopulateAccessibilityEvent(
same(mTextView),
same(accessibilityEvent));
MotionEvent motionEvent = MotionEvent.obtain(0, 0, 0, 0, 0, 0);
helper.dispatchHoverEvent(motionEvent);
verify(delegate).dispatchHoverEvent(eq(motionEvent));
helper.getAccessibilityNodeProvider(mTextView);
verify(delegate).getAccessibilityNodeProvider(same(mTextView));
helper.onInitializeAccessibilityEvent(mTextView, accessibilityEvent);
verify(delegate).onInitializeAccessibilityEvent(
same(mTextView),
eq(accessibilityEvent));
AccessibilityNodeInfoCompat accessibilityNodeInfo = AccessibilityNodeInfoCompat.obtain();
helper.onInitializeAccessibilityNodeInfo(mTextView, accessibilityNodeInfo);
verify(delegate).onInitializeAccessibilityNodeInfo(
same(mTextView),
same(accessibilityNodeInfo));
helper.onPopulateAccessibilityEvent(mTextView, accessibilityEvent);
verify(delegate).onPopulateAccessibilityEvent(
same(mTextView),
same(accessibilityEvent));
FrameLayout parent = new FrameLayout(InstrumentationRegistry.getTargetContext());
helper.onRequestSendAccessibilityEvent(parent, mTextView, accessibilityEvent);
verify(delegate).onRequestSendAccessibilityEvent(
same(parent),
same(mTextView),
same(accessibilityEvent));
}
private void initTextView() {
SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
ssb.setSpan(LINK_SPAN, 1, 2, 0 /* flags */);
@ -254,7 +313,7 @@ public class LinkAccessibilityHelperTest {
mTextView.setSingleLine(false);
mTextView.setText(text);
mTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
mHelper = new TestLinkAccessibilityHelper(mTextView);
mHelper = new TestPreOLinkAccessibilityHelper(mTextView);
int measureExactly500dp = View.MeasureSpec.makeMeasureSpec(dp2Px(500),
View.MeasureSpec.EXACTLY);
@ -270,9 +329,9 @@ public class LinkAccessibilityHelperTest {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDisplayMetrics);
}
private static class TestLinkAccessibilityHelper extends LinkAccessibilityHelper {
public static class TestPreOLinkAccessibilityHelper extends PreOLinkAccessibilityHelper {
TestLinkAccessibilityHelper(TextView view) {
TestPreOLinkAccessibilityHelper(TextView view) {
super(view);
}

View file

@ -16,35 +16,29 @@
package com.android.setupwizardlib.items;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.not;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.robolectric.RuntimeEnvironment.application;
import android.support.v7.widget.SwitchCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import com.android.setupwizardlib.view.CheckableLinearLayout;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
public class ExpandableSwitchItemTest {
private TextView mSummaryView;
@ -71,6 +65,14 @@ public class ExpandableSwitchItemTest {
assertEquals("Should be collapsed initially", "TestSummary", mItem.getSummary());
assertEquals("Summary view should display collapsed summary",
"TestSummary", mSummaryView.getText());
assertFalse("Expandable switch item itself should not be focusable", view.isFocusable());
View switchContent = view.findViewById(R.id.suw_items_expandable_switch_content);
assertThat(switchContent).isInstanceOf(CheckableLinearLayout.class);
assertThat(switchContent.isFocusable())
.named("expandable content focusable")
.isTrue();
}
@Test
@ -132,57 +134,25 @@ public class ExpandableSwitchItemTest {
mItem.onBindView(view);
final View titleView = view.findViewById(R.id.suw_items_title);
assertThat("state_checked should not be set initially",
toArrayList(titleView.getDrawableState()),
not(hasItem(android.R.attr.state_checked)));
assertThat(titleView.getDrawableState()).asList().named("Drawable state")
.doesNotContain(android.R.attr.state_checked);
mItem.setExpanded(true);
mItem.onBindView(view);
assertThat("state_checked should not be set initially",
toArrayList(titleView.getDrawableState()),
hasItem(android.R.attr.state_checked));
assertThat(titleView.getDrawableState()).asList().named("Drawable state")
.contains(android.R.attr.state_checked);
mItem.setExpanded(false);
mItem.onBindView(view);
assertThat("state_checked should not be set initially",
toArrayList(titleView.getDrawableState()),
not(hasItem(android.R.attr.state_checked)));
}
private ArrayList<Integer> toArrayList(int[] array) {
ArrayList<Integer> arrayList = new ArrayList<>(array.length);
for (int i : array) {
arrayList.add(i);
}
return arrayList;
assertThat(titleView.getDrawableState()).asList().named("Drawable state")
.doesNotContain(android.R.attr.state_checked);
}
private ViewGroup createLayout() {
ViewGroup root = new FrameLayout(application);
ViewGroup content = new FrameLayout(application);
content.setId(R.id.suw_items_expandable_switch_content);
root.addView(content);
TextView titleView = new TextView(application);
titleView.setId(R.id.suw_items_title);
content.addView(titleView);
mSummaryView = new TextView(application);
mSummaryView.setId(R.id.suw_items_summary);
content.addView(mSummaryView);
FrameLayout iconContainer = new FrameLayout(application);
iconContainer.setId(R.id.suw_items_icon_container);
content.addView(iconContainer);
ImageView iconView = new ImageView(application);
iconView.setId(R.id.suw_items_icon);
iconContainer.addView(iconView);
SwitchCompat switchView = new SwitchCompat(application);
switchView.setId(R.id.suw_items_switch);
root.addView(switchView);
ViewGroup root =
(ViewGroup) LayoutInflater.from(application)
.inflate(R.layout.suw_items_expandable_switch, null);
mSummaryView = root.findViewById(R.id.suw_items_summary);
return root;
}

View file

@ -16,12 +16,15 @@
package com.android.setupwizardlib.items;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.robolectric.RuntimeEnvironment.application;
import android.annotation.TargetApi;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.v7.widget.SwitchCompat;
import android.view.LayoutInflater;
@ -31,20 +34,29 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
public class SwitchItemTest {
private SwitchCompat mSwitch;
@Test
public void testLayout() {
Assume.assumeTrue(VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP);
SwitchItem item = new SwitchItem();
LayoutInflater inflater = LayoutInflater.from(application);
ViewGroup layout = (ViewGroup) inflater.inflate(item.getDefaultLayoutResource(), null);
assertThat(layout.getClipToPadding()).isFalse();
}
@Test
public void testChecked() {
SwitchItem item = new SwitchItem();

View file

@ -25,7 +25,6 @@ import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@ -35,7 +34,7 @@ import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = Config.ALL_SDKS)
@Config(sdk = Config.ALL_SDKS)
public class DimensionConsistencyTest {
// Visual height of the framework switch widget

View file

@ -1,7 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- SUW lib prefixes styleable resources -->
<issue id="CustomViewStyleable" severity="ignore" />
<issue id="ExtraTranslation" severity="ignore" />
<issue id="GradleDependency" severity="ignore" />
<issue id="MissingTranslation" severity="ignore" />
<!-- Stop lint from complaining about SDK version checks in the "platform" variant -->
<issue id="ObsoleteSdkInt" severity="ignore" />
<issue id="RtlEnabled" severity="ignore" />
</lint>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2017 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:topLeftRadius="4dp" android:topRightRadius="4dp"/>
<solid android:color="?attr/suwEditTextBackgroundColor"/>
</shape>

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2017 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false">
<layer-list>
<item android:drawable="@drawable/suw_edit_text_bg_shape" android:bottom="1dp"/>
<item android:gravity="bottom">
<shape>
<size android:height="1dp"/>
<solid android:color="?android:attr/textColorSecondary"/>
</shape>
</item>
</layer-list>
</item>
<item android:state_focused="false" android:state_pressed="false">
<layer-list>
<item android:drawable="@drawable/suw_edit_text_bg_shape" android:bottom="1dp" />
<item android:gravity="bottom">
<shape>
<size android:height="1dp"/>
<solid android:color="?android:attr/textColorSecondary"/>
</shape>
</item>
</layer-list>
</item>
<item>
<layer-list>
<item android:drawable="@drawable/suw_edit_text_bg_shape" android:bottom="2dp" />
<item android:gravity="bottom">
<shape>
<size android:height="2dp"/>
<solid android:color="?android:attr/colorAccent"/>
</shape>
</item>
</layer-list>
</item>
</selector>

View file

@ -0,0 +1,194 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2017 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.
-->
<!-- Asset for 4 color indeterminate progress bar, which is a ring with 4 shades of blue -->
<animated-vector xmlns:aapt="http://schemas.android.com/aapt"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="MissingPrefix">
<!-- Ignore MissingPrefix: aapt:attr tags take the name attribute without "android:" prefix -->
<!-- TODO(yukl): Update to a newer version of lint which properly handles this case -->
<aapt:attr name="android:drawable">
<vector android:width="823dp" android:height="823dp" android:viewportHeight="823"
android:viewportWidth="823">
<group android:name="blue1" android:translateX="411.5" android:translateY="411.5">
<path android:name="blue1_path"
android:pathData="M0 -395 C218,-395 395,-218 395,0 C395,218 218,395 0,395 C-218,395 -395,218 -395,0 C-395,-218 -218,-395 0,-395c "
android:strokeAlpha="1" android:strokeColor="#4688f1"
android:strokeLineCap="round" android:strokeLineJoin="round"
android:strokeWidth="27" />
</group>
<group android:name="blue2" android:translateX="411.5" android:translateY="411.5">
<path android:name="blue2_path"
android:pathData=" M0 -395 C218,-395 395,-218 395,0 C395,218 218,395 0,395 C-218,395 -395,218 -395,0 C-395,-218 -218,-395 0,-395c "
android:strokeAlpha="1" android:strokeColor="#7dacf4"
android:strokeLineCap="round" android:strokeLineJoin="round"
android:strokeWidth="28" />
</group>
<group android:name="blue3" android:translateX="411.5" android:translateY="411.5">
<path android:name="blue3_path"
android:pathData=" M0 -395 C218,-395 395,-218 395,0 C395,218 218,395 0,395 C-218,395 -395,218 -395,0 C-395,-218 -218,-395 0,-395c "
android:strokeAlpha="1" android:strokeColor="#c7dbfb"
android:strokeLineCap="round" android:strokeLineJoin="round"
android:strokeWidth="29" />
</group>
<group android:name="blue4" android:translateX="411.5" android:translateY="411.5">
<path android:name="blue4_path"
android:pathData=" M0 -395 C218,-395 395,-218 395,0 C395,218 218,395 0,395 C-218,395 -395,218 -395,0 C-395,-218 -218,-395 0,-395c "
android:strokeAlpha="1" android:strokeColor="#e8f0fd"
android:strokeLineCap="round" android:strokeLineJoin="round"
android:strokeWidth="30" />
</group>
</vector>
</aapt:attr>
<target android:name="blue1_path">
<aapt:attr name="android:animation">
<objectAnimator android:duration="1983" android:propertyName="trimPathStart"
android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M0,0 l.21,0 c.571,0 .194,.755 .79,1" />
</aapt:attr>
</objectAnimator>
</aapt:attr>
</target>
<target android:name="blue1_path">
<aapt:attr name="android:animation">
<objectAnimator android:duration="1983" android:propertyName="trimPathEnd"
android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M0,0 c0.606,0.315 0.2,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</aapt:attr>
</target>
<target android:name="blue1">
<aapt:attr name="android:animation">
<objectAnimator android:duration="1983" android:propertyName="rotation"
android:repeatCount="infinite" android:valueFrom="0" android:valueTo="355"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M0,0 c0.829,0.228 0.2,0.915 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</aapt:attr>
</target>
<target android:name="blue2_path">
<aapt:attr name="android:animation">
<objectAnimator android:duration="1983" android:propertyName="trimPathStart"
android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M0,0 l.21,0 c.571,0 .145,.831 .79,1" />
</aapt:attr>
</objectAnimator>
</aapt:attr>
</target>
<target android:name="blue2_path">
<aapt:attr name="android:animation">
<objectAnimator android:duration="1983" android:propertyName="trimPathEnd"
android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M0,0 c0.606,0.315 0.2,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</aapt:attr>
</target>
<target android:name="blue2">
<aapt:attr name="android:animation">
<objectAnimator android:duration="1983" android:propertyName="rotation"
android:repeatCount="infinite" android:valueFrom="0" android:valueTo="355"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M0,0 c0.792,0.233 0.2,0.915 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</aapt:attr>
</target>
<target android:name="blue3_path">
<aapt:attr name="android:animation">
<objectAnimator android:duration="1983" android:propertyName="trimPathStart"
android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M0,0 l.21,0 c.6138,0 .007,.883 .79,1" />
</aapt:attr>
</objectAnimator>
</aapt:attr>
</target>
<target android:name="blue3_path">
<aapt:attr name="android:animation">
<objectAnimator android:duration="1983" android:propertyName="trimPathEnd"
android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M0,0 c0.606,0.315 0.2,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</aapt:attr>
</target>
<target android:name="blue3">
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator android:duration="1983" android:propertyName="rotation"
android:repeatCount="infinite" android:valueFrom="0" android:valueTo="355"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 c0.762,0.225 0.2,0.915 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</set>
</aapt:attr>
</target>
<target android:name="blue4_path">
<aapt:attr name="android:animation">
<objectAnimator android:duration="1983" android:propertyName="trimPathStart"
android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M0,0 l.21,0 c.572,0 0,1 .79,1" />
</aapt:attr>
</objectAnimator>
</aapt:attr>
</target>
<target android:name="blue4_path">
<aapt:attr name="android:animation">
<objectAnimator android:duration="1983" android:propertyName="trimPathEnd"
android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M0,0 c0.606,0.315 0.2,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</aapt:attr>
</target>
<target android:name="blue4">
<aapt:attr name="android:animation">
<objectAnimator android:duration="1983" android:propertyName="rotation"
android:repeatCount="infinite" android:valueFrom="0" android:valueTo="355"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M0,0 c0.606,0.172 0.2,0.915 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</aapt:attr>
</target>
</animated-vector>

View file

@ -21,6 +21,11 @@
android:layout_height="match_parent"
android:orientation="vertical">
<ViewStub
android:id="@+id/suw_layout_sticky_header"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<FrameLayout
android:id="@+id/suw_layout_content"
android:layout_width="match_parent"

View file

@ -23,10 +23,11 @@
<ImageView
android:id="@+id/suw_layout_icon"
style="@style/SuwGlifIcon"
style="?attr/suwGlifIconStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null" />
android:contentDescription="@null"
android:visibility="gone" />
<TextView
android:id="@+id/suw_layout_title"

View file

@ -22,6 +22,11 @@
android:layout_height="match_parent"
android:orientation="vertical">
<ViewStub
android:id="@+id/suw_layout_sticky_header"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!-- Ignore UnusedAttribute: scrollIndicators is new in M. Default to no indicators in older
versions. -->
<com.android.setupwizardlib.view.StickyHeaderListView

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2017 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.
-->
<com.android.setupwizardlib.GlifLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/setup_wizard_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="UnusedResources">
<!-- Ignore UnusedResources: can be used by clients -->
<com.android.setupwizardlib.view.FillContentLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1">
<ProgressBar
android:id="@+id/suw_large_progress_bar"
style="@style/SuwFourColorIndeterminateProgressBar"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.android.setupwizardlib.view.FillContentLayout>
</com.android.setupwizardlib.GlifLayout>

View file

@ -22,6 +22,11 @@
android:layout_height="match_parent"
android:orientation="vertical">
<ViewStub
android:id="@+id/suw_layout_sticky_header"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!-- Ignore UnusedAttribute: scrollIndicators is new in M. Default to no indicators in older
versions. -->
<com.android.setupwizardlib.view.BottomScrollView

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2015 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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="suw_next_button_label" msgid="7269625133873553978">"Next"</string>
<string name="suw_back_button_label" msgid="1460929053642711025">"Back"</string>
<string name="suw_more_button_label" msgid="7769076059705546563">"More"</string>
</resources>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2015 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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="suw_next_button_label" msgid="7269625133873553978">"Next"</string>
<string name="suw_back_button_label" msgid="1460929053642711025">"Back"</string>
<string name="suw_more_button_label" msgid="7769076059705546563">"More"</string>
</resources>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2017 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.
-->
<resources>
<style name="SuwBase.ProgressBarLarge" parent="@android:style/Widget.Holo.ProgressBar.Large" />
</resources>

View file

@ -42,6 +42,16 @@
<item name="android:fontFamily">sans-serif-medium</item>
</style>
<style name="SuwBase.ProgressBarLarge" parent="@android:style/Widget.Material.ProgressBar.Large" />
<style name="SuwFourColorIndeterminateProgressBar" parent="SuwBase.ProgressBarLarge">
<item name="android:layout_gravity">center</item>
<item name="android:indeterminate">true</item>
<item name="android:indeterminateDrawable">@drawable/suw_fourcolor_progress_bar</item>
<item name="android:indeterminateTint">@null</item>
<item name="android:indeterminateTintMode">@null</item>
</style>
<!-- Items styles -->
<style name="SuwItemContainer">
@ -83,4 +93,11 @@
<item name="suwNavBarButtonBackground">@drawable/suw_navbar_btn_bg</item>
</style>
<style name="SuwEditText" parent="@android:style/Widget.Material.EditText">
<item name="android:background">@drawable/suw_edittext_bg</item>
<item name="android:minHeight">@dimen/suw_edit_text_min_height</item>
<item name="android:paddingLeft">@dimen/suw_edit_text_padding_horizontal</item>
<item name="android:paddingRight">@dimen/suw_edit_text_padding_horizontal</item>
</style>
</resources>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2017 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.
-->
<resources>
<style name="SuwAlertDialogTheme" parent="android:Theme.DeviceDefault.Dialog.Alert" />
<style name="SuwAlertDialogTheme.Light" parent="android:Theme.DeviceDefault.Light.Dialog.Alert" />
</resources>

View file

@ -20,6 +20,7 @@
<!-- Theme attributes -->
<attr name="suwLayoutTheme" format="reference" />
<attr name="suwMarginSides" format="dimension|reference" />
<attr name="suwEditTextBackgroundColor" format="color" />
<!-- Subset of values in "gravity" in frameworks/base/core/res/res/values/attrs.xml. Only
horizontal values are listed here as the header does not support vertical gravity. -->
@ -37,13 +38,17 @@
<!-- Push object to the end of its container, not changing its size. -->
<flag name="end" value="0x00800005" />
</attr>
<attr name="suwGlifIconStyle" format="reference" />
<attr name="suwButtonAllCaps" format="boolean" />
<attr name="suwButtonCornerRadius" format="dimension" />
<attr name="suwButtonFontFamily" format="string|reference" />
<attr name="suwCardBackground" format="color|reference" />
<attr name="suwFillContentLayoutStyle" format="reference" />
<attr name="suwDividerCondition">
<enum name="either" value="0" />
<enum name="both" value="1" />
</attr>
<attr name="suwFillContentLayoutStyle" format="reference" />
<attr name="suwListItemIconColor" format="color" />
<attr name="suwNavBarBackgroundColor" format="color" />
<attr name="suwNavBarButtonBackground" format="color|reference" />
@ -102,6 +107,8 @@
<attr name="suwBackgroundBaseColor" format="color" />
<attr name="suwColorPrimary" />
<attr name="suwFooter" format="reference" />
<attr name="suwLayoutFullscreen" format="boolean" />
<attr name="suwStickyHeader" format="reference" />
</declare-styleable>
<declare-styleable name="SuwStatusBarBackgroundLayout">

View file

@ -21,6 +21,8 @@
<color name="suw_color_accent_dark">#ff448aff</color>
<color name="suw_color_accent_light">#ff3367d6</color>
<color name="suw_color_background_dark">#ff303030</color>
<color name="suw_color_background_light">#fffafafa</color>
<color name="suw_link_color_dark">#ff448aff</color>
<color name="suw_link_color_light">#ff3367d6</color>
<color name="suw_list_item_icon_color_dark">#b3ffffff</color>
@ -40,7 +42,11 @@
<!-- GLIF colors -->
<color name="suw_color_accent_glif_dark">#ff4285f4</color>
<color name="suw_color_accent_glif_light">#ff4285f4</color>
<color name="suw_color_accent_glif_v3">#ff1a73e8</color>
<color name="suw_glif_background_color_dark">#ff000000</color>
<color name="suw_glif_background_color_light">#ffffffff</color>
<color name="suw_glif_edit_text_bg_light_color">#fff1f3f4</color>
<color name="suw_glif_v3_nav_bar_color_light">#ffffffff</color>
<color name="suw_glif_v3_nav_bar_divider_color_light">#1f000000</color>
</resources>

View file

@ -27,4 +27,8 @@
ButtonBarLayout -->
<item name="suw_original_weight" type="id" />
<!-- Secondary font for use with headings, title, and other non-body text -->
<string name="suwFontSecondary" translatable="false">google-sans</string>
<string name="suwFontSecondaryMedium" translatable="false">google-sans-medium</string>
</resources>

View file

@ -20,16 +20,21 @@
<!-- General -->
<dimen name="suw_layout_margin_sides">40dp</dimen>
<dimen name="suw_glif_button_corner_radius">2dp</dimen>
<!-- Calculated by (suw_glif_margin_sides - 4dp internal padding of button) -->
<dimen name="suw_glif_button_margin_end">20dp</dimen>
<!-- Calculated by (suw_glif_margin_sides - suw_glif_button_padding) -->
<dimen name="suw_glif_button_margin_start">8dp</dimen>
<dimen name="suw_glif_button_padding">16dp</dimen>
<!-- Negative of suw_glif_button_padding -->
<dimen name="suw_glif_negative_button_padding">-16dp</dimen>
<dimen name="suw_glif_footer_padding_vertical">8dp</dimen>
<dimen name="suw_glif_footer_min_height">80dp</dimen>
<dimen name="suw_glif_footer_min_height">72dp</dimen>
<dimen name="suw_glif_margin_sides">24dp</dimen>
<dimen name="suw_glif_margin_top">48dp</dimen>
<dimen name="suw_glif_v3_button_corner_radius">4dp</dimen>
<!-- Content styles -->
<dimen name="suw_check_box_line_spacing_extra">4sp</dimen>
<dimen name="suw_check_box_margin_bottom">12dp</dimen>
@ -135,4 +140,8 @@
<dimen name="suw_progress_bar_margin_vertical">-7dp</dimen>
<dimen name="suw_glif_progress_bar_margin_vertical">7dp</dimen>
<!-- Edit Text dimensions -->
<dimen name="suw_edit_text_min_height">56dp</dimen>
<dimen name="suw_edit_text_padding_horizontal">12dp</dimen>
</resources>

View file

@ -30,11 +30,11 @@
<item name="suwDividerInsetStartNoIcon">?attr/suwMarginSides</item>
<item name="suwGlifHeaderGravity">center_horizontal</item>
<item name="suwScrollIndicators">top|bottom</item>
<item name="suwEditTextBackgroundColor">@color/suw_glif_edit_text_bg_light_color</item> <!-- TODO: Change color -->
<item name="android:editTextStyle">@style/SuwEditText</item>
<item name="android:alertDialogTheme" tools:targetApi="honeycomb">@style/SuwAlertDialogTheme</item>
</style>
<!-- Deprecated. Use SuwThemeGlifV2 instead -->
<style name="SuwThemeGlifPixel" parent="SuwThemeGlifV2" />
<style name="SuwThemeGlifV2.Light" parent="SuwThemeGlif.Light">
<item name="android:colorBackground">@color/suw_glif_background_color_light</item>
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
@ -46,11 +46,11 @@
<item name="suwDividerInsetStartNoIcon">?attr/suwMarginSides</item>
<item name="suwGlifHeaderGravity">center_horizontal</item>
<item name="suwScrollIndicators">top|bottom</item>
<item name="suwEditTextBackgroundColor">@color/suw_glif_edit_text_bg_light_color</item>
<item name="android:editTextStyle">@style/SuwEditText</item>
<item name="android:alertDialogTheme" tools:targetApi="honeycomb">@style/SuwAlertDialogTheme.Light</item>
</style>
<!-- Deprecated. Use SuwThemeGlifV2.Light instead -->
<style name="SuwThemeGlifPixel.Light" parent="SuwThemeGlifV2.Light" />
<style name="Animation.SuwWindowAnimation" parent="@android:style/Animation.Activity">
<item name="android:activityOpenEnterAnimation">@anim/suw_slide_next_in</item>
<item name="android:activityOpenExitAnimation">@anim/suw_slide_next_out</item>
@ -86,11 +86,10 @@
<item name="android:textAlignment" tools:targetApi="jelly_bean_mr1">gravity</item>
</style>
<style name="TextAppearance.SuwDescription.Light" parent="TextAppearance.SuwDescription">
<item name="android:fontFamily" tools:ignore="NewApi">sans-serif-light</item>
</style>
<style name="TextAppearance.SuwDescription.Secondary" parent="TextAppearance.SuwDescription">
<!-- Ignore UnusedResources: Used by clients -->
<style name="TextAppearance.SuwDescription.Secondary"
parent="TextAppearance.SuwDescription"
tools:ignore="UnusedResources">
<item name="android:textColor">?android:attr/textColorSecondary</item>
</style>
@ -196,14 +195,19 @@
<item name="android:buttonStyle">@style/SuwGlifButton.Tertiary</item>
<item name="android:theme">@style/SuwGlifButton.Tertiary</item>
<item name="android:background">@null</item>
<item name="android:fontFamily" tools:targetApi="jelly_bean">sans-serif</item>
<item name="android:layout_gravity">?attr/suwGlifHeaderGravity</item>
<item name="android:padding">0dp</item>
<item name="android:layout_marginLeft">@dimen/suw_glif_negative_button_padding</item>
<item name="android:layout_marginRight">@dimen/suw_glif_negative_button_padding</item>
<!-- Always lowercase instead of reading attr/suwButtonAllCaps, since this is a tertiary
button -->
<item name="android:textAllCaps" tools:targetApi="ice_cream_sandwich">false</item>
</style>
<style name="SuwGlifButton.Tertiary" parent="SuwGlifButton.BaseTertiary" />
<!-- Ignore UnusedResources: used by clients -->
<style name="SuwGlifButton.Tertiary"
parent="SuwGlifButton.BaseTertiary"
tools:ignore="UnusedResources" />
<!-- The start and end paddings are asymmetric because start buttons are borderless buttons
which aligns the text label. -->
@ -266,7 +270,7 @@
<item name="android:layout_marginLeft">?attr/suwMarginSides</item>
<item name="android:layout_marginRight">?attr/suwMarginSides</item>
<item name="android:layout_marginTop">@dimen/suw_glif_header_title_margin_top</item>
<item name="android:fontFamily" tools:targetApi="jelly_bean">google-sans</item>
<item name="android:fontFamily" tools:targetApi="jelly_bean">@string/suwFontSecondary</item>
<item name="android:textAlignment" tools:targetApi="jelly_bean_mr1">gravity</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
@ -309,4 +313,9 @@
<item name="suwNavBarTextColor">?android:attr/textColorPrimary</item>
</style>
<style name="SuwEditText" parent="@android:style/Widget.EditText">
<item name="android:minHeight">@dimen/suw_edit_text_min_height</item>
</style>
</resources>

View file

@ -77,6 +77,8 @@ public class GlifLayout extends TemplateLayout {
@Nullable
private ColorStateList mBackgroundBaseColor;
private boolean mLayoutFullscreen = true;
public GlifLayout(Context context) {
this(context, 0, 0);
}
@ -139,7 +141,18 @@ public class GlifLayout extends TemplateLayout {
inflateFooter(footer);
}
final int stickyHeader = a.getResourceId(R.styleable.SuwGlifLayout_suwStickyHeader, 0);
if (stickyHeader != 0) {
inflateStickyHeader(stickyHeader);
}
mLayoutFullscreen = a.getBoolean(R.styleable.SuwGlifLayout_suwLayoutFullscreen, true);
a.recycle();
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && mLayoutFullscreen) {
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
}
@Override
@ -160,17 +173,31 @@ public class GlifLayout extends TemplateLayout {
/**
* Sets the footer of the layout, which is at the bottom of the content area outside the
* scrolling container. The footer can only be inflated once per layout.
* scrolling container. The footer can only be inflated once per instance of this layout.
*
* @param footer The layout to be inflated as footer.
* @return The root of the inflated footer view.
*/
public View inflateFooter(@LayoutRes int footer) {
ViewStub footerStub = (ViewStub) findManagedViewById(R.id.suw_layout_footer);
ViewStub footerStub = findManagedViewById(R.id.suw_layout_footer);
footerStub.setLayoutResource(footer);
return footerStub.inflate();
}
/**
* Sets the sticky header (i.e. header that doesn't scroll) of the layout, which is at the top
* of the content area outside of the scrolling container. The header can only be inflated once
* per instance of this layout.
*
* @param header The layout to be inflated as the header.
* @return The root of the inflated header view.
*/
public View inflateStickyHeader(@LayoutRes int header) {
ViewStub stickyHeaderStub = findManagedViewById(R.id.suw_layout_sticky_header);
stickyHeaderStub.setLayoutResource(header);
return stickyHeaderStub.inflate();
}
public ScrollView getScrollView() {
final View view = findManagedViewById(R.id.suw_scroll_view);
return view instanceof ScrollView ? (ScrollView) view : null;
@ -280,9 +307,6 @@ public class GlifLayout extends TemplateLayout {
patternBg.setBackgroundDrawable(background);
}
}
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
}
public boolean isProgressBarShown() {

View file

@ -23,7 +23,6 @@ import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
@ -96,7 +95,6 @@ public class GlifPatternDrawable extends Drawable {
private int mColor;
private Paint mTempPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private ColorFilter mColorFilter;
public GlifPatternDrawable(int color) {
setColor(color);
@ -140,17 +138,10 @@ public class GlifPatternDrawable extends Drawable {
canvas.clipRect(bounds);
scaleCanvasToBounds(canvas, bitmap, bounds);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
&& canvas.isHardwareAccelerated()) {
mTempPaint.setColorFilter(mColorFilter);
canvas.drawBitmap(bitmap, 0, 0, mTempPaint);
} else {
// Software renderer doesn't work properly with ColorMatrix filter on ALPHA_8 bitmaps.
canvas.drawColor(Color.BLACK);
mTempPaint.setColor(Color.WHITE);
canvas.drawBitmap(bitmap, 0, 0, mTempPaint);
canvas.drawColor(mColor);
}
canvas.drawColor(Color.BLACK);
mTempPaint.setColor(Color.WHITE);
canvas.drawBitmap(bitmap, 0, 0, mTempPaint);
canvas.drawColor(mColor);
canvas.restore();
}
@ -299,12 +290,6 @@ public class GlifPatternDrawable extends Drawable {
final int g = Color.green(color);
final int b = Color.blue(color);
mColor = Color.argb(COLOR_ALPHA_INT, r, g, b);
mColorFilter = new ColorMatrixColorFilter(new float[] {
0, 0, 0, 1 - COLOR_ALPHA, r * COLOR_ALPHA,
0, 0, 0, 1 - COLOR_ALPHA, g * COLOR_ALPHA,
0, 0, 0, 1 - COLOR_ALPHA, b * COLOR_ALPHA,
0, 0, 0, 0, 255
});
invalidateSelf();
}

View file

@ -103,7 +103,9 @@ public class TemplateLayout extends FrameLayout {
* by this view but not currently added to the view hierarchy. e.g. recycler view or list view
* headers that are not currently shown.
*/
public View findManagedViewById(int id) {
// Returning generic type is the common pattern used for findViewBy* methods
@SuppressWarnings("TypeParameterUnusedInFormals")
public <T extends View> T findManagedViewById(int id) {
return findViewById(id);
}

View file

@ -39,6 +39,7 @@ public final class ConsecutiveTapsGestureDetector {
private final View mView;
private final OnConsecutiveTapsListener mListener;
private final int mConsecutiveTapTouchSlopSquare;
private final int mConsecutiveTapTimeout;
private int mConsecutiveTapsCounter = 0;
private MotionEvent mPreviousTapEvent;
@ -54,6 +55,7 @@ public final class ConsecutiveTapsGestureDetector {
mView = view;
int doubleTapSlop = ViewConfiguration.get(mView.getContext()).getScaledDoubleTapSlop();
mConsecutiveTapTouchSlopSquare = doubleTapSlop * doubleTapSlop;
mConsecutiveTapTimeout = ViewConfiguration.getDoubleTapTimeout();
}
/**
@ -109,6 +111,8 @@ public final class ConsecutiveTapsGestureDetector {
double deltaX = mPreviousTapEvent.getX() - currentTapEvent.getX();
double deltaY = mPreviousTapEvent.getY() - currentTapEvent.getY();
return (deltaX * deltaX + deltaY * deltaY <= mConsecutiveTapTouchSlopSquare);
long deltaTime = currentTapEvent.getEventTime() - mPreviousTapEvent.getEventTime();
return (deltaX * deltaX + deltaY * deltaY <= mConsecutiveTapTouchSlopSquare)
&& deltaTime < mConsecutiveTapTimeout;
}
}

View file

@ -83,6 +83,7 @@ public class ButtonBarItem extends AbstractItem implements ItemInflater.ItemPare
return mVisible;
}
@Override
public int getViewId() {
return getId();
}

View file

@ -21,10 +21,13 @@ import android.content.ContextWrapper;
import android.graphics.Typeface;
import android.os.Build;
import android.support.annotation.Nullable;
import android.text.Selection;
import android.text.Spannable;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
/**
* A clickable span that will listen for click events and send it back to the context. To use this
@ -86,11 +89,19 @@ public class LinkSpan extends ClickableSpan {
public void onClick(View view) {
if (dispatchClick(view)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// Prevent the touch event from bubbling up to the parent views.
view.cancelPendingInputEvents();
}
} else {
Log.w(TAG, "Dropping click event. No listener attached.");
}
if (view instanceof TextView) {
// Remove the highlight effect when the click happens by clearing the selection
CharSequence text = ((TextView) view).getText();
if (text instanceof Spannable) {
Selection.setSelection((Spannable) text, 0);
}
}
}
private boolean dispatchClick(View view) {

View file

@ -19,7 +19,9 @@ package com.android.setupwizardlib.template;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.support.annotation.DrawableRes;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import com.android.setupwizardlib.R;
@ -44,8 +46,8 @@ public class IconMixin implements Mixin {
final TypedArray a =
context.obtainStyledAttributes(attrs, R.styleable.SuwIconMixin, defStyleAttr, 0);
final Drawable icon = a.getDrawable(R.styleable.SuwIconMixin_android_icon);
if (icon != null) {
final @DrawableRes int icon = a.getResourceId(R.styleable.SuwIconMixin_android_icon, 0);
if (icon != 0) {
setIcon(icon);
}
@ -61,6 +63,22 @@ public class IconMixin implements Mixin {
final ImageView iconView = getView();
if (iconView != null) {
iconView.setImageDrawable(icon);
iconView.setVisibility(icon != null ? View.VISIBLE : View.GONE);
}
}
/**
* Sets the icon on this layout. The icon can also be set in XML using {@code android:icon}.
*
* @param icon A drawable icon resource.
*/
public void setIcon(@DrawableRes int icon) {
final ImageView iconView = getView();
if (iconView != null) {
// Note: setImageResource on the ImageView is overridden in AppCompatImageView for
// support lib users, which enables vector drawable compat to work on versions pre-L.
iconView.setImageResource(icon);
iconView.setVisibility(icon != 0 ? View.VISIBLE : View.GONE);
}
}
@ -72,6 +90,24 @@ public class IconMixin implements Mixin {
return iconView != null ? iconView.getDrawable() : null;
}
/**
* Sets the content description of the icon view
*/
public void setContentDescription(CharSequence description) {
final ImageView iconView = getView();
if (iconView != null) {
iconView.setContentDescription(description);
}
}
/**
* @return The content description of the icon view
*/
public CharSequence getContentDescription() {
final ImageView iconView = getView();
return iconView != null ? iconView.getContentDescription() : null;
}
/**
* @return The ImageView responsible for displaying the icon.
*/

View file

@ -27,6 +27,7 @@ import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.annotation.AnyRes;
import android.support.annotation.ColorRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
import android.support.annotation.VisibleForTesting;
@ -75,6 +76,15 @@ public class Partner {
return entry.resources.getString(entry.id);
}
/**
* Convenience method to get color from partner overlay, or if not available, the color from
* the original context.
*/
public static int getColor(Context context, @ColorRes int id) {
final ResourceEntry resourceEntry = getResourceEntry(context, id);
return resourceEntry.resources.getColor(resourceEntry.id);
}
/**
* Convenience method to get a CharSequence from partner overlay, or if not available, the text
* from the original context.

View file

@ -24,6 +24,7 @@ import android.content.res.TypedArray;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Handler;
import android.support.annotation.RequiresPermission;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@ -183,12 +184,25 @@ public class SystemBarHelper {
}
}
/**
* Sets whether the back button on the software navigation bar is visible. This only works if
* you have the STATUS_BAR permission. Otherwise framework will filter out this flag and this
* method call will not have any effect.
*
* <p>IMPORTANT: Do not assume that users have no way to go back when the back button is hidden.
* Many devices have physical back buttons, and accessibility services like TalkBack may have
* gestures mapped to back. Please use onBackPressed, onKeyDown, or other similar ways to
* make sure back button events are still handled (or ignored) properly.
*/
@RequiresPermission("android.permission.STATUS_BAR")
public static void setBackButtonVisible(final Window window, final boolean visible) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
if (visible) {
removeVisibilityFlag(window, STATUS_BAR_DISABLE_BACK);
removeImmersiveFlagsFromDecorView(window, STATUS_BAR_DISABLE_BACK);
} else {
addVisibilityFlag(window, STATUS_BAR_DISABLE_BACK);
addImmersiveFlagsToDecorView(window, STATUS_BAR_DISABLE_BACK);
}
}
}
@ -217,7 +231,7 @@ public class SystemBarHelper {
* {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} only takes effect when it is added to a view
* instead of the window.
*/
@TargetApi(VERSION_CODES.LOLLIPOP)
@TargetApi(VERSION_CODES.HONEYCOMB)
private static void addImmersiveFlagsToDecorView(final Window window, final int vis) {
getDecorView(window, new OnDecorViewInstalledListener() {
@Override
@ -227,7 +241,7 @@ public class SystemBarHelper {
});
}
@TargetApi(VERSION_CODES.LOLLIPOP)
@TargetApi(VERSION_CODES.HONEYCOMB)
private static void removeImmersiveFlagsFromDecorView(final Window window, final int vis) {
getDecorView(window, new OnDecorViewInstalledListener() {
@Override

View file

@ -27,6 +27,8 @@ import android.support.annotation.VisibleForTesting;
import com.android.setupwizardlib.R;
import java.util.Arrays;
public class WizardManagerHelper {
private static final String ACTION_NEXT = "com.android.wizard.NEXT";
@ -45,6 +47,8 @@ public class WizardManagerHelper {
static final String EXTRA_IS_FIRST_RUN = "firstRun";
@VisibleForTesting
static final String EXTRA_IS_DEFERRED_SETUP = "deferredSetup";
@VisibleForTesting
static final String EXTRA_IS_PRE_DEFERRED_SETUP = "preDeferredSetup";
public static final String EXTRA_THEME = "theme";
public static final String EXTRA_USE_IMMERSIVE_MODE = "useImmersiveMode";
@ -75,12 +79,6 @@ public class WizardManagerHelper {
*/
public static final String THEME_GLIF_V2 = "glif_v2";
/**
* @deprecated Use {@link #THEME_GLIF_V2} instead.
*/
@Deprecated
public static final String THEME_GLIF_PIXEL = THEME_GLIF_V2;
/**
* Passed in a setup wizard intent as {@link #EXTRA_THEME}. This is the default theme used in
* setup wizard for O DR.
@ -88,10 +86,16 @@ public class WizardManagerHelper {
public static final String THEME_GLIF_V2_LIGHT = "glif_v2_light";
/**
* @deprecated Use {@link #THEME_GLIF_V2_LIGHT} instead.
* Passed in a setup wizard intent as {@link #EXTRA_THEME}. This is the dark variant of the
* theme used in setup wizard for P.
*/
@Deprecated
public static final String THEME_GLIF_PIXEL_LIGHT = THEME_GLIF_V2_LIGHT;
public static final String THEME_GLIF_V3 = "glif_v3";
/**
* Passed in a setup wizard intent as {@link #EXTRA_THEME}. This is the default theme used in
* setup wizard for P.
*/
public static final String THEME_GLIF_V3_LIGHT = "glif_v3_light";
/**
* Get an intent that will invoke the next step of setup wizard.
@ -140,13 +144,14 @@ public class WizardManagerHelper {
*/
public static void copyWizardManagerExtras(Intent srcIntent, Intent dstIntent) {
dstIntent.putExtra(EXTRA_WIZARD_BUNDLE, srcIntent.getBundleExtra(EXTRA_WIZARD_BUNDLE));
dstIntent.putExtra(EXTRA_THEME, srcIntent.getStringExtra(EXTRA_THEME));
dstIntent.putExtra(EXTRA_IS_FIRST_RUN,
srcIntent.getBooleanExtra(EXTRA_IS_FIRST_RUN, false));
dstIntent.putExtra(EXTRA_IS_DEFERRED_SETUP,
srcIntent.getBooleanExtra(EXTRA_IS_DEFERRED_SETUP, false));
dstIntent.putExtra(EXTRA_SCRIPT_URI, srcIntent.getStringExtra(EXTRA_SCRIPT_URI));
dstIntent.putExtra(EXTRA_ACTION_ID, srcIntent.getStringExtra(EXTRA_ACTION_ID));
for (String key : Arrays.asList(
EXTRA_IS_FIRST_RUN, EXTRA_IS_DEFERRED_SETUP, EXTRA_IS_PRE_DEFERRED_SETUP)) {
dstIntent.putExtra(key, srcIntent.getBooleanExtra(key, false));
}
for (String key : Arrays.asList(EXTRA_THEME, EXTRA_SCRIPT_URI, EXTRA_ACTION_ID)) {
dstIntent.putExtra(key, srcIntent.getStringExtra(key));
}
}
/**
@ -212,6 +217,18 @@ public class WizardManagerHelper {
&& originalIntent.getBooleanExtra(EXTRA_IS_DEFERRED_SETUP, false);
}
/**
* Checks whether an intent is running in "pre-deferred" setup wizard flow.
*
* @param originalIntent The original intent that was used to start the step, usually via
* {@link android.app.Activity#getIntent()}.
* @return true if the intent passed in was running in "pre-deferred" setup wizard.
*/
public static boolean isPreDeferredSetupWizard(Intent originalIntent) {
return originalIntent != null
&& originalIntent.getBooleanExtra(EXTRA_IS_PRE_DEFERRED_SETUP, false);
}
/**
* Checks the intent whether the extra indicates that the light theme should be used or not. If
* the theme is not specified in the intent, or the theme specified is unknown, the value def
@ -236,10 +253,12 @@ public class WizardManagerHelper {
*/
public static boolean isLightTheme(String theme, boolean def) {
if (THEME_HOLO_LIGHT.equals(theme) || THEME_MATERIAL_LIGHT.equals(theme)
|| THEME_GLIF_LIGHT.equals(theme) || THEME_GLIF_V2_LIGHT.equals(theme)) {
|| THEME_GLIF_LIGHT.equals(theme) || THEME_GLIF_V2_LIGHT.equals(theme)
|| THEME_GLIF_V3_LIGHT.equals(theme)) {
return true;
} else if (THEME_HOLO.equals(theme) || THEME_MATERIAL.equals(theme)
|| THEME_GLIF.equals(theme) || THEME_GLIF_V2.equals(theme)) {
|| THEME_GLIF.equals(theme) || THEME_GLIF_V2.equals(theme)
|| THEME_GLIF_V3.equals(theme)) {
return false;
} else {
return def;
@ -284,6 +303,10 @@ public class WizardManagerHelper {
public static @StyleRes int getThemeRes(String theme, @StyleRes int defaultTheme) {
if (theme != null) {
switch (theme) {
case THEME_GLIF_V3_LIGHT:
return R.style.SuwThemeGlifV3_Light;
case THEME_GLIF_V3:
return R.style.SuwThemeGlifV3;
case THEME_GLIF_V2_LIGHT:
return R.style.SuwThemeGlifV2_Light;
case THEME_GLIF_V2:

View file

@ -58,6 +58,10 @@ public class CheckableLinearLayout extends LinearLayout implements Checkable {
super(context, attrs, defStyleAttr, defStyleRes);
}
{
setFocusable(true);
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
if (mChecked) {

View file

@ -137,7 +137,8 @@ public class Illustration extends FrameLayout {
if (mAspectRatio != 0.0f) {
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int illustrationHeight = (int) (parentWidth / mAspectRatio);
illustrationHeight -= illustrationHeight % mBaselineGridSize;
illustrationHeight =
(int) (illustrationHeight - (illustrationHeight % mBaselineGridSize));
setPadding(0, illustrationHeight, 0, 0);
}
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {

View file

@ -23,9 +23,11 @@ import android.graphics.SurfaceTexture;
import android.graphics.drawable.Animatable;
import android.media.MediaPlayer;
import android.os.Build.VERSION_CODES;
import android.support.annotation.Nullable;
import android.support.annotation.RawRes;
import android.support.annotation.VisibleForTesting;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
@ -51,8 +53,11 @@ public class IllustrationVideoView extends TextureView implements Animatable,
MediaPlayer.OnSeekCompleteListener,
MediaPlayer.OnInfoListener {
private static final String TAG = "IllustrationVideoView";
protected float mAspectRatio = 1.0f; // initial guess until we know
@Nullable // Can be null when media player fails to initialize
protected MediaPlayer mMediaPlayer;
private @RawRes int mVideoResId = 0;
@ -129,15 +134,20 @@ public class IllustrationVideoView extends TextureView implements Animatable,
mMediaPlayer = MediaPlayer.create(getContext(), mVideoResId);
mMediaPlayer.setSurface(mSurface);
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.setOnSeekCompleteListener(this);
mMediaPlayer.setOnInfoListener(this);
if (mMediaPlayer != null) {
mMediaPlayer.setSurface(mSurface);
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.setOnSeekCompleteListener(this);
mMediaPlayer.setOnInfoListener(this);
float aspectRatio = (float) mMediaPlayer.getVideoHeight() / mMediaPlayer.getVideoWidth();
if (mAspectRatio != aspectRatio) {
mAspectRatio = aspectRatio;
requestLayout();
float aspectRatio =
(float) mMediaPlayer.getVideoHeight() / mMediaPlayer.getVideoWidth();
if (mAspectRatio != aspectRatio) {
mAspectRatio = aspectRatio;
requestLayout();
}
} else {
Log.wtf(TAG, "Unable to initialize media player for video view");
}
if (getWindowVisibility() == View.VISIBLE) {
start();

View file

@ -0,0 +1,83 @@
/*
* Copyright (C) 2018 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 com.android.setupwizardlib.view;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.view.MotionEvent;
import android.widget.TextView;
/**
* A movement method that tracks the last result of whether touch events are handled. This is
* used to patch the return value of {@link TextView#onTouchEvent} so that it consumes the touch
* events only when the movement method says the event is consumed.
*/
public interface TouchableMovementMethod {
/**
* @return The last touch event received in {@link MovementMethod#onTouchEvent}
*/
MotionEvent getLastTouchEvent();
/**
* @return The return value of the last {@link MovementMethod#onTouchEvent}, or whether the
* last touch event should be considered handled by the text view
*/
boolean isLastTouchEventHandled();
/**
* An extension of LinkMovementMethod that tracks whether the event is handled when it is
* touched.
*/
class TouchableLinkMovementMethod extends LinkMovementMethod
implements TouchableMovementMethod {
public static TouchableLinkMovementMethod getInstance() {
return new TouchableLinkMovementMethod();
}
boolean mLastEventResult = false;
MotionEvent mLastEvent;
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
mLastEvent = event;
boolean result = super.onTouchEvent(widget, buffer, event);
if (event.getAction() == MotionEvent.ACTION_DOWN) {
// Unfortunately, LinkMovementMethod extends ScrollMovementMethod, and it always
// consume the down event. So here we use the selection instead as a hint of whether
// the down event landed on a link.
mLastEventResult = Selection.getSelectionStart(buffer) != -1;
} else {
mLastEventResult = result;
}
return result;
}
@Override
public MotionEvent getLastTouchEvent() {
return mLastEvent;
}
@Override
public boolean isLastTouchEventHandled() {
return mLastEventResult;
}
}
}

View file

@ -15,9 +15,10 @@
limitations under the License.
-->
<!-- TODO(yukl): Bump this file to v28 once we can properly test that -->
<!-- These styles are only included in the platform build, to make sure that they do not
override the corresponding styles in the compatibility build. -->
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- General styles -->
@ -26,6 +27,7 @@
Theme.AppCompat. -->
<style name="SuwThemeMaterial" parent="android:Theme.Material.NoActionBar">
<item name="android:colorAccent">@color/suw_color_accent_dark</item>
<item name="android:colorBackground">@color/suw_color_background_dark</item>
<item name="android:indeterminateTint">@color/suw_progress_bar_color_dark</item>
<!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
<item name="android:indeterminateTintMode">src_in</item>
@ -34,12 +36,14 @@
<item name="android:listPreferredItemPaddingStart">?attr/suwMarginSides</item>
<item name="android:navigationBarColor">@android:color/black</item>
<item name="android:statusBarColor">@android:color/black</item>
<item name="android:textAppearanceListItemSmall">@android:style/TextAppearance.Material.Body1</item>
<item name="android:textAppearanceListItemSmall">@style/TextAppearance.SuwItemSummary</item>
<item name="android:textColorLink">@color/suw_link_color_dark</item>
<item name="android:windowAnimationStyle">@style/Animation.SuwWindowAnimation</item>
<item name="android:windowDisablePreview">true</item>
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="suwButtonAllCaps">true</item>
<item name="suwButtonFontFamily">sans-serif</item>
<item name="suwCardBackground">@drawable/suw_card_bg</item>
<item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
<item name="suwDividerInsetEnd">0dp</item>
@ -54,6 +58,7 @@
<style name="SuwThemeMaterial.Light" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:colorAccent">@color/suw_color_accent_light</item>
<item name="android:colorBackground">@color/suw_color_background_light</item>
<item name="android:indeterminateTint">@color/suw_progress_bar_color_light</item>
<!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
<item name="android:indeterminateTintMode">src_in</item>
@ -62,12 +67,14 @@
<item name="android:listPreferredItemPaddingStart">?attr/suwMarginSides</item>
<item name="android:navigationBarColor">@android:color/black</item>
<item name="android:statusBarColor">@android:color/black</item>
<item name="android:textAppearanceListItemSmall">@android:style/TextAppearance.Material.Body1</item>
<item name="android:textAppearanceListItemSmall">@style/TextAppearance.SuwItemSummary</item>
<item name="android:textColorLink">@color/suw_link_color_light</item>
<item name="android:windowAnimationStyle">@style/Animation.SuwWindowAnimation</item>
<item name="android:windowDisablePreview">true</item>
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="suwButtonAllCaps">true</item>
<item name="suwButtonFontFamily">sans-serif</item>
<item name="suwCardBackground">@drawable/suw_card_bg</item>
<item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
<item name="suwDividerInsetEnd">0dp</item>
@ -84,7 +91,7 @@
<style name="SuwThemeGlif" parent="android:Theme.Material.NoActionBar">
<item name="android:colorAccent">@color/suw_color_accent_glif_dark</item>
<item name="android:colorBackground">@color/suw_glif_background_color_dark</item>
<item name="android:colorPrimary">@color/suw_color_accent_glif_dark</item>
<item name="android:colorPrimary">?android:attr/colorAccent</item>
<item name="android:indeterminateTint">?android:attr/colorPrimary</item>
<!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
<item name="android:indeterminateTintMode">src_in</item>
@ -100,12 +107,16 @@
<item name="android:windowDisablePreview">true</item>
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="suwButtonAllCaps">true</item>
<item name="suwButtonCornerRadius">@dimen/suw_glif_button_corner_radius</item>
<item name="suwButtonFontFamily">sans-serif</item>
<item name="suwColorPrimary">?android:attr/colorPrimary</item>
<item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
<item name="suwDividerInsetEnd">0dp</item>
<item name="suwDividerInsetStart">@dimen/suw_items_glif_icon_divider_inset</item>
<item name="suwDividerInsetStartNoIcon">@dimen/suw_items_glif_text_divider_inset</item>
<item name="suwGlifHeaderGravity">start</item>
<item name="suwGlifIconStyle">@style/SuwGlifIcon</item>
<item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
<item name="suwItemDescriptionTitleStyle">@style/SuwItemTitle.GlifDescription</item>
<item name="suwListItemIconColor">@color/suw_list_item_icon_color_dark</item>
@ -116,7 +127,7 @@
<style name="SuwThemeGlif.Light" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:colorAccent">@color/suw_color_accent_glif_light</item>
<item name="android:colorBackground">@color/suw_glif_background_color_light</item>
<item name="android:colorPrimary">@color/suw_color_accent_glif_light</item>
<item name="android:colorPrimary">?android:attr/colorAccent</item>
<item name="android:indeterminateTint">?android:attr/colorPrimary</item>
<!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
<item name="android:indeterminateTintMode">src_in</item>
@ -132,12 +143,16 @@
<item name="android:windowDisablePreview">true</item>
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="suwButtonAllCaps">true</item>
<item name="suwButtonCornerRadius">@dimen/suw_glif_button_corner_radius</item>
<item name="suwButtonFontFamily">sans-serif</item>
<item name="suwColorPrimary">?android:attr/colorPrimary</item>
<item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
<item name="suwDividerInsetEnd">0dp</item>
<item name="suwDividerInsetStart">@dimen/suw_items_glif_icon_divider_inset</item>
<item name="suwDividerInsetStartNoIcon">@dimen/suw_items_glif_text_divider_inset</item>
<item name="suwGlifHeaderGravity">start</item>
<item name="suwGlifIconStyle">@style/SuwGlifIcon</item>
<item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
<item name="suwItemDescriptionTitleStyle">@style/SuwItemTitle.GlifDescription</item>
<item name="suwListItemIconColor">@color/suw_list_item_icon_color_light</item>
@ -145,6 +160,27 @@
<item name="suwScrollIndicators">bottom</item>
</style>
<style name="SuwThemeGlifV3" parent="SuwThemeGlifV2">
<item name="android:colorAccent">@color/suw_color_accent_glif_v3</item>
<item name="suwButtonAllCaps">false</item>
<item name="suwButtonCornerRadius">@dimen/suw_glif_v3_button_corner_radius</item>
<item name="suwButtonFontFamily">@string/suwFontSecondaryMedium</item>
</style>
<style name="SuwThemeGlifV3.Light" parent="SuwThemeGlifV2.Light">
<item name="android:colorAccent">@color/suw_color_accent_glif_v3</item>
<item name="android:navigationBarColor">@color/suw_glif_v3_nav_bar_color_light</item>
<!-- Ignore NewApi: For some reason lint seems to think this API is new in v28 (b/73514594) -->
<item name="android:navigationBarDividerColor" tools:ignore="NewApi">@color/suw_glif_v3_nav_bar_divider_color_light</item>
<!-- Ignore NewApi: For some reason lint seems to think this API is new in v28 (b/73514594) -->
<item name="android:windowLightNavigationBar" tools:ignore="NewApi">true</item>
<item name="suwButtonAllCaps">false</item>
<item name="suwButtonCornerRadius">@dimen/suw_glif_v3_button_corner_radius</item>
<item name="suwButtonFontFamily">@string/suwFontSecondaryMedium</item>
</style>
<!-- Button styles -->
<style name="SuwGlifButton.Primary" parent="android:Widget.Material.Button.Colored">
@ -154,8 +190,13 @@
<item name="android:buttonStyle">@style/SuwGlifButton.Primary</item>
<!-- Values used in styles -->
<item name="android:fontFamily">?attr/suwButtonFontFamily</item>
<item name="android:paddingLeft">@dimen/suw_glif_button_padding</item>
<item name="android:paddingRight">@dimen/suw_glif_button_padding</item>
<item name="android:textAllCaps">?attr/suwButtonAllCaps</item>
<!-- Values used in themes -->
<item name="android:buttonCornerRadius" tools:ignore="NewApi">?attr/suwButtonCornerRadius</item>
</style>
<style name="SuwGlifButton.Secondary" parent="android:Widget.Material.Button.Borderless.Colored">
@ -166,11 +207,14 @@
<item name="android:theme">@style/SuwGlifButton.Secondary</item>
<!-- Values used in styles -->
<item name="android:fontFamily">?attr/suwButtonFontFamily</item>
<item name="android:minWidth">0dp</item>
<item name="android:paddingLeft">@dimen/suw_glif_button_padding</item>
<item name="android:paddingRight">@dimen/suw_glif_button_padding</item>
<item name="android:textAllCaps">?attr/suwButtonAllCaps</item>
<!-- Values used in themes -->
<item name="android:buttonCornerRadius" tools:ignore="NewApi">?attr/suwButtonCornerRadius</item>
<item name="android:colorControlHighlight">@color/suw_flat_button_highlight</item>
</style>

View file

@ -20,16 +20,18 @@ import android.content.Context;
import android.text.Annotation;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.TextAppearanceSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.TextView;
import com.android.setupwizardlib.span.LinkSpan;
import com.android.setupwizardlib.span.LinkSpan.OnLinkClickListener;
import com.android.setupwizardlib.span.SpanHelper;
import com.android.setupwizardlib.view.TouchableMovementMethod.TouchableLinkMovementMethod;
/**
* An extension of TextView that automatically replaces the annotation tags as specified in
@ -112,7 +114,7 @@ public class RichTextView extends TextView implements OnLinkClickListener {
// nullifying any return values of MovementMethod.onTouchEvent.
// To still allow propagating touch events to the parent when this view doesn't have
// links, we only set the movement method here if the text contains links.
setMovementMethod(LinkMovementMethod.getInstance());
setMovementMethod(TouchableLinkMovementMethod.getInstance());
} else {
setMovementMethod(null);
}
@ -121,6 +123,11 @@ public class RichTextView extends TextView implements OnLinkClickListener {
// as individual TextViews consume touch events and thereby reducing the focus window
// shown by Talkback. Disable focus if there are no links
setFocusable(hasLinks);
// Do not "reveal" (i.e. scroll to) this view when this view is focused. Since this view is
// focusable in touch mode, we may be focused when the screen is first shown, and starting
// a screen halfway scrolled down is confusing to the user.
setRevealOnFocusHint(false);
setFocusableInTouchMode(hasLinks);
}
private boolean hasLinks(CharSequence text) {
@ -132,6 +139,25 @@ public class RichTextView extends TextView implements OnLinkClickListener {
return false;
}
@Override
@SuppressWarnings("ClickableViewAccessibility") // super.onTouchEvent is called
public boolean onTouchEvent(MotionEvent event) {
// Since View#onTouchEvent always return true if the view is clickable (which is the case
// when a TextView has a movement method), override the implementation to allow the movement
// method, if it implements TouchableMovementMethod, to say that the touch is not handled,
// allowing the event to bubble up to the parent view.
boolean superResult = super.onTouchEvent(event);
MovementMethod movementMethod = getMovementMethod();
if (movementMethod instanceof TouchableMovementMethod) {
TouchableMovementMethod touchableMovementMethod =
(TouchableMovementMethod) movementMethod;
if (touchableMovementMethod.getLastTouchEvent() == event) {
return touchableMovementMethod.isLastTouchEventHandled();
}
}
return superResult;
}
public void setOnLinkClickListener(OnLinkClickListener listener) {
mOnLinkClickListener = listener;
}

View file

@ -23,6 +23,11 @@
android:layout_height="match_parent"
android:orientation="vertical">
<ViewStub
android:id="@+id/suw_layout_sticky_header"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!-- Ignore UnusedAttribute: scrollIndicators is new in M. Default to no indicators in older
versions. -->
<com.android.setupwizardlib.view.HeaderRecyclerView

View file

@ -107,10 +107,12 @@ public class GlifRecyclerLayout extends GlifLayout {
}
@Override
public View findManagedViewById(int id) {
// Returning generic type is the common pattern used for findViewBy* methods
@SuppressWarnings("TypeParameterUnusedInFormals")
public <T extends View> T findManagedViewById(int id) {
final View header = mRecyclerMixin.getHeader();
if (header != null) {
final View view = header.findViewById(id);
final T view = header.findViewById(id);
if (view != null) {
return view;
}

View file

@ -127,10 +127,12 @@ public class SetupWizardRecyclerLayout extends SetupWizardLayout {
}
@Override
public View findManagedViewById(int id) {
// Returning generic type is the common pattern used for findViewBy* methods
@SuppressWarnings("TypeParameterUnusedInFormals")
public <T extends View> T findManagedViewById(int id) {
final View header = mRecyclerMixin.getHeader();
if (header != null) {
final View view = header.findViewById(id);
final T view = header.findViewById(id);
if (view != null) {
return view;
}

View file

@ -143,9 +143,9 @@ public class RecyclerItemAdapter extends RecyclerView.Adapter<ItemViewHolder>
@Override
public void onBindViewHolder(ItemViewHolder holder, int position) {
final IItem item = getItem(position);
item.onBindView(holder.itemView);
holder.setEnabled(item.isEnabled());
holder.setItem(item);
item.onBindView(holder.itemView);
}
@Override

View file

@ -27,7 +27,6 @@ import static org.robolectric.RuntimeEnvironment.application;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.OnScrollListener;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import org.junit.Before;
@ -38,7 +37,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@RunWith(SuwLibRobolectricTestRunner.class)
public class RecyclerViewScrollHandlingDelegateTest {

View file

@ -13,6 +13,23 @@ android {
publishNonDefault true
flavorDimensions 'compat'
productFlavors {
// DEPRECATED: Platform version that will not include the compatibility libraries
platformDeprecated {
dimension 'compat'
// TODO(yukl): Bump this file to v28 once we can properly test that
minSdkVersion 27
}
// Provides backwards compatibility for Gingerbread or above, using support libraries.
gingerbreadCompat {
dimension 'compat'
minSdkVersion 9
}
}
sourceSets {
main {
manifest.srcFile 'main/AndroidManifest.xml'
@ -21,43 +38,7 @@ android {
res.srcDirs = ['main/res']
}
productFlavors {
// Platform version that will not include the compatibility libraries
platform {
minSdkVersion 23
dependencies {
// Read the dependencies from the "deps" map in the extra properties.
//
// For builds in the Android tree we want to build the dependencies from source
// for reproducible builds, for example in build.gradle define something like
// this:
// ext {
// deps = ['project-name': project(':project-path')]
// }
//
// For standalone project clients, since the source may not be available, we
// fetch the dependencies from maven. For example in standalone.gradle define
// something like this:
// ext {
// deps = ['project-name': 'com.example.group:project-name:1.0.0']
// }
//
platformCompile deps['support-annotations']
}
}
// Provides backwards compatibility for Gingerbread or above, using support libraries.
gingerbreadCompat {
minSdkVersion 9
dependencies {
gingerbreadCompatCompile deps['support-appcompat-v7']
gingerbreadCompatCompile deps['support-recyclerview-v7']
}
}
}
platform {
platformDeprecated {
java.srcDirs = ['platform/src']
res.srcDirs = ['platform/res']
}
@ -68,3 +49,26 @@ android {
}
}
}
dependencies {
// Read the dependencies from the "deps" map in the extra properties.
//
// For builds in the Android tree we want to build the dependencies from source
// for reproducible builds, for example in build.gradle define something like
// this:
// ext {
// deps = ['project-name': project(':project-path')]
// }
//
// For standalone project clients, since the source may not be available, we
// fetch the dependencies from maven. For example in standalone.gradle define
// something like this:
// ext {
// deps = ['project-name': 'com.example.group:project-name:1.0.0']
// }
//
platformDeprecatedCompile deps['support-annotations']
gingerbreadCompatCompile deps['support-appcompat-v7']
gingerbreadCompatCompile deps['support-recyclerview-v7']
}

View file

@ -5,6 +5,18 @@ apply from: 'standalone-rules.gradle'
apply from: '../tools/gradle/dist-library-instrumentation-tests.gradle'
apply from: '../tools/gradle/dist-unit-tests.gradle'
apply plugin: 'net.ltgt.errorprone'
buildscript {
repositories {
maven { url "$rootDir/prebuilts/tools/common/m2/repository" }
}
dependencies {
classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.0.13"
}
}
// Add targets for tests
android.sourceSets {
androidTest {
@ -13,16 +25,17 @@ android.sourceSets {
res.srcDirs = ['test/instrumentation/res']
dependencies {
androidTestCompile 'com.android.support.test:rules:0.5'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestCompile 'junit:junit:4.+'
androidTestCompile 'org.mockito:mockito-core:1.9.5'
androidTestImplementation 'com.android.support.test:rules:1.0.0'
androidTestImplementation 'com.android.support.test:runner:1.0.0'
androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestImplementation 'com.google.truth:truth:0.31'
androidTestImplementation 'junit:junit:4.+'
androidTestImplementation 'org.mockito:mockito-core:1.9.5'
}
}
androidTestPlatform {
androidTestPlatformDeprecated {
java.srcDirs = ['platform/test/src']
}
@ -38,12 +51,13 @@ android.sourceSets {
java.srcDirs = ['test/robotest/src']
dependencies {
testCompile 'org.robolectric:robolectric:3.+'
testCompile 'org.robolectric:shadows-core:3.+'
testCompile 'junit:junit:4.+'
testCompile 'org.mockito:mockito-core:1.9.5'
testImplementation 'org.robolectric:robolectric:3.6.1'
testImplementation 'org.robolectric:shadows-framework:3.6.1'
testImplementation 'junit:junit:4.+'
testImplementation 'com.google.truth:truth:0.31'
testImplementation 'org.mockito:mockito-core:1.9.5'
// Workaround for https://github.com/robolectric/robolectric/issues/2566
testCompile 'org.khronos:opengl-api:gl1.1-android-2.1_r1'
testImplementation 'org.khronos:opengl-api:gl1.1-android-2.1_r1'
}
}
@ -51,6 +65,9 @@ android.sourceSets {
java.srcDirs = ['gingerbread/test/robotest/src', 'recyclerview/test/robotest/src']
}
}
android.testOptions.unitTests.includeAndroidResources = true
android.defaultConfig.testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
android.lintOptions {
abortOnError true
@ -64,14 +81,3 @@ android.lintOptions {
android.libraryVariants.all { variant ->
variant.assemble.dependsOn(tasks.findByName('lint'))
}
// For compatibility with existing continuous test configurations, copy the file to
// setup-wizard-libTest.apk
// TODO: Remove this once continuous test configurations are updated to handle the new file name
task createLegacyTestApk(type: Copy) {
from "${project.ext.distDir}/setup-wizard-lib-gingerbreadCompat-debug-androidTest.apk"
into "${project.ext.distDir}"
rename ('setup-wizard-lib-gingerbreadCompat-debug-androidTest.apk', 'setup-wizard-libTest.apk')
}
tasks.dist.finalizedBy createLegacyTestApk

View file

@ -16,5 +16,5 @@
apply from: 'standalone-rules.gradle'
android.compileSdkVersion 25
android.buildToolsVersion '25.0.0'
android.compileSdkVersion 26
android.buildToolsVersion '26.0.0'

View file

@ -19,7 +19,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.android.setupwizardlib.test">
<uses-sdk tools:overrideLibrary="android.support.test,android.app,android.support.test.rule" />
<uses-sdk tools:overrideLibrary="android.support.test.rules,android.support.test.runner" />
<application>
<activity android:name=".util.DrawingTestActivity" />

View file

@ -16,6 +16,8 @@
package com.android.setupwizardlib.template;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.Matchers.eq;
@ -27,10 +29,12 @@ import android.content.res.XmlResourceParser;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.util.Xml;
import android.view.View;
import android.widget.ImageView;
import com.android.setupwizardlib.TemplateLayout;
@ -74,6 +78,26 @@ public class IconMixinTest {
mixin.setIcon(drawable);
assertSame(drawable, mIconView.getDrawable());
assertEquals(View.VISIBLE, mIconView.getVisibility());
}
@Test
public void setIcon_resourceId_shouldSetIcon() {
int icon = android.R.drawable.ic_menu_add;
IconMixin mixin = new IconMixin(mTemplateLayout, null, 0);
mixin.setIcon(icon);
Drawable drawable = mIconView.getDrawable();
assertThat(drawable).isInstanceOf(BitmapDrawable.class);
assertEquals(View.VISIBLE, mIconView.getVisibility());
}
@Test
public void setIcon_shouldSetVisibilityToGone_whenIconIsNull() {
IconMixin mixin = new IconMixin(mTemplateLayout, null, 0);
mixin.setIcon(null);
assertEquals(View.GONE, mIconView.getVisibility());
}
@Test
@ -101,5 +125,20 @@ public class IconMixinTest {
.getDrawable(android.R.drawable.ic_menu_add);
final BitmapDrawable actual = (BitmapDrawable) mIconView.getDrawable();
assertEquals(expected.getBitmap(), actual.getBitmap());
assertEquals(View.VISIBLE, mIconView.getVisibility());
}
@Test
public void setContentDescription_shouldSetContentDescriptionOnIconView() {
IconMixin mixin = new IconMixin(mTemplateLayout, null, 0);
mixin.setContentDescription("hello world");
assertThat(mIconView.getContentDescription()).isEqualTo("hello world");
}
@Test
public void getContentDescription_shouldReturnContentDescriptionFromView() {
IconMixin mixin = new IconMixin(mTemplateLayout, null, 0);
mIconView.setContentDescription("aloha");
assertThat(mixin.getContentDescription()).isEqualTo("aloha");
}
}

View file

@ -137,7 +137,7 @@ public class GlifPatternDrawableTest {
assertEquals("Matrices should match", expected, canvas.getMatrix());
}
@SmallTest
@Test
public void testScaleToCanvasMaxSize() {
final Canvas canvas = new Canvas();
final Matrix expected = new Matrix(canvas.getMatrix());

View file

@ -16,6 +16,8 @@
package com.android.setupwizardlib.test;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import android.annotation.SuppressLint;
@ -31,6 +33,7 @@ import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.SmallTest;
import android.support.test.rule.UiThreadTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
@ -131,7 +134,9 @@ public class SystemBarHelperTest {
@Test
public void testShowSystemBarsWindow() {
final Window window = createWindowWithSystemUiVisibility(0x456);
SystemBarHelper.showSystemBars(window, InstrumentationRegistry.getContext());
Context context = new ContextThemeWrapper(
InstrumentationRegistry.getContext(), android.R.style.Theme);
SystemBarHelper.showSystemBars(window, context);
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
assertEquals(
"DEFAULT_IMMERSIVE_FLAGS should be removed from window's systemUiVisibility",
@ -191,11 +196,15 @@ public class SystemBarHelperTest {
@UiThreadTest
@Test
public void testSetBackButtonVisibleTrue() {
final Window window = createWindowWithSystemUiVisibility(0x456);
final Window window = createWindowWithSystemUiVisibility(STATUS_BAR_DISABLE_BACK | 0x456);
SystemBarHelper.setBackButtonVisible(window, true);
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
assertEquals("View visibility should be 0x456", 0x456,
window.getAttributes().systemUiVisibility);
assertThat(window.getAttributes().systemUiVisibility)
.named("window sysUiVisibility")
.isEqualTo(0x456);
assertThat(window.getDecorView().getSystemUiVisibility())
.named("decor view sysUiVisibility")
.isEqualTo(0x456);
}
}
@ -205,8 +214,12 @@ public class SystemBarHelperTest {
final Window window = createWindowWithSystemUiVisibility(0x456);
SystemBarHelper.setBackButtonVisible(window, false);
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
assertEquals("STATUS_BAR_DISABLE_BACK should be added to systemUiVisibility",
0x456 | STATUS_BAR_DISABLE_BACK, window.getAttributes().systemUiVisibility);
assertThat(window.getAttributes().systemUiVisibility)
.named("window sysUiVisibility")
.isEqualTo(0x456 | STATUS_BAR_DISABLE_BACK);
assertThat(window.getDecorView().getSystemUiVisibility())
.named("decor view sysUiVisibility")
.isEqualTo(0x456 | STATUS_BAR_DISABLE_BACK);
}
}

View file

@ -32,6 +32,8 @@ import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.annotation.IdRes;
import android.view.ContextThemeWrapper;
import android.view.View;
@ -53,7 +55,7 @@ import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
public class GlifLayoutTest {
private Context mContext;
@ -266,6 +268,74 @@ public class GlifLayoutTest {
assertNotNull(layout.findViewById(android.R.id.text1));
}
@Test
public void inflateStickyHeader_shouldAddViewToLayout() {
GlifLayout layout = new GlifLayout(mContext);
final View view = layout.inflateStickyHeader(android.R.layout.simple_list_item_1);
assertEquals(android.R.id.text1, view.getId());
assertNotNull(layout.findViewById(android.R.id.text1));
}
@Config(qualifiers = "sw600dp")
@Test
public void inflateStickyHeader_whenOnTablets_shouldAddViewToLayout() {
inflateStickyHeader_shouldAddViewToLayout();
}
@Test
public void inflateStickyHeader_whenInXml_shouldAddViewToLayout() {
GlifLayout layout = new GlifLayout(
mContext,
Robolectric.buildAttributeSet()
.addAttribute(R.attr.suwStickyHeader, "@android:layout/simple_list_item_1")
.build());
assertNotNull(layout.findViewById(android.R.id.text1));
}
@Test
public void inflateStickyHeader_whenOnBlankTemplate_shouldAddViewToLayout() {
GlifLayout layout = new GlifLayout(mContext, R.layout.suw_glif_blank_template);
final View view = layout.inflateStickyHeader(android.R.layout.simple_list_item_1);
assertEquals(android.R.id.text1, view.getId());
assertNotNull(layout.findViewById(android.R.id.text1));
}
@Config(qualifiers = "sw600dp")
@Test
public void inflateStickyHeader_whenOnBlankTemplateTablet_shouldAddViewToLayout() {
inflateStickyHeader_whenOnBlankTemplate_shouldAddViewToLayout();
}
@Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.NEWEST_SDK)
@Test
public void createFromXml_shouldSetLayoutFullscreen_whenLayoutFullscreenIsNotSet() {
GlifLayout layout = new GlifLayout(
mContext,
Robolectric.buildAttributeSet()
.build());
if (VERSION.SDK_INT >= VERSION_CODES.M) {
assertEquals(
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN,
layout.getSystemUiVisibility() & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
}
@Test
public void createFromXml_shouldNotSetLayoutFullscreen_whenLayoutFullscreenIsFalse() {
GlifLayout layout = new GlifLayout(
mContext,
Robolectric.buildAttributeSet()
.addAttribute(R.attr.suwLayoutFullscreen, "false")
.build());
assertEquals(
0,
layout.getSystemUiVisibility() & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
private Drawable getPhoneBackground(GlifLayout layout) {
final StatusBarBackgroundLayout patternBg =
(StatusBarBackgroundLayout) layout.findManagedViewById(R.id.suw_pattern_bg);

View file

@ -0,0 +1,118 @@
/*
* Copyright (C) 2018 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 com.android.setupwizardlib.gesture;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.robolectric.RuntimeEnvironment.application;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@RunWith(SuwLibRobolectricTestRunner.class)
public class ConsecutiveTapsGestureDetectorTest {
@Mock
private ConsecutiveTapsGestureDetector.OnConsecutiveTapsListener mListener;
private ConsecutiveTapsGestureDetector mDetector;
private int mSlop;
private int mTapTimeout;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
View view = new View(application);
view.measure(500, 500);
view.layout(0, 0, 500, 500);
mDetector = new ConsecutiveTapsGestureDetector(mListener, view);
mSlop = ViewConfiguration.get(application).getScaledDoubleTapSlop();
mTapTimeout = ViewConfiguration.getDoubleTapTimeout();
}
@Test
public void onTouchEvent_shouldTriggerCallbackOnFourTaps() {
InOrder inOrder = inOrder(mListener);
tap(0, 25f, 25f);
inOrder.verify(mListener).onConsecutiveTaps(eq(1));
tap(100, 25f, 25f);
inOrder.verify(mListener).onConsecutiveTaps(eq(2));
tap(200, 25f, 25f);
inOrder.verify(mListener).onConsecutiveTaps(eq(3));
tap(300, 25f, 25f);
inOrder.verify(mListener).onConsecutiveTaps(eq(4));
}
@Test
public void onTouchEvent_tapOnDifferentLocation_shouldResetCounter() {
InOrder inOrder = inOrder(mListener);
tap(0, 25f, 25f);
inOrder.verify(mListener).onConsecutiveTaps(eq(1));
tap(100, 25f, 25f);
inOrder.verify(mListener).onConsecutiveTaps(eq(2));
tap(200, 25f + mSlop * 2, 25f);
inOrder.verify(mListener).onConsecutiveTaps(eq(1));
tap(300, 25f + mSlop * 2, 25f);
inOrder.verify(mListener).onConsecutiveTaps(eq(2));
}
@Test
public void onTouchEvent_tapAfterTimeout_shouldResetCounter() {
InOrder inOrder = inOrder(mListener);
tap(0, 25f, 25f);
inOrder.verify(mListener).onConsecutiveTaps(eq(1));
tap(100, 25f, 25f);
inOrder.verify(mListener).onConsecutiveTaps(eq(2));
tap(200 + mTapTimeout, 25f, 25f);
inOrder.verify(mListener).onConsecutiveTaps(eq(1));
tap(300 + mTapTimeout, 25f, 25f);
inOrder.verify(mListener).onConsecutiveTaps(eq(2));
}
private void tap(int timeMillis, float x, float y) {
mDetector.onTouchEvent(
MotionEvent.obtain(timeMillis, timeMillis, MotionEvent.ACTION_DOWN, x, y, 0));
mDetector.onTouchEvent(
MotionEvent.obtain(timeMillis, timeMillis + 10, MotionEvent.ACTION_UP, x, y, 0));
}
}

View file

@ -39,7 +39,6 @@ import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.items.ButtonItem.OnClickListener;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@ -50,9 +49,7 @@ import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(
constants = BuildConfig.class,
sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
public class ButtonItemTest {
private ViewGroup mParent;

View file

@ -24,7 +24,6 @@ import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import org.junit.Before;
@ -36,9 +35,7 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(
constants = BuildConfig.class,
sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
public class ItemGroupTest {
private static final Item CHILD_1 = new EqualsItem("Child 1");

View file

@ -1,126 +0,0 @@
/*
* Copyright (C) 2017 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 com.android.setupwizardlib.robolectric;
import org.robolectric.annotation.Config;
import org.robolectric.internal.GradleManifestFactory;
import org.robolectric.internal.ManifestIdentifier;
import org.robolectric.res.FileFsFile;
import org.robolectric.util.Logger;
import org.robolectric.util.ReflectionHelpers;
import java.io.File;
import java.net.URL;
/**
* Modified GradleManifestFactory to patch an issue where some build variants have merged
* resources under res/merged/variant/type while others have it under bundles/variant/type/res.
*
* The change is that in the .exists() checks below we check for specific folders for the build
* variant rather than checking existence at the parent directory.
*/
class PatchedGradleManifestFactory extends GradleManifestFactory {
@Override
public ManifestIdentifier identify(Config config) {
if (config.constants() == Void.class) {
Logger.error("Field 'constants' not specified in @Config annotation");
Logger.error("This is required when using Robolectric with Gradle!");
throw new RuntimeException("No 'constants' field in @Config annotation!");
}
final String buildOutputDir = getBuildOutputDir(config);
final String type = getType(config);
final String flavor = getFlavor(config);
final String abiSplit = getAbiSplit(config);
final String packageName = config.packageName().isEmpty()
? config.constants().getPackage().getName()
: config.packageName();
final FileFsFile res;
final FileFsFile assets;
final FileFsFile manifest;
if (FileFsFile.from(buildOutputDir, "data-binding-layout-out", flavor, type).exists()) {
// Android gradle plugin 1.5.0+ puts the merged layouts in data-binding-layout-out.
// https://github.com/robolectric/robolectric/issues/2143
res = FileFsFile.from(buildOutputDir, "data-binding-layout-out", flavor, type);
} else if (FileFsFile.from(buildOutputDir, "res", "merged", flavor, type).exists()) {
// res/merged added in Android Gradle plugin 1.3-beta1
res = FileFsFile.from(buildOutputDir, "res", "merged", flavor, type);
} else if (FileFsFile.from(buildOutputDir, "res", flavor, type).exists()) {
res = FileFsFile.from(buildOutputDir, "res", flavor, type);
} else {
res = FileFsFile.from(buildOutputDir, "bundles", flavor, type, "res");
}
if (FileFsFile.from(buildOutputDir, "assets", flavor, type).exists()) {
assets = FileFsFile.from(buildOutputDir, "assets", flavor, type);
} else {
assets = FileFsFile.from(buildOutputDir, "bundles", flavor, type, "assets");
}
String manifestName = config.manifest();
URL manifestUrl = getClass().getClassLoader().getResource(manifestName);
if (manifestUrl != null && manifestUrl.getProtocol().equals("file")) {
manifest = FileFsFile.from(manifestUrl.getPath());
} else if (FileFsFile.from(buildOutputDir, "manifests", "full", flavor, abiSplit, type,
manifestName).exists()) {
manifest = FileFsFile.from(
buildOutputDir, "manifests", "full", flavor, abiSplit, type, manifestName);
} else if (FileFsFile.from(buildOutputDir, "manifests", "aapt", flavor, abiSplit, type,
manifestName).exists()) {
// Android gradle plugin 2.2.0+ can put library manifest files inside of "aapt"
// instead of "full"
manifest = FileFsFile.from(buildOutputDir, "manifests", "aapt", flavor, abiSplit,
type, manifestName);
} else {
manifest = FileFsFile.from(buildOutputDir, "bundles", flavor, abiSplit, type,
manifestName);
}
return new ManifestIdentifier(manifest, res, assets, packageName, null);
}
private static String getBuildOutputDir(Config config) {
return config.buildDir() + File.separator + "intermediates";
}
private static String getType(Config config) {
try {
return ReflectionHelpers.getStaticField(config.constants(), "BUILD_TYPE");
} catch (Throwable e) {
return null;
}
}
private static String getFlavor(Config config) {
try {
return ReflectionHelpers.getStaticField(config.constants(), "FLAVOR");
} catch (Throwable e) {
return null;
}
}
private static String getAbiSplit(Config config) {
try {
return config.abiSplit();
} catch (Throwable e) {
return null;
}
}
}

View file

@ -20,42 +20,13 @@ import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.internal.ManifestFactory;
public class SuwLibRobolectricTestRunner extends RobolectricTestRunner {
private String mModuleRootPath;
public SuwLibRobolectricTestRunner(Class<?> testClass) throws InitializationError {
super(testClass);
}
// Hack to determine the module root path in the build folder (e.g. out/gradle/setup-wizard-lib)
private void updateModuleRootPath(Config config) {
String moduleRoot = config.constants().getResource("").toString()
.replace("file:", "").replace("jar:", "");
mModuleRootPath =
moduleRoot.substring(0, moduleRoot.lastIndexOf("/build")) + "/setup-wizard-lib";
}
/**
* Return the default config used to run Robolectric tests.
*/
@Override
protected Config buildGlobalConfig() {
Config parent = super.buildGlobalConfig();
updateModuleRootPath(parent);
return new Config.Builder(parent)
.setBuildDir(mModuleRootPath + "/build")
.build();
}
@Override
protected ManifestFactory getManifestFactory(Config config) {
return new PatchedGradleManifestFactory();
}
@Override
protected void runChild(FrameworkMethod method, RunNotifier notifier) {
System.out.println("===== Running " + method + " =====");

View file

@ -0,0 +1,51 @@
/*
* Copyright (C) 2018 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 com.android.setupwizardlib.shadow;
import android.util.Log;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@Implements(Log.class)
public class ShadowLog extends org.robolectric.shadows.ShadowLog {
public static boolean sWtfIsFatal = true;
public static class TerribleFailure extends RuntimeException {
public TerribleFailure(String msg, Throwable cause) {
super(msg, cause);
}
}
@Implementation
public static void wtf(String tag, String msg) {
org.robolectric.shadows.ShadowLog.wtf(tag, msg);
if (sWtfIsFatal) {
throw new TerribleFailure(msg, null);
}
}
@Implementation
public static void wtf(String tag, String msg, Throwable throwable) {
org.robolectric.shadows.ShadowLog.wtf(tag, msg, throwable);
if (sWtfIsFatal) {
throw new TerribleFailure(msg, throwable);
}
}
}

View file

@ -16,26 +16,28 @@
package com.android.setupwizardlib.span;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertSame;
import static org.robolectric.RuntimeEnvironment.application;
import android.content.Context;
import android.content.ContextWrapper;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class LinkSpanTest {
@Test
public void testOnClick() {
public void onClick_shouldCallListenerOnContext() {
final TestContext context = new TestContext(application);
final TextView textView = new TextView(context);
final LinkSpan linkSpan = new LinkSpan("test_id");
@ -46,7 +48,7 @@ public class LinkSpanTest {
}
@Test
public void testNonImplementingContext() {
public void onClick_contextDoesNotImplementOnClickListener_shouldBeNoOp() {
final TextView textView = new TextView(application);
final LinkSpan linkSpan = new LinkSpan("test_id");
@ -57,7 +59,7 @@ public class LinkSpanTest {
}
@Test
public void testWrappedListener() {
public void onClick_contextWrapsOnClickListener_shouldCallWrappedListener() {
final TestContext context = new TestContext(application);
final Context wrapperContext = new ContextWrapper(context);
final TextView textView = new TextView(wrapperContext);
@ -68,6 +70,28 @@ public class LinkSpanTest {
assertSame("Clicked LinkSpan should be passed to setup", linkSpan, context.clickedSpan);
}
@Test
public void onClick_shouldClearSelection() {
final TestContext context = new TestContext(application);
final TextView textView = new TextView(context);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setFocusable(true);
textView.setFocusableInTouchMode(true);
final LinkSpan linkSpan = new LinkSpan("test_id");
SpannableStringBuilder text = new SpannableStringBuilder("Lorem ipsum dolor sit");
textView.setText(text);
text.setSpan(linkSpan, /* start= */ 0, /* end= */ 5, /* flags= */ 0);
// Simulate the touch effect set by TextView when touched.
Selection.setSelection(text, /* start= */ 0, /* end= */ 5);
linkSpan.onClick(textView);
assertThat(Selection.getSelectionStart(textView.getText())).isEqualTo(0);
assertThat(Selection.getSelectionEnd(textView.getText())).isEqualTo(0);
}
@SuppressWarnings("deprecation")
private static class TestContext extends ContextWrapper implements LinkSpan.OnClickListener {
public LinkSpan clickedSpan = null;

View file

@ -31,7 +31,6 @@ import android.widget.AbsListView.OnScrollListener;
import android.widget.BaseAdapter;
import android.widget.ListView;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import org.junit.Before;
@ -42,7 +41,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@RunWith(SuwLibRobolectricTestRunner.class)
public class ListViewScrollHandlingDelegateTest {

View file

@ -34,7 +34,6 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.TemplateLayout;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import com.android.setupwizardlib.template.RequireScrollMixin.OnRequireScrollStateChangedListener;
@ -48,7 +47,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@RunWith(SuwLibRobolectricTestRunner.class)
public class RequireScrollMixinTest {

View file

@ -23,7 +23,6 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.robolectric.RuntimeEnvironment.application;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import com.android.setupwizardlib.view.BottomScrollView;
import com.android.setupwizardlib.view.BottomScrollView.BottomScrollListener;
@ -36,7 +35,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@RunWith(SuwLibRobolectricTestRunner.class)
public class ScrollViewScrollHandlingDelegateTest {

View file

@ -26,7 +26,6 @@ import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@ -36,7 +35,7 @@ import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = Config.ALL_SDKS)
@Config(sdk = Config.ALL_SDKS)
public class GlifDimensionTest {
private Context mContext;

View file

@ -16,8 +16,10 @@
package com.android.setupwizardlib.util;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.robolectric.RuntimeEnvironment.application;
import android.annotation.TargetApi;
@ -29,8 +31,8 @@ import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.ContextThemeWrapper;
import android.widget.Button;
import android.widget.ProgressBar;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@ -41,7 +43,7 @@ import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = {Config.OLDEST_SDK, Config.NEWEST_SDK})
@Config(sdk = {Config.OLDEST_SDK, Config.NEWEST_SDK})
public class GlifStyleTest {
private Context mContext;
@ -58,9 +60,8 @@ public class GlifStyleTest {
Robolectric.buildAttributeSet()
.setStyleAttribute("@style/SuwGlifButton.Tertiary")
.build());
assertNull("Background of tertiary button should be null", button.getBackground());
assertNull("Tertiary button should have no transformation method",
button.getTransformationMethod());
assertThat(button.getBackground()).named("background").isNotNull();
assertThat(button.getTransformationMethod()).named("transformation method").isNull();
if (VERSION.SDK_INT < VERSION_CODES.M) {
// Robolectric resolved the wrong theme attribute on versions >= M
// https://github.com/robolectric/robolectric/issues/2940
@ -76,6 +77,15 @@ public class GlifStyleTest {
assertEquals(0x00000000, activity.getWindow().getStatusBarColor());
}
@Test
public void glifLoadingScreen_shouldHaveProgressBar() {
GlifThemeActivity activity = Robolectric.setupActivity(GlifThemeActivity.class);
activity.setContentView(R.layout.suw_glif_loading_screen);
assertTrue("Progress bar should exist",
activity.findViewById(R.id.suw_large_progress_bar) instanceof ProgressBar);
}
private static class GlifThemeActivity extends Activity {
@Override

View file

@ -0,0 +1,84 @@
/*
* Copyright (C) 2018 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 com.android.setupwizardlib.util;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.app.Activity;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.Button;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.NEWEST_SDK)
public class GlifV3StyleTest {
@Test
public void activityWithGlifV3Theme_shouldUseLightNavBarOnV27OrAbove() {
GlifThemeActivity activity = Robolectric.setupActivity(GlifThemeActivity.class);
if (VERSION.SDK_INT >= VERSION_CODES.O_MR1) {
assertEquals(
activity.getWindow().getNavigationBarColor(),
Color.WHITE);
int vis = activity.getWindow().getDecorView().getSystemUiVisibility();
assertTrue((vis & View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0);
} else if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
assertEquals(
activity.getWindow().getNavigationBarColor(),
Color.BLACK);
}
// Nav bar color is not customizable pre-L
}
@Test
public void buttonWithGlifV3_shouldBeGoogleSans() {
GlifThemeActivity activity = Robolectric.setupActivity(GlifThemeActivity.class);
Button button = new Button(
activity,
Robolectric.buildAttributeSet()
.setStyleAttribute("@style/SuwGlifButton.Primary")
.build());
assertThat(button.getTypeface()).isEqualTo(Typeface.create("google-sans", 0));
// Button should not be all caps
assertThat(button.getTransformationMethod()).isNull();
}
private static class GlifThemeActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
setTheme(R.style.SuwThemeGlifV3_Light);
super.onCreate(savedInstanceState);
}
}
}

View file

@ -31,29 +31,33 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import com.android.setupwizardlib.util.Partner.ResourceEntry;
import com.android.setupwizardlib.util.PartnerTest.ShadowApplicationPackageManager;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.res.builder.DefaultPackageManager;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowResources;
import java.util.Arrays;
import java.util.Collections;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@Config(
sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK },
shadows = ShadowApplicationPackageManager.class)
public class PartnerTest {
private static final String ACTION_PARTNER_CUSTOMIZATION =
@ -62,7 +66,7 @@ public class PartnerTest {
private Context mContext;
private Resources mPartnerResources;
private TestPackageManager mPackageManager;
private ShadowApplicationPackageManager mPackageManager;
@Before
public void setUp() throws Exception {
@ -71,8 +75,9 @@ public class PartnerTest {
mContext = spy(application);
mPartnerResources = spy(ShadowResources.getSystem());
mPackageManager = new TestPackageManager();
RuntimeEnvironment.setRobolectricPackageManager(mPackageManager);
mPackageManager =
(ShadowApplicationPackageManager) Shadows.shadowOf(application.getPackageManager());
mPackageManager.partnerResources = mPartnerResources;
}
@Test
@ -126,6 +131,20 @@ public class PartnerTest {
assertTrue("Partner value should come from overlay", entry.isOverlay);
}
@Test
public void getColor_shouldReturnPartnerValueIfPresent() {
final int expectedPartnerColor = 1111;
doReturn(12345).when(mPartnerResources)
.getIdentifier(eq("suw_color_accent_dark"), eq("color"), anyString());
doReturn(expectedPartnerColor).when(mPartnerResources).getColor(eq(12345));
mPackageManager.addResolveInfoForIntent(
new Intent(ACTION_PARTNER_CUSTOMIZATION),
Arrays.asList(createResolveInfo("test.partner.package", true, true)));
final int foundColor = Partner.getColor(mContext, R.color.suw_color_accent_dark);
assertEquals("Partner color should be overlayed to: " + expectedPartnerColor,
expectedPartnerColor, foundColor);
}
@Test
public void testLoadDefaultValue() {
mPackageManager.addResolveInfoForIntent(
@ -173,13 +192,18 @@ public class PartnerTest {
return info;
}
private class TestPackageManager extends DefaultPackageManager {
@Implements(className = "android.app.ApplicationPackageManager")
public static class ShadowApplicationPackageManager extends
org.robolectric.shadows.ShadowApplicationPackageManager {
public Resources partnerResources;
@Implementation
@Override
public Resources getResourcesForApplication(ApplicationInfo app)
throws NameNotFoundException {
if (app != null && "test.partner.package".equals(app.packageName)) {
return mPartnerResources;
return partnerResources;
} else {
return super.getResourcesForApplication(app);
}

View file

@ -31,7 +31,6 @@ import android.provider.Settings.Global;
import android.provider.Settings.Secure;
import android.support.annotation.StyleRes;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@ -39,8 +38,12 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = Config.NEWEST_SDK)
@Config(sdk = Config.NEWEST_SDK)
public class WizardManagerHelperTest {
@Test
@ -87,6 +90,14 @@ public class WizardManagerHelperTest {
WizardManagerHelper.isDeferredSetupWizard(intent));
}
@Test
public void testIsPreDeferredSetupTrue() {
final Intent intent = new Intent();
intent.putExtra("preDeferredSetup", true);
assertTrue("Is pre-deferred setup wizard should be true",
WizardManagerHelper.isPreDeferredSetupWizard(intent));
}
@Test
public void testIsSetupWizardFalse() {
final Intent intent = new Intent();
@ -96,75 +107,57 @@ public class WizardManagerHelperTest {
}
@Test
public void testHoloIsNotLightTheme() {
final Intent intent = new Intent();
intent.putExtra("theme", "holo");
assertFalse("Theme holo should not be light theme",
WizardManagerHelper.isLightTheme(intent, true));
public void isLightTheme_shouldReturnTrue_whenThemeIsLight() {
List<String> lightThemes = Arrays.asList(
"holo_light",
"material_light",
"glif_light",
"glif_v2_light",
"glif_v3_light"
);
ArrayList<String> unexpectedIntentThemes = new ArrayList<>();
ArrayList<String> unexpectedStringThemes = new ArrayList<>();
for (final String theme : lightThemes) {
Intent intent = new Intent();
intent.putExtra(WizardManagerHelper.EXTRA_THEME, theme);
if (!WizardManagerHelper.isLightTheme(intent, false)) {
unexpectedIntentThemes.add(theme);
}
if (!WizardManagerHelper.isLightTheme(theme, false)) {
unexpectedStringThemes.add(theme);
}
}
assertTrue("Intent themes " + unexpectedIntentThemes + " should be light",
unexpectedIntentThemes.isEmpty());
assertTrue("String themes " + unexpectedStringThemes + " should be light",
unexpectedStringThemes.isEmpty());
}
@Test
public void testHoloLightIsLightTheme() {
final Intent intent = new Intent();
intent.putExtra("theme", "holo_light");
assertTrue("Theme holo_light should be light theme",
WizardManagerHelper.isLightTheme(intent, false));
}
@Test
public void testMaterialIsNotLightTheme() {
final Intent intent = new Intent();
intent.putExtra("theme", "material");
assertFalse("Theme material should not be light theme",
WizardManagerHelper.isLightTheme(intent, true));
}
@Test
public void testMaterialLightIsLightTheme() {
final Intent intent = new Intent();
intent.putExtra("theme", "material_light");
assertTrue("Theme material_light should be light theme",
WizardManagerHelper.isLightTheme(intent, false));
}
@Test
public void testGlifIsDarkTheme() {
final Intent intent = new Intent();
intent.putExtra("theme", "glif");
assertFalse("Theme glif should be dark theme",
WizardManagerHelper.isLightTheme(intent, false));
assertFalse("Theme glif should be dark theme",
WizardManagerHelper.isLightTheme(intent, true));
}
@Test
public void testGlifLightIsLightTheme() {
final Intent intent = new Intent();
intent.putExtra("theme", "glif_light");
assertTrue("Theme glif_light should be light theme",
WizardManagerHelper.isLightTheme(intent, false));
assertTrue("Theme glif_light should be light theme",
WizardManagerHelper.isLightTheme(intent, true));
}
@Test
public void testGlifV2IsDarkTheme() {
final Intent intent = new Intent();
intent.putExtra("theme", "glif_v2");
assertFalse("Theme glif_v2 should be dark theme",
WizardManagerHelper.isLightTheme(intent, false));
assertFalse("Theme glif_v2 should be dark theme",
WizardManagerHelper.isLightTheme(intent, true));
}
@Test
public void testGlifV2LightIsLightTheme() {
final Intent intent = new Intent();
intent.putExtra("theme", "glif_v2_light");
assertTrue("Theme glif_v2_light should be light theme",
WizardManagerHelper.isLightTheme(intent, false));
assertTrue("Theme glif_v2_light should be light theme",
WizardManagerHelper.isLightTheme(intent, true));
public void isLightTheme_shouldReturnFalse_whenThemeIsNotLight() {
List<String> lightThemes = Arrays.asList(
"holo",
"material",
"glif",
"glif_v2",
"glif_v3"
);
ArrayList<String> unexpectedIntentThemes = new ArrayList<>();
ArrayList<String> unexpectedStringThemes = new ArrayList<>();
for (final String theme : lightThemes) {
Intent intent = new Intent();
intent.putExtra(WizardManagerHelper.EXTRA_THEME, theme);
if (WizardManagerHelper.isLightTheme(intent, true)) {
unexpectedIntentThemes.add(theme);
}
if (WizardManagerHelper.isLightTheme(theme, true)) {
unexpectedStringThemes.add(theme);
}
}
assertTrue("Intent themes " + unexpectedIntentThemes + " should not be light",
unexpectedIntentThemes.isEmpty());
assertTrue("String themes " + unexpectedStringThemes + " should not be light",
unexpectedStringThemes.isEmpty());
}
@Test
@ -187,19 +180,15 @@ public class WizardManagerHelperTest {
}
@Test
public void testIsLightThemeString() {
assertTrue("isLightTheme should return true for material_light",
WizardManagerHelper.isLightTheme("material_light", false));
assertFalse("isLightTheme should return false for material",
WizardManagerHelper.isLightTheme("material", false));
assertTrue("isLightTheme should return true for holo_light",
WizardManagerHelper.isLightTheme("holo_light", false));
assertFalse("isLightTheme should return false for holo",
WizardManagerHelper.isLightTheme("holo", false));
assertTrue("isLightTheme should return default value true",
WizardManagerHelper.isLightTheme("abracadabra", true));
assertFalse("isLightTheme should return default value false",
WizardManagerHelper.isLightTheme("abracadabra", false));
public void testGetThemeResGlifV3Light() {
assertEquals(R.style.SuwThemeGlifV3_Light,
WizardManagerHelper.getThemeRes("glif_v3_light", 0));
}
@Test
public void testGetThemeResGlifV3() {
assertEquals(R.style.SuwThemeGlifV3,
WizardManagerHelper.getThemeRes("glif_v3", 0));
}
@Test
@ -266,6 +255,7 @@ public class WizardManagerHelperTest {
.putExtra(WizardManagerHelper.EXTRA_WIZARD_BUNDLE, wizardBundle)
.putExtra(WizardManagerHelper.EXTRA_IS_FIRST_RUN, true)
.putExtra(WizardManagerHelper.EXTRA_IS_DEFERRED_SETUP, true)
.putExtra(WizardManagerHelper.EXTRA_IS_PRE_DEFERRED_SETUP, true)
// Script URI and Action ID are kept for backwards compatibility
.putExtra(WizardManagerHelper.EXTRA_SCRIPT_URI, "test_script_uri")
.putExtra(WizardManagerHelper.EXTRA_ACTION_ID, "test_action_id");
@ -284,6 +274,8 @@ public class WizardManagerHelperTest {
intent.getBooleanExtra(WizardManagerHelper.EXTRA_IS_FIRST_RUN, false));
assertTrue("EXTRA_IS_DEFERRED_SETUP should be copied",
intent.getBooleanExtra(WizardManagerHelper.EXTRA_IS_DEFERRED_SETUP, false));
assertTrue("EXTRA_IS_PRE_DEFERRED_SETUP should be copied",
intent.getBooleanExtra(WizardManagerHelper.EXTRA_IS_PRE_DEFERRED_SETUP, false));
// Script URI and Action ID are replaced by Wizard Bundle in M, but are kept for backwards
// compatibility

View file

@ -22,7 +22,6 @@ import static org.robolectric.RuntimeEnvironment.application;
import android.view.View;
import android.view.View.MeasureSpec;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import org.junit.Test;
@ -31,7 +30,7 @@ import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = {Config.OLDEST_SDK, Config.NEWEST_SDK})
@Config(sdk = {Config.OLDEST_SDK, Config.NEWEST_SDK})
public class FillContentLayoutTest {
@Test

View file

@ -18,6 +18,7 @@ package com.android.setupwizardlib.view;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
@ -31,9 +32,10 @@ import android.os.Build.VERSION_CODES;
import android.support.annotation.RawRes;
import android.view.Surface;
import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import com.android.setupwizardlib.shadow.ShadowLog;
import com.android.setupwizardlib.shadow.ShadowLog.TerribleFailure;
import com.android.setupwizardlib.view.IllustrationVideoViewTest.ShadowMockMediaPlayer;
import com.android.setupwizardlib.view.IllustrationVideoViewTest.ShadowSurface;
@ -48,15 +50,15 @@ import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.internal.Shadow;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowMediaPlayer;
import org.robolectric.util.ReflectionHelpers;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(
constants = BuildConfig.class,
sdk = Config.NEWEST_SDK,
shadows = {
ShadowLog.class,
ShadowMockMediaPlayer.class,
ShadowSurface.class
})
@ -77,6 +79,17 @@ public class IllustrationVideoViewTest {
ShadowMockMediaPlayer.reset();
}
@Test
public void nullMediaPlayer_shouldThrowWtf() {
ShadowMockMediaPlayer.sMediaPlayer = null;
try {
createDefaultView();
fail("WTF should be thrown for null media player");
} catch (TerribleFailure e) {
// pass
}
}
@Test
public void testPausedWhenWindowFocusLost() {
createDefaultView();

View file

@ -14,39 +14,45 @@
* limitations under the License.
*/
package com.android.setupwizardlib.test;
package com.android.setupwizardlib.view;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.robolectric.RuntimeEnvironment.application;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.ContextWrapper;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.text.Annotation;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.TextAppearanceSpan;
import android.view.MotionEvent;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import com.android.setupwizardlib.span.LinkSpan;
import com.android.setupwizardlib.span.LinkSpan.OnLinkClickListener;
import com.android.setupwizardlib.view.RichTextView;
import com.android.setupwizardlib.view.TouchableMovementMethod.TouchableLinkMovementMethod;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import java.util.Arrays;
@RunWith(AndroidJUnit4.class)
@SmallTest
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
public class RichTextViewTest {
@Test
@ -55,12 +61,14 @@ public class RichTextViewTest {
SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
ssb.setSpan(link, 1, 2, 0 /* flags */);
RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
RichTextView textView = new RichTextView(application);
textView.setText(ssb);
final CharSequence text = textView.getText();
assertTrue("Text should be spanned", text instanceof Spanned);
assertThat(textView.getMovementMethod()).isInstanceOf(TouchableLinkMovementMethod.class);
Object[] spans = ((Spanned) text).getSpans(0, text.length(), Annotation.class);
assertEquals("Annotation should be removed " + Arrays.toString(spans), 0, spans.length);
@ -77,7 +85,7 @@ public class RichTextViewTest {
SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
ssb.setSpan(link, 1, 2, 0 /* flags */);
RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
RichTextView textView = new RichTextView(application);
textView.setText(ssb);
OnLinkClickListener listener = mock(OnLinkClickListener.class);
@ -99,7 +107,7 @@ public class RichTextViewTest {
SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
ssb.setSpan(link, 1, 2, 0 /* flags */);
TestContext context = spy(new TestContext(InstrumentationRegistry.getTargetContext()));
TestContext context = spy(new TestContext(application));
RichTextView textView = new RichTextView(context);
textView.setText(ssb);
@ -110,13 +118,51 @@ public class RichTextViewTest {
verify(context).onClick(eq(spans[0]));
}
@Test
public void onTouchEvent_clickOnLinks_shouldReturnTrue() {
Annotation link = new Annotation("link", "foobar");
SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
ssb.setSpan(link, 0, 2, 0 /* flags */);
RichTextView textView = new RichTextView(application);
textView.setText(ssb);
TouchableLinkMovementMethod mockMovementMethod = mock(TouchableLinkMovementMethod.class);
textView.setMovementMethod(mockMovementMethod);
MotionEvent motionEvent =
MotionEvent.obtain(123, 22, MotionEvent.ACTION_DOWN, 0, 0, 0);
doReturn(motionEvent).when(mockMovementMethod).getLastTouchEvent();
doReturn(true).when(mockMovementMethod).isLastTouchEventHandled();
assertThat(textView.onTouchEvent(motionEvent)).isTrue();
}
@Test
public void onTouchEvent_clickOutsideLinks_shouldReturnFalse() {
Annotation link = new Annotation("link", "foobar");
SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
ssb.setSpan(link, 0, 2, 0 /* flags */);
RichTextView textView = new RichTextView(application);
textView.setText(ssb);
TouchableLinkMovementMethod mockMovementMethod = mock(TouchableLinkMovementMethod.class);
textView.setMovementMethod(mockMovementMethod);
MotionEvent motionEvent =
MotionEvent.obtain(123, 22, MotionEvent.ACTION_DOWN, 0, 0, 0);
doReturn(motionEvent).when(mockMovementMethod).getLastTouchEvent();
doReturn(false).when(mockMovementMethod).isLastTouchEventHandled();
assertThat(textView.onTouchEvent(motionEvent)).isFalse();
}
@Test
public void testTextStyle() {
Annotation link = new Annotation("textAppearance", "foobar");
SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
ssb.setSpan(link, 1, 2, 0 /* flags */);
RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
RichTextView textView = new RichTextView(application);
textView.setText(ssb);
final CharSequence text = textView.getText();
@ -137,7 +183,7 @@ public class RichTextViewTest {
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder("Linked");
spannableStringBuilder.setSpan(testLink, 0, 3, 0);
RichTextView view = new RichTextView(InstrumentationRegistry.getContext());
RichTextView view = new RichTextView(application);
view.setText(spannableStringBuilder);
assertTrue("TextView should be focusable since it contains spans", view.isFocusable());
@ -147,7 +193,7 @@ public class RichTextViewTest {
@SuppressLint("SetTextI18n") // It's OK. This is just a test.
@Test
public void testTextContainingNoLinksAreNotFocusable() {
RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
RichTextView textView = new RichTextView(application);
textView.setText("Thou shall not be focusable!");
assertFalse("TextView should not be focusable since it does not contain any span",
@ -160,16 +206,23 @@ public class RichTextViewTest {
@SuppressLint("SetTextI18n") // It's OK. This is just a test.
@Test
public void testRichTextViewFocusChangesWithTextChange() {
RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
RichTextView textView = new RichTextView(application);
textView.setText("Thou shall not be focusable!");
assertFalse(textView.isFocusable());
assertFalse(textView.isFocusableInTouchMode());
SpannableStringBuilder spannableStringBuilder =
new SpannableStringBuilder("I am focusable");
spannableStringBuilder.setSpan(new Annotation("link", "focus:on_me"), 0, 1, 0);
textView.setText(spannableStringBuilder);
assertTrue(textView.isFocusable());
if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
assertTrue(textView.isFocusableInTouchMode());
assertFalse(textView.getRevealOnFocusHint());
} else {
assertFalse(textView.isFocusableInTouchMode());
}
}
public static class TestContext extends ContextWrapper implements LinkSpan.OnClickListener {

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="setup_wizard_next_button_label" msgid="6681282266022780599">"Next"</string>
<string name="setup_wizard_back_button_label" msgid="2863826823307023546">"Back"</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="setup_wizard_next_button_label" msgid="6681282266022780599">"Next"</string>
<string name="setup_wizard_back_button_label" msgid="2863826823307023546">"Back"</string>
</resources>

View file

@ -6,4 +6,4 @@ export TARGET_BUILD_DENSITY="alldpi"
export TARGET_BUILD_TYPE="release"
export TARGET_BUILD_APPS="setup-wizard-lib"
./gradlew buildProjectFull test
./gradlew buildProjectFull test coverage

View file

@ -1,6 +1,6 @@
// Set the default SDK and build tools version for all apps
compileSdkVersion 25
buildToolsVersion = '25.0.0'
compileSdkVersion 28
buildToolsVersion = '28.0.0'
// enable Java7
compileOptions.sourceCompatibility JavaVersion.VERSION_1_7

View file

@ -10,6 +10,7 @@
*/
apply plugin: 'dist'
apply plugin: 'jacoco'
// If unit tests are run as part of the build, dist the test XML reports to host-test-reports/*.zip
android.unitTestVariants.all { variant ->
@ -28,11 +29,80 @@ android.unitTestVariants.all { variant ->
archiveName = task.name + 'Result.zip'
destinationDir = junitReport.destination.parentFile
}
zipTask.mustRunAfter task
task.finalizedBy zipTask
// Copy the test reports to dist/host-test-reports
// The file path and format should match GradleHostBasedTest class in TradeFed.
tasks.dist.dependsOn zipTask
tasks.dist.mustRunAfter zipTask
dist.file zipTask.archivePath.path, "host-test-reports/${zipTask.archiveName}"
}
}
/*
* The section below adds code coverage to all the unitTest targets. By default, the jacoco plugin
* only adds to the 'java' plugin and not the Android ones.
*
* For each unitTest task "fooUnitTest", a new target "fooUnitTestCoverage" will be generated for
* to generate the jacoco report.
*/
android.testOptions.unitTests.all {
// Fix robolectric tests reporting 0 coverage on newer versions of the plugin.
jacoco {
includeNoLocationClasses = true
}
}
// Define the main coverage task if it does not exist. This task generates coverage report for all
// unit tests.
def coverageTask = tasks.findByName('coverage') ?: tasks.create('coverage') {
group = "Reporting"
description = "Generate Jacoco coverage reports"
}
android.unitTestVariants.all { variant ->
def testTaskName = "test${variant.name.capitalize()}"
def testTask = tasks.findByName(testTaskName)
// Create coverage task of form 'testFlavorCoverageUnitTestCoverage' depending on
// 'testFlavorCoverageUnitTest'
def jacocoTask = tasks.create("${testTaskName}Coverage", JacocoReport) {
group = "Reporting"
description = "Generate a Jacoco coverage report for robolectric tests on ${variant.name}."
classDirectories = fileTree(
dir: "${project.buildDir}/intermediates/classes/" +
"${variant.productFlavors[0].name}/${variant.buildType.name}",
excludes: ['**/R.class',
'**/R$*.class',
'**/BuildConfig.*',
'**/Manifest*.*']
)
sourceDirectories = files(variant.testedVariant.sourceSets.collect { it.java.srcDirs })
executionData = files("${project.buildDir}/jacoco/${testTaskName}.exec")
reports {
xml.enabled = true
html.enabled = true
}
}
jacocoTask.dependsOn testTask
// Create a zip file of the HTML coverage reports
def zipTask = tasks.create("zipResultsOf${jacocoTask.name.capitalize()}", Zip) {
from jacocoTask.reports.html.destination
archiveName = "${testTaskName}HtmlCoverage.zip"
destinationDir = jacocoTask.reports.html.destination.parentFile
}
jacocoTask.finalizedBy zipTask
// Copy the coverage reports to dist/host-test-coverage
// The file path and format should match JacocoLogForwarder class in TradeFed.
tasks.dist.mustRunAfter jacocoTask
dist.file jacocoTask.reports.xml.destination.path, "host-test-coverage/${jacocoTask.name}.xml"
tasks.dist.mustRunAfter zipTask
dist.file zipTask.archivePath.path, "host-test-coverage/${zipTask.archiveName}"
coverageTask.dependsOn(jacocoTask)
}

2
tools/root.mk Normal file
View file

@ -0,0 +1,2 @@
default:
TARGET_BUILD_APPS=setup-wizard-lib ./gradlew