1 /* 2 * Copyright 2018 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.setupcompat; 18 19 import android.app.Activity; 20 import android.content.Context; 21 import android.content.res.TypedArray; 22 import android.os.Build; 23 import android.os.Build.VERSION; 24 import android.os.Build.VERSION_CODES; 25 import android.os.PersistableBundle; 26 import android.util.AttributeSet; 27 import android.view.LayoutInflater; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.ViewTreeObserver; 31 import android.view.WindowManager; 32 import androidx.annotation.VisibleForTesting; 33 import com.google.android.setupcompat.internal.FocusChangedMetricHelper; 34 import com.google.android.setupcompat.internal.LifecycleFragment; 35 import com.google.android.setupcompat.internal.PersistableBundles; 36 import com.google.android.setupcompat.internal.SetupCompatServiceInvoker; 37 import com.google.android.setupcompat.internal.TemplateLayout; 38 import com.google.android.setupcompat.logging.CustomEvent; 39 import com.google.android.setupcompat.logging.LoggingObserver; 40 import com.google.android.setupcompat.logging.LoggingObserver.SetupCompatUiEvent.LayoutInflatedEvent; 41 import com.google.android.setupcompat.logging.MetricKey; 42 import com.google.android.setupcompat.logging.SetupMetricsLogger; 43 import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper; 44 import com.google.android.setupcompat.template.FooterBarMixin; 45 import com.google.android.setupcompat.template.FooterButton; 46 import com.google.android.setupcompat.template.StatusBarMixin; 47 import com.google.android.setupcompat.template.SystemNavBarMixin; 48 import com.google.android.setupcompat.util.BuildCompatUtils; 49 import com.google.android.setupcompat.util.Logger; 50 import com.google.android.setupcompat.util.WizardManagerHelper; 51 import com.google.errorprone.annotations.CanIgnoreReturnValue; 52 53 /** A templatization layout with consistent style used in Setup Wizard or app itself. */ 54 public class PartnerCustomizationLayout extends TemplateLayout { 55 56 private static final Logger LOG = new Logger("PartnerCustomizationLayout"); 57 58 /** 59 * Attribute indicating whether usage of partner theme resources is allowed. This corresponds to 60 * the {@code app:sucUsePartnerResource} XML attribute. Note that when running in setup wizard, 61 * this is always overridden to true. 62 */ 63 private boolean usePartnerResourceAttr; 64 65 /** 66 * Attribute indicating whether using full dynamic colors or not. This corresponds to the {@code 67 * app:sucFullDynamicColor} XML attribute. 68 */ 69 private boolean useFullDynamicColorAttr; 70 71 /** 72 * Attribute indicating whether usage of dynamic is allowed. This corresponds to the existence of 73 * {@code app:sucFullDynamicColor} XML attribute. 74 */ 75 private boolean useDynamicColor; 76 77 private Activity activity; 78 79 private PersistableBundle layoutTypeBundle; 80 81 @CanIgnoreReturnValue PartnerCustomizationLayout(Context context)82 public PartnerCustomizationLayout(Context context) { 83 this(context, 0, 0); 84 } 85 86 @CanIgnoreReturnValue PartnerCustomizationLayout(Context context, int template)87 public PartnerCustomizationLayout(Context context, int template) { 88 this(context, template, 0); 89 } 90 91 @CanIgnoreReturnValue PartnerCustomizationLayout(Context context, int template, int containerId)92 public PartnerCustomizationLayout(Context context, int template, int containerId) { 93 super(context, template, containerId); 94 init(null, R.attr.sucLayoutTheme); 95 } 96 97 @CanIgnoreReturnValue PartnerCustomizationLayout(Context context, AttributeSet attrs)98 public PartnerCustomizationLayout(Context context, AttributeSet attrs) { 99 super(context, attrs); 100 init(attrs, R.attr.sucLayoutTheme); 101 } 102 103 @CanIgnoreReturnValue PartnerCustomizationLayout(Context context, AttributeSet attrs, int defStyleAttr)104 public PartnerCustomizationLayout(Context context, AttributeSet attrs, int defStyleAttr) { 105 super(context, attrs, defStyleAttr); 106 init(attrs, defStyleAttr); 107 } 108 109 @VisibleForTesting 110 final ViewTreeObserver.OnWindowFocusChangeListener windowFocusChangeListener = 111 this::onFocusChanged; 112 init(AttributeSet attrs, int defStyleAttr)113 private void init(AttributeSet attrs, int defStyleAttr) { 114 if (isInEditMode()) { 115 return; 116 } 117 118 TypedArray a = 119 getContext() 120 .obtainStyledAttributes( 121 attrs, R.styleable.SucPartnerCustomizationLayout, defStyleAttr, 0); 122 123 boolean layoutFullscreen = 124 a.getBoolean(R.styleable.SucPartnerCustomizationLayout_sucLayoutFullscreen, true); 125 126 a.recycle(); 127 128 if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && layoutFullscreen) { 129 setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); 130 } 131 132 registerMixin( 133 StatusBarMixin.class, new StatusBarMixin(this, activity.getWindow(), attrs, defStyleAttr)); 134 registerMixin(SystemNavBarMixin.class, new SystemNavBarMixin(this, activity.getWindow())); 135 registerMixin(FooterBarMixin.class, new FooterBarMixin(this, attrs, defStyleAttr)); 136 137 getMixin(SystemNavBarMixin.class).applyPartnerCustomizations(attrs, defStyleAttr); 138 139 // Override the FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_TRANSLUCENT_STATUS, 140 // FLAG_TRANSLUCENT_NAVIGATION and SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN attributes of window forces 141 // showing status bar and navigation bar. 142 if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 143 activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 144 activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 145 activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); 146 } 147 } 148 149 @Override onInflateTemplate(LayoutInflater inflater, int template)150 protected View onInflateTemplate(LayoutInflater inflater, int template) { 151 if (template == 0) { 152 template = R.layout.partner_customization_layout; 153 } 154 return inflateTemplate(inflater, 0, template); 155 } 156 157 /** 158 * {@inheritDoc} 159 * 160 * <p>This method sets all these flags before onTemplateInflated since it will be too late and get 161 * incorrect flag value on PartnerCustomizationLayout if sets them after onTemplateInflated. 162 */ 163 @Override onBeforeTemplateInflated(AttributeSet attrs, int defStyleAttr)164 protected void onBeforeTemplateInflated(AttributeSet attrs, int defStyleAttr) { 165 166 // Sets default value to true since this timing 167 // before PartnerCustomization members initialization 168 usePartnerResourceAttr = true; 169 170 activity = lookupActivityFromContext(getContext()); 171 172 boolean isSetupFlow = WizardManagerHelper.isAnySetupWizard(activity.getIntent()); 173 174 TypedArray a = 175 getContext() 176 .obtainStyledAttributes( 177 attrs, R.styleable.SucPartnerCustomizationLayout, defStyleAttr, 0); 178 179 if (!a.hasValue(R.styleable.SucPartnerCustomizationLayout_sucUsePartnerResource)) { 180 // TODO: Enable Log.WTF after other client already set sucUsePartnerResource. 181 LOG.e("Attribute sucUsePartnerResource not found in " + activity.getComponentName()); 182 } 183 184 usePartnerResourceAttr = 185 isSetupFlow 186 || a.getBoolean(R.styleable.SucPartnerCustomizationLayout_sucUsePartnerResource, true); 187 188 useDynamicColor = a.hasValue(R.styleable.SucPartnerCustomizationLayout_sucFullDynamicColor); 189 useFullDynamicColorAttr = 190 a.getBoolean(R.styleable.SucPartnerCustomizationLayout_sucFullDynamicColor, false); 191 192 a.recycle(); 193 194 LOG.atDebug( 195 "activity=" 196 + activity.getClass().getSimpleName() 197 + " isSetupFlow=" 198 + isSetupFlow 199 + " enablePartnerResourceLoading=" 200 + enablePartnerResourceLoading() 201 + " usePartnerResourceAttr=" 202 + usePartnerResourceAttr 203 + " useDynamicColor=" 204 + useDynamicColor 205 + " useFullDynamicColorAttr=" 206 + useFullDynamicColorAttr); 207 } 208 209 @Override findContainer(int containerId)210 protected ViewGroup findContainer(int containerId) { 211 if (containerId == 0) { 212 containerId = R.id.suc_layout_content; 213 } 214 return super.findContainer(containerId); 215 } 216 217 @Override onAttachedToWindow()218 protected void onAttachedToWindow() { 219 super.onAttachedToWindow(); 220 LifecycleFragment.attachNow(activity); 221 if (WizardManagerHelper.isAnySetupWizard(activity.getIntent())) { 222 getViewTreeObserver().addOnWindowFocusChangeListener(windowFocusChangeListener); 223 } 224 getMixin(FooterBarMixin.class).onAttachedToWindow(); 225 } 226 227 @Override onDetachedFromWindow()228 protected void onDetachedFromWindow() { 229 super.onDetachedFromWindow(); 230 if (VERSION.SDK_INT >= Build.VERSION_CODES.Q 231 && WizardManagerHelper.isAnySetupWizard(activity.getIntent())) { 232 FooterBarMixin footerBarMixin = getMixin(FooterBarMixin.class); 233 footerBarMixin.onDetachedFromWindow(); 234 FooterButton primaryButton = footerBarMixin.getPrimaryButton(); 235 FooterButton secondaryButton = footerBarMixin.getSecondaryButton(); 236 PersistableBundle primaryButtonMetrics = 237 primaryButton != null 238 ? primaryButton.getMetrics("PrimaryFooterButton") 239 : PersistableBundle.EMPTY; 240 PersistableBundle secondaryButtonMetrics = 241 secondaryButton != null 242 ? secondaryButton.getMetrics("SecondaryFooterButton") 243 : PersistableBundle.EMPTY; 244 245 PersistableBundle layoutTypeMetrics = 246 (layoutTypeBundle != null) ? layoutTypeBundle : PersistableBundle.EMPTY; 247 248 PersistableBundle persistableBundle = 249 PersistableBundles.mergeBundles( 250 footerBarMixin.getLoggingMetrics(), 251 primaryButtonMetrics, 252 secondaryButtonMetrics, 253 layoutTypeMetrics); 254 255 SetupMetricsLogger.logCustomEvent( 256 getContext(), 257 CustomEvent.create(MetricKey.get("SetupCompatMetrics", activity), persistableBundle)); 258 } 259 getViewTreeObserver().removeOnWindowFocusChangeListener(windowFocusChangeListener); 260 } 261 262 /** 263 * PartnerCustomizationLayout is a template layout for different type of GlifLayout. This method 264 * allows each type of layout to report its "GlifLayoutType". 265 */ setLayoutTypeMetrics(PersistableBundle bundle)266 public void setLayoutTypeMetrics(PersistableBundle bundle) { 267 this.layoutTypeBundle = bundle; 268 } 269 270 /** Returns a {@link PersistableBundle} contains key "GlifLayoutType". */ 271 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) getLayoutTypeMetrics()272 public PersistableBundle getLayoutTypeMetrics() { 273 return this.layoutTypeBundle; 274 } 275 lookupActivityFromContext(Context context)276 public static Activity lookupActivityFromContext(Context context) { 277 return PartnerConfigHelper.lookupActivityFromContext(context); 278 } 279 280 /** 281 * Returns true if partner resource loading is enabled. If true, and other necessary conditions 282 * for loading theme attributes are met, this layout will use customized theme attributes from OEM 283 * overlays. This is intended to be used with flag-based development, to allow a flag to control 284 * the rollout of partner resource loading. 285 */ enablePartnerResourceLoading()286 protected boolean enablePartnerResourceLoading() { 287 return true; 288 } 289 290 /** Returns if the current layout/activity applies partner customized configurations or not. */ shouldApplyPartnerResource()291 public boolean shouldApplyPartnerResource() { 292 if (!enablePartnerResourceLoading()) { 293 return false; 294 } 295 if (!usePartnerResourceAttr) { 296 return false; 297 } 298 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { 299 return false; 300 } 301 if (!PartnerConfigHelper.get(getContext()).isAvailable()) { 302 return false; 303 } 304 return true; 305 } 306 307 /** 308 * Returns {@code true} if the current layout/activity applies dynamic color. Otherwise, returns 309 * {@code false}. 310 */ shouldApplyDynamicColor()311 public boolean shouldApplyDynamicColor() { 312 if (!useDynamicColor) { 313 return false; 314 } 315 if (!BuildCompatUtils.isAtLeastS()) { 316 return false; 317 } 318 if (!PartnerConfigHelper.get(getContext()).isAvailable()) { 319 return false; 320 } 321 return true; 322 } 323 324 /** 325 * Returns {@code true} if the current layout/activity applies full dynamic color. Otherwise, 326 * returns {@code false}. This method combines the result of {@link #shouldApplyDynamicColor()} 327 * and the value of the {@code app:sucFullDynamicColor}. 328 */ useFullDynamicColor()329 public boolean useFullDynamicColor() { 330 return shouldApplyDynamicColor() && useFullDynamicColorAttr; 331 } 332 333 /** 334 * Sets a logging observer for {@link FooterBarMixin}. The logging observer is used to log 335 * impressions and clicks on the layout and footer bar buttons. 336 * 337 * @throws UnsupportedOperationException if the primary or secondary button has been set before 338 * the logging observer is set 339 */ setLoggingObserver(LoggingObserver loggingObserver)340 public void setLoggingObserver(LoggingObserver loggingObserver) { 341 getMixin(FooterBarMixin.class).setLoggingObserver(loggingObserver); 342 loggingObserver.log(new LayoutInflatedEvent(this)); 343 } 344 345 /** 346 * Invoke the method onFocusStatusChanged when onWindowFocusChangeListener receive onFocusChanged. 347 */ onFocusChanged(boolean hasFocus)348 private void onFocusChanged(boolean hasFocus) { 349 SetupCompatServiceInvoker.get(getContext()) 350 .onFocusStatusChanged( 351 FocusChangedMetricHelper.getScreenName(activity), 352 FocusChangedMetricHelper.getExtraBundle( 353 activity, PartnerCustomizationLayout.this, hasFocus)); 354 } 355 } 356