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 * <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 * <!-- Content here --> 91 * 92 * </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