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