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 java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.EnumMap;
41 import java.util.List;
42 
43 /** The helper reads and caches the partner configurations from SUW. */
44 public class PartnerConfigHelper {
45 
46   private static final String TAG = PartnerConfigHelper.class.getSimpleName();
47 
48   @VisibleForTesting
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_DYNAMIC_COLOR_ENABLED_METHOD = "isDynamicColorEnabled";
64 
65   @VisibleForTesting static Bundle suwDayNightEnabledBundle = null;
66 
67   @VisibleForTesting public static Bundle applyExtendedPartnerConfigBundle = null;
68 
69   @VisibleForTesting public static Bundle applyDynamicColorBundle = null;
70 
71   private static PartnerConfigHelper instance = null;
72 
73   @VisibleForTesting Bundle resultBundle = null;
74 
75   @VisibleForTesting
76   final EnumMap<PartnerConfig, Object> partnerResourceCache = new EnumMap<>(PartnerConfig.class);
77 
78   private static ContentObserver contentObserver;
79 
80   private static int savedConfigUiMode;
81 
82   private static int savedOrientation = Configuration.ORIENTATION_PORTRAIT;
83 
get(@onNull Context context)84   public static synchronized PartnerConfigHelper get(@NonNull Context context) {
85     if (!isValidInstance(context)) {
86       instance = new PartnerConfigHelper(context);
87     }
88     return instance;
89   }
90 
isValidInstance(@onNull Context context)91   private static boolean isValidInstance(@NonNull Context context) {
92     Configuration currentConfig = context.getResources().getConfiguration();
93     if (instance == null) {
94       savedConfigUiMode = currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
95       savedOrientation = currentConfig.orientation;
96       return false;
97     } else {
98       if (isSetupWizardDayNightEnabled(context)
99           && (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) != savedConfigUiMode) {
100         savedConfigUiMode = currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
101         resetInstance();
102         return false;
103       } else if (currentConfig.orientation != savedOrientation) {
104         savedOrientation = currentConfig.orientation;
105         resetInstance();
106         return false;
107       }
108     }
109     return true;
110   }
111 
PartnerConfigHelper(Context context)112   private PartnerConfigHelper(Context context) {
113     getPartnerConfigBundle(context);
114 
115     registerContentObserver(context);
116   }
117 
118   /**
119    * Returns whether partner customized config values are available. This is true if setup wizard's
120    * content provider returns us a non-empty bundle, even if all the values are default, and none
121    * are customized by the overlay APK.
122    */
isAvailable()123   public boolean isAvailable() {
124     return resultBundle != null && !resultBundle.isEmpty();
125   }
126 
127   /**
128    * Returns whether the given {@code resourceConfig} are available. This is true if setup wizard's
129    * content provider returns us a non-empty bundle, and this result bundle includes the given
130    * {@code resourceConfig} even if all the values are default, and none are customized by the
131    * overlay APK.
132    */
isPartnerConfigAvailable(PartnerConfig resourceConfig)133   public boolean isPartnerConfigAvailable(PartnerConfig resourceConfig) {
134     return isAvailable() && resultBundle.containsKey(resourceConfig.getResourceName());
135   }
136 
137   /**
138    * Returns the color of given {@code resourceConfig}, or 0 if the given {@code resourceConfig} is
139    * not found. If the {@code ResourceType} of the given {@code resourceConfig} is not color,
140    * IllegalArgumentException will be thrown.
141    *
142    * @param context The context of client activity
143    * @param resourceConfig The {@link PartnerConfig} of target resource
144    */
145   @ColorInt
getColor(@onNull Context context, PartnerConfig resourceConfig)146   public int getColor(@NonNull Context context, PartnerConfig resourceConfig) {
147     if (resourceConfig.getResourceType() != ResourceType.COLOR) {
148       throw new IllegalArgumentException("Not a color resource");
149     }
150 
151     if (partnerResourceCache.containsKey(resourceConfig)) {
152       return (int) partnerResourceCache.get(resourceConfig);
153     }
154 
155     int result = 0;
156     try {
157       ResourceEntry resourceEntry =
158           getResourceEntryFromKey(context, resourceConfig.getResourceName());
159       Resources resource = resourceEntry.getResources();
160       int resId = resourceEntry.getResourceId();
161 
162       // for @null
163       TypedValue outValue = new TypedValue();
164       resource.getValue(resId, outValue, true);
165       if (outValue.type == TypedValue.TYPE_REFERENCE && outValue.data == 0) {
166         return result;
167       }
168 
169       if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
170         result = resource.getColor(resId, null);
171       } else {
172         result = resource.getColor(resId);
173       }
174       partnerResourceCache.put(resourceConfig, result);
175     } catch (NullPointerException exception) {
176       // fall through
177     }
178     return result;
179   }
180 
181   /**
182    * Returns the {@code Drawable} of given {@code resourceConfig}, or {@code null} if the given
183    * {@code resourceConfig} is not found. If the {@code ResourceType} of the given {@code
184    * resourceConfig} is not drawable, IllegalArgumentException will be thrown.
185    *
186    * @param context The context of client activity
187    * @param resourceConfig The {@code PartnerConfig} of target resource
188    */
189   @Nullable
getDrawable(@onNull Context context, PartnerConfig resourceConfig)190   public Drawable getDrawable(@NonNull Context context, PartnerConfig resourceConfig) {
191     if (resourceConfig.getResourceType() != ResourceType.DRAWABLE) {
192       throw new IllegalArgumentException("Not a drawable resource");
193     }
194 
195     if (partnerResourceCache.containsKey(resourceConfig)) {
196       return (Drawable) partnerResourceCache.get(resourceConfig);
197     }
198 
199     Drawable result = null;
200     try {
201       ResourceEntry resourceEntry =
202           getResourceEntryFromKey(context, resourceConfig.getResourceName());
203       Resources resource = resourceEntry.getResources();
204       int resId = resourceEntry.getResourceId();
205 
206       // for @null
207       TypedValue outValue = new TypedValue();
208       resource.getValue(resId, outValue, true);
209       if (outValue.type == TypedValue.TYPE_REFERENCE && outValue.data == 0) {
210         return result;
211       }
212 
213       if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
214         result = resource.getDrawable(resId, null);
215       } else {
216         result = resource.getDrawable(resId);
217       }
218       partnerResourceCache.put(resourceConfig, result);
219     } catch (NullPointerException | NotFoundException exception) {
220       // fall through
221     }
222     return result;
223   }
224 
225   /**
226    * Returns the string of the given {@code resourceConfig}, or {@code null} if the given {@code
227    * resourceConfig} is not found. If the {@code ResourceType} of the given {@code resourceConfig}
228    * is not string, IllegalArgumentException will be thrown.
229    *
230    * @param context The context of client activity
231    * @param resourceConfig The {@code PartnerConfig} of target resource
232    */
233   @Nullable
getString(@onNull Context context, PartnerConfig resourceConfig)234   public String getString(@NonNull Context context, PartnerConfig resourceConfig) {
235     if (resourceConfig.getResourceType() != ResourceType.STRING) {
236       throw new IllegalArgumentException("Not a string resource");
237     }
238 
239     if (partnerResourceCache.containsKey(resourceConfig)) {
240       return (String) partnerResourceCache.get(resourceConfig);
241     }
242 
243     String result = null;
244     try {
245       ResourceEntry resourceEntry =
246           getResourceEntryFromKey(context, resourceConfig.getResourceName());
247       Resources resource = resourceEntry.getResources();
248       int resId = resourceEntry.getResourceId();
249 
250       result = resource.getString(resId);
251       partnerResourceCache.put(resourceConfig, result);
252     } catch (NullPointerException exception) {
253       // fall through
254     }
255     return result;
256   }
257 
258   /**
259    * Returns the string array of the given {@code resourceConfig}, or {@code null} if the given
260    * {@code resourceConfig} is not found. If the {@code ResourceType} of the given {@code
261    * resourceConfig} is not string, IllegalArgumentException will be thrown.
262    *
263    * @param context The context of client activity
264    * @param resourceConfig The {@code PartnerConfig} of target resource
265    */
266   @NonNull
getStringArray(@onNull Context context, PartnerConfig resourceConfig)267   public List<String> getStringArray(@NonNull Context context, PartnerConfig resourceConfig) {
268     if (resourceConfig.getResourceType() != ResourceType.STRING_ARRAY) {
269       throw new IllegalArgumentException("Not a string array resource");
270     }
271 
272     String[] result;
273     List<String> listResult = new ArrayList<>();
274 
275     try {
276       ResourceEntry resourceEntry =
277           getResourceEntryFromKey(context, resourceConfig.getResourceName());
278       Resources resource = resourceEntry.getResources();
279       int resId = resourceEntry.getResourceId();
280 
281       result = resource.getStringArray(resId);
282       Collections.addAll(listResult, result);
283     } catch (NullPointerException exception) {
284       // fall through
285     }
286 
287     return listResult;
288   }
289 
290   /**
291    * Returns the boolean of given {@code resourceConfig}, or {@code defaultValue} if the given
292    * {@code resourceName} is not found. If the {@code ResourceType} of the given {@code
293    * resourceConfig} is not boolean, IllegalArgumentException will be thrown.
294    *
295    * @param context The context of client activity
296    * @param resourceConfig The {@code PartnerConfig} of target resource
297    * @param defaultValue The default value
298    */
getBoolean( @onNull Context context, PartnerConfig resourceConfig, boolean defaultValue)299   public boolean getBoolean(
300       @NonNull Context context, PartnerConfig resourceConfig, boolean defaultValue) {
301     if (resourceConfig.getResourceType() != ResourceType.BOOL) {
302       throw new IllegalArgumentException("Not a bool resource");
303     }
304 
305     if (partnerResourceCache.containsKey(resourceConfig)) {
306       return (boolean) partnerResourceCache.get(resourceConfig);
307     }
308 
309     boolean result = defaultValue;
310     try {
311       ResourceEntry resourceEntry =
312           getResourceEntryFromKey(context, resourceConfig.getResourceName());
313       Resources resource = resourceEntry.getResources();
314       int resId = resourceEntry.getResourceId();
315 
316       result = resource.getBoolean(resId);
317       partnerResourceCache.put(resourceConfig, result);
318     } catch (NullPointerException exception) {
319       // fall through
320     }
321     return result;
322   }
323 
324   /**
325    * Returns the dimension of given {@code resourceConfig}. The default return value is 0.
326    *
327    * @param context The context of client activity
328    * @param resourceConfig The {@code PartnerConfig} of target resource
329    */
getDimension(@onNull Context context, PartnerConfig resourceConfig)330   public float getDimension(@NonNull Context context, PartnerConfig resourceConfig) {
331     return getDimension(context, resourceConfig, 0);
332   }
333 
334   /**
335    * Returns the dimension of given {@code resourceConfig}. If the given {@code resourceConfig} is
336    * not found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
337    * resourceConfig} is not dimension, will throw IllegalArgumentException.
338    *
339    * @param context The context of client activity
340    * @param resourceConfig The {@code PartnerConfig} of target resource
341    * @param defaultValue The default value
342    */
getDimension( @onNull Context context, PartnerConfig resourceConfig, float defaultValue)343   public float getDimension(
344       @NonNull Context context, PartnerConfig resourceConfig, float defaultValue) {
345     if (resourceConfig.getResourceType() != ResourceType.DIMENSION) {
346       throw new IllegalArgumentException("Not a dimension resource");
347     }
348 
349     if (partnerResourceCache.containsKey(resourceConfig)) {
350       return getDimensionFromTypedValue(
351           context, (TypedValue) partnerResourceCache.get(resourceConfig));
352     }
353 
354     float result = defaultValue;
355     try {
356       ResourceEntry resourceEntry =
357           getResourceEntryFromKey(context, resourceConfig.getResourceName());
358       Resources resource = resourceEntry.getResources();
359       int resId = resourceEntry.getResourceId();
360 
361       result = resource.getDimension(resId);
362       TypedValue value = getTypedValueFromResource(resource, resId, TypedValue.TYPE_DIMENSION);
363       partnerResourceCache.put(resourceConfig, value);
364       result =
365           getDimensionFromTypedValue(
366               context, (TypedValue) partnerResourceCache.get(resourceConfig));
367     } catch (NullPointerException exception) {
368       // fall through
369     }
370     return result;
371   }
372 
373   /**
374    * Returns the float of given {@code resourceConfig}. The default return value is 0.
375    *
376    * @param context The context of client activity
377    * @param resourceConfig The {@code PartnerConfig} of target resource
378    */
getFraction(@onNull Context context, PartnerConfig resourceConfig)379   public float getFraction(@NonNull Context context, PartnerConfig resourceConfig) {
380     return getFraction(context, resourceConfig, 0.0f);
381   }
382 
383   /**
384    * Returns the float of given {@code resourceConfig}. If the given {@code resourceConfig} not
385    * found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
386    * resourceConfig} is not fraction, will throw IllegalArgumentException.
387    *
388    * @param context The context of client activity
389    * @param resourceConfig The {@code PartnerConfig} of target resource
390    * @param defaultValue The default value
391    */
getFraction( @onNull Context context, PartnerConfig resourceConfig, float defaultValue)392   public float getFraction(
393       @NonNull Context context, PartnerConfig resourceConfig, float defaultValue) {
394     if (resourceConfig.getResourceType() != ResourceType.FRACTION) {
395       throw new IllegalArgumentException("Not a fraction resource");
396     }
397 
398     if (partnerResourceCache.containsKey(resourceConfig)) {
399       return (float) partnerResourceCache.get(resourceConfig);
400     }
401 
402     float result = defaultValue;
403     try {
404       ResourceEntry resourceEntry =
405           getResourceEntryFromKey(context, resourceConfig.getResourceName());
406       Resources resource = resourceEntry.getResources();
407       int resId = resourceEntry.getResourceId();
408 
409       result = resource.getFraction(resId, 1, 1);
410       partnerResourceCache.put(resourceConfig, result);
411     } catch (NullPointerException exception) {
412       // fall through
413     }
414     return result;
415   }
416 
417   /**
418    * Returns the integer of given {@code resourceConfig}. If the given {@code resourceConfig} is not
419    * found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
420    * resourceConfig} is not dimension, will throw IllegalArgumentException.
421    *
422    * @param context The context of client activity
423    * @param resourceConfig The {@code PartnerConfig} of target resource
424    * @param defaultValue The default value
425    */
getInteger(@onNull Context context, PartnerConfig resourceConfig, int defaultValue)426   public int getInteger(@NonNull Context context, PartnerConfig resourceConfig, int defaultValue) {
427     if (resourceConfig.getResourceType() != ResourceType.INTEGER) {
428       throw new IllegalArgumentException("Not a integer resource");
429     }
430 
431     if (partnerResourceCache.containsKey(resourceConfig)) {
432       return (int) partnerResourceCache.get(resourceConfig);
433     }
434 
435     int result = defaultValue;
436     try {
437       ResourceEntry resourceEntry =
438           getResourceEntryFromKey(context, resourceConfig.getResourceName());
439       Resources resource = resourceEntry.getResources();
440       int resId = resourceEntry.getResourceId();
441 
442       result = resource.getInteger(resId);
443       partnerResourceCache.put(resourceConfig, result);
444     } catch (NullPointerException exception) {
445       // fall through
446     }
447     return result;
448   }
449 
450   /**
451    * Returns the {@link ResourceEntry} of given {@code resourceConfig}, or {@code null} if the given
452    * {@code resourceConfig} is not found. If the {@link ResourceType} of the given {@code
453    * resourceConfig} is not illustration, IllegalArgumentException will be thrown.
454    *
455    * @param context The context of client activity
456    * @param resourceConfig The {@link PartnerConfig} of target resource
457    */
458   @Nullable
getIllustrationResourceEntry( @onNull Context context, PartnerConfig resourceConfig)459   public ResourceEntry getIllustrationResourceEntry(
460       @NonNull Context context, PartnerConfig resourceConfig) {
461     if (resourceConfig.getResourceType() != ResourceType.ILLUSTRATION) {
462       throw new IllegalArgumentException("Not a illustration resource");
463     }
464 
465     if (partnerResourceCache.containsKey(resourceConfig)) {
466       return (ResourceEntry) partnerResourceCache.get(resourceConfig);
467     }
468 
469     try {
470       ResourceEntry resourceEntry =
471           getResourceEntryFromKey(context, resourceConfig.getResourceName());
472 
473       Resources resource = resourceEntry.getResources();
474       int resId = resourceEntry.getResourceId();
475 
476       // TODO: The illustration resource entry validation should validate is it a video
477       // resource or not?
478       // for @null
479       TypedValue outValue = new TypedValue();
480       resource.getValue(resId, outValue, true);
481       if (outValue.type == TypedValue.TYPE_REFERENCE && outValue.data == 0) {
482         return null;
483       }
484 
485       partnerResourceCache.put(resourceConfig, resourceEntry);
486       return resourceEntry;
487     } catch (NullPointerException exception) {
488       // fall through
489     }
490 
491     return null;
492   }
493 
getPartnerConfigBundle(Context context)494   private void getPartnerConfigBundle(Context context) {
495     if (resultBundle == null || resultBundle.isEmpty()) {
496       try {
497         resultBundle =
498             context
499                 .getContentResolver()
500                 .call(
501                     getContentUri(),
502                     SUW_GET_PARTNER_CONFIG_METHOD,
503                     /* arg= */ null,
504                     /* extras= */ null);
505         partnerResourceCache.clear();
506       } catch (IllegalArgumentException | SecurityException exception) {
507         Log.w(TAG, "Fail to get config from suw provider");
508       }
509     }
510   }
511 
512   @Nullable
513   @VisibleForTesting
getResourceEntryFromKey(Context context, String resourceName)514   ResourceEntry getResourceEntryFromKey(Context context, String resourceName) {
515     Bundle resourceEntryBundle = resultBundle.getBundle(resourceName);
516     Bundle fallbackBundle = resultBundle.getBundle(KEY_FALLBACK_CONFIG);
517     if (fallbackBundle != null) {
518       resourceEntryBundle.putBundle(KEY_FALLBACK_CONFIG, fallbackBundle.getBundle(resourceName));
519     }
520 
521     return adjustResourceEntryDayNightMode(
522         context, ResourceEntry.fromBundle(context, resourceEntryBundle));
523   }
524 
525   /**
526    * Force to day mode if setup wizard does not support day/night mode and current system is in
527    * night mode.
528    */
adjustResourceEntryDayNightMode( Context context, ResourceEntry resourceEntry)529   private static ResourceEntry adjustResourceEntryDayNightMode(
530       Context context, ResourceEntry resourceEntry) {
531     Resources resource = resourceEntry.getResources();
532     Configuration configuration = resource.getConfiguration();
533     if (!isSetupWizardDayNightEnabled(context) && Util.isNightMode(configuration)) {
534       if (resourceEntry == null) {
535         Log.w(TAG, "resourceEntry is null, skip to force day mode.");
536         return resourceEntry;
537       }
538       configuration.uiMode =
539           Configuration.UI_MODE_NIGHT_NO
540               | (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
541       resource.updateConfiguration(configuration, resource.getDisplayMetrics());
542     }
543 
544     return resourceEntry;
545   }
546 
547   @VisibleForTesting
resetInstance()548   public static synchronized void resetInstance() {
549     instance = null;
550     suwDayNightEnabledBundle = null;
551     applyExtendedPartnerConfigBundle = null;
552     applyDynamicColorBundle = null;
553   }
554 
555   /**
556    * Checks whether SetupWizard supports the DayNight theme during setup flow; if return false setup
557    * flow should force to light theme.
558    *
559    * <p>Returns true if the setupwizard is listening to system DayNight theme setting.
560    */
isSetupWizardDayNightEnabled(@onNull Context context)561   public static boolean isSetupWizardDayNightEnabled(@NonNull Context context) {
562     if (suwDayNightEnabledBundle == null) {
563       try {
564         suwDayNightEnabledBundle =
565             context
566                 .getContentResolver()
567                 .call(
568                     getContentUri(),
569                     IS_SUW_DAY_NIGHT_ENABLED_METHOD,
570                     /* arg= */ null,
571                     /* extras= */ null);
572       } catch (IllegalArgumentException | SecurityException exception) {
573         Log.w(TAG, "SetupWizard DayNight supporting status unknown; return as false.");
574         suwDayNightEnabledBundle = null;
575         return false;
576       }
577     }
578 
579     return (suwDayNightEnabledBundle != null
580         && suwDayNightEnabledBundle.getBoolean(IS_SUW_DAY_NIGHT_ENABLED_METHOD, false));
581   }
582 
583   /** Returns true if the SetupWizard supports the extended partner configs during setup flow. */
shouldApplyExtendedPartnerConfig(@onNull Context context)584   public static boolean shouldApplyExtendedPartnerConfig(@NonNull Context context) {
585     if (applyExtendedPartnerConfigBundle == null) {
586       try {
587         applyExtendedPartnerConfigBundle =
588             context
589                 .getContentResolver()
590                 .call(
591                     getContentUri(),
592                     IS_EXTENDED_PARTNER_CONFIG_ENABLED_METHOD,
593                     /* arg= */ null,
594                     /* extras= */ null);
595       } catch (IllegalArgumentException | SecurityException exception) {
596         Log.w(
597             TAG,
598             "SetupWizard extended partner configs supporting status unknown; return as false.");
599         applyExtendedPartnerConfigBundle = null;
600         return false;
601       }
602     }
603 
604     return (applyExtendedPartnerConfigBundle != null
605         && applyExtendedPartnerConfigBundle.getBoolean(
606             IS_EXTENDED_PARTNER_CONFIG_ENABLED_METHOD, false));
607   }
608 
609   /** Returns true if the SetupWizard supports the dynamic color during setup flow. */
isSetupWizardDynamicColorEnabled(@onNull Context context)610   public static boolean isSetupWizardDynamicColorEnabled(@NonNull Context context) {
611     if (applyDynamicColorBundle == null) {
612       try {
613         applyDynamicColorBundle =
614             context
615                 .getContentResolver()
616                 .call(
617                     getContentUri(),
618                     IS_DYNAMIC_COLOR_ENABLED_METHOD,
619                     /* arg= */ null,
620                     /* extras= */ null);
621       } catch (IllegalArgumentException | SecurityException exception) {
622         Log.w(TAG, "SetupWizard dynamic color supporting status unknown; return as false.");
623         applyDynamicColorBundle = null;
624         return false;
625       }
626     }
627 
628     return (applyDynamicColorBundle != null
629         && applyDynamicColorBundle.getBoolean(IS_DYNAMIC_COLOR_ENABLED_METHOD, false));
630   }
631 
632   @VisibleForTesting
getContentUri()633   static Uri getContentUri() {
634     return new Uri.Builder()
635         .scheme(ContentResolver.SCHEME_CONTENT)
636         .authority(SUW_AUTHORITY)
637         .build();
638   }
639 
getTypedValueFromResource(Resources resource, int resId, int type)640   private static TypedValue getTypedValueFromResource(Resources resource, int resId, int type) {
641     TypedValue value = new TypedValue();
642     resource.getValue(resId, value, true);
643     if (value.type != type) {
644       throw new NotFoundException(
645           "Resource ID #0x"
646               + Integer.toHexString(resId)
647               + " type #0x"
648               + Integer.toHexString(value.type)
649               + " is not valid");
650     }
651     return value;
652   }
653 
getDimensionFromTypedValue(Context context, TypedValue value)654   private static float getDimensionFromTypedValue(Context context, TypedValue value) {
655     DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
656     return value.getDimension(displayMetrics);
657   }
658 
registerContentObserver(Context context)659   private static void registerContentObserver(Context context) {
660     if (isSetupWizardDayNightEnabled(context)) {
661       if (contentObserver != null) {
662         unregisterContentObserver(context);
663       }
664 
665       Uri contentUri = getContentUri();
666       try {
667         contentObserver =
668             new ContentObserver(null) {
669               @Override
670               public void onChange(boolean selfChange) {
671                 super.onChange(selfChange);
672                 resetInstance();
673               }
674             };
675         context
676             .getContentResolver()
677             .registerContentObserver(contentUri, /* notifyForDescendants= */ true, contentObserver);
678       } catch (SecurityException | NullPointerException | IllegalArgumentException e) {
679         Log.w(TAG, "Failed to register content observer for " + contentUri + ": " + e);
680       }
681     }
682   }
683 
unregisterContentObserver(Context context)684   private static void unregisterContentObserver(Context context) {
685     try {
686       context.getContentResolver().unregisterContentObserver(contentObserver);
687       contentObserver = null;
688     } catch (SecurityException | NullPointerException | IllegalArgumentException e) {
689       Log.w(TAG, "Failed to unregister content observer: " + e);
690     }
691   }
692 }
693