• 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.content.ContentResolver;
20 import android.content.Context;
21 import android.content.res.Configuration;
22 import android.content.res.Resources;
23 import android.content.res.Resources.NotFoundException;
24 import android.database.ContentObserver;
25 import android.graphics.drawable.Drawable;
26 import android.net.Uri;
27 import android.os.Build;
28 import android.os.Build.VERSION_CODES;
29 import android.os.Bundle;
30 import android.util.DisplayMetrics;
31 import android.util.Log;
32 import android.util.TypedValue;
33 import androidx.annotation.ColorInt;
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 import androidx.annotation.VisibleForTesting;
37 import com.google.android.setupcompat.partnerconfig.PartnerConfig.ResourceType;
38 import com.google.android.setupcompat.util.BuildCompatUtils;
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.EnumMap;
42 import java.util.List;
43 
44 /** The helper reads and caches the partner configurations from SUW. */
45 public class PartnerConfigHelper {
46 
47   private static final String TAG = PartnerConfigHelper.class.getSimpleName();
48 
49   public static final String SUW_AUTHORITY = "com.google.android.setupwizard.partner";
50 
51   @VisibleForTesting public static final String SUW_GET_PARTNER_CONFIG_METHOD = "getOverlayConfig";
52 
53   @VisibleForTesting public static final String KEY_FALLBACK_CONFIG = "fallbackConfig";
54 
55   @VisibleForTesting
56   public static final String IS_SUW_DAY_NIGHT_ENABLED_METHOD = "isSuwDayNightEnabled";
57 
58   @VisibleForTesting
59   public static final String IS_EXTENDED_PARTNER_CONFIG_ENABLED_METHOD =
60       "isExtendedPartnerConfigEnabled";
61 
62   @VisibleForTesting
63   public static final String IS_MATERIAL_YOU_STYLE_ENABLED_METHOD = "IsMaterialYouStyleEnabled";
64 
65   @VisibleForTesting
66   public static final String IS_DYNAMIC_COLOR_ENABLED_METHOD = "isDynamicColorEnabled";
67 
68   @VisibleForTesting
69   public static final String IS_NEUTRAL_BUTTON_STYLE_ENABLED_METHOD = "isNeutralButtonStyleEnabled";
70 
71   @VisibleForTesting
72   public static final String GET_SUW_DEFAULT_THEME_STRING_METHOD = "suwDefaultThemeString";
73 
74   @VisibleForTesting public static final String SUW_PACKAGE_NAME = "com.google.android.setupwizard";
75   @VisibleForTesting public static final String MATERIAL_YOU_RESOURCE_SUFFIX = "_material_you";
76 
77   @VisibleForTesting static Bundle suwDayNightEnabledBundle = null;
78 
79   @VisibleForTesting public static Bundle applyExtendedPartnerConfigBundle = null;
80 
81   @VisibleForTesting public static Bundle applyMaterialYouConfigBundle = null;
82 
83   @VisibleForTesting public static Bundle applyDynamicColorBundle = null;
84 
85   @VisibleForTesting public static Bundle applyNeutralButtonStyleBundle = null;
86 
87   @VisibleForTesting public static Bundle suwDefaultThemeBundle = null;
88 
89   private static PartnerConfigHelper instance = null;
90 
91   @VisibleForTesting Bundle resultBundle = null;
92 
93   @VisibleForTesting
94   final EnumMap<PartnerConfig, Object> partnerResourceCache = new EnumMap<>(PartnerConfig.class);
95 
96   private static ContentObserver contentObserver;
97 
98   private static int savedConfigUiMode;
99 
100   @VisibleForTesting public static int savedOrientation = Configuration.ORIENTATION_PORTRAIT;
101 
102   /**
103    * When testing related to fake PartnerConfigHelper instance, should sync the following saved
104    * config with testing environment.
105    */
106   @VisibleForTesting public static int savedScreenHeight = Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
107 
108   @VisibleForTesting public static int savedScreenWidth = Configuration.SCREEN_WIDTH_DP_UNDEFINED;
109 
get(@onNull Context context)110   public static synchronized PartnerConfigHelper get(@NonNull Context context) {
111     if (!isValidInstance(context)) {
112       instance = new PartnerConfigHelper(context);
113     }
114     return instance;
115   }
116 
isValidInstance(@onNull Context context)117   private static boolean isValidInstance(@NonNull Context context) {
118     Configuration currentConfig = context.getResources().getConfiguration();
119     if (instance == null) {
120       savedConfigUiMode = currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
121       savedOrientation = currentConfig.orientation;
122       savedScreenWidth = currentConfig.screenWidthDp;
123       savedScreenHeight = currentConfig.screenHeightDp;
124       return false;
125     } else {
126       boolean uiModeChanged =
127           isSetupWizardDayNightEnabled(context)
128               && (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) != savedConfigUiMode;
129       if (uiModeChanged
130           || currentConfig.orientation != savedOrientation
131           || currentConfig.screenWidthDp != savedScreenWidth
132           || currentConfig.screenHeightDp != savedScreenHeight) {
133         savedConfigUiMode = currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
134         savedOrientation = currentConfig.orientation;
135         savedScreenHeight = currentConfig.screenHeightDp;
136         savedScreenWidth = currentConfig.screenWidthDp;
137         resetInstance();
138         return false;
139       }
140     }
141     return true;
142   }
143 
PartnerConfigHelper(Context context)144   private PartnerConfigHelper(Context context) {
145     getPartnerConfigBundle(context);
146 
147     registerContentObserver(context);
148   }
149 
150   /**
151    * Returns whether partner customized config values are available. This is true if setup wizard's
152    * content provider returns us a non-empty bundle, even if all the values are default, and none
153    * are customized by the overlay APK.
154    */
isAvailable()155   public boolean isAvailable() {
156     return resultBundle != null && !resultBundle.isEmpty();
157   }
158 
159   /**
160    * Returns whether the given {@code resourceConfig} are available. This is true if setup wizard's
161    * content provider returns us a non-empty bundle, and this result bundle includes the given
162    * {@code resourceConfig} even if all the values are default, and none are customized by the
163    * overlay APK.
164    */
isPartnerConfigAvailable(PartnerConfig resourceConfig)165   public boolean isPartnerConfigAvailable(PartnerConfig resourceConfig) {
166     return isAvailable() && resultBundle.containsKey(resourceConfig.getResourceName());
167   }
168 
169   /**
170    * Returns the color of given {@code resourceConfig}, or 0 if the given {@code resourceConfig} is
171    * not found. If the {@code ResourceType} of the given {@code resourceConfig} is not color,
172    * IllegalArgumentException will be thrown.
173    *
174    * @param context The context of client activity
175    * @param resourceConfig The {@link PartnerConfig} of target resource
176    */
177   @ColorInt
getColor(@onNull Context context, PartnerConfig resourceConfig)178   public int getColor(@NonNull Context context, PartnerConfig resourceConfig) {
179     if (resourceConfig.getResourceType() != ResourceType.COLOR) {
180       throw new IllegalArgumentException("Not a color resource");
181     }
182 
183     if (partnerResourceCache.containsKey(resourceConfig)) {
184       return (int) partnerResourceCache.get(resourceConfig);
185     }
186 
187     int result = 0;
188     try {
189       ResourceEntry resourceEntry =
190           getResourceEntryFromKey(context, resourceConfig.getResourceName());
191       Resources resource = resourceEntry.getResources();
192       int resId = resourceEntry.getResourceId();
193 
194       // for @null
195       TypedValue outValue = new TypedValue();
196       resource.getValue(resId, outValue, true);
197       if (outValue.type == TypedValue.TYPE_REFERENCE && outValue.data == 0) {
198         return result;
199       }
200 
201       if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
202         result = resource.getColor(resId, null);
203       } else {
204         result = resource.getColor(resId);
205       }
206       partnerResourceCache.put(resourceConfig, result);
207     } catch (NullPointerException exception) {
208       // fall through
209     }
210     return result;
211   }
212 
213   /**
214    * Returns the {@code Drawable} of given {@code resourceConfig}, or {@code null} if the given
215    * {@code resourceConfig} is not found. If the {@code ResourceType} of the given {@code
216    * resourceConfig} is not drawable, IllegalArgumentException will be thrown.
217    *
218    * @param context The context of client activity
219    * @param resourceConfig The {@code PartnerConfig} of target resource
220    */
221   @Nullable
getDrawable(@onNull Context context, PartnerConfig resourceConfig)222   public Drawable getDrawable(@NonNull Context context, PartnerConfig resourceConfig) {
223     if (resourceConfig.getResourceType() != ResourceType.DRAWABLE) {
224       throw new IllegalArgumentException("Not a drawable resource");
225     }
226 
227     if (partnerResourceCache.containsKey(resourceConfig)) {
228       return (Drawable) partnerResourceCache.get(resourceConfig);
229     }
230 
231     Drawable result = null;
232     try {
233       ResourceEntry resourceEntry =
234           getResourceEntryFromKey(context, resourceConfig.getResourceName());
235       Resources resource = resourceEntry.getResources();
236       int resId = resourceEntry.getResourceId();
237 
238       // for @null
239       TypedValue outValue = new TypedValue();
240       resource.getValue(resId, outValue, true);
241       if (outValue.type == TypedValue.TYPE_REFERENCE && outValue.data == 0) {
242         return result;
243       }
244 
245       if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
246         result = resource.getDrawable(resId, null);
247       } else {
248         result = resource.getDrawable(resId);
249       }
250       partnerResourceCache.put(resourceConfig, result);
251     } catch (NullPointerException | NotFoundException exception) {
252       // fall through
253     }
254     return result;
255   }
256 
257   /**
258    * Returns the string of the given {@code resourceConfig}, or {@code null} if the given {@code
259    * resourceConfig} is not found. If the {@code ResourceType} of the given {@code resourceConfig}
260    * is not string, IllegalArgumentException will be thrown.
261    *
262    * @param context The context of client activity
263    * @param resourceConfig The {@code PartnerConfig} of target resource
264    */
265   @Nullable
getString(@onNull Context context, PartnerConfig resourceConfig)266   public String getString(@NonNull Context context, PartnerConfig resourceConfig) {
267     if (resourceConfig.getResourceType() != ResourceType.STRING) {
268       throw new IllegalArgumentException("Not a string resource");
269     }
270 
271     if (partnerResourceCache.containsKey(resourceConfig)) {
272       return (String) partnerResourceCache.get(resourceConfig);
273     }
274 
275     String result = null;
276     try {
277       ResourceEntry resourceEntry =
278           getResourceEntryFromKey(context, resourceConfig.getResourceName());
279       Resources resource = resourceEntry.getResources();
280       int resId = resourceEntry.getResourceId();
281 
282       result = resource.getString(resId);
283       partnerResourceCache.put(resourceConfig, result);
284     } catch (NullPointerException exception) {
285       // fall through
286     }
287     return result;
288   }
289 
290   /**
291    * Returns the string array of the given {@code resourceConfig}, or {@code null} if the given
292    * {@code resourceConfig} is not found. If the {@code ResourceType} of the given {@code
293    * resourceConfig} is not string, IllegalArgumentException will be thrown.
294    *
295    * @param context The context of client activity
296    * @param resourceConfig The {@code PartnerConfig} of target resource
297    */
298   @NonNull
getStringArray(@onNull Context context, PartnerConfig resourceConfig)299   public List<String> getStringArray(@NonNull Context context, PartnerConfig resourceConfig) {
300     if (resourceConfig.getResourceType() != ResourceType.STRING_ARRAY) {
301       throw new IllegalArgumentException("Not a string array resource");
302     }
303 
304     String[] result;
305     List<String> listResult = new ArrayList<>();
306 
307     try {
308       ResourceEntry resourceEntry =
309           getResourceEntryFromKey(context, resourceConfig.getResourceName());
310       Resources resource = resourceEntry.getResources();
311       int resId = resourceEntry.getResourceId();
312 
313       result = resource.getStringArray(resId);
314       Collections.addAll(listResult, result);
315     } catch (NullPointerException exception) {
316       // fall through
317     }
318 
319     return listResult;
320   }
321 
322   /**
323    * Returns the boolean of given {@code resourceConfig}, or {@code defaultValue} if the given
324    * {@code resourceName} is not found. If the {@code ResourceType} of the given {@code
325    * resourceConfig} is not boolean, IllegalArgumentException will be thrown.
326    *
327    * @param context The context of client activity
328    * @param resourceConfig The {@code PartnerConfig} of target resource
329    * @param defaultValue The default value
330    */
getBoolean( @onNull Context context, PartnerConfig resourceConfig, boolean defaultValue)331   public boolean getBoolean(
332       @NonNull Context context, PartnerConfig resourceConfig, boolean defaultValue) {
333     if (resourceConfig.getResourceType() != ResourceType.BOOL) {
334       throw new IllegalArgumentException("Not a bool resource");
335     }
336 
337     if (partnerResourceCache.containsKey(resourceConfig)) {
338       return (boolean) partnerResourceCache.get(resourceConfig);
339     }
340 
341     boolean result = defaultValue;
342     try {
343       ResourceEntry resourceEntry =
344           getResourceEntryFromKey(context, resourceConfig.getResourceName());
345       Resources resource = resourceEntry.getResources();
346       int resId = resourceEntry.getResourceId();
347 
348       result = resource.getBoolean(resId);
349       partnerResourceCache.put(resourceConfig, result);
350     } catch (NullPointerException | NotFoundException exception) {
351       // fall through
352     }
353     return result;
354   }
355 
356   /**
357    * Returns the dimension of given {@code resourceConfig}. The default return value is 0.
358    *
359    * @param context The context of client activity
360    * @param resourceConfig The {@code PartnerConfig} of target resource
361    */
getDimension(@onNull Context context, PartnerConfig resourceConfig)362   public float getDimension(@NonNull Context context, PartnerConfig resourceConfig) {
363     return getDimension(context, resourceConfig, 0);
364   }
365 
366   /**
367    * Returns the dimension of given {@code resourceConfig}. If the given {@code resourceConfig} is
368    * not found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
369    * resourceConfig} is not dimension, will throw IllegalArgumentException.
370    *
371    * @param context The context of client activity
372    * @param resourceConfig The {@code PartnerConfig} of target resource
373    * @param defaultValue The default value
374    */
getDimension( @onNull Context context, PartnerConfig resourceConfig, float defaultValue)375   public float getDimension(
376       @NonNull Context context, PartnerConfig resourceConfig, float defaultValue) {
377     if (resourceConfig.getResourceType() != ResourceType.DIMENSION) {
378       throw new IllegalArgumentException("Not a dimension resource");
379     }
380 
381     if (partnerResourceCache.containsKey(resourceConfig)) {
382       return getDimensionFromTypedValue(
383           context, (TypedValue) partnerResourceCache.get(resourceConfig));
384     }
385 
386     float result = defaultValue;
387     try {
388       ResourceEntry resourceEntry =
389           getResourceEntryFromKey(context, resourceConfig.getResourceName());
390       Resources resource = resourceEntry.getResources();
391       int resId = resourceEntry.getResourceId();
392 
393       result = resource.getDimension(resId);
394       TypedValue value = getTypedValueFromResource(resource, resId, TypedValue.TYPE_DIMENSION);
395       partnerResourceCache.put(resourceConfig, value);
396       result =
397           getDimensionFromTypedValue(
398               context, (TypedValue) partnerResourceCache.get(resourceConfig));
399     } catch (NullPointerException | NotFoundException exception) {
400       // fall through
401     }
402     return result;
403   }
404 
405   /**
406    * Returns the float of given {@code resourceConfig}. The default return value is 0.
407    *
408    * @param context The context of client activity
409    * @param resourceConfig The {@code PartnerConfig} of target resource
410    */
getFraction(@onNull Context context, PartnerConfig resourceConfig)411   public float getFraction(@NonNull Context context, PartnerConfig resourceConfig) {
412     return getFraction(context, resourceConfig, 0.0f);
413   }
414 
415   /**
416    * Returns the float of given {@code resourceConfig}. If the given {@code resourceConfig} not
417    * found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
418    * resourceConfig} is not fraction, will throw IllegalArgumentException.
419    *
420    * @param context The context of client activity
421    * @param resourceConfig The {@code PartnerConfig} of target resource
422    * @param defaultValue The default value
423    */
getFraction( @onNull Context context, PartnerConfig resourceConfig, float defaultValue)424   public float getFraction(
425       @NonNull Context context, PartnerConfig resourceConfig, float defaultValue) {
426     if (resourceConfig.getResourceType() != ResourceType.FRACTION) {
427       throw new IllegalArgumentException("Not a fraction resource");
428     }
429 
430     if (partnerResourceCache.containsKey(resourceConfig)) {
431       return (float) partnerResourceCache.get(resourceConfig);
432     }
433 
434     float result = defaultValue;
435     try {
436       ResourceEntry resourceEntry =
437           getResourceEntryFromKey(context, resourceConfig.getResourceName());
438       Resources resource = resourceEntry.getResources();
439       int resId = resourceEntry.getResourceId();
440 
441       result = resource.getFraction(resId, 1, 1);
442       partnerResourceCache.put(resourceConfig, result);
443     } catch (NullPointerException | NotFoundException exception) {
444       // fall through
445     }
446     return result;
447   }
448 
449   /**
450    * Returns the integer of given {@code resourceConfig}. If the given {@code resourceConfig} is not
451    * found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
452    * resourceConfig} is not dimension, will throw IllegalArgumentException.
453    *
454    * @param context The context of client activity
455    * @param resourceConfig The {@code PartnerConfig} of target resource
456    * @param defaultValue The default value
457    */
getInteger(@onNull Context context, PartnerConfig resourceConfig, int defaultValue)458   public int getInteger(@NonNull Context context, PartnerConfig resourceConfig, int defaultValue) {
459     if (resourceConfig.getResourceType() != ResourceType.INTEGER) {
460       throw new IllegalArgumentException("Not a integer resource");
461     }
462 
463     if (partnerResourceCache.containsKey(resourceConfig)) {
464       return (int) partnerResourceCache.get(resourceConfig);
465     }
466 
467     int result = defaultValue;
468     try {
469       ResourceEntry resourceEntry =
470           getResourceEntryFromKey(context, resourceConfig.getResourceName());
471       Resources resource = resourceEntry.getResources();
472       int resId = resourceEntry.getResourceId();
473 
474       result = resource.getInteger(resId);
475       partnerResourceCache.put(resourceConfig, result);
476     } catch (NullPointerException | NotFoundException exception) {
477       // fall through
478     }
479     return result;
480   }
481 
482   /**
483    * Returns the {@link ResourceEntry} of given {@code resourceConfig}, or {@code null} if the given
484    * {@code resourceConfig} is not found. If the {@link ResourceType} of the given {@code
485    * resourceConfig} is not illustration, IllegalArgumentException will be thrown.
486    *
487    * @param context The context of client activity
488    * @param resourceConfig The {@link PartnerConfig} of target resource
489    */
490   @Nullable
getIllustrationResourceEntry( @onNull Context context, PartnerConfig resourceConfig)491   public ResourceEntry getIllustrationResourceEntry(
492       @NonNull Context context, PartnerConfig resourceConfig) {
493     if (resourceConfig.getResourceType() != ResourceType.ILLUSTRATION) {
494       throw new IllegalArgumentException("Not a illustration resource");
495     }
496 
497     if (partnerResourceCache.containsKey(resourceConfig)) {
498       return (ResourceEntry) partnerResourceCache.get(resourceConfig);
499     }
500 
501     try {
502       ResourceEntry resourceEntry =
503           getResourceEntryFromKey(context, resourceConfig.getResourceName());
504 
505       Resources resource = resourceEntry.getResources();
506       int resId = resourceEntry.getResourceId();
507 
508       // TODO: The illustration resource entry validation should validate is it a video
509       // resource or not?
510       // for @null
511       TypedValue outValue = new TypedValue();
512       resource.getValue(resId, outValue, true);
513       if (outValue.type == TypedValue.TYPE_REFERENCE && outValue.data == 0) {
514         return null;
515       }
516 
517       partnerResourceCache.put(resourceConfig, resourceEntry);
518       return resourceEntry;
519     } catch (NullPointerException exception) {
520       // fall through
521     }
522 
523     return null;
524   }
525 
getPartnerConfigBundle(Context context)526   private void getPartnerConfigBundle(Context context) {
527     if (resultBundle == null || resultBundle.isEmpty()) {
528       try {
529         resultBundle =
530             context
531                 .getContentResolver()
532                 .call(
533                     getContentUri(),
534                     SUW_GET_PARTNER_CONFIG_METHOD,
535                     /* arg= */ null,
536                     /* extras= */ null);
537         partnerResourceCache.clear();
538         Log.i(
539             TAG, "PartnerConfigsBundle=" + (resultBundle != null ? resultBundle.size() : "(null)"));
540       } catch (IllegalArgumentException | SecurityException exception) {
541         Log.w(TAG, "Fail to get config from suw provider");
542       }
543     }
544   }
545 
546   @Nullable
547   @VisibleForTesting
getResourceEntryFromKey(Context context, String resourceName)548   ResourceEntry getResourceEntryFromKey(Context context, String resourceName) {
549     Bundle resourceEntryBundle = resultBundle.getBundle(resourceName);
550     Bundle fallbackBundle = resultBundle.getBundle(KEY_FALLBACK_CONFIG);
551     if (fallbackBundle != null) {
552       resourceEntryBundle.putBundle(KEY_FALLBACK_CONFIG, fallbackBundle.getBundle(resourceName));
553     }
554     ResourceEntry adjustResourceEntry =
555         adjustResourceEntryDefaultValue(
556             context, ResourceEntry.fromBundle(context, resourceEntryBundle));
557     return adjustResourceEntryDayNightMode(context, adjustResourceEntry);
558   }
559 
560   /**
561    * Force to day mode if setup wizard does not support day/night mode and current system is in
562    * night mode.
563    */
adjustResourceEntryDayNightMode( Context context, ResourceEntry resourceEntry)564   private static ResourceEntry adjustResourceEntryDayNightMode(
565       Context context, ResourceEntry resourceEntry) {
566     Resources resource = resourceEntry.getResources();
567     Configuration configuration = resource.getConfiguration();
568     if (!isSetupWizardDayNightEnabled(context) && Util.isNightMode(configuration)) {
569       if (resourceEntry == null) {
570         Log.w(TAG, "resourceEntry is null, skip to force day mode.");
571         return resourceEntry;
572       }
573       configuration.uiMode =
574           Configuration.UI_MODE_NIGHT_NO
575               | (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
576       resource.updateConfiguration(configuration, resource.getDisplayMetrics());
577     }
578 
579     return resourceEntry;
580   }
581 
582   // Check the MNStyle flag and replace the inputResourceEntry.resourceName &
583   // inputResourceEntry.resourceId after T, that means if using Gliv4 before S, will always use
584   // glifv3 resources.
adjustResourceEntryDefaultValue(Context context, ResourceEntry inputResourceEntry)585   ResourceEntry adjustResourceEntryDefaultValue(Context context, ResourceEntry inputResourceEntry) {
586     if (BuildCompatUtils.isAtLeastT() && shouldApplyMaterialYouStyle(context)) {
587       // If not overlay resource
588       try {
589         if (SUW_PACKAGE_NAME.equals(inputResourceEntry.getPackageName())) {
590           String resourceTypeName =
591               inputResourceEntry
592                   .getResources()
593                   .getResourceTypeName(inputResourceEntry.getResourceId());
594           // try to update resourceName & resourceId
595           String materialYouResourceName =
596               inputResourceEntry.getResourceName().concat(MATERIAL_YOU_RESOURCE_SUFFIX);
597           int materialYouResourceId =
598               inputResourceEntry
599                   .getResources()
600                   .getIdentifier(
601                       materialYouResourceName,
602                       resourceTypeName,
603                       inputResourceEntry.getPackageName());
604           if (materialYouResourceId != 0) {
605             Log.i(TAG, "use material you resource:" + materialYouResourceName);
606             return new ResourceEntry(
607                 inputResourceEntry.getPackageName(),
608                 materialYouResourceName,
609                 materialYouResourceId,
610                 inputResourceEntry.getResources());
611           }
612         }
613       } catch (NotFoundException ex) {
614         // fall through
615       }
616     }
617     return inputResourceEntry;
618   }
619 
620   @VisibleForTesting
resetInstance()621   public static synchronized void resetInstance() {
622     instance = null;
623     suwDayNightEnabledBundle = null;
624     applyExtendedPartnerConfigBundle = null;
625     applyMaterialYouConfigBundle = null;
626     applyDynamicColorBundle = null;
627     applyNeutralButtonStyleBundle = null;
628     suwDefaultThemeBundle = null;
629   }
630 
631   /**
632    * Checks whether SetupWizard supports the DayNight theme during setup flow; if return false setup
633    * flow should force to light theme.
634    *
635    * <p>Returns true if the setupwizard is listening to system DayNight theme setting.
636    */
isSetupWizardDayNightEnabled(@onNull Context context)637   public static boolean isSetupWizardDayNightEnabled(@NonNull Context context) {
638     if (suwDayNightEnabledBundle == null) {
639       try {
640         suwDayNightEnabledBundle =
641             context
642                 .getContentResolver()
643                 .call(
644                     getContentUri(),
645                     IS_SUW_DAY_NIGHT_ENABLED_METHOD,
646                     /* arg= */ null,
647                     /* extras= */ null);
648       } catch (IllegalArgumentException | SecurityException exception) {
649         Log.w(TAG, "SetupWizard DayNight supporting status unknown; return as false.");
650         suwDayNightEnabledBundle = null;
651         return false;
652       }
653     }
654 
655     return (suwDayNightEnabledBundle != null
656         && suwDayNightEnabledBundle.getBoolean(IS_SUW_DAY_NIGHT_ENABLED_METHOD, false));
657   }
658 
659   /** Returns true if the SetupWizard supports the extended partner configs during setup flow. */
shouldApplyExtendedPartnerConfig(@onNull Context context)660   public static boolean shouldApplyExtendedPartnerConfig(@NonNull Context context) {
661     if (applyExtendedPartnerConfigBundle == null) {
662       try {
663         applyExtendedPartnerConfigBundle =
664             context
665                 .getContentResolver()
666                 .call(
667                     getContentUri(),
668                     IS_EXTENDED_PARTNER_CONFIG_ENABLED_METHOD,
669                     /* arg= */ null,
670                     /* extras= */ null);
671       } catch (IllegalArgumentException | SecurityException exception) {
672         Log.w(
673             TAG,
674             "SetupWizard extended partner configs supporting status unknown; return as false.");
675         applyExtendedPartnerConfigBundle = null;
676         return false;
677       }
678     }
679 
680     return (applyExtendedPartnerConfigBundle != null
681         && applyExtendedPartnerConfigBundle.getBoolean(
682             IS_EXTENDED_PARTNER_CONFIG_ENABLED_METHOD, false));
683   }
684 
685   /**
686    * Returns true if the SetupWizard is flow enabled "Material You(Glifv4)" style, or the result of
687    * shouldApplyExtendedPartnerConfig() in SDK S as fallback.
688    */
shouldApplyMaterialYouStyle(@onNull Context context)689   public static boolean shouldApplyMaterialYouStyle(@NonNull Context context) {
690     if (applyMaterialYouConfigBundle == null || applyMaterialYouConfigBundle.isEmpty()) {
691       try {
692         applyMaterialYouConfigBundle =
693             context
694                 .getContentResolver()
695                 .call(
696                     getContentUri(),
697                     IS_MATERIAL_YOU_STYLE_ENABLED_METHOD,
698                     /* arg= */ null,
699                     /* extras= */ null);
700         // The suw version did not support the flag yet, fallback to
701         // shouldApplyExtendedPartnerConfig() for SDK S.
702         if (applyMaterialYouConfigBundle != null
703             && applyMaterialYouConfigBundle.isEmpty()
704             && !BuildCompatUtils.isAtLeastT()) {
705           return shouldApplyExtendedPartnerConfig(context);
706         }
707       } catch (IllegalArgumentException | SecurityException exception) {
708         Log.w(TAG, "SetupWizard Material You configs supporting status unknown; return as false.");
709         applyMaterialYouConfigBundle = null;
710         return false;
711       }
712     }
713 
714     return (applyMaterialYouConfigBundle != null
715         && applyMaterialYouConfigBundle.getBoolean(IS_MATERIAL_YOU_STYLE_ENABLED_METHOD, false));
716   }
717 
718   /**
719    * Returns default glif theme name string from setupwizard, or if the setupwizard has not
720    * supported this api, return a null string.
721    */
722   @Nullable
getSuwDefaultThemeString(@onNull Context context)723   public static String getSuwDefaultThemeString(@NonNull Context context) {
724     if (suwDefaultThemeBundle == null || suwDefaultThemeBundle.isEmpty()) {
725       try {
726         suwDefaultThemeBundle =
727             context
728                 .getContentResolver()
729                 .call(
730                     getContentUri(),
731                     GET_SUW_DEFAULT_THEME_STRING_METHOD,
732                     /* arg= */ null,
733                     /* extras= */ null);
734       } catch (IllegalArgumentException | SecurityException exception) {
735         Log.w(TAG, "SetupWizard default theme status unknown; return as null.");
736         suwDefaultThemeBundle = null;
737         return null;
738       }
739     }
740     if (suwDefaultThemeBundle == null || suwDefaultThemeBundle.isEmpty()) {
741       return null;
742     }
743     return suwDefaultThemeBundle.getString(GET_SUW_DEFAULT_THEME_STRING_METHOD);
744   }
745 
746   /** Returns true if the SetupWizard supports the dynamic color during setup flow. */
isSetupWizardDynamicColorEnabled(@onNull Context context)747   public static boolean isSetupWizardDynamicColorEnabled(@NonNull Context context) {
748     if (applyDynamicColorBundle == null) {
749       try {
750         applyDynamicColorBundle =
751             context
752                 .getContentResolver()
753                 .call(
754                     getContentUri(),
755                     IS_DYNAMIC_COLOR_ENABLED_METHOD,
756                     /* arg= */ null,
757                     /* extras= */ null);
758       } catch (IllegalArgumentException | SecurityException exception) {
759         Log.w(TAG, "SetupWizard dynamic color supporting status unknown; return as false.");
760         applyDynamicColorBundle = null;
761         return false;
762       }
763     }
764 
765     return (applyDynamicColorBundle != null
766         && applyDynamicColorBundle.getBoolean(IS_DYNAMIC_COLOR_ENABLED_METHOD, false));
767   }
768 
769   /** Returns true if the SetupWizard supports the neutral button style during setup flow. */
isNeutralButtonStyleEnabled(@onNull Context context)770   public static boolean isNeutralButtonStyleEnabled(@NonNull Context context) {
771     if (applyNeutralButtonStyleBundle == null) {
772       try {
773         applyNeutralButtonStyleBundle =
774             context
775                 .getContentResolver()
776                 .call(
777                     getContentUri(),
778                     IS_NEUTRAL_BUTTON_STYLE_ENABLED_METHOD,
779                     /* arg= */ null,
780                     /* extras= */ null);
781       } catch (IllegalArgumentException | SecurityException exception) {
782         Log.w(TAG, "Neutral button style supporting status unknown; return as false.");
783         applyNeutralButtonStyleBundle = null;
784         return false;
785       }
786     }
787 
788     return (applyNeutralButtonStyleBundle != null
789         && applyNeutralButtonStyleBundle.getBoolean(IS_NEUTRAL_BUTTON_STYLE_ENABLED_METHOD, false));
790   }
791 
792   @VisibleForTesting
getContentUri()793   static Uri getContentUri() {
794     return new Uri.Builder()
795         .scheme(ContentResolver.SCHEME_CONTENT)
796         .authority(SUW_AUTHORITY)
797         .build();
798   }
799 
getTypedValueFromResource(Resources resource, int resId, int type)800   private static TypedValue getTypedValueFromResource(Resources resource, int resId, int type) {
801     TypedValue value = new TypedValue();
802     resource.getValue(resId, value, true);
803     if (value.type != type) {
804       throw new NotFoundException(
805           "Resource ID #0x"
806               + Integer.toHexString(resId)
807               + " type #0x"
808               + Integer.toHexString(value.type)
809               + " is not valid");
810     }
811     return value;
812   }
813 
getDimensionFromTypedValue(Context context, TypedValue value)814   private static float getDimensionFromTypedValue(Context context, TypedValue value) {
815     DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
816     return value.getDimension(displayMetrics);
817   }
818 
registerContentObserver(Context context)819   private static void registerContentObserver(Context context) {
820     if (isSetupWizardDayNightEnabled(context)) {
821       if (contentObserver != null) {
822         unregisterContentObserver(context);
823       }
824 
825       Uri contentUri = getContentUri();
826       try {
827         contentObserver =
828             new ContentObserver(null) {
829               @Override
830               public void onChange(boolean selfChange) {
831                 super.onChange(selfChange);
832                 resetInstance();
833               }
834             };
835         context
836             .getContentResolver()
837             .registerContentObserver(contentUri, /* notifyForDescendants= */ true, contentObserver);
838       } catch (SecurityException | NullPointerException | IllegalArgumentException e) {
839         Log.w(TAG, "Failed to register content observer for " + contentUri + ": " + e);
840       }
841     }
842   }
843 
unregisterContentObserver(Context context)844   private static void unregisterContentObserver(Context context) {
845     try {
846       context.getContentResolver().unregisterContentObserver(contentObserver);
847       contentObserver = null;
848     } catch (SecurityException | NullPointerException | IllegalArgumentException e) {
849       Log.w(TAG, "Failed to unregister content observer: " + e);
850     }
851   }
852 }
853