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