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