• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.android.setupdesign;
18 
19 import android.annotation.TargetApi;
20 import android.app.Activity;
21 import android.content.Context;
22 import android.content.res.ColorStateList;
23 import android.content.res.Resources.Theme;
24 import android.content.res.TypedArray;
25 import android.graphics.Color;
26 import android.graphics.drawable.ColorDrawable;
27 import android.graphics.drawable.Drawable;
28 import android.os.Build;
29 import android.os.Build.VERSION;
30 import android.os.Build.VERSION_CODES;
31 import android.os.Handler;
32 import android.os.Looper;
33 import android.os.PersistableBundle;
34 import android.util.AttributeSet;
35 import android.util.TypedValue;
36 import android.view.LayoutInflater;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.view.ViewStub;
40 import android.view.ViewTreeObserver;
41 import android.widget.LinearLayout;
42 import android.widget.ProgressBar;
43 import android.widget.ScrollView;
44 import android.widget.TextView;
45 import androidx.annotation.ColorInt;
46 import androidx.annotation.LayoutRes;
47 import androidx.annotation.NonNull;
48 import androidx.annotation.Nullable;
49 import androidx.annotation.StringRes;
50 import androidx.window.embedding.ActivityEmbeddingController;
51 import com.google.android.setupcompat.PartnerCustomizationLayout;
52 import com.google.android.setupcompat.logging.CustomEvent;
53 import com.google.android.setupcompat.logging.MetricKey;
54 import com.google.android.setupcompat.logging.SetupMetricsLogger;
55 import com.google.android.setupcompat.partnerconfig.PartnerConfig;
56 import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper;
57 import com.google.android.setupcompat.template.FooterBarMixin;
58 import com.google.android.setupcompat.template.StatusBarMixin;
59 import com.google.android.setupcompat.template.SystemNavBarMixin;
60 import com.google.android.setupcompat.util.ForceTwoPaneHelper;
61 import com.google.android.setupcompat.util.KeyboardHelper;
62 import com.google.android.setupcompat.util.Logger;
63 import com.google.android.setupcompat.util.WizardManagerHelper;
64 import com.google.android.setupdesign.template.DescriptionMixin;
65 import com.google.android.setupdesign.template.FloatingBackButtonMixin;
66 import com.google.android.setupdesign.template.HeaderMixin;
67 import com.google.android.setupdesign.template.IconMixin;
68 import com.google.android.setupdesign.template.IllustrationProgressMixin;
69 import com.google.android.setupdesign.template.ProfileMixin;
70 import com.google.android.setupdesign.template.ProgressBarMixin;
71 import com.google.android.setupdesign.template.RequireScrollMixin;
72 import com.google.android.setupdesign.template.ScrollViewScrollHandlingDelegate;
73 import com.google.android.setupdesign.util.DescriptionStyler;
74 import com.google.android.setupdesign.util.LayoutStyler;
75 
76 /**
77  * Layout for the GLIF theme used in Setup Wizard for N.
78  *
79  * <p>Example usage:
80  *
81  * <pre>{@code
82  * &lt;com.google.android.setupdesign.GlifLayout
83  *     xmlns:android="http://schemas.android.com/apk/res/android"
84  *     xmlns:app="http://schemas.android.com/apk/res-auto"
85  *     android:layout_width="match_parent"
86  *     android:layout_height="match_parent"
87  *     android:icon="@drawable/my_icon"
88  *     app:sucHeaderText="@string/my_title">
89  *
90  *     &lt;!-- Content here -->
91  *
92  * &lt;/com.google.android.setupdesign.GlifLayout>
93  * }</pre>
94  */
95 public class GlifLayout extends PartnerCustomizationLayout {
96 
97   private static final Logger LOG = new Logger(GlifLayout.class);
98 
99   private ColorStateList primaryColor;
100 
101   private boolean backgroundPatterned = true;
102 
103   private boolean applyPartnerHeavyThemeResource = false;
104 
105   private ViewTreeObserver.OnScrollChangedListener onScrollChangedListener =
106       new ViewTreeObserver.OnScrollChangedListener() {
107         @Override
108         public void onScrollChanged() {
109           ScrollView scrollView = getScrollView();
110           if (scrollView != null) {
111             // direction > 0 means view can scroll down, direction < 0 means view can scroll
112             // up. Here we use direction > 0 to detect whether the view can be scrolling down
113             // or not.
114             onScrolling(!scrollView.canScrollVertically(/* direction= */ 1));
115           }
116         }
117       };
118 
119   /** The color of the background. If null, the color will inherit from primaryColor. */
120   @Nullable private ColorStateList backgroundBaseColor;
121 
GlifLayout(Context context)122   public GlifLayout(Context context) {
123     this(context, 0, 0);
124   }
125 
GlifLayout(Context context, int template)126   public GlifLayout(Context context, int template) {
127     this(context, template, 0);
128   }
129 
GlifLayout(Context context, int template, int containerId)130   public GlifLayout(Context context, int template, int containerId) {
131     super(context, template, containerId);
132     init(null, R.attr.sudLayoutTheme);
133   }
134 
GlifLayout(Context context, AttributeSet attrs)135   public GlifLayout(Context context, AttributeSet attrs) {
136     super(context, attrs);
137     init(attrs, R.attr.sudLayoutTheme);
138   }
139 
140   @TargetApi(VERSION_CODES.HONEYCOMB)
GlifLayout(Context context, AttributeSet attrs, int defStyleAttr)141   public GlifLayout(Context context, AttributeSet attrs, int defStyleAttr) {
142     super(context, attrs, defStyleAttr);
143     init(attrs, defStyleAttr);
144   }
145 
146   // All the constructors delegate to this init method. The 3-argument constructor is not
147   // available in LinearLayout before v11, so call super with the exact same arguments.
init(AttributeSet attrs, int defStyleAttr)148   private void init(AttributeSet attrs, int defStyleAttr) {
149     if (isInEditMode()) {
150       return;
151     }
152 
153     TypedArray a =
154         getContext().obtainStyledAttributes(attrs, R.styleable.SudGlifLayout, defStyleAttr, 0);
155     boolean usePartnerHeavyTheme =
156         a.getBoolean(R.styleable.SudGlifLayout_sudUsePartnerHeavyTheme, false);
157     applyPartnerHeavyThemeResource = shouldApplyPartnerResource() && usePartnerHeavyTheme;
158 
159     registerMixin(HeaderMixin.class, new HeaderMixin(this, attrs, defStyleAttr));
160     registerMixin(DescriptionMixin.class, new DescriptionMixin(this, attrs, defStyleAttr));
161     registerMixin(IconMixin.class, new IconMixin(this, attrs, defStyleAttr));
162     registerMixin(ProfileMixin.class, new ProfileMixin(this, attrs, defStyleAttr));
163     registerMixin(ProgressBarMixin.class, new ProgressBarMixin(this, attrs, defStyleAttr));
164     registerMixin(IllustrationProgressMixin.class, new IllustrationProgressMixin(this));
165     registerMixin(
166         FloatingBackButtonMixin.class, new FloatingBackButtonMixin(this, attrs, defStyleAttr));
167     final RequireScrollMixin requireScrollMixin = new RequireScrollMixin(this);
168     registerMixin(RequireScrollMixin.class, requireScrollMixin);
169 
170     final ScrollView scrollView = getScrollView();
171     if (scrollView != null) {
172       requireScrollMixin.setScrollHandlingDelegate(
173           new ScrollViewScrollHandlingDelegate(requireScrollMixin, scrollView));
174     }
175 
176     ColorStateList primaryColor = a.getColorStateList(R.styleable.SudGlifLayout_sudColorPrimary);
177     if (primaryColor != null) {
178       setPrimaryColor(primaryColor);
179     }
180     if (shouldApplyPartnerHeavyThemeResource()) {
181       updateContentBackgroundColorWithPartnerConfig();
182     }
183 
184     View view = findManagedViewById(R.id.sud_layout_content);
185     if (view != null) {
186       if (shouldApplyPartnerResource()) {
187         // The margin of content is defined by @style/SudContentFrame. The Setupdesign library
188         // cannot obtain the content resource ID of the client, so the value of the content margin
189         // cannot be adjusted through GlifLayout. If the margin sides are changed through the
190         // partner config, it can only be based on the increased or decreased value to adjust the
191         // value of padding. In this way, the value of content margin plus padding will be equal to
192         // the value of partner config.
193         LayoutStyler.applyPartnerCustomizationExtraPaddingStyle(view);
194       }
195 
196       // {@class GlifPreferenceLayout} Inherited from {@class GlifRecyclerLayout}. The API would
197       // be called twice from GlifRecyclerLayout and GlifLayout, so it should skip the API here
198       // when the instance is GlifPreferenceLayout.
199       if (!(this instanceof GlifPreferenceLayout)) {
200         tryApplyPartnerCustomizationContentPaddingTopStyle(view);
201       }
202     }
203 
204     updateLandscapeMiddleHorizontalSpacing();
205 
206     updateViewFocusable();
207 
208     ColorStateList backgroundColor =
209         a.getColorStateList(R.styleable.SudGlifLayout_sudBackgroundBaseColor);
210     setBackgroundBaseColor(backgroundColor);
211 
212     boolean backgroundPatterned =
213         a.getBoolean(R.styleable.SudGlifLayout_sudBackgroundPatterned, true);
214     setBackgroundPatterned(backgroundPatterned);
215 
216     final int stickyHeader = a.getResourceId(R.styleable.SudGlifLayout_sudStickyHeader, 0);
217     if (stickyHeader != 0) {
218       inflateStickyHeader(stickyHeader);
219     }
220 
221     if (PartnerConfigHelper.isGlifExpressiveEnabled(getContext())) {
222       initScrollingListener();
223     }
224 
225     initBackButton();
226 
227     a.recycle();
228   }
229 
230   @Override
onFinishInflate()231   protected void onFinishInflate() {
232     super.onFinishInflate();
233     getMixin(IconMixin.class).tryApplyPartnerCustomizationStyle();
234     getMixin(HeaderMixin.class).tryApplyPartnerCustomizationStyle();
235     getMixin(DescriptionMixin.class).tryApplyPartnerCustomizationStyle();
236     getMixin(ProgressBarMixin.class).tryApplyPartnerCustomizationStyle();
237     getMixin(ProfileMixin.class).tryApplyPartnerCustomizationStyle();
238     getMixin(FloatingBackButtonMixin.class).tryApplyPartnerCustomizationStyle();
239     tryApplyPartnerCustomizationStyleToShortDescription();
240   }
241 
updateViewFocusable()242   private void updateViewFocusable() {
243     if (KeyboardHelper.isKeyboardFocusEnhancementEnabled(getContext())) {
244       View headerView = this.findManagedViewById(R.id.sud_header_scroll_view);
245       if (headerView != null) {
246         headerView.setFocusable(false);
247       }
248       View view = this.findManagedViewById(R.id.sud_scroll_view);
249       if (view != null) {
250         view.setFocusable(false);
251       }
252     }
253   }
254 
255   // TODO: remove when all sud_layout_description has migrated to
256   // DescriptionMixin(sud_layout_subtitle)
tryApplyPartnerCustomizationStyleToShortDescription()257   private void tryApplyPartnerCustomizationStyleToShortDescription() {
258     TextView description = this.findManagedViewById(R.id.sud_layout_description);
259     if (description != null) {
260       if (applyPartnerHeavyThemeResource) {
261         DescriptionStyler.applyPartnerCustomizationHeavyStyle(description);
262       } else if (shouldApplyPartnerResource()) {
263         DescriptionStyler.applyPartnerCustomizationLightStyle(description);
264       }
265     }
266   }
267 
updateLandscapeMiddleHorizontalSpacing()268   protected void updateLandscapeMiddleHorizontalSpacing() {
269     int horizontalSpacing =
270         getResources().getDimensionPixelSize(R.dimen.sud_glif_land_middle_horizontal_spacing);
271     if (shouldApplyPartnerResource()
272         && PartnerConfigHelper.get(getContext())
273             .isPartnerConfigAvailable(PartnerConfig.CONFIG_LAND_MIDDLE_HORIZONTAL_SPACING)) {
274       horizontalSpacing =
275           (int)
276               PartnerConfigHelper.get(getContext())
277                   .getDimension(getContext(), PartnerConfig.CONFIG_LAND_MIDDLE_HORIZONTAL_SPACING);
278     }
279 
280     View headerView = this.findManagedViewById(R.id.sud_landscape_header_area);
281     if (headerView != null) {
282       int layoutMarginEnd;
283       if (shouldApplyPartnerResource()
284           && PartnerConfigHelper.get(getContext())
285               .isPartnerConfigAvailable(PartnerConfig.CONFIG_LAYOUT_MARGIN_END)) {
286         layoutMarginEnd =
287             (int)
288                 PartnerConfigHelper.get(getContext())
289                     .getDimension(getContext(), PartnerConfig.CONFIG_LAYOUT_MARGIN_END);
290       } else {
291         TypedArray a = getContext().obtainStyledAttributes(new int[] {R.attr.sudMarginEnd});
292         layoutMarginEnd = a.getDimensionPixelSize(0, 0);
293         a.recycle();
294       }
295       int paddingEnd = (horizontalSpacing / 2) - layoutMarginEnd;
296       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
297         headerView.setPadding(
298             headerView.getPaddingStart(),
299             headerView.getPaddingTop(),
300             paddingEnd,
301             headerView.getPaddingBottom());
302       } else {
303         headerView.setPadding(
304             headerView.getPaddingLeft(),
305             headerView.getPaddingTop(),
306             paddingEnd,
307             headerView.getPaddingBottom());
308       }
309     }
310 
311     View contentView = this.findManagedViewById(R.id.sud_landscape_content_area);
312     if (contentView != null) {
313       int layoutMarginStart;
314       if (shouldApplyPartnerResource()
315           && PartnerConfigHelper.get(getContext())
316               .isPartnerConfigAvailable(PartnerConfig.CONFIG_LAYOUT_MARGIN_START)) {
317         layoutMarginStart =
318             (int)
319                 PartnerConfigHelper.get(getContext())
320                     .getDimension(getContext(), PartnerConfig.CONFIG_LAYOUT_MARGIN_START);
321       } else {
322         TypedArray a = getContext().obtainStyledAttributes(new int[] {R.attr.sudMarginStart});
323         layoutMarginStart = a.getDimensionPixelSize(0, 0);
324         a.recycle();
325       }
326       int paddingStart = 0;
327       if (headerView != null) {
328         paddingStart = (horizontalSpacing / 2) - layoutMarginStart;
329       }
330       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
331         contentView.setPadding(
332             paddingStart,
333             contentView.getPaddingTop(),
334             contentView.getPaddingEnd(),
335             contentView.getPaddingBottom());
336       } else {
337         contentView.setPadding(
338             paddingStart,
339             contentView.getPaddingTop(),
340             contentView.getPaddingRight(),
341             contentView.getPaddingBottom());
342       }
343     }
344   }
345 
346   @Override
onInflateTemplate(LayoutInflater inflater, @LayoutRes int template)347   protected View onInflateTemplate(LayoutInflater inflater, @LayoutRes int template) {
348     if (template == 0) {
349       template = R.layout.sud_glif_template;
350 
351       // if the activity is embedded should apply an embedded layout.
352       if (isEmbeddedActivityOnePaneEnabled(getContext())) {
353         if (isGlifExpressiveEnabled()) {
354           template = R.layout.sud_glif_expressive_embedded_template;
355         } else {
356           template = R.layout.sud_glif_embedded_template;
357         }
358         // TODO add unit test for this case.
359       } else if (isGlifExpressiveEnabled()) {
360         template = R.layout.sud_glif_expressive_template;
361       } else if (ForceTwoPaneHelper.isForceTwoPaneEnable(getContext())) {
362         template = R.layout.sud_glif_template_two_pane;
363       }
364     }
365 
366     return inflateTemplate(inflater, R.style.SudThemeGlif_Light, template);
367   }
368 
369   @Override
findContainer(int containerId)370   protected ViewGroup findContainer(int containerId) {
371     if (containerId == 0) {
372       containerId = R.id.sud_layout_content;
373     }
374     return super.findContainer(containerId);
375   }
376 
377   @Override
onDetachedFromWindow()378   protected void onDetachedFromWindow() {
379     super.onDetachedFromWindow();
380     // Log metrics of UI component
381     if (VERSION.SDK_INT >= Build.VERSION_CODES.Q
382         && WizardManagerHelper.isAnySetupWizard(activity.getIntent())
383         && PartnerConfigHelper.isGlifExpressiveEnabled(getContext())) {
384 
385       FloatingBackButtonMixin floatingBackButtonMixin = getMixin(FloatingBackButtonMixin.class);
386       PersistableBundle backButtonMetrics =
387           floatingBackButtonMixin != null
388               ? floatingBackButtonMixin.getMetrics()
389               : PersistableBundle.EMPTY;
390 
391       CustomEvent customEvent =
392           CustomEvent.create(MetricKey.get("SetupDesignMetrics", activity), backButtonMetrics);
393       SetupMetricsLogger.logCustomEvent(getContext(), customEvent);
394 
395       LOG.atVerbose("SetupDesignMetrics=" + CustomEvent.toBundle(customEvent));
396     }
397     ScrollView scrollView = getScrollView();
398     if (scrollView != null) {
399       scrollView.getViewTreeObserver().removeOnScrollChangedListener(onScrollChangedListener);
400     }
401   }
402 
403   /**
404    * Sets the sticky header (i.e. header that doesn't scroll) of the layout, which is at the top of
405    * the content area outside of the scrolling container. The header can only be inflated once per
406    * instance of this layout.
407    *
408    * @param header The layout to be inflated as the header
409    * @return The root of the inflated header view
410    */
inflateStickyHeader(@ayoutRes int header)411   public View inflateStickyHeader(@LayoutRes int header) {
412     ViewStub stickyHeaderStub = findManagedViewById(R.id.sud_layout_sticky_header);
413     stickyHeaderStub.setLayoutResource(header);
414     return stickyHeaderStub.inflate();
415   }
416 
getScrollView()417   public ScrollView getScrollView() {
418     final View view = findManagedViewById(R.id.sud_scroll_view);
419     return view instanceof ScrollView ? (ScrollView) view : null;
420   }
421 
getHeaderTextView()422   public TextView getHeaderTextView() {
423     return getMixin(HeaderMixin.class).getTextView();
424   }
425 
setHeaderText(int title)426   public void setHeaderText(int title) {
427     getMixin(HeaderMixin.class).setText(title);
428   }
429 
setHeaderText(CharSequence title)430   public void setHeaderText(CharSequence title) {
431     getMixin(HeaderMixin.class).setText(title);
432   }
433 
getHeaderText()434   public CharSequence getHeaderText() {
435     return getMixin(HeaderMixin.class).getText();
436   }
437 
getDescriptionTextView()438   public TextView getDescriptionTextView() {
439     return getMixin(DescriptionMixin.class).getTextView();
440   }
441 
442   /**
443    * Sets the description text and also sets the text visibility to visible. This can also be set
444    * via the XML attribute {@code app:sudDescriptionText}.
445    *
446    * @param title The resource ID of the text to be set as description
447    */
setDescriptionText(@tringRes int title)448   public void setDescriptionText(@StringRes int title) {
449     getMixin(DescriptionMixin.class).setText(title);
450   }
451 
452   /**
453    * Sets the description text and also sets the text visibility to visible. This can also be set
454    * via the XML attribute {@code app:sudDescriptionText}.
455    *
456    * @param title The text to be set as description
457    */
setDescriptionText(CharSequence title)458   public void setDescriptionText(CharSequence title) {
459     getMixin(DescriptionMixin.class).setText(title);
460   }
461 
462   /** Returns the current description text. */
getDescriptionText()463   public CharSequence getDescriptionText() {
464     return getMixin(DescriptionMixin.class).getText();
465   }
466 
setHeaderColor(ColorStateList color)467   public void setHeaderColor(ColorStateList color) {
468     getMixin(HeaderMixin.class).setTextColor(color);
469   }
470 
getHeaderColor()471   public ColorStateList getHeaderColor() {
472     return getMixin(HeaderMixin.class).getTextColor();
473   }
474 
setIcon(Drawable icon)475   public void setIcon(Drawable icon) {
476     getMixin(IconMixin.class).setIcon(icon);
477   }
478 
getIcon()479   public Drawable getIcon() {
480     return getMixin(IconMixin.class).getIcon();
481   }
482 
483   /**
484    * Sets the visibility of header area in landscape mode. These views includes icon, header title
485    * and subtitle. It can make the content view become full screen when set false.
486    */
487   @TargetApi(Build.VERSION_CODES.S)
setLandscapeHeaderAreaVisible(boolean visible)488   public void setLandscapeHeaderAreaVisible(boolean visible) {
489     View view = this.findManagedViewById(R.id.sud_landscape_header_area);
490     if (view == null) {
491       return;
492     }
493     if (visible) {
494       view.setVisibility(View.VISIBLE);
495     } else {
496       view.setVisibility(View.GONE);
497     }
498     updateLandscapeMiddleHorizontalSpacing();
499   }
500 
501   /**
502    * Sets the primary color of this layout, which will be used to determine the color of the
503    * progress bar and the background pattern.
504    */
setPrimaryColor(@onNull ColorStateList color)505   public void setPrimaryColor(@NonNull ColorStateList color) {
506     primaryColor = color;
507     updateBackground();
508     getMixin(ProgressBarMixin.class).setColor(color);
509   }
510 
getPrimaryColor()511   public ColorStateList getPrimaryColor() {
512     return primaryColor;
513   }
514 
515   /**
516    * Sets the base color of the background view, which is the status bar for phones and the full-
517    * screen background for tablets. If {@link #isBackgroundPatterned()} is true, the pattern will be
518    * drawn with this color.
519    *
520    * @param color The color to use as the base color of the background. If {@code null}, {@link
521    *     #getPrimaryColor()} will be used
522    */
setBackgroundBaseColor(@ullable ColorStateList color)523   public void setBackgroundBaseColor(@Nullable ColorStateList color) {
524     backgroundBaseColor = color;
525     updateBackground();
526   }
527 
528   /**
529    * @return The base color of the background. {@code null} indicates the background will be drawn
530    *     with {@link #getPrimaryColor()}.
531    */
532   @Nullable
getBackgroundBaseColor()533   public ColorStateList getBackgroundBaseColor() {
534     return backgroundBaseColor;
535   }
536 
537   /**
538    * Sets whether the background should be {@link GlifPatternDrawable}. If {@code false}, the
539    * background will be a solid color.
540    */
setBackgroundPatterned(boolean patterned)541   public void setBackgroundPatterned(boolean patterned) {
542     backgroundPatterned = patterned;
543     updateBackground();
544   }
545 
546   /** Returns true if this view uses {@link GlifPatternDrawable} as background. */
isBackgroundPatterned()547   public boolean isBackgroundPatterned() {
548     return backgroundPatterned;
549   }
550 
updateBackground()551   private void updateBackground() {
552     final View patternBg = findManagedViewById(R.id.suc_layout_status);
553     if (patternBg != null) {
554       int backgroundColor = 0;
555       if (backgroundBaseColor != null) {
556         backgroundColor = backgroundBaseColor.getDefaultColor();
557       } else if (primaryColor != null) {
558         backgroundColor = primaryColor.getDefaultColor();
559       }
560       Drawable background =
561           backgroundPatterned
562               ? new GlifPatternDrawable(backgroundColor)
563               : new ColorDrawable(backgroundColor);
564       getMixin(StatusBarMixin.class).setStatusBarBackground(background);
565     }
566   }
567 
isProgressBarShown()568   public boolean isProgressBarShown() {
569     return getMixin(ProgressBarMixin.class).isShown();
570   }
571 
setProgressBarShown(boolean shown)572   public void setProgressBarShown(boolean shown) {
573     getMixin(ProgressBarMixin.class).setShown(shown);
574   }
575 
peekProgressBar()576   public ProgressBar peekProgressBar() {
577     return getMixin(ProgressBarMixin.class).peekProgressBar();
578   }
579 
580   /**
581    * Returns if the current layout/activity applies heavy partner customized configurations or not.
582    */
shouldApplyPartnerHeavyThemeResource()583   public boolean shouldApplyPartnerHeavyThemeResource() {
584 
585     return applyPartnerHeavyThemeResource
586         || (shouldApplyPartnerResource()
587             && PartnerConfigHelper.shouldApplyExtendedPartnerConfig(getContext()));
588   }
589 
590   /** Check if the one pane layout is enabled in embedded activity */
isEmbeddedActivityOnePaneEnabled(Context context)591   protected boolean isEmbeddedActivityOnePaneEnabled(Context context) {
592     boolean embeddedActivityOnePaneEnabled =
593         PartnerConfigHelper.isEmbeddedActivityOnePaneEnabled(context);
594     boolean activityEmbedded =
595         ActivityEmbeddingController.getInstance(context)
596             .isActivityEmbedded(PartnerCustomizationLayout.lookupActivityFromContext(context));
597     LOG.atVerbose(
598         "isEmbeddedActivityOnePaneEnabled = "
599             + embeddedActivityOnePaneEnabled
600             + "; isActivityEmbedded = "
601             + activityEmbedded);
602     return embeddedActivityOnePaneEnabled && activityEmbedded;
603   }
604 
605   /** Updates the background color of this layout with the partner-customizable background color. */
updateContentBackgroundColorWithPartnerConfig()606   private void updateContentBackgroundColorWithPartnerConfig() {
607     // If full dynamic color enabled which means this activity is running outside of setup
608     // flow, the colors should refer to R.style.SudFullDynamicColorThemeGlifV3.
609     if (useFullDynamicColor()) {
610       return;
611     }
612 
613     @ColorInt
614     int color =
615         PartnerConfigHelper.get(getContext())
616             .getColor(getContext(), PartnerConfig.CONFIG_LAYOUT_BACKGROUND_COLOR);
617     this.getRootView().setBackgroundColor(color);
618   }
619 
620   @TargetApi(VERSION_CODES.JELLY_BEAN_MR1)
tryApplyPartnerCustomizationContentPaddingTopStyle(View view)621   protected void tryApplyPartnerCustomizationContentPaddingTopStyle(View view) {
622     Context context = view.getContext();
623     boolean partnerPaddingTopAvailable =
624         PartnerConfigHelper.get(context)
625             .isPartnerConfigAvailable(PartnerConfig.CONFIG_CONTENT_PADDING_TOP);
626 
627     if (shouldApplyPartnerResource() && partnerPaddingTopAvailable) {
628       int paddingTop =
629           (int)
630               PartnerConfigHelper.get(context)
631                   .getDimension(context, PartnerConfig.CONFIG_CONTENT_PADDING_TOP);
632 
633       if (paddingTop != view.getPaddingTop()) {
634         view.setPadding(
635             view.getPaddingStart(), paddingTop, view.getPaddingEnd(), view.getPaddingBottom());
636       }
637     }
638   }
639 
640   // TODO: b/397835857 - Add unit test for initScrollingListener.
initScrollingListener()641   protected void initScrollingListener() {
642     ScrollView scrollView = getScrollView();
643 
644     if (scrollView != null) {
645       scrollView.getViewTreeObserver().addOnScrollChangedListener(onScrollChangedListener);
646 
647       // This is for the case that the view has been first visited to handle the initial state of
648       // the footer bar.
649       new Handler(Looper.getMainLooper())
650           .postDelayed(
651               () -> {
652                 if (isContentScrollable(scrollView)) {
653                   onScrolling(/* isBottom= */ false);
654                 }
655               },
656               100L);
657     }
658   }
659 
isContentScrollable(ScrollView scrollView)660   private boolean isContentScrollable(ScrollView scrollView) {
661     View child = scrollView.getChildAt(0);
662     if (child != null) {
663       return child.getHeight() > scrollView.getHeight();
664     }
665     return false;
666   }
667 
onScrolling(boolean isBottom)668   protected void onScrolling(boolean isBottom) {
669     FooterBarMixin footerBarMixin = getMixin(FooterBarMixin.class);
670     SystemNavBarMixin systemNavBarMixin = getMixin(SystemNavBarMixin.class);
671     if (footerBarMixin != null) {
672       LinearLayout footerContainer = footerBarMixin.getButtonContainer();
673       if (footerContainer != null) {
674         if (isBottom) {
675           footerContainer.setBackgroundColor(Color.TRANSPARENT);
676           if (systemNavBarMixin != null) {
677             systemNavBarMixin.setSystemNavBarBackground(Color.TRANSPARENT);
678           }
679         } else {
680           footerContainer.setBackgroundColor(getFooterBackgroundColorFromStyle());
681           if (systemNavBarMixin != null) {
682             systemNavBarMixin.setSystemNavBarBackground(getFooterBackgroundColorFromStyle());
683           }
684         }
685       }
686     }
687   }
688 
689   /**
690    * Make button visible and register the {@link Activity#onBackPressed()} to the on click event of
691    * the floating back button. It works when {@link
692    * PartnerConfigHelper#isGlifExpressiveEnabled(Context)} return true.
693    */
initBackButton()694   protected void initBackButton() {
695     if (PartnerConfigHelper.isGlifExpressiveEnabled(getContext())) {
696       Activity activity = PartnerCustomizationLayout.lookupActivityFromContext(getContext());
697 
698       FloatingBackButtonMixin floatingBackButtonMixin = getMixin(FloatingBackButtonMixin.class);
699       if (floatingBackButtonMixin != null) {
700         floatingBackButtonMixin.setVisibility(VISIBLE);
701         floatingBackButtonMixin.setOnClickListener(v -> activity.onBackPressed());
702       } else {
703         LOG.w("FloatingBackButtonMixin button is null");
704       }
705     } else {
706       LOG.atDebug("isGlifExpressiveEnabled is false");
707     }
708   }
709 
710   /** Gets footer bar background color from theme style. */
getFooterBackgroundColorFromStyle()711   public int getFooterBackgroundColorFromStyle() {
712     TypedValue typedValue = new TypedValue();
713     Theme theme = getContext().getTheme();
714     theme.resolveAttribute(R.attr.sudFooterBackgroundColor, typedValue, true);
715     return typedValue.data;
716   }
717 
isGlifExpressiveEnabled()718   protected boolean isGlifExpressiveEnabled() {
719     return PartnerConfigHelper.isGlifExpressiveEnabled(getContext())
720         && Build.VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM;
721   }
722 }
723