• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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