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