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