• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 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.partnerconfig;
18 
19 import android.app.Activity;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.ContextWrapper;
23 import android.content.pm.PackageManager;
24 import android.content.pm.PackageManager.NameNotFoundException;
25 import android.content.res.Configuration;
26 import android.content.res.Resources;
27 import android.content.res.Resources.NotFoundException;
28 import android.database.ContentObserver;
29 import android.graphics.drawable.Drawable;
30 import android.net.Uri;
31 import android.os.Build;
32 import android.os.Build.VERSION_CODES;
33 import android.os.Bundle;
34 import android.util.DisplayMetrics;
35 import android.util.Log;
36 import android.util.TypedValue;
37 import androidx.annotation.ColorInt;
38 import androidx.annotation.NonNull;
39 import androidx.annotation.Nullable;
40 import androidx.annotation.VisibleForTesting;
41 import androidx.window.embedding.ActivityEmbeddingController;
42 import com.google.android.setupcompat.partnerconfig.PartnerConfig.ResourceType;
43 import com.google.android.setupcompat.util.BuildCompatUtils;
44 import java.util.ArrayList;
45 import java.util.Collections;
46 import java.util.EnumMap;
47 import java.util.List;
48 import java.util.Objects;
49 
50 /** The helper reads and caches the partner configurations from SUW. */
51 public class PartnerConfigHelper {
52 
53   private static final String TAG = PartnerConfigHelper.class.getSimpleName();
54 
55   public static final String SUW_AUTHORITY = "com.google.android.setupwizard.partner";
56 
57   @VisibleForTesting public static final String SUW_GET_PARTNER_CONFIG_METHOD = "getOverlayConfig";
58 
59   @VisibleForTesting public static final String KEY_FALLBACK_CONFIG = "fallbackConfig";
60 
61   @VisibleForTesting
62   public static final String IS_SUW_DAY_NIGHT_ENABLED_METHOD = "isSuwDayNightEnabled";
63 
64   @VisibleForTesting
65   public static final String IS_EXTENDED_PARTNER_CONFIG_ENABLED_METHOD =
66       "isExtendedPartnerConfigEnabled";
67 
68   @VisibleForTesting
69   public static final String IS_MATERIAL_YOU_STYLE_ENABLED_METHOD = "IsMaterialYouStyleEnabled";
70 
71   @VisibleForTesting
72   public static final String IS_DYNAMIC_COLOR_ENABLED_METHOD = "isDynamicColorEnabled";
73 
74   @VisibleForTesting
75   public static final String IS_FULL_DYNAMIC_COLOR_ENABLED_METHOD = "isFullDynamicColorEnabled";
76 
77   @VisibleForTesting
78   public static final String IS_NEUTRAL_BUTTON_STYLE_ENABLED_METHOD = "isNeutralButtonStyleEnabled";
79 
80   @VisibleForTesting
81   public static final String IS_FONT_WEIGHT_ENABLED_METHOD = "isFontWeightEnabled";
82 
83   @VisibleForTesting
84   public static final String IS_EMBEDDED_ACTIVITY_ONE_PANE_ENABLED_METHOD =
85       "isEmbeddedActivityOnePaneEnabled";
86 
87   @VisibleForTesting
88   public static final String IS_FORCE_TWO_PANE_ENABLED_METHOD = "isForceTwoPaneEnabled";
89 
90   @VisibleForTesting
91   public static final String IS_GLIF_EXPRESSIVE_ENABLED = "isGlifExpressiveEnabled";
92 
93   @VisibleForTesting
94   public static final String IS_ENHANCED_SETUP_DESIGN_METRICS_ENABLED =
95       "isEnhancedSetupDesignMetricsEnabled";
96 
97   /** The method name to get the if the keyboard focus enhancement enabled */
98   @VisibleForTesting
99   public static final String IS_KEYBOARD_FOCUS_ENHANCEMENT_ENABLED_METHOD =
100       "isKeyboardFocusEnhancementEnabled";
101 
102   @VisibleForTesting
103   public static final String GET_SUW_DEFAULT_THEME_STRING_METHOD = "suwDefaultThemeString";
104 
105   @VisibleForTesting public static final String SUW_PACKAGE_NAME = "com.google.android.setupwizard";
106   @VisibleForTesting public static final String MATERIAL_YOU_RESOURCE_SUFFIX = "_material_you";
107   @VisibleForTesting public static final String GLIF_EXPRESSIVE_RESOURCE_SUFFIX = "_expressive";
108 
109   @VisibleForTesting
110   public static final String EMBEDDED_ACTIVITY_RESOURCE_SUFFIX = "_embedded_activity";
111 
112   @VisibleForTesting public static Bundle suwDayNightEnabledBundle = null;
113 
114   @VisibleForTesting public static Bundle applyExtendedPartnerConfigBundle = null;
115 
116   @VisibleForTesting public static Bundle applyMaterialYouConfigBundle = null;
117 
118   @VisibleForTesting public static Bundle applyDynamicColorBundle = null;
119   @VisibleForTesting public static Bundle applyFullDynamicColorBundle = null;
120 
121   @VisibleForTesting public static Bundle applyNeutralButtonStyleBundle = null;
122 
123   @VisibleForTesting public static Bundle applyFontWeightBundle = null;
124 
125   @VisibleForTesting public static Bundle applyEmbeddedActivityOnePaneBundle = null;
126 
127   @VisibleForTesting public static Bundle suwDefaultThemeBundle = null;
128 
129   @VisibleForTesting public static Bundle keyboardFocusEnhancementBundle = null;
130 
131   private static PartnerConfigHelper instance = null;
132 
133   @VisibleForTesting Bundle resultBundle = null;
134 
135   @VisibleForTesting
136   final EnumMap<PartnerConfig, Object> partnerResourceCache = new EnumMap<>(PartnerConfig.class);
137 
138   private static ContentObserver contentObserver;
139 
140   private static int savedConfigUiMode;
141 
142   private static boolean savedConfigEmbeddedActivityMode;
143 
144   @VisibleForTesting static Bundle applyTransitionBundle = null;
145 
146   @SuppressWarnings("NonFinalStaticField")
147   @VisibleForTesting
148   public static Bundle applyForceTwoPaneBundle = null;
149 
150   @VisibleForTesting public static Bundle applyGlifExpressiveBundle = null;
151 
152   @VisibleForTesting public static Bundle enableMetricsLoggingBundle = null;
153 
154   @VisibleForTesting public static int savedOrientation = Configuration.ORIENTATION_PORTRAIT;
155 
156   /** The method name to get if transition settings is set from client. */
157   public static final String APPLY_GLIF_THEME_CONTROLLED_TRANSITION_METHOD =
158       "applyGlifThemeControlledTransition";
159 
160   /**
161    * When testing related to fake PartnerConfigHelper instance, should sync the following saved
162    * config with testing environment.
163    */
164   @VisibleForTesting public static int savedScreenHeight = Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
165 
166   @VisibleForTesting public static int savedScreenWidth = Configuration.SCREEN_WIDTH_DP_UNDEFINED;
167 
168   /** A string to be a suffix of resource name which is associating to force two pane feature. */
169   @VisibleForTesting static final String FORCE_TWO_PANE_SUFFIX = "_two_pane";
170 
get(@onNull Context context)171   public static synchronized PartnerConfigHelper get(@NonNull Context context) {
172     if (!isValidInstance(context)) {
173       instance = new PartnerConfigHelper(context);
174     }
175     return instance;
176   }
177 
isValidInstance(@onNull Context context)178   private static boolean isValidInstance(@NonNull Context context) {
179     Configuration currentConfig = context.getResources().getConfiguration();
180     if (instance == null) {
181       savedConfigEmbeddedActivityMode =
182           isEmbeddedActivityOnePaneEnabled(context) && BuildCompatUtils.isAtLeastU();
183       savedConfigUiMode = currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
184       savedOrientation = currentConfig.orientation;
185       savedScreenWidth = currentConfig.screenWidthDp;
186       savedScreenHeight = currentConfig.screenHeightDp;
187       return false;
188     } else {
189       boolean uiModeChanged =
190           isSetupWizardDayNightEnabled(context)
191               && (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) != savedConfigUiMode;
192       boolean embeddedActivityModeChanged =
193           isEmbeddedActivityOnePaneEnabled(context) && BuildCompatUtils.isAtLeastU();
194       if (uiModeChanged
195           || embeddedActivityModeChanged != savedConfigEmbeddedActivityMode
196           || currentConfig.orientation != savedOrientation
197           || currentConfig.screenWidthDp != savedScreenWidth
198           || currentConfig.screenHeightDp != savedScreenHeight) {
199         savedConfigUiMode = currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
200         savedOrientation = currentConfig.orientation;
201         savedScreenHeight = currentConfig.screenHeightDp;
202         savedScreenWidth = currentConfig.screenWidthDp;
203         resetInstance();
204         return false;
205       }
206     }
207     return true;
208   }
209 
PartnerConfigHelper(Context context)210   private PartnerConfigHelper(Context context) {
211     getPartnerConfigBundle(context);
212 
213     registerContentObserver(context);
214   }
215 
216   /**
217    * Returns whether partner customized config values are available. This is true if setup wizard's
218    * content provider returns us a non-empty bundle, even if all the values are default, and none
219    * are customized by the overlay APK.
220    */
isAvailable()221   public boolean isAvailable() {
222     return resultBundle != null && !resultBundle.isEmpty();
223   }
224 
225   /**
226    * Returns whether the given {@code resourceConfig} are available. This is true if setup wizard's
227    * content provider returns us a non-empty bundle, and this result bundle includes the given
228    * {@code resourceConfig} even if all the values are default, and none are customized by the
229    * overlay APK.
230    */
isPartnerConfigAvailable(PartnerConfig resourceConfig)231   public boolean isPartnerConfigAvailable(PartnerConfig resourceConfig) {
232     return isAvailable() && resultBundle.containsKey(resourceConfig.getResourceName());
233   }
234 
235   /**
236    * Returns the color of given {@code resourceConfig}, or 0 if the given {@code resourceConfig} is
237    * not found. If the {@code ResourceType} of the given {@code resourceConfig} is not color,
238    * IllegalArgumentException will be thrown.
239    *
240    * @param context The context of client activity
241    * @param resourceConfig The {@link PartnerConfig} of target resource
242    */
243   @ColorInt
getColor(@onNull Context context, PartnerConfig resourceConfig)244   public int getColor(@NonNull Context context, PartnerConfig resourceConfig) {
245     if (resourceConfig.getResourceType() != ResourceType.COLOR) {
246       throw new IllegalArgumentException("Not a color resource");
247     }
248 
249     if (partnerResourceCache.containsKey(resourceConfig)) {
250       return (int) partnerResourceCache.get(resourceConfig);
251     }
252 
253     int result = 0;
254     try {
255       ResourceEntry resourceEntry =
256           getResourceEntryFromKey(context, resourceConfig.getResourceName());
257       Resources resource = resourceEntry.getResources();
258       int resId = resourceEntry.getResourceId();
259 
260       // for @null
261       TypedValue outValue = new TypedValue();
262       resource.getValue(resId, outValue, true);
263       if (outValue.type == TypedValue.TYPE_REFERENCE && outValue.data == 0) {
264         return result;
265       }
266 
267       if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
268         result = resource.getColor(resId, null);
269       } else {
270         result = resource.getColor(resId);
271       }
272       partnerResourceCache.put(resourceConfig, result);
273     } catch (NullPointerException exception) {
274       // fall through
275     }
276     return result;
277   }
278 
279   /**
280    * Returns the {@code Drawable} of given {@code resourceConfig}, or {@code null} if the given
281    * {@code resourceConfig} is not found. If the {@code ResourceType} of the given {@code
282    * resourceConfig} is not drawable, IllegalArgumentException will be thrown.
283    *
284    * @param context The context of client activity
285    * @param resourceConfig The {@code PartnerConfig} of target resource
286    */
287   @Nullable
getDrawable(@onNull Context context, PartnerConfig resourceConfig)288   public Drawable getDrawable(@NonNull Context context, PartnerConfig resourceConfig) {
289     if (resourceConfig.getResourceType() != ResourceType.DRAWABLE) {
290       throw new IllegalArgumentException("Not a drawable resource");
291     }
292 
293     if (partnerResourceCache.containsKey(resourceConfig)) {
294       return (Drawable) partnerResourceCache.get(resourceConfig);
295     }
296 
297     Drawable result = null;
298     try {
299       ResourceEntry resourceEntry =
300           getResourceEntryFromKey(context, resourceConfig.getResourceName());
301       Resources resource = resourceEntry.getResources();
302       int resId = resourceEntry.getResourceId();
303 
304       // for @null
305       TypedValue outValue = new TypedValue();
306       resource.getValue(resId, outValue, true);
307       if (outValue.type == TypedValue.TYPE_REFERENCE && outValue.data == 0) {
308         return result;
309       }
310 
311       if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
312         result = resource.getDrawable(resId, null);
313       } else {
314         result = resource.getDrawable(resId);
315       }
316       partnerResourceCache.put(resourceConfig, result);
317     } catch (NullPointerException | NotFoundException exception) {
318       // fall through
319     }
320     return result;
321   }
322 
323   /**
324    * Returns the string of the given {@code resourceConfig}, or {@code null} if the given {@code
325    * resourceConfig} is not found. If the {@code ResourceType} of the given {@code resourceConfig}
326    * is not string, IllegalArgumentException will be thrown.
327    *
328    * @param context The context of client activity
329    * @param resourceConfig The {@code PartnerConfig} of target resource
330    */
331   @Nullable
getString(@onNull Context context, PartnerConfig resourceConfig)332   public String getString(@NonNull Context context, PartnerConfig resourceConfig) {
333     if (resourceConfig.getResourceType() != ResourceType.STRING) {
334       throw new IllegalArgumentException("Not a string resource");
335     }
336 
337     if (partnerResourceCache.containsKey(resourceConfig)) {
338       return (String) partnerResourceCache.get(resourceConfig);
339     }
340 
341     String result = null;
342     try {
343       ResourceEntry resourceEntry =
344           getResourceEntryFromKey(context, resourceConfig.getResourceName());
345       Resources resource = resourceEntry.getResources();
346       int resId = resourceEntry.getResourceId();
347 
348       result = resource.getString(resId);
349       partnerResourceCache.put(resourceConfig, result);
350     } catch (NullPointerException exception) {
351       // fall through
352     }
353     return result;
354   }
355 
356   /**
357    * Returns the string array of the given {@code resourceConfig}, or {@code null} if the given
358    * {@code resourceConfig} is not found. If the {@code ResourceType} of the given {@code
359    * resourceConfig} is not string, IllegalArgumentException will be thrown.
360    *
361    * @param context The context of client activity
362    * @param resourceConfig The {@code PartnerConfig} of target resource
363    */
364   @NonNull
getStringArray(@onNull Context context, PartnerConfig resourceConfig)365   public List<String> getStringArray(@NonNull Context context, PartnerConfig resourceConfig) {
366     if (resourceConfig.getResourceType() != ResourceType.STRING_ARRAY) {
367       throw new IllegalArgumentException("Not a string array resource");
368     }
369 
370     String[] result;
371     List<String> listResult = new ArrayList<>();
372 
373     try {
374       ResourceEntry resourceEntry =
375           getResourceEntryFromKey(context, resourceConfig.getResourceName());
376       Resources resource = resourceEntry.getResources();
377       int resId = resourceEntry.getResourceId();
378 
379       result = resource.getStringArray(resId);
380       Collections.addAll(listResult, result);
381     } catch (NullPointerException exception) {
382       // fall through
383     }
384 
385     return listResult;
386   }
387 
388   /**
389    * Returns the boolean of given {@code resourceConfig}, or {@code defaultValue} if the given
390    * {@code resourceName} is not found. If the {@code ResourceType} of the given {@code
391    * resourceConfig} is not boolean, IllegalArgumentException will be thrown.
392    *
393    * @param context The context of client activity
394    * @param resourceConfig The {@code PartnerConfig} of target resource
395    * @param defaultValue The default value
396    */
getBoolean( @onNull Context context, PartnerConfig resourceConfig, boolean defaultValue)397   public boolean getBoolean(
398       @NonNull Context context, PartnerConfig resourceConfig, boolean defaultValue) {
399     if (resourceConfig.getResourceType() != ResourceType.BOOL) {
400       throw new IllegalArgumentException("Not a bool resource");
401     }
402 
403     if (partnerResourceCache.containsKey(resourceConfig)) {
404       return (boolean) partnerResourceCache.get(resourceConfig);
405     }
406 
407     boolean result = defaultValue;
408     try {
409       ResourceEntry resourceEntry =
410           getResourceEntryFromKey(context, resourceConfig.getResourceName());
411       Resources resource = resourceEntry.getResources();
412       int resId = resourceEntry.getResourceId();
413 
414       result = resource.getBoolean(resId);
415       partnerResourceCache.put(resourceConfig, result);
416     } catch (NullPointerException | NotFoundException exception) {
417       // fall through
418     }
419     return result;
420   }
421 
422   /**
423    * Returns the dimension of given {@code resourceConfig}. The default return value is 0.
424    *
425    * @param context The context of client activity
426    * @param resourceConfig The {@code PartnerConfig} of target resource
427    */
getDimension(@onNull Context context, PartnerConfig resourceConfig)428   public float getDimension(@NonNull Context context, PartnerConfig resourceConfig) {
429     return getDimension(context, resourceConfig, 0);
430   }
431 
432   /**
433    * Returns the dimension of given {@code resourceConfig}. If the given {@code resourceConfig} is
434    * not found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
435    * resourceConfig} is not dimension, will throw IllegalArgumentException.
436    *
437    * @param context The context of client activity
438    * @param resourceConfig The {@code PartnerConfig} of target resource
439    * @param defaultValue The default value
440    */
getDimension( @onNull Context context, PartnerConfig resourceConfig, float defaultValue)441   public float getDimension(
442       @NonNull Context context, PartnerConfig resourceConfig, float defaultValue) {
443     if (resourceConfig.getResourceType() != ResourceType.DIMENSION) {
444       throw new IllegalArgumentException("Not a dimension resource");
445     }
446 
447     if (partnerResourceCache.containsKey(resourceConfig)) {
448       return getDimensionFromTypedValue(
449           context, (TypedValue) partnerResourceCache.get(resourceConfig));
450     }
451 
452     float result = defaultValue;
453     try {
454       ResourceEntry resourceEntry =
455           getResourceEntryFromKey(context, resourceConfig.getResourceName());
456       Resources resource = resourceEntry.getResources();
457       int resId = resourceEntry.getResourceId();
458 
459       result = resource.getDimension(resId);
460       TypedValue value = getTypedValueFromResource(resource, resId, TypedValue.TYPE_DIMENSION);
461       partnerResourceCache.put(resourceConfig, value);
462       result =
463           getDimensionFromTypedValue(
464               context, (TypedValue) partnerResourceCache.get(resourceConfig));
465     } catch (NullPointerException | NotFoundException exception) {
466       // fall through
467     }
468     return result;
469   }
470 
471   /**
472    * Returns the float of given {@code resourceConfig}. The default return value is 0.
473    *
474    * @param context The context of client activity
475    * @param resourceConfig The {@code PartnerConfig} of target resource
476    */
getFraction(@onNull Context context, PartnerConfig resourceConfig)477   public float getFraction(@NonNull Context context, PartnerConfig resourceConfig) {
478     return getFraction(context, resourceConfig, 0.0f);
479   }
480 
481   /**
482    * Returns the float of given {@code resourceConfig}. If the given {@code resourceConfig} not
483    * found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
484    * resourceConfig} is not fraction, will throw IllegalArgumentException.
485    *
486    * @param context The context of client activity
487    * @param resourceConfig The {@code PartnerConfig} of target resource
488    * @param defaultValue The default value
489    */
getFraction( @onNull Context context, PartnerConfig resourceConfig, float defaultValue)490   public float getFraction(
491       @NonNull Context context, PartnerConfig resourceConfig, float defaultValue) {
492     if (resourceConfig.getResourceType() != ResourceType.FRACTION) {
493       throw new IllegalArgumentException("Not a fraction resource");
494     }
495 
496     if (partnerResourceCache.containsKey(resourceConfig)) {
497       return (float) partnerResourceCache.get(resourceConfig);
498     }
499 
500     float result = defaultValue;
501     try {
502       ResourceEntry resourceEntry =
503           getResourceEntryFromKey(context, resourceConfig.getResourceName());
504       Resources resource = resourceEntry.getResources();
505       int resId = resourceEntry.getResourceId();
506 
507       result = resource.getFraction(resId, 1, 1);
508       partnerResourceCache.put(resourceConfig, result);
509     } catch (NullPointerException | NotFoundException exception) {
510       // fall through
511     }
512     return result;
513   }
514 
515   /**
516    * Returns the integer of given {@code resourceConfig}. If the given {@code resourceConfig} is not
517    * found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
518    * resourceConfig} is not dimension, will throw IllegalArgumentException.
519    *
520    * @param context The context of client activity
521    * @param resourceConfig The {@code PartnerConfig} of target resource
522    * @param defaultValue The default value
523    */
getInteger(@onNull Context context, PartnerConfig resourceConfig, int defaultValue)524   public int getInteger(@NonNull Context context, PartnerConfig resourceConfig, int defaultValue) {
525     if (resourceConfig.getResourceType() != ResourceType.INTEGER) {
526       throw new IllegalArgumentException("Not a integer resource");
527     }
528 
529     if (partnerResourceCache.containsKey(resourceConfig)) {
530       return (int) partnerResourceCache.get(resourceConfig);
531     }
532 
533     int result = defaultValue;
534     try {
535       ResourceEntry resourceEntry =
536           getResourceEntryFromKey(context, resourceConfig.getResourceName());
537       Resources resource = resourceEntry.getResources();
538       int resId = resourceEntry.getResourceId();
539 
540       result = resource.getInteger(resId);
541       partnerResourceCache.put(resourceConfig, result);
542     } catch (NullPointerException | NotFoundException exception) {
543       // fall through
544     }
545     return result;
546   }
547 
548   /**
549    * Returns the {@link ResourceEntry} of given {@code resourceConfig}, or {@code null} if the given
550    * {@code resourceConfig} is not found. If the {@link ResourceType} of the given {@code
551    * resourceConfig} is not illustration, IllegalArgumentException will be thrown.
552    *
553    * @param context The context of client activity
554    * @param resourceConfig The {@link PartnerConfig} of target resource
555    */
556   @Nullable
getIllustrationResourceEntry( @onNull Context context, PartnerConfig resourceConfig)557   public ResourceEntry getIllustrationResourceEntry(
558       @NonNull Context context, PartnerConfig resourceConfig) {
559     if (resourceConfig.getResourceType() != ResourceType.ILLUSTRATION) {
560       throw new IllegalArgumentException("Not a illustration resource");
561     }
562 
563     if (partnerResourceCache.containsKey(resourceConfig)) {
564       return (ResourceEntry) partnerResourceCache.get(resourceConfig);
565     }
566 
567     try {
568       ResourceEntry resourceEntry =
569           getResourceEntryFromKey(context, resourceConfig.getResourceName());
570 
571       Resources resource = resourceEntry.getResources();
572       int resId = resourceEntry.getResourceId();
573 
574       // TODO: The illustration resource entry validation should validate is it a video
575       // resource or not?
576       // for @null
577       TypedValue outValue = new TypedValue();
578       resource.getValue(resId, outValue, true);
579       if (outValue.type == TypedValue.TYPE_REFERENCE && outValue.data == 0) {
580         return null;
581       }
582 
583       partnerResourceCache.put(resourceConfig, resourceEntry);
584       return resourceEntry;
585     } catch (NullPointerException exception) {
586       // fall through
587     }
588 
589     return null;
590   }
591 
getPartnerConfigBundle(Context context)592   private void getPartnerConfigBundle(Context context) {
593     if (resultBundle == null || resultBundle.isEmpty()) {
594       try {
595         resultBundle =
596             context
597                 .getContentResolver()
598                 .call(
599                     getContentUri(),
600                     SUW_GET_PARTNER_CONFIG_METHOD,
601                     /* arg= */ null,
602                     /* extras= */ null);
603         partnerResourceCache.clear();
604         Log.i(
605             TAG, "PartnerConfigsBundle=" + (resultBundle != null ? resultBundle.size() : "(null)"));
606       } catch (IllegalArgumentException | SecurityException exception) {
607         Log.w(TAG, "Fail to get config from suw provider");
608       }
609     }
610   }
611 
612   @Nullable
613   @VisibleForTesting
getResourceEntryFromKey(Context context, String resourceName)614   ResourceEntry getResourceEntryFromKey(Context context, String resourceName) {
615     Bundle resourceEntryBundle = resultBundle.getBundle(resourceName);
616     Bundle fallbackBundle = resultBundle.getBundle(KEY_FALLBACK_CONFIG);
617     if (fallbackBundle != null) {
618       resourceEntryBundle.putBundle(KEY_FALLBACK_CONFIG, fallbackBundle.getBundle(resourceName));
619     }
620 
621     ResourceEntry resourceEntry = ResourceEntry.fromBundle(context, resourceEntryBundle);
622 
623     if (BuildCompatUtils.isAtLeastU() && isActivityEmbedded(context)) {
624       resourceEntry = adjustEmbeddedActivityResourceEntryDefaultValue(context, resourceEntry);
625     } else if (BuildCompatUtils.isAtLeastV() && isGlifExpressiveEnabled(context)) {
626       resourceEntry = adjustGlifExpressiveResourceEntryDefaultValue(context, resourceEntry);
627     } else if (BuildCompatUtils.isAtLeastU() && isForceTwoPaneEnabled(context)) {
628       resourceEntry = adjustForceTwoPaneResourceEntryDefaultValue(context, resourceEntry);
629     } else if (BuildCompatUtils.isAtLeastT() && shouldApplyMaterialYouStyle(context)) {
630       resourceEntry = adjustMaterialYouResourceEntryDefaultValue(context, resourceEntry);
631     }
632 
633     return adjustResourceEntryDayNightMode(context, resourceEntry);
634   }
635 
636   @VisibleForTesting
isActivityEmbedded(Context context)637   boolean isActivityEmbedded(Context context) {
638     Activity activity;
639     try {
640       activity = lookupActivityFromContext(context);
641     } catch (IllegalArgumentException e) {
642       Log.w(TAG, "Not a Activity instance in parent tree");
643       return false;
644     }
645 
646     return isEmbeddedActivityOnePaneEnabled(context)
647         && ActivityEmbeddingController.getInstance(activity).isActivityEmbedded(activity);
648   }
649 
lookupActivityFromContext(Context context)650   public static Activity lookupActivityFromContext(Context context) {
651     if (context instanceof Activity) {
652       return (Activity) context;
653     } else if (context instanceof ContextWrapper) {
654       return lookupActivityFromContext(((ContextWrapper) context).getBaseContext());
655     } else {
656       throw new IllegalArgumentException("Cannot find instance of Activity in parent tree");
657     }
658   }
659 
660   /**
661    * Force to day mode if setup wizard does not support day/night mode and current system is in
662    * night mode.
663    */
adjustResourceEntryDayNightMode( Context context, ResourceEntry resourceEntry)664   private static ResourceEntry adjustResourceEntryDayNightMode(
665       Context context, ResourceEntry resourceEntry) {
666     Resources resource = resourceEntry.getResources();
667     Configuration configuration = resource.getConfiguration();
668     if (!isSetupWizardDayNightEnabled(context) && Util.isNightMode(configuration)) {
669       if (resourceEntry == null) {
670         Log.w(TAG, "resourceEntry is null, skip to force day mode.");
671         return resourceEntry;
672       }
673       configuration.uiMode =
674           Configuration.UI_MODE_NIGHT_NO
675               | (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
676       resource.updateConfiguration(configuration, resource.getDisplayMetrics());
677     }
678 
679     return resourceEntry;
680   }
681 
682   // Check the MNStyle flag and replace the inputResourceEntry.resourceName &
683   // inputResourceEntry.resourceId after T, that means if using Gliv4 before S, will always use
684   // glifv3 resources.
adjustMaterialYouResourceEntryDefaultValue( Context context, ResourceEntry inputResourceEntry)685   ResourceEntry adjustMaterialYouResourceEntryDefaultValue(
686       Context context, ResourceEntry inputResourceEntry) {
687     // If not overlay resource
688     try {
689       if (Objects.equals(inputResourceEntry.getPackageName(), SUW_PACKAGE_NAME)) {
690         String resourceTypeName =
691             inputResourceEntry
692                 .getResources()
693                 .getResourceTypeName(inputResourceEntry.getResourceId());
694         // try to update resourceName & resourceId
695         String materialYouResourceName =
696             inputResourceEntry.getResourceName().concat(MATERIAL_YOU_RESOURCE_SUFFIX);
697         int materialYouResourceId =
698             inputResourceEntry
699                 .getResources()
700                 .getIdentifier(
701                     materialYouResourceName, resourceTypeName, inputResourceEntry.getPackageName());
702         if (materialYouResourceId != 0) {
703           Log.i(TAG, "use material you resource:" + materialYouResourceName);
704           return new ResourceEntry(
705               inputResourceEntry.getPackageName(),
706               materialYouResourceName,
707               materialYouResourceId,
708               inputResourceEntry.getResources());
709         }
710       }
711     } catch (NotFoundException ex) {
712       // fall through
713     }
714     return inputResourceEntry;
715   }
716 
717   // Check the embedded activity flag and replace the inputResourceEntry.resourceName &
718   // inputResourceEntry.resourceId, and try to find the embedded resource from the different
719   // package.
adjustEmbeddedActivityResourceEntryDefaultValue( Context context, ResourceEntry inputResourceEntry)720   ResourceEntry adjustEmbeddedActivityResourceEntryDefaultValue(
721       Context context, ResourceEntry inputResourceEntry) {
722     // If not overlay resource
723     try {
724       String resourceTypeName =
725           inputResourceEntry.getResources().getResourceTypeName(inputResourceEntry.getResourceId());
726       // For the first time to get embedded activity resource id, it may get from setup wizard
727       // package or Overlay package.
728       String embeddedActivityResourceName =
729           inputResourceEntry.getResourceName().concat(EMBEDDED_ACTIVITY_RESOURCE_SUFFIX);
730       int embeddedActivityResourceId =
731           inputResourceEntry
732               .getResources()
733               .getIdentifier(
734                   embeddedActivityResourceName,
735                   resourceTypeName,
736                   inputResourceEntry.getPackageName());
737       if (embeddedActivityResourceId != 0) {
738         Log.i(TAG, "use embedded activity resource:" + embeddedActivityResourceName);
739         return new ResourceEntry(
740             inputResourceEntry.getPackageName(),
741             embeddedActivityResourceName,
742             embeddedActivityResourceId,
743             inputResourceEntry.getResources());
744       } else {
745         // If resource id is not available from the Overlay package, try to get it from setup wizard
746         // package.
747         PackageManager manager = context.getPackageManager();
748         Resources resources = manager.getResourcesForApplication(SUW_PACKAGE_NAME);
749         embeddedActivityResourceId =
750             resources.getIdentifier(
751                 embeddedActivityResourceName, resourceTypeName, SUW_PACKAGE_NAME);
752         if (embeddedActivityResourceId != 0) {
753           return new ResourceEntry(
754               SUW_PACKAGE_NAME,
755               embeddedActivityResourceName,
756               embeddedActivityResourceId,
757               resources);
758         }
759       }
760     } catch (NotFoundException | NameNotFoundException ex) {
761       // fall through
762     }
763     return inputResourceEntry;
764   }
765 
766   // Retrieve {@code resourceEntry} with _two_pane suffix resource from the partner resource,
767   // otherwise fallback to origin partner resource if two pane resource not available.
adjustForceTwoPaneResourceEntryDefaultValue( Context context, ResourceEntry resourceEntry)768   ResourceEntry adjustForceTwoPaneResourceEntryDefaultValue(
769       Context context, ResourceEntry resourceEntry) {
770     if (context == null) {
771       return resourceEntry;
772     }
773 
774     try {
775       String resourceTypeName =
776           resourceEntry.getResources().getResourceTypeName(resourceEntry.getResourceId());
777       String forceTwoPaneResourceName =
778           resourceEntry.getResourceName().concat(FORCE_TWO_PANE_SUFFIX);
779       int twoPaneResourceId =
780           resourceEntry
781               .getResources()
782               .getIdentifier(
783                   forceTwoPaneResourceName, resourceTypeName, resourceEntry.getPackageName());
784       if (twoPaneResourceId != Resources.ID_NULL) {
785         Log.i(TAG, "two pane resource=" + forceTwoPaneResourceName);
786         return new ResourceEntry(
787             resourceEntry.getPackageName(),
788             forceTwoPaneResourceName,
789             twoPaneResourceId,
790             resourceEntry.getResources());
791       } else {
792         // If resource id is not available from the Overlay package, try to get it from setup wizard
793         // package.
794         PackageManager packageManager = context.getPackageManager();
795         Resources resources = packageManager.getResourcesForApplication(SUW_PACKAGE_NAME);
796         twoPaneResourceId =
797             resources.getIdentifier(forceTwoPaneResourceName, resourceTypeName, SUW_PACKAGE_NAME);
798         if (twoPaneResourceId != 0) {
799           return new ResourceEntry(
800               SUW_PACKAGE_NAME, forceTwoPaneResourceName, twoPaneResourceId, resources);
801         }
802       }
803     } catch (NameNotFoundException | NotFoundException ignore) {
804       // fall through
805     }
806     return resourceEntry;
807   }
808 
809   // Check the GlifExpressive flag and replace the inputResourceEntry.resourceName &
810   // inputResourceEntry.resourceId after V, that means if using GlifExpressive theme before V, will
811   // always use glifv4 resources.
adjustGlifExpressiveResourceEntryDefaultValue( Context context, ResourceEntry inputResourceEntry)812   ResourceEntry adjustGlifExpressiveResourceEntryDefaultValue(
813       Context context, ResourceEntry inputResourceEntry) {
814     // If not overlay resource
815     try {
816       if (Objects.equals(inputResourceEntry.getPackageName(), SUW_PACKAGE_NAME)) {
817         String resourceTypeName =
818             inputResourceEntry
819                 .getResources()
820                 .getResourceTypeName(inputResourceEntry.getResourceId());
821         // try to update resourceName & resourceId
822         String glifExpressiveResourceName =
823             inputResourceEntry.getResourceName().concat(GLIF_EXPRESSIVE_RESOURCE_SUFFIX);
824         int glifExpressiveResourceId =
825             inputResourceEntry
826                 .getResources()
827                 .getIdentifier(
828                     glifExpressiveResourceName,
829                     resourceTypeName,
830                     inputResourceEntry.getPackageName());
831         if (glifExpressiveResourceId != 0) {
832           Log.i(TAG, "use expressive resource:" + glifExpressiveResourceName);
833           return new ResourceEntry(
834               inputResourceEntry.getPackageName(),
835               glifExpressiveResourceName,
836               glifExpressiveResourceId,
837               inputResourceEntry.getResources());
838         }
839       }
840     } catch (NotFoundException ex) {
841       // fall through
842     }
843     return inputResourceEntry;
844   }
845 
846   @VisibleForTesting
resetInstance()847   public static synchronized void resetInstance() {
848     instance = null;
849     suwDayNightEnabledBundle = null;
850     applyExtendedPartnerConfigBundle = null;
851     applyMaterialYouConfigBundle = null;
852     applyDynamicColorBundle = null;
853     applyFullDynamicColorBundle = null;
854     applyNeutralButtonStyleBundle = null;
855     applyEmbeddedActivityOnePaneBundle = null;
856     suwDefaultThemeBundle = null;
857     applyTransitionBundle = null;
858     applyForceTwoPaneBundle = null;
859     applyGlifExpressiveBundle = null;
860     keyboardFocusEnhancementBundle = null;
861     enableMetricsLoggingBundle = null;
862   }
863 
864   /**
865    * Checks whether SetupWizard supports the DayNight theme during setup flow; if return false setup
866    * flow should force to light theme.
867    *
868    * <p>Returns true if the setupwizard is listening to system DayNight theme setting.
869    */
isSetupWizardDayNightEnabled(@onNull Context context)870   public static boolean isSetupWizardDayNightEnabled(@NonNull Context context) {
871     if (suwDayNightEnabledBundle == null) {
872       try {
873         suwDayNightEnabledBundle =
874             context
875                 .getContentResolver()
876                 .call(
877                     getContentUri(),
878                     IS_SUW_DAY_NIGHT_ENABLED_METHOD,
879                     /* arg= */ null,
880                     /* extras= */ null);
881       } catch (IllegalArgumentException | SecurityException exception) {
882         Log.w(TAG, "SetupWizard DayNight supporting status unknown; return as false.");
883         suwDayNightEnabledBundle = null;
884         return false;
885       }
886     }
887 
888     return (suwDayNightEnabledBundle != null
889         && suwDayNightEnabledBundle.getBoolean(IS_SUW_DAY_NIGHT_ENABLED_METHOD, false));
890   }
891 
892   /** Returns true if the SetupWizard supports the extended partner configs during setup flow. */
shouldApplyExtendedPartnerConfig(@onNull Context context)893   public static boolean shouldApplyExtendedPartnerConfig(@NonNull Context context) {
894     if (applyExtendedPartnerConfigBundle == null) {
895       try {
896         applyExtendedPartnerConfigBundle =
897             context
898                 .getContentResolver()
899                 .call(
900                     getContentUri(),
901                     IS_EXTENDED_PARTNER_CONFIG_ENABLED_METHOD,
902                     /* arg= */ null,
903                     /* extras= */ null);
904       } catch (IllegalArgumentException | SecurityException exception) {
905         Log.w(
906             TAG,
907             "SetupWizard extended partner configs supporting status unknown; return as false.");
908         applyExtendedPartnerConfigBundle = null;
909         return false;
910       }
911     }
912 
913     return (applyExtendedPartnerConfigBundle != null
914         && applyExtendedPartnerConfigBundle.getBoolean(
915             IS_EXTENDED_PARTNER_CONFIG_ENABLED_METHOD, false));
916   }
917 
918   /**
919    * Returns true if the SetupWizard is flow enabled "Material You(Glifv4)" style, or the result of
920    * shouldApplyExtendedPartnerConfig() in SDK S as fallback.
921    */
shouldApplyMaterialYouStyle(@onNull Context context)922   public static boolean shouldApplyMaterialYouStyle(@NonNull Context context) {
923     if (applyMaterialYouConfigBundle == null || applyMaterialYouConfigBundle.isEmpty()) {
924       try {
925         applyMaterialYouConfigBundle =
926             context
927                 .getContentResolver()
928                 .call(
929                     getContentUri(),
930                     IS_MATERIAL_YOU_STYLE_ENABLED_METHOD,
931                     /* arg= */ null,
932                     /* extras= */ null);
933         // The suw version did not support the flag yet, fallback to
934         // shouldApplyExtendedPartnerConfig() for SDK S.
935         if (applyMaterialYouConfigBundle != null
936             && applyMaterialYouConfigBundle.isEmpty()
937             && !BuildCompatUtils.isAtLeastT()) {
938           return shouldApplyExtendedPartnerConfig(context);
939         }
940       } catch (IllegalArgumentException | SecurityException exception) {
941         Log.w(TAG, "SetupWizard Material You configs supporting status unknown; return as false.");
942         applyMaterialYouConfigBundle = null;
943         return false;
944       }
945     }
946 
947     return ((applyMaterialYouConfigBundle != null
948             && applyMaterialYouConfigBundle.getBoolean(IS_MATERIAL_YOU_STYLE_ENABLED_METHOD, false))
949         || isGlifExpressiveEnabled(context));
950   }
951 
952   /**
953    * Returns default glif theme name string from setupwizard, or if the setupwizard has not
954    * supported this api, return a null string.
955    */
956   @Nullable
getSuwDefaultThemeString(@onNull Context context)957   public static String getSuwDefaultThemeString(@NonNull Context context) {
958     if (suwDefaultThemeBundle == null || suwDefaultThemeBundle.isEmpty()) {
959       try {
960         suwDefaultThemeBundle =
961             context
962                 .getContentResolver()
963                 .call(
964                     getContentUri(),
965                     GET_SUW_DEFAULT_THEME_STRING_METHOD,
966                     /* arg= */ null,
967                     /* extras= */ null);
968       } catch (IllegalArgumentException | SecurityException exception) {
969         Log.w(TAG, "SetupWizard default theme status unknown; return as null.");
970         suwDefaultThemeBundle = null;
971         return null;
972       }
973     }
974     if (suwDefaultThemeBundle == null || suwDefaultThemeBundle.isEmpty()) {
975       return null;
976     }
977     return suwDefaultThemeBundle.getString(GET_SUW_DEFAULT_THEME_STRING_METHOD);
978   }
979 
980   /** Returns true if the SetupWizard supports the dynamic color during setup flow. */
isSetupWizardDynamicColorEnabled(@onNull Context context)981   public static boolean isSetupWizardDynamicColorEnabled(@NonNull Context context) {
982     if (applyDynamicColorBundle == null) {
983       try {
984         applyDynamicColorBundle =
985             context
986                 .getContentResolver()
987                 .call(
988                     getContentUri(),
989                     IS_DYNAMIC_COLOR_ENABLED_METHOD,
990                     /* arg= */ null,
991                     /* extras= */ null);
992       } catch (IllegalArgumentException | SecurityException exception) {
993         Log.w(TAG, "SetupWizard dynamic color supporting status unknown; return as false.");
994         applyDynamicColorBundle = null;
995         return false;
996       }
997     }
998 
999     return (applyDynamicColorBundle != null
1000         && applyDynamicColorBundle.getBoolean(IS_DYNAMIC_COLOR_ENABLED_METHOD, false));
1001   }
1002 
1003   /** Returns {@code true} if the SetupWizard supports the full dynamic color during setup flow. */
isSetupWizardFullDynamicColorEnabled(@onNull Context context)1004   public static boolean isSetupWizardFullDynamicColorEnabled(@NonNull Context context) {
1005     if (applyFullDynamicColorBundle == null) {
1006       try {
1007         applyFullDynamicColorBundle =
1008             context
1009                 .getContentResolver()
1010                 .call(
1011                     getContentUri(),
1012                     IS_FULL_DYNAMIC_COLOR_ENABLED_METHOD,
1013                     /* arg= */ null,
1014                     /* extras= */ null);
1015       } catch (IllegalArgumentException | SecurityException exception) {
1016         Log.w(TAG, "SetupWizard full dynamic color supporting status unknown; return as false.");
1017         applyFullDynamicColorBundle = null;
1018         return false;
1019       }
1020     }
1021 
1022     return (applyFullDynamicColorBundle != null
1023         && applyFullDynamicColorBundle.getBoolean(IS_FULL_DYNAMIC_COLOR_ENABLED_METHOD, false));
1024   }
1025 
1026   /** Returns true if the SetupWizard supports the one-pane embedded activity during setup flow. */
isEmbeddedActivityOnePaneEnabled(@onNull Context context)1027   public static boolean isEmbeddedActivityOnePaneEnabled(@NonNull Context context) {
1028     if (applyEmbeddedActivityOnePaneBundle == null) {
1029       try {
1030         applyEmbeddedActivityOnePaneBundle =
1031             context
1032                 .getContentResolver()
1033                 .call(
1034                     getContentUri(),
1035                     IS_EMBEDDED_ACTIVITY_ONE_PANE_ENABLED_METHOD,
1036                     /* arg= */ null,
1037                     /* extras= */ null);
1038       } catch (IllegalArgumentException | SecurityException exception) {
1039         Log.w(
1040             TAG,
1041             "SetupWizard one-pane support in embedded activity status unknown; return as false.");
1042         applyEmbeddedActivityOnePaneBundle = null;
1043         return false;
1044       }
1045     }
1046 
1047     return (applyEmbeddedActivityOnePaneBundle != null
1048         && applyEmbeddedActivityOnePaneBundle.getBoolean(
1049             IS_EMBEDDED_ACTIVITY_ONE_PANE_ENABLED_METHOD, false));
1050   }
1051 
1052   /** Returns true if the SetupWizard supports the neutral button style during setup flow. */
isNeutralButtonStyleEnabled(@onNull Context context)1053   public static boolean isNeutralButtonStyleEnabled(@NonNull Context context) {
1054     if (applyNeutralButtonStyleBundle == null) {
1055       try {
1056         applyNeutralButtonStyleBundle =
1057             context
1058                 .getContentResolver()
1059                 .call(
1060                     getContentUri(),
1061                     IS_NEUTRAL_BUTTON_STYLE_ENABLED_METHOD,
1062                     /* arg= */ null,
1063                     /* extras= */ null);
1064       } catch (IllegalArgumentException | SecurityException exception) {
1065         Log.w(TAG, "Neutral button style supporting status unknown; return as false.");
1066         applyNeutralButtonStyleBundle = null;
1067         return false;
1068       }
1069     }
1070 
1071     return (applyNeutralButtonStyleBundle != null
1072         && applyNeutralButtonStyleBundle.getBoolean(IS_NEUTRAL_BUTTON_STYLE_ENABLED_METHOD, false));
1073   }
1074 
1075   /** Returns true if the SetupWizard supports the font weight customization during setup flow. */
isFontWeightEnabled(@onNull Context context)1076   public static boolean isFontWeightEnabled(@NonNull Context context) {
1077     if (applyFontWeightBundle == null) {
1078       try {
1079         applyFontWeightBundle =
1080             context
1081                 .getContentResolver()
1082                 .call(
1083                     getContentUri(),
1084                     IS_FONT_WEIGHT_ENABLED_METHOD,
1085                     /* arg= */ null,
1086                     /* extras= */ null);
1087       } catch (IllegalArgumentException | SecurityException exception) {
1088         Log.w(TAG, "Font weight supporting status unknown; return as false.");
1089         applyFontWeightBundle = null;
1090         return false;
1091       }
1092     }
1093 
1094     return (applyFontWeightBundle != null
1095         && applyFontWeightBundle.getBoolean(IS_FONT_WEIGHT_ENABLED_METHOD, true));
1096   }
1097 
1098   /**
1099    * Returns the system property to indicate the transition settings is set by Glif theme rather
1100    * than the client.
1101    */
isGlifThemeControlledTransitionApplied(@onNull Context context)1102   public static boolean isGlifThemeControlledTransitionApplied(@NonNull Context context) {
1103     if (applyTransitionBundle == null || applyTransitionBundle.isEmpty()) {
1104       try {
1105         applyTransitionBundle =
1106             context
1107                 .getContentResolver()
1108                 .call(
1109                     getContentUri(),
1110                     APPLY_GLIF_THEME_CONTROLLED_TRANSITION_METHOD,
1111                     /* arg= */ null,
1112                     /* extras= */ null);
1113       } catch (IllegalArgumentException | SecurityException exception) {
1114         Log.w(
1115             TAG,
1116             "applyGlifThemeControlledTransition unknown; return applyGlifThemeControlledTransition"
1117                 + " as default value");
1118       }
1119     }
1120     if (applyTransitionBundle != null && !applyTransitionBundle.isEmpty()) {
1121       return applyTransitionBundle.getBoolean(APPLY_GLIF_THEME_CONTROLLED_TRANSITION_METHOD, true);
1122     }
1123     return true;
1124   }
1125 
1126   /** Returns a boolean indicate whether the force two pane feature enable or not. */
isForceTwoPaneEnabled(@onNull Context context)1127   public static boolean isForceTwoPaneEnabled(@NonNull Context context) {
1128     if (applyForceTwoPaneBundle == null || applyForceTwoPaneBundle.isEmpty()) {
1129       try {
1130         applyForceTwoPaneBundle =
1131             context
1132                 .getContentResolver()
1133                 .call(
1134                     getContentUri(),
1135                     IS_FORCE_TWO_PANE_ENABLED_METHOD,
1136                     /* arg= */ null,
1137                     /* extras= */ null);
1138       } catch (IllegalArgumentException | SecurityException exception) {
1139         Log.w(TAG, "isForceTwoPaneEnabled status is unknown; return as false.");
1140       }
1141     }
1142     if (applyForceTwoPaneBundle != null && !applyForceTwoPaneBundle.isEmpty()) {
1143       return applyForceTwoPaneBundle.getBoolean(IS_FORCE_TWO_PANE_ENABLED_METHOD, false);
1144     }
1145     return false;
1146   }
1147 
1148   /** Returns whether the keyboard focus enhancement is enabled. */
isKeyboardFocusEnhancementEnabled(@onNull Context context)1149   public static boolean isKeyboardFocusEnhancementEnabled(@NonNull Context context) {
1150     if (keyboardFocusEnhancementBundle == null || keyboardFocusEnhancementBundle.isEmpty()) {
1151       try {
1152         keyboardFocusEnhancementBundle =
1153             context
1154                 .getContentResolver()
1155                 .call(
1156                     getContentUri(),
1157                     IS_KEYBOARD_FOCUS_ENHANCEMENT_ENABLED_METHOD,
1158                     /* arg= */ null,
1159                     /* extras= */ null);
1160       } catch (IllegalArgumentException | SecurityException exception) {
1161         Log.w(TAG, "SetupWizard keyboard focus enhancement status unknown; return as false.");
1162         keyboardFocusEnhancementBundle = null;
1163         return false;
1164       }
1165     }
1166     if (keyboardFocusEnhancementBundle == null || keyboardFocusEnhancementBundle.isEmpty()) {
1167       return false;
1168     }
1169     return keyboardFocusEnhancementBundle.getBoolean(IS_KEYBOARD_FOCUS_ENHANCEMENT_ENABLED_METHOD);
1170   }
1171 
1172   /**
1173    * Returns true if the SetupWizard supports Glif Expressive style inside or outside setup flow.
1174    */
isGlifExpressiveEnabled(@onNull Context context)1175   public static boolean isGlifExpressiveEnabled(@NonNull Context context) {
1176 
1177     if (applyGlifExpressiveBundle == null || applyGlifExpressiveBundle.isEmpty()) {
1178       try {
1179         applyGlifExpressiveBundle =
1180             context
1181                 .getContentResolver()
1182                 .call(
1183                     getContentUri(),
1184                     IS_GLIF_EXPRESSIVE_ENABLED,
1185                     /* arg= */ null,
1186                     /* extras= */ null);
1187       } catch (IllegalArgumentException | SecurityException exception) {
1188         Log.w(TAG, "isGlifExpressiveEnabled status is unknown; return as false.");
1189       }
1190     }
1191     if (applyGlifExpressiveBundle != null && !applyGlifExpressiveBundle.isEmpty()) {
1192       return applyGlifExpressiveBundle.getBoolean(IS_GLIF_EXPRESSIVE_ENABLED, false);
1193     }
1194 
1195     return false;
1196   }
1197 
1198   /** Returns true if the SetupWizard enable the UI component logging. */
isEnhancedSetupDesignMetricsEnabled(@onNull Context context)1199   public static boolean isEnhancedSetupDesignMetricsEnabled(@NonNull Context context) {
1200     if (enableMetricsLoggingBundle == null || enableMetricsLoggingBundle.isEmpty()) {
1201       try {
1202         enableMetricsLoggingBundle =
1203             context
1204                 .getContentResolver()
1205                 .call(
1206                     getContentUri(),
1207                     IS_ENHANCED_SETUP_DESIGN_METRICS_ENABLED,
1208                     /* arg= */ null,
1209                     /* extras= */ null);
1210       } catch (IllegalArgumentException | SecurityException exception) {
1211         Log.w(TAG, "Method " + IS_ENHANCED_SETUP_DESIGN_METRICS_ENABLED + " is unknown");
1212         enableMetricsLoggingBundle = null;
1213         return false;
1214       }
1215     }
1216 
1217     if (enableMetricsLoggingBundle != null && !enableMetricsLoggingBundle.isEmpty()) {
1218       return enableMetricsLoggingBundle.getBoolean(IS_ENHANCED_SETUP_DESIGN_METRICS_ENABLED, false);
1219     }
1220 
1221     return false;
1222   }
1223 
1224   @VisibleForTesting
getContentUri()1225   static Uri getContentUri() {
1226     return new Uri.Builder()
1227         .scheme(ContentResolver.SCHEME_CONTENT)
1228         .authority(SUW_AUTHORITY)
1229         .build();
1230   }
1231 
getTypedValueFromResource(Resources resource, int resId, int type)1232   private static TypedValue getTypedValueFromResource(Resources resource, int resId, int type) {
1233     TypedValue value = new TypedValue();
1234     resource.getValue(resId, value, true);
1235     if (value.type != type) {
1236       throw new NotFoundException(
1237           "Resource ID #0x"
1238               + Integer.toHexString(resId)
1239               + " type #0x"
1240               + Integer.toHexString(value.type)
1241               + " is not valid");
1242     }
1243     return value;
1244   }
1245 
getDimensionFromTypedValue(Context context, TypedValue value)1246   private static float getDimensionFromTypedValue(Context context, TypedValue value) {
1247     DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
1248     return value.getDimension(displayMetrics);
1249   }
1250 
registerContentObserver(Context context)1251   private static void registerContentObserver(Context context) {
1252     if (isSetupWizardDayNightEnabled(context)) {
1253       if (contentObserver != null) {
1254         unregisterContentObserver(context);
1255       }
1256 
1257       Uri contentUri = getContentUri();
1258       try {
1259         contentObserver =
1260             new ContentObserver(null) {
1261               @Override
1262               public void onChange(boolean selfChange) {
1263                 super.onChange(selfChange);
1264                 resetInstance();
1265               }
1266             };
1267         context
1268             .getContentResolver()
1269             .registerContentObserver(contentUri, /* notifyForDescendants= */ true, contentObserver);
1270       } catch (SecurityException | NullPointerException | IllegalArgumentException e) {
1271         Log.w(TAG, "Failed to register content observer for " + contentUri + ": " + e);
1272       }
1273     }
1274   }
1275 
unregisterContentObserver(Context context)1276   private static void unregisterContentObserver(Context context) {
1277     try {
1278       context.getContentResolver().unregisterContentObserver(contentObserver);
1279       contentObserver = null;
1280     } catch (SecurityException | NullPointerException | IllegalArgumentException e) {
1281       Log.w(TAG, "Failed to unregister content observer: " + e);
1282     }
1283   }
1284 }
1285