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