• 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.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