diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2021-07-15 02:04:01 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2021-07-15 02:04:01 +0000 |
commit | 9618a34816f554188aec351db5b7983968a6920b (patch) | |
tree | 096454abf50a73063ca997f864b3d5f3255a5899 | |
parent | d497370655ea8c6f15169af88cd9c75406e51395 (diff) | |
parent | 33cf4c96e8c4986edf036ec2c51228d5b9905bd2 (diff) | |
download | setupcompat-android12-mainline-captiveportallogin-release.tar.gz |
Snap for 7550930 from 33cf4c96e8c4986edf036ec2c51228d5b9905bd2 to mainline-captiveportallogin-releaseandroid-mainline-12.0.0_r6android-mainline-12.0.0_r23android12-mainline-captiveportallogin-release
Change-Id: Id70096008f2743abb6ee77efce30f7216ebf962e
45 files changed, 2974 insertions, 612 deletions
@@ -2,6 +2,69 @@ // Build the setup compat library. // +package { + default_applicable_licenses: ["external_setupcompat_license"], +} + +// Added automatically by a large-scale-change +// See: http://go/android-license-faq +license { + name: "external_setupcompat_license", + visibility: [":__subpackages__"], + license_kinds: [ + "SPDX-license-identifier-Apache-2.0", + ], + license_text: [ + "LICENSE", + ], +} + +filegroup { + name: "Aidls", + srcs: [ + "main/aidl/com/google/android/setupcompat/ISetupCompatService.aidl", + ], + path: "main/aidl", +} + +filegroup { + name: "AidlsPortal", + srcs: [ + "main/aidl/com/google/android/setupcompat/portal/*.aidl", + ], + path: "main/aidl", +} + +filegroup { + name: "Srcs", + srcs: [ + "main/java/com/google/android/setupcompat/*.java", + "main/java/com/google/android/setupcompat/internal/*.java", + "main/java/com/google/android/setupcompat/logging/*.java", + "main/java/com/google/android/setupcompat/logging/internal/*.java", + "main/java/com/google/android/setupcompat/template/*.java", + "main/java/com/google/android/setupcompat/util/*.java", + "main/java/com/google/android/setupcompat/view/*.java", + ], + path: "main/java", +} + +filegroup { + name: "SrcsPartnerConfig", + srcs: [ + "partnerconfig/java/**/*.java", + ], + path: "partnerconfig/java", +} + +filegroup { + name: "SrcsPortal", + srcs: [ + "main/java/com/google/android/setupcompat/portal/*.java", + ], + path: "main/java", +} + android_library { name: "setupcompat", manifest: "AndroidManifest.xml", @@ -9,9 +72,11 @@ android_library { "main/res", ], srcs: [ - "main/java/**/*.java", - "partnerconfig/java/**/*.java", - "main/aidl/**/*.aidl", + ":Aidls", + ":AidlsPortal", + ":Srcs", + ":SrcsPartnerConfig", + ":SrcsPortal", ], static_libs: [ "androidx.annotation_annotation", diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..d97975c --- /dev/null +++ b/METADATA @@ -0,0 +1,3 @@ +third_party { + license_type: NOTICE +} @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License.
\ No newline at end of file diff --git a/exempting_lint_checks.txt b/exempting_lint_checks.txt new file mode 100644 index 0000000..90b8caf --- /dev/null +++ b/exempting_lint_checks.txt @@ -0,0 +1,7 @@ +third_party/java_src/android_libs/setupcompat/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java: CustomViewStyleable: attrs, R.styleable.SucPartnerCustomizationLayout, defStyleAttr, 0); +third_party/java_src/android_libs/setupcompat/main/java/com/google/android/setupcompat/internal/TemplateLayout.java: CustomViewStyleable: getContext().obtainStyledAttributes(attrs, R.styleable.SucTemplateLayout, defStyleAttr, 0); +third_party/java_src/android_libs/setupcompat/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java: CustomViewStyleable: attrs, R.styleable.SucPartnerCustomizationLayout, defStyleAttr, 0); +third_party/java_src/android_libs/setupcompat/main/java/com/google/android/setupcompat/internal/TemplateLayout.java: CustomViewStyleable: getContext().obtainStyledAttributes(attrs, R.styleable.SucTemplateLayout, defStyleAttr, 0); +third_party/java_src/android_libs/setupcompat/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java: CustomViewStyleable: attrs, R.styleable.SucPartnerCustomizationLayout, defStyleAttr, 0); +third_party/java_src/android_libs/setupcompat/main/java/com/google/android/setupcompat/internal/TemplateLayout.java: CustomViewStyleable: getContext().obtainStyledAttributes(attrs, R.styleable.SucTemplateLayout, defStyleAttr, 0); +third_party/java_src/android_libs/setupcompat/main/java/com/google/android/setupcompat/util/SystemBarHelper.java: AnnotateVersionCheck: public static void setImeInsetView(final View view) { diff --git a/grandfathered_lint_checks.txt b/grandfathered_lint_checks.txt deleted file mode 100644 index e69de29..0000000 --- a/grandfathered_lint_checks.txt +++ /dev/null diff --git a/lint-baseline.xml b/lint-baseline.xml new file mode 100644 index 0000000..43e81f8 --- /dev/null +++ b/lint-baseline.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0"> + + <issue + id="NewApi" + message="`android:Widget.Material.Button.Colored` requires API level 23 (current min is 14)" + errorLine1=" <style name="SucPartnerCustomizationButton.Primary" parent="android:Widget.Material.Button.Colored">" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="external/setupcompat/main/res/values/styles.xml" + line="40" + column="57"/> + </issue> + + <issue + id="NewApi" + message="`android:fontFamily` requires API level 16 (current min is 14)" + errorLine1=" <item name="android:fontFamily">?attr/sucFooterBarButtonFontFamily</item>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="external/setupcompat/main/res/values/styles.xml" + line="48" + column="15"/> + </issue> + + <issue + id="NewApi" + message="`android:stateListAnimator` requires API level 21 (current min is 14)" + errorLine1=" <item name="android:stateListAnimator">@null</item>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="external/setupcompat/main/res/values/styles.xml" + line="54" + column="15"/> + </issue> + + <issue + id="NewApi" + message="`android:Widget.Material.Button.Borderless.Colored` requires API level 21 (current min is 14)" + errorLine1=" <style name="SucPartnerCustomizationButton.Secondary" parent="android:Widget.Material.Button.Borderless.Colored">" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="external/setupcompat/main/res/values/styles.xml" + line="60" + column="59"/> + </issue> + + <issue + id="NewApi" + message="`android:fontFamily` requires API level 16 (current min is 14)" + errorLine1=" <item name="android:fontFamily">?attr/sucFooterBarButtonFontFamily</item>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="external/setupcompat/main/res/values/styles.xml" + line="68" + column="15"/> + </issue> + +</issues> diff --git a/main/aidl/com/google/android/setupcompat/portal/IPortalProgressCallback.aidl b/main/aidl/com/google/android/setupcompat/portal/IPortalProgressCallback.aidl new file mode 100644 index 0000000..13303d8 --- /dev/null +++ b/main/aidl/com/google/android/setupcompat/portal/IPortalProgressCallback.aidl @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.setupcompat.portal; + +import android.os.Bundle; + +/** + * Interface for progress service to update progress to SUW. Clients should + * update progress at least once a minute, or set a pending reason to stop + * update progress. Without progress update and pending reason. We considering + * the progress service is no response will suspend it and unbinde service. + */ +interface IPortalProgressCallback { + /** + * Sets completed amount. + */ + Bundle setProgressCount(int completed, int failed, int total) = 0; + + /** + * Sets completed percentage. + */ + Bundle setProgressPercentage(int percentage) = 1; + + /** + * Sets the summary shows on portal activity. + */ + Bundle setSummary(String summary) = 2; + + /** + * Let SUW knows the progress is pending now, and stop update progress. + * @param reasonResId The resource identifier. + * @param quantity The number used to get the correct string for the current language's + * plural rules + * @param formatArgs The format arguments that will be used for substitution. + */ + Bundle setPendingReason(int reasonResId, int quantity, in int[] formatArgs, int reason) = 3; + + /** + * Once the progress completed, and service can be destroy. Call this function. + * SUW will unbind the service. + * @param resId The resource identifier. + * @param quantity The number used to get the correct string for the current language's + * plural rules + * @param formatArgs The format arguments that will be used for substitution. + */ + Bundle setComplete(int resId, int quantity, in int[] formatArgs) = 4; + + /** + * Once the progress failed, and not able to finish the progress. Should call + * this function. SUW will unbind service after receive setFailure. Client can + * registerProgressService again once the service is ready to execute. + * @param resId The resource identifier. + * @param quantity The number used to get the correct string for the current language's + * plural rules + * @param formatArgs The format arguments that will be used for substitution. + */ + Bundle setFailure(int resId, int quantity, in int[] formatArgs) = 5; +}
\ No newline at end of file diff --git a/main/aidl/com/google/android/setupcompat/portal/IPortalProgressService.aidl b/main/aidl/com/google/android/setupcompat/portal/IPortalProgressService.aidl new file mode 100644 index 0000000..d741125 --- /dev/null +++ b/main/aidl/com/google/android/setupcompat/portal/IPortalProgressService.aidl @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.setupcompat.portal; + +import android.os.Bundle; +import com.google.android.setupcompat.portal.IPortalProgressCallback; + +/** + * Interface of progress service, all servics needs to run during onboarding, and would like + * to consolidate notifications with SetupNotificationService, should implement this interface. + * So that SetupNotificationService can bind the progress service and run below + * SetupNotificationService. + */ +interface IPortalProgressService { + /** + * Called when the Portal notification is started. + */ + oneway void onPortalSessionStart() = 0; + + /** + * Provides a non-null callback after service connected. + */ + oneway void onSetCallback(IPortalProgressCallback callback) = 1; + + /** + * Called when progress timed out. + */ + oneway void onSuspend() = 2; + + /** + * User clicks "User mobile data" on portal activity. + * All running progress should agree to use mobile data. + */ + oneway void onAllowMobileData(boolean allowed) = 3; + + /** + * Portal service calls to get remaining downloading size(MB) from progress service. + */ + @nullable + Bundle onGetRemainingValues() = 4; +}
\ No newline at end of file diff --git a/main/aidl/com/google/android/setupcompat/portal/IPortalRegisterResultListener.aidl b/main/aidl/com/google/android/setupcompat/portal/IPortalRegisterResultListener.aidl new file mode 100644 index 0000000..56d39e5 --- /dev/null +++ b/main/aidl/com/google/android/setupcompat/portal/IPortalRegisterResultListener.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.setupcompat.portal; + +/** Listener to listen the result of registerProgressService */ +interface IPortalRegisterResultListener { + void onResult(in Bundle result) = 0; +}
\ No newline at end of file diff --git a/main/aidl/com/google/android/setupcompat/portal/ISetupNotificationService.aidl b/main/aidl/com/google/android/setupcompat/portal/ISetupNotificationService.aidl new file mode 100644 index 0000000..9f9b1d8 --- /dev/null +++ b/main/aidl/com/google/android/setupcompat/portal/ISetupNotificationService.aidl @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.setupcompat.portal; + +import android.os.UserHandle; +import com.google.android.setupcompat.portal.IPortalRegisterResultListener; +import com.google.android.setupcompat.portal.NotificationComponent; +import com.google.android.setupcompat.portal.ProgressServiceComponent; + +/** + * Declares the interface for notification related service methods. + */ +interface ISetupNotificationService { + /** Register a notification without progress service */ + boolean registerNotification(in NotificationComponent component) = 0; + void unregisterNotification(in NotificationComponent component) = 1; + + /** Register a progress service */ + void registerProgressService(in ProgressServiceComponent component, in UserHandle userHandle, IPortalRegisterResultListener listener) = 2; + + /** Checks the progress connection still alive or not. */ + boolean isProgressServiceAlive(in ProgressServiceComponent component, in UserHandle userHandle) = 3; + + /** Checks portal avaailable or not. */ + boolean isPortalAvailable() = 4; +}
\ No newline at end of file diff --git a/main/aidl/com/google/android/setupcompat/portal/NotificationComponent.aidl b/main/aidl/com/google/android/setupcompat/portal/NotificationComponent.aidl new file mode 100644 index 0000000..5de3f76 --- /dev/null +++ b/main/aidl/com/google/android/setupcompat/portal/NotificationComponent.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.setupcompat.portal; + +parcelable NotificationComponent;
\ No newline at end of file diff --git a/main/aidl/com/google/android/setupcompat/portal/ProgressServiceComponent.aidl b/main/aidl/com/google/android/setupcompat/portal/ProgressServiceComponent.aidl new file mode 100644 index 0000000..6a6e120 --- /dev/null +++ b/main/aidl/com/google/android/setupcompat/portal/ProgressServiceComponent.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package com.google.android.setupcompat.portal; + + parcelable ProgressServiceComponent;
\ No newline at end of file diff --git a/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java b/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java index fac4b39..e5ba0c5 100644 --- a/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java +++ b/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java @@ -26,7 +26,6 @@ import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.PersistableBundle; import android.util.AttributeSet; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -42,12 +41,14 @@ import com.google.android.setupcompat.template.FooterBarMixin; import com.google.android.setupcompat.template.FooterButton; import com.google.android.setupcompat.template.StatusBarMixin; import com.google.android.setupcompat.template.SystemNavBarMixin; +import com.google.android.setupcompat.util.BuildCompatUtils; +import com.google.android.setupcompat.util.Logger; import com.google.android.setupcompat.util.WizardManagerHelper; /** A templatization layout with consistent style used in Setup Wizard or app itself. */ public class PartnerCustomizationLayout extends TemplateLayout { - // Log tags can have at most 23 characters on N or before. - private static final String TAG = "PartnerCustomizedLayout"; + + private static final Logger LOG = new Logger("PartnerCustomizationLayout"); /** * Attribute indicating whether usage of partner theme resources is allowed. This corresponds to @@ -56,6 +57,18 @@ public class PartnerCustomizationLayout extends TemplateLayout { */ private boolean usePartnerResourceAttr; + /** + * Attribute indicating whether using full dynamic colors or not. This corresponds to the {@code + * app:sucFullDynamicColor} XML attribute. + */ + private boolean useFullDynamicColorAttr; + + /** + * Attribute indicating whether usage of dynamic is allowed. This corresponds to the existence of + * {@code app:sucFullDynamicColor} XML attribute. + */ + private boolean useDynamicColor; + private Activity activity; public PartnerCustomizationLayout(Context context) { @@ -83,6 +96,9 @@ public class PartnerCustomizationLayout extends TemplateLayout { } private void init(AttributeSet attrs, int defStyleAttr) { + if (isInEditMode()) { + return; + } TypedArray a = getContext() @@ -132,15 +148,13 @@ public class PartnerCustomizationLayout extends TemplateLayout { @Override protected void onBeforeTemplateInflated(AttributeSet attrs, int defStyleAttr) { - boolean isSetupFlow; - // Sets default value to true since this timing // before PartnerCustomization members initialization usePartnerResourceAttr = true; activity = lookupActivityFromContext(getContext()); - isSetupFlow = WizardManagerHelper.isAnySetupWizard(activity.getIntent()); + boolean isSetupFlow = WizardManagerHelper.isAnySetupWizard(activity.getIntent()); TypedArray a = getContext() @@ -149,27 +163,32 @@ public class PartnerCustomizationLayout extends TemplateLayout { if (!a.hasValue(R.styleable.SucPartnerCustomizationLayout_sucUsePartnerResource)) { // TODO: Enable Log.WTF after other client already set sucUsePartnerResource. - Log.e(TAG, "Attribute sucUsePartnerResource not found in " + activity.getComponentName()); + LOG.e("Attribute sucUsePartnerResource not found in " + activity.getComponentName()); } usePartnerResourceAttr = isSetupFlow || a.getBoolean(R.styleable.SucPartnerCustomizationLayout_sucUsePartnerResource, true); + useDynamicColor = a.hasValue(R.styleable.SucPartnerCustomizationLayout_sucFullDynamicColor); + useFullDynamicColorAttr = + a.getBoolean(R.styleable.SucPartnerCustomizationLayout_sucFullDynamicColor, false); + a.recycle(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d( - TAG, - "activity=" - + activity.getClass().getSimpleName() - + " isSetupFlow=" - + isSetupFlow - + " enablePartnerResourceLoading=" - + enablePartnerResourceLoading() - + " usePartnerResourceAttr=" - + usePartnerResourceAttr); - } + LOG.atDebug( + "activity=" + + activity.getClass().getSimpleName() + + " isSetupFlow=" + + isSetupFlow + + " enablePartnerResourceLoading=" + + enablePartnerResourceLoading() + + " usePartnerResourceAttr=" + + usePartnerResourceAttr + + " useDynamicColor=" + + useDynamicColor + + " useFullDynamicColorAttr=" + + useFullDynamicColorAttr); } @Override @@ -216,7 +235,7 @@ public class PartnerCustomizationLayout extends TemplateLayout { } } - private static Activity lookupActivityFromContext(Context context) { + public static Activity lookupActivityFromContext(Context context) { if (context instanceof Activity) { return (Activity) context; } else if (context instanceof ContextWrapper) { @@ -252,4 +271,30 @@ public class PartnerCustomizationLayout extends TemplateLayout { } return true; } + + /** + * Returns {@code true} if the current layout/activity applies dynamic color. Otherwise, returns + * {@code false}. + */ + public boolean shouldApplyDynamicColor() { + if (!useDynamicColor) { + return false; + } + if (!BuildCompatUtils.isAtLeastS()) { + return false; + } + if (!PartnerConfigHelper.get(getContext()).isAvailable()) { + return false; + } + return true; + } + + /** + * Returns {@code true} if the current layout/activity applies full dynamic color. Otherwise, + * returns {@code false}. This method combines the result of {@link #shouldApplyDynamicColor()} + * and the value of the {@code app:sucFullDynamicColor}. + */ + public boolean useFullDynamicColor() { + return shouldApplyDynamicColor() && useFullDynamicColorAttr; + } } diff --git a/main/java/com/google/android/setupcompat/internal/FallbackThemeWrapper.java b/main/java/com/google/android/setupcompat/internal/FallbackThemeWrapper.java index af17a62..574f614 100644 --- a/main/java/com/google/android/setupcompat/internal/FallbackThemeWrapper.java +++ b/main/java/com/google/android/setupcompat/internal/FallbackThemeWrapper.java @@ -18,8 +18,8 @@ package com.google.android.setupcompat.internal; import android.content.Context; import android.content.res.Resources.Theme; -import androidx.annotation.StyleRes; import android.view.ContextThemeWrapper; +import androidx.annotation.StyleRes; /** * Same as {@link ContextThemeWrapper}, but the base context's theme attributes take precedence over diff --git a/main/java/com/google/android/setupcompat/internal/FooterButtonPartnerConfig.java b/main/java/com/google/android/setupcompat/internal/FooterButtonPartnerConfig.java index 39b50cf..5f8bf67 100644 --- a/main/java/com/google/android/setupcompat/internal/FooterButtonPartnerConfig.java +++ b/main/java/com/google/android/setupcompat/internal/FooterButtonPartnerConfig.java @@ -27,7 +27,9 @@ public class FooterButtonPartnerConfig { private final PartnerConfig buttonIconConfig; private final PartnerConfig buttonTextColorConfig; private final PartnerConfig buttonTextSizeConfig; + private final PartnerConfig buttonMinHeightConfig; private final PartnerConfig buttonTextTypeFaceConfig; + private final PartnerConfig buttonTextStyleConfig; private final PartnerConfig buttonRadiusConfig; private final PartnerConfig buttonRippleColorAlphaConfig; private final int partnerTheme; @@ -40,14 +42,18 @@ public class FooterButtonPartnerConfig { PartnerConfig buttonIconConfig, PartnerConfig buttonTextColorConfig, PartnerConfig buttonTextSizeConfig, + PartnerConfig buttonMinHeightConfig, PartnerConfig buttonTextTypeFaceConfig, + PartnerConfig buttonTextStyleConfig, PartnerConfig buttonRadiusConfig, PartnerConfig buttonRippleColorAlphaConfig) { this.partnerTheme = partnerTheme; this.buttonTextColorConfig = buttonTextColorConfig; this.buttonTextSizeConfig = buttonTextSizeConfig; + this.buttonMinHeightConfig = buttonMinHeightConfig; this.buttonTextTypeFaceConfig = buttonTextTypeFaceConfig; + this.buttonTextStyleConfig = buttonTextStyleConfig; this.buttonBackgroundConfig = buttonBackgroundConfig; this.buttonDisableAlphaConfig = buttonDisableAlphaConfig; this.buttonDisableBackgroundConfig = buttonDisableBackgroundConfig; @@ -80,6 +86,10 @@ public class FooterButtonPartnerConfig { return buttonTextColorConfig; } + public PartnerConfig getButtonMinHeightConfig() { + return buttonMinHeightConfig; + } + public PartnerConfig getButtonTextSizeConfig() { return buttonTextSizeConfig; } @@ -88,6 +98,10 @@ public class FooterButtonPartnerConfig { return buttonTextTypeFaceConfig; } + public PartnerConfig getButtonTextStyleConfig() { + return buttonTextStyleConfig; + } + public PartnerConfig getButtonRadiusConfig() { return buttonRadiusConfig; } @@ -105,15 +119,19 @@ public class FooterButtonPartnerConfig { private PartnerConfig buttonIconConfig = null; private PartnerConfig buttonTextColorConfig = null; private PartnerConfig buttonTextSizeConfig = null; + private PartnerConfig buttonMinHeight = null; private PartnerConfig buttonTextTypeFaceConfig = null; + private PartnerConfig buttonTextStyleConfig = null; private PartnerConfig buttonRadiusConfig = null; private PartnerConfig buttonRippleColorAlphaConfig = null; private int partnerTheme; public Builder(FooterButton footerButton) { this.footerButton = footerButton; - // default partnerTheme should be the same as footerButton.getTheme(); - this.partnerTheme = this.footerButton.getTheme(); + if (this.footerButton != null) { + // default partnerTheme should be the same as footerButton.getTheme(); + this.partnerTheme = this.footerButton.getTheme(); + } } public Builder setButtonBackgroundConfig(PartnerConfig buttonBackgroundConfig) { @@ -146,11 +164,21 @@ public class FooterButtonPartnerConfig { return this; } + public Builder setButtonMinHeight(PartnerConfig buttonMinHeightConfig) { + this.buttonMinHeight = buttonMinHeightConfig; + return this; + } + public Builder setTextTypeFaceConfig(PartnerConfig buttonTextTypeFaceConfig) { this.buttonTextTypeFaceConfig = buttonTextTypeFaceConfig; return this; } + public Builder setTextStyleConfig(PartnerConfig buttonTextStyleConfig) { + this.buttonTextStyleConfig = buttonTextStyleConfig; + return this; + } + public Builder setButtonRadiusConfig(PartnerConfig buttonRadiusConfig) { this.buttonRadiusConfig = buttonRadiusConfig; return this; @@ -175,7 +203,9 @@ public class FooterButtonPartnerConfig { buttonIconConfig, buttonTextColorConfig, buttonTextSizeConfig, + buttonMinHeight, buttonTextTypeFaceConfig, + buttonTextStyleConfig, buttonRadiusConfig, buttonRippleColorAlphaConfig); } diff --git a/main/java/com/google/android/setupcompat/internal/PersistableBundles.java b/main/java/com/google/android/setupcompat/internal/PersistableBundles.java index 1197645..3b7d5a5 100644 --- a/main/java/com/google/android/setupcompat/internal/PersistableBundles.java +++ b/main/java/com/google/android/setupcompat/internal/PersistableBundles.java @@ -22,16 +22,18 @@ import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.PersistableBundle; import android.util.ArrayMap; -import android.util.Log; +import com.google.android.setupcompat.util.Logger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** Contains utility methods related to {@link PersistableBundle}. */ -@TargetApi(VERSION_CODES.Q) +@TargetApi(VERSION_CODES.LOLLIPOP_MR1) public final class PersistableBundles { + private static final Logger LOG = new Logger("PersistableBundles"); + /** * Merges two or more {@link PersistableBundle}. Ensures no conflict of keys occurred during * merge. @@ -121,7 +123,7 @@ public final class PersistableBundles { for (String key : baseBundle.keySet()) { Object value = baseBundle.get(key); if (!isSupportedDataType(value)) { - Log.w(TAG, String.format("Unknown/unsupported data type [%s] for key %s", value, key)); + LOG.w(String.format("Unknown/unsupported data type [%s] for key %s", value, key)); continue; } map.put(key, baseBundle.get(key)); @@ -141,6 +143,4 @@ public final class PersistableBundles { private PersistableBundles() { throw new AssertionError("Should not be instantiated"); } - - private static final String TAG = "SetupCompat.PersistBls"; } diff --git a/main/java/com/google/android/setupcompat/internal/SetupCompatServiceInvoker.java b/main/java/com/google/android/setupcompat/internal/SetupCompatServiceInvoker.java index a1ca156..149da54 100644 --- a/main/java/com/google/android/setupcompat/internal/SetupCompatServiceInvoker.java +++ b/main/java/com/google/android/setupcompat/internal/SetupCompatServiceInvoker.java @@ -21,9 +21,9 @@ import android.content.Context; import android.os.Bundle; import android.os.RemoteException; import androidx.annotation.VisibleForTesting; -import android.util.Log; import com.google.android.setupcompat.ISetupCompatService; import com.google.android.setupcompat.logging.internal.SetupMetricsLoggingConstants.MetricType; +import com.google.android.setupcompat.util.Logger; import java.util.concurrent.ExecutorService; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; @@ -41,11 +41,14 @@ import java.util.concurrent.TimeoutException; */ public class SetupCompatServiceInvoker { + private static final Logger LOG = new Logger("SetupCompatServiceInvoker"); + + @SuppressLint("DefaultLocale") public void logMetricEvent(@MetricType int metricType, Bundle args) { try { loggingExecutor.execute(() -> invokeLogMetric(metricType, args)); } catch (RejectedExecutionException e) { - Log.e(TAG, String.format("Metric of type %d dropped since queue is full.", metricType), e); + LOG.e(String.format("Metric of type %d dropped since queue is full.", metricType), e); } } @@ -53,7 +56,7 @@ public class SetupCompatServiceInvoker { try { setupCompatExecutor.execute(() -> invokeBindBack(screenName, bundle)); } catch (RejectedExecutionException e) { - Log.e(TAG, String.format("Screen %s bind back fail.", screenName), e); + LOG.e(String.format("Screen %s bind back fail.", screenName), e); } } @@ -66,10 +69,10 @@ public class SetupCompatServiceInvoker { if (setupCompatService != null) { setupCompatService.logMetric(metricType, args, Bundle.EMPTY); } else { - Log.w(TAG, "logMetric failed since service reference is null. Are the permissions valid?"); + LOG.w("logMetric failed since service reference is null. Are the permissions valid?"); } - } catch (InterruptedException | TimeoutException | RemoteException e) { - Log.e(TAG, String.format("Exception occurred while trying to log metric = [%s]", args), e); + } catch (InterruptedException | TimeoutException | RemoteException | IllegalStateException e) { + LOG.e(String.format("Exception occurred while trying to log metric = [%s]", args), e); } } @@ -81,11 +84,10 @@ public class SetupCompatServiceInvoker { if (setupCompatService != null) { setupCompatService.validateActivity(screenName, bundle); } else { - Log.w(TAG, "BindBack failed since service reference is null. Are the permissions valid?"); + LOG.w("BindBack failed since service reference is null. Are the permissions valid?"); } } catch (InterruptedException | TimeoutException | RemoteException e) { - Log.e( - TAG, + LOG.e( String.format("Exception occurred while %s trying bind back to SetupWizard.", screenName), e); } @@ -125,5 +127,4 @@ public class SetupCompatServiceInvoker { private static SetupCompatServiceInvoker instance; private static final long MAX_WAIT_TIME_FOR_CONNECTION_MS = TimeUnit.SECONDS.toMillis(10); - private static final String TAG = "SucServiceInvoker"; } diff --git a/main/java/com/google/android/setupcompat/internal/SetupCompatServiceProvider.java b/main/java/com/google/android/setupcompat/internal/SetupCompatServiceProvider.java index 2043a81..e75d991 100644 --- a/main/java/com/google/android/setupcompat/internal/SetupCompatServiceProvider.java +++ b/main/java/com/google/android/setupcompat/internal/SetupCompatServiceProvider.java @@ -26,8 +26,8 @@ import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import android.util.Log; import com.google.android.setupcompat.ISetupCompatService; +import com.google.android.setupcompat.util.Logger; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -40,6 +40,8 @@ import java.util.function.UnaryOperator; */ public class SetupCompatServiceProvider { + private static final Logger LOG = new Logger("SetupCompatServiceProvider"); + /** * Returns an instance of {@link ISetupCompatService} if one already exists. If not, attempts to * rebind if the current state allows such an operation and waits until {@code waitTime} for @@ -94,7 +96,7 @@ public class SetupCompatServiceProvider { } CountDownLatch connectedStateLatch = getConnectedCondition(); - Log.i(TAG, "Waiting for service to get connected"); + LOG.atInfo("Waiting for service to get connected"); boolean stateChanged = connectedStateLatch.await(timeout, timeUnit); if (!stateChanged) { // Even though documentation states that disconnected service should connect again, @@ -104,13 +106,10 @@ public class SetupCompatServiceProvider { String.format("Failed to acquire connection after [%s %s]", timeout, timeUnit)); } currentServiceState = getCurrentServiceState(); - if (Log.isLoggable(TAG, Log.INFO)) { - Log.i( - TAG, - String.format( - "Finished waiting for service to get connected. Current state = %s", - currentServiceState.state)); - } + LOG.atInfo( + String.format( + "Finished waiting for service to get connected. Current state = %s", + currentServiceState.state)); return currentServiceState.compatService; } @@ -126,11 +125,11 @@ public class SetupCompatServiceProvider { private synchronized void requestServiceBind() { ServiceContext currentServiceState = getCurrentServiceState(); if (currentServiceState.state == State.CONNECTED) { - Log.i(TAG, "Refusing to rebind since current state is already connected"); + LOG.atInfo("Refusing to rebind since current state is already connected"); return; } if (currentServiceState.state != State.NOT_STARTED) { - Log.i(TAG, "Unbinding existing service connection."); + LOG.atInfo("Unbinding existing service connection."); context.unbindService(serviceConnection); } @@ -139,7 +138,7 @@ public class SetupCompatServiceProvider { bindAllowed = context.bindService(COMPAT_SERVICE_INTENT, serviceConnection, Context.BIND_AUTO_CREATE); } catch (SecurityException e) { - Log.e(TAG, "Unable to bind to compat service", e); + LOG.e("Unable to bind to compat service. " + e); bindAllowed = false; } @@ -149,12 +148,12 @@ public class SetupCompatServiceProvider { // in the normal world if (getCurrentState() != State.CONNECTED) { swapServiceContextAndNotify(new ServiceContext(State.BINDING)); - Log.i(TAG, "Context#bindService went through, now waiting for service connection"); + LOG.atInfo("Context#bindService went through, now waiting for service connection"); } } else { // SetupWizard is not installed/calling app does not have permissions to bind. swapServiceContextAndNotify(new ServiceContext(State.BIND_FAILED)); - Log.e(TAG, "Context#bindService did not succeed."); + LOG.e("Context#bindService did not succeed."); } } @@ -174,12 +173,9 @@ public class SetupCompatServiceProvider { } private void swapServiceContextAndNotify(ServiceContext latestServiceContext) { - if (Log.isLoggable(TAG, Log.INFO)) { - Log.i( - TAG, - String.format( - "State changed: %s -> %s", serviceContext.state, latestServiceContext.state)); - } + LOG.atInfo( + String.format("State changed: %s -> %s", serviceContext.state, latestServiceContext.state)); + serviceContext = latestServiceContext; CountDownLatch countDownLatch = getAndClearConnectedCondition(); if (countDownLatch != null) { @@ -221,7 +217,7 @@ public class SetupCompatServiceProvider { State state = State.CONNECTED; if (binder == null) { state = State.DISCONNECTED; - Log.w(TAG, "Binder is null when onServiceConnected was called!"); + LOG.w("Binder is null when onServiceConnected was called!"); } swapServiceContextAndNotify( new ServiceContext(state, ISetupCompatService.Stub.asInterface(binder))); @@ -336,6 +332,4 @@ public class SetupCompatServiceProvider { // lint error. @SuppressLint("StaticFieldLeak") private static volatile SetupCompatServiceProvider instance; - - private static final String TAG = "SucServiceProvider"; } diff --git a/main/java/com/google/android/setupcompat/internal/TemplateLayout.java b/main/java/com/google/android/setupcompat/internal/TemplateLayout.java index 34179d6..25a3c5b 100644 --- a/main/java/com/google/android/setupcompat/internal/TemplateLayout.java +++ b/main/java/com/google/android/setupcompat/internal/TemplateLayout.java @@ -20,15 +20,15 @@ import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.os.Build.VERSION_CODES; -import androidx.annotation.Keep; -import androidx.annotation.LayoutRes; -import androidx.annotation.StyleRes; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.FrameLayout; +import androidx.annotation.Keep; +import androidx.annotation.LayoutRes; +import androidx.annotation.StyleRes; import com.google.android.setupcompat.R; import com.google.android.setupcompat.template.Mixin; import java.util.HashMap; diff --git a/main/java/com/google/android/setupcompat/logging/internal/PartnerCustomizedResourceListMetric.java b/main/java/com/google/android/setupcompat/logging/internal/PartnerCustomizedResourceListMetric.java index 7d0b731..2aa1240 100644 --- a/main/java/com/google/android/setupcompat/logging/internal/PartnerCustomizedResourceListMetric.java +++ b/main/java/com/google/android/setupcompat/logging/internal/PartnerCustomizedResourceListMetric.java @@ -30,24 +30,28 @@ import com.google.android.setupcompat.logging.SetupMetricsLogger; @TargetApi(VERSION_CODES.Q) public class PartnerCustomizedResourceListMetric { - public static void logMetrics(Context context, String screenName, Bundle bundle) { + public static void logMetrics(Context context, String deviceDisplayName, Bundle bundle) { PersistableBundle logBundle = - buildLogBundleFromResourceConfigBundle(context.getPackageName(), bundle); + buildLogBundleFromResourceConfigBundle(context.getPackageName(), deviceDisplayName, bundle); if (!logBundle.isEmpty()) { SetupMetricsLogger.logCustomEvent( context, - CustomEvent.create(MetricKey.get("PartnerCustomizationResource", screenName), logBundle)); + CustomEvent.create( + MetricKey.get("PartnerCustomizationResource", "NoScreenName"), logBundle)); } } @VisibleForTesting public static PersistableBundle buildLogBundleFromResourceConfigBundle( - String defaultPackageName, Bundle resourceConfigBundle) { + String defaultPackageName, String deviceDisplayName, Bundle resourceConfigBundle) { PersistableBundle persistableBundle = new PersistableBundle(); + persistableBundle.putString("deviceDisplayName", deviceDisplayName); for (String key : resourceConfigBundle.keySet()) { Bundle resourceExtra = resourceConfigBundle.getBundle(key); if (!resourceExtra.getString("packageName", defaultPackageName).equals(defaultPackageName)) { persistableBundle.putBoolean(resourceExtra.getString("resourceName", key), true); + } else { + persistableBundle.putBoolean(resourceExtra.getString("resourceName", key), false); } } diff --git a/main/java/com/google/android/setupcompat/portal/NotificationComponent.java b/main/java/com/google/android/setupcompat/portal/NotificationComponent.java new file mode 100644 index 0000000..a90963b --- /dev/null +++ b/main/java/com/google/android/setupcompat/portal/NotificationComponent.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.setupcompat.portal; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.IntDef; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A class that represents how a persistent notification is to be presented to the user using the + * {@link com.google.android.setupcompat.portal.ISetupNotificationService}. + */ +public class NotificationComponent implements Parcelable { + + @NotificationType private final int notificationType; + private Bundle extraBundle = new Bundle(); + + private NotificationComponent(@NotificationType int notificationType) { + this.notificationType = notificationType; + } + + protected NotificationComponent(Parcel in) { + this(in.readInt()); + extraBundle = in.readBundle(Bundle.class.getClassLoader()); + } + + public int getIntExtra(String key, int defValue) { + return extraBundle.getInt(key, defValue); + } + + @NotificationType + public int getNotificationType() { + return notificationType; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(notificationType); + dest.writeBundle(extraBundle); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<NotificationComponent> CREATOR = + new Creator<NotificationComponent>() { + @Override + public NotificationComponent createFromParcel(Parcel in) { + return new NotificationComponent(in); + } + + @Override + public NotificationComponent[] newArray(int size) { + return new NotificationComponent[size]; + } + }; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + NotificationType.INITIAL_ONGOING, + NotificationType.PREDEFERRED, + NotificationType.PREDEFERRED_PREPARING, + NotificationType.DEFERRED, + NotificationType.DEFERRED_ONGOING, + NotificationType.PORTAL + }) + public @interface NotificationType { + int UNKNOWN = 0; + int INITIAL_ONGOING = 1; + int PREDEFERRED = 2; + int PREDEFERRED_PREPARING = 3; + int DEFERRED = 4; + int DEFERRED_ONGOING = 5; + int PORTAL = 6; + int MAX = 7; + } + + public static class Builder { + + private final NotificationComponent component; + + public Builder(@NotificationType int notificationType) { + component = new NotificationComponent(notificationType); + } + + public Builder putIntExtra(String key, int value) { + component.extraBundle.putInt(key, value); + return this; + } + + public NotificationComponent build() { + return component; + } + } +} diff --git a/main/java/com/google/android/setupcompat/portal/PortalConstants.java b/main/java/com/google/android/setupcompat/portal/PortalConstants.java new file mode 100644 index 0000000..52d8700 --- /dev/null +++ b/main/java/com/google/android/setupcompat/portal/PortalConstants.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.setupcompat.portal; + +import androidx.annotation.IntDef; +import androidx.annotation.StringDef; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Constant values used for Portal */ +public class PortalConstants { + + /** Enumeration of pending reasons, for {@link IPortalProgressCallback#setPendingReason}. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + PendingReason.IN_PROGRESS, + PendingReason.PROGRESS_REQUEST_ANY_NETWORK, + PendingReason.PROGRESS_REQUEST_WIFI, + PendingReason.PROGRESS_REQUEST_MOBILE, + PendingReason.PROGRESS_RETRY, + PendingReason.PROGRESS_REQUEST_REMOVED, + PendingReason.MAX + }) + public @interface PendingReason { + /** + * Don't used this, use {@link IPortalProgressCallback#setProgressCount} ot {@link + * IPortalProgressCallback#setProgressPercentage} will reset pending reason to in progress. + */ + int IN_PROGRESS = 0; + + /** Clients required network. */ + int PROGRESS_REQUEST_ANY_NETWORK = 1; + + /** Clients required a wifi network. */ + int PROGRESS_REQUEST_WIFI = 2; + + /** Client required a mobile data */ + int PROGRESS_REQUEST_MOBILE = 3; + + /** Client needs to wait for retry */ + int PROGRESS_RETRY = 4; + + /** Client required to remove added task */ + int PROGRESS_REQUEST_REMOVED = 5; + + int MAX = 6; + } + + /** Bundle keys used in {@link IPortalProgressService#onGetRemainingValues}. */ + @Retention(RetentionPolicy.SOURCE) + @StringDef({RemainingValues.REMAINING_SIZE_TO_BE_DOWNLOAD_IN_KB}) + public @interface RemainingValues { + /** Remaining size to download in MB. */ + String REMAINING_SIZE_TO_BE_DOWNLOAD_IN_KB = "RemainingSizeInKB"; + } + + private PortalConstants() {} +} diff --git a/main/java/com/google/android/setupcompat/portal/PortalHelper.java b/main/java/com/google/android/setupcompat/portal/PortalHelper.java new file mode 100644 index 0000000..4d1965a --- /dev/null +++ b/main/java/com/google/android/setupcompat/portal/PortalHelper.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.setupcompat.portal; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Process; +import android.os.RemoteException; +import android.os.UserHandle; +import androidx.annotation.NonNull; +import com.google.android.setupcompat.internal.Preconditions; +import com.google.android.setupcompat.portal.PortalConstants.RemainingValues; +import com.google.android.setupcompat.util.Logger; + +/** This class is responsible for safely executing methods on SetupNotificationService. */ +public class PortalHelper { + + private static final Logger LOG = new Logger("PortalHelper"); + + public static final String EXTRA_KEY_IS_SETUP_WIZARD = "isSetupWizard"; + + public static final String ACTION_BIND_SETUP_NOTIFICATION_SERVICE = + "com.google.android.setupcompat.portal.SetupNotificationService.BIND"; + + public static final String RESULT_BUNDLE_KEY_RESULT = "Result"; + public static final String RESULT_BUNDLE_KEY_ERROR = "Error"; + public static final String RESULT_BUNDLE_KEY_PORTAL_NOTIFICATION_AVAILABLE = + "PortalNotificationAvailable"; + + public static final Intent NOTIFICATION_SERVICE_INTENT = + new Intent(ACTION_BIND_SETUP_NOTIFICATION_SERVICE) + .setPackage("com.google.android.setupwizard"); + + /** + * Binds SetupNotificationService. For more detail see {@link Context#bindService(Intent, + * ServiceConnection, int)} + */ + public static boolean bindSetupNotificationService( + @NonNull Context context, @NonNull ServiceConnection connection) { + Preconditions.checkNotNull(context, "Context cannot be null"); + Preconditions.checkNotNull(connection, "ServiceConnection cannot be null"); + try { + return context.bindService(NOTIFICATION_SERVICE_INTENT, connection, Context.BIND_AUTO_CREATE); + } catch (SecurityException e) { + LOG.e("Exception occurred while binding SetupNotificationService", e); + return false; + } + } + + /** + * Registers a progress service to SUW service. The function response for bind service and invoke + * function safely, and returns the result using {@link RegisterCallback}. + * + * @param context The application context. + * @param component Identifies the progress service to execute. + * @param callback Receives register result. {@link RegisterCallback#onSuccess} called while + * register succeed. {@link RegisterCallback#onFailure} called while register failed. + */ + public static void registerProgressService( + @NonNull Context context, + @NonNull ProgressServiceComponent component, + @NonNull RegisterCallback callback) { + Preconditions.checkNotNull(context, "Context cannot be null"); + Preconditions.checkNotNull(component, "ProgressServiceComponent cannot be null"); + Preconditions.checkNotNull(callback, "RegisterCallback cannot be null"); + + ServiceConnection connection = + new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + if (binder != null) { + ISetupNotificationService service = + ISetupNotificationService.Stub.asInterface(binder); + try { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + final ServiceConnection serviceConnection = this; + service.registerProgressService( + component, + getCurrentUserHandle(), + new IPortalRegisterResultListener.Stub() { + @Override + public void onResult(Bundle result) { + if (result.getBoolean(RESULT_BUNDLE_KEY_RESULT, false)) { + callback.onSuccess( + result.getBoolean( + RESULT_BUNDLE_KEY_PORTAL_NOTIFICATION_AVAILABLE, false)); + } else { + callback.onFailure( + new IllegalStateException( + result.getString(RESULT_BUNDLE_KEY_ERROR, "Unknown error"))); + } + context.unbindService(serviceConnection); + } + }); + } else { + callback.onFailure( + new IllegalStateException( + "SetupNotificationService is not supported before Android N")); + context.unbindService(this); + } + } catch (RemoteException | NullPointerException e) { + callback.onFailure(e); + context.unbindService(this); + } + } else { + callback.onFailure( + new IllegalStateException("SetupNotification should not return null binder")); + context.unbindService(this); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + // Do nothing when service disconnected + } + }; + + if (!bindSetupNotificationService(context, connection)) { + LOG.e("Failed to bind SetupNotificationService."); + callback.onFailure(new SecurityException("Failed to bind SetupNotificationService.")); + } + } + + public static void isPortalAvailable( + @NonNull Context context, @NonNull final PortalAvailableResultListener listener) { + ServiceConnection connection = + new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + if (binder != null) { + ISetupNotificationService service = + ISetupNotificationService.Stub.asInterface(binder); + + try { + listener.onResult(service.isPortalAvailable()); + } catch (RemoteException e) { + LOG.e("Failed to invoke SetupNotificationService#isPortalAvailable"); + listener.onResult(false); + } + } + context.unbindService(this); + } + + @Override + public void onServiceDisconnected(ComponentName name) {} + }; + + if (!bindSetupNotificationService(context, connection)) { + LOG.e( + "Failed to bind SetupNotificationService. Do you have permission" + + " \"com.google.android.setupwizard.SETUP_PROGRESS_SERVICE\""); + listener.onResult(false); + } + } + + public static void isProgressServiceAlive( + @NonNull final Context context, + @NonNull final ProgressServiceComponent component, + @NonNull final ProgressServiceAliveResultListener listener) { + Preconditions.checkNotNull(context, "Context cannot be null"); + Preconditions.checkNotNull(component, "ProgressServiceComponent cannot be null"); + Preconditions.checkNotNull(listener, "ProgressServiceAliveResultCallback cannot be null"); + + ServiceConnection connection = + new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + if (binder != null) { + ISetupNotificationService service = + ISetupNotificationService.Stub.asInterface(binder); + + try { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + listener.onResult( + service.isProgressServiceAlive(component, getCurrentUserHandle())); + } else { + listener.onResult(false); + } + + } catch (RemoteException e) { + LOG.w("Failed to invoke SetupNotificationService#isProgressServiceAlive"); + listener.onResult(false); + } + } + context.unbindService(this); + } + + @Override + public void onServiceDisconnected(ComponentName name) {} + }; + + if (!bindSetupNotificationService(context, connection)) { + LOG.e( + "Failed to bind SetupNotificationService. Do you have permission" + + " \"com.google.android.setupwizard.SETUP_PROGRESS_SERVICE\""); + listener.onResult(false); + } + } + + private static UserHandle getCurrentUserHandle() { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return UserHandle.getUserHandleForUid(Process.myUid()); + } else { + return null; + } + } + + /** + * Creates the {@code Bundle} including the bind progress service result. + * + * @param succeed whether bind service success or not. + * @param errorMsg describe the reason why bind service failed. + * @return A bundle include bind result and error message. + */ + public static Bundle createResultBundle( + boolean succeed, String errorMsg, boolean isPortalNotificationAvailable) { + Bundle bundle = new Bundle(); + bundle.putBoolean(RESULT_BUNDLE_KEY_RESULT, succeed); + if (!succeed) { + bundle.putString(RESULT_BUNDLE_KEY_ERROR, errorMsg); + } + bundle.putBoolean( + RESULT_BUNDLE_KEY_PORTAL_NOTIFICATION_AVAILABLE, isPortalNotificationAvailable); + return bundle; + } + + /** + * Returns {@code true}, if the intent is bound from SetupWizard, otherwise returns false. + * + * @param intent that received when onBind. + */ + public static boolean isFromSUW(Intent intent) { + return intent != null && intent.getBooleanExtra(EXTRA_KEY_IS_SETUP_WIZARD, false); + } + + /** A callback for accepting the results of SetupNotificationService. */ + public interface RegisterCallback { + void onSuccess(boolean isPortalNow); + + void onFailure(Throwable throwable); + } + + public interface RegisterNotificationCallback { + void onSuccess(); + + void onFailure(Throwable throwable); + } + + public interface ProgressServiceAliveResultListener { + void onResult(boolean isAlive); + } + + public interface PortalAvailableResultListener { + void onResult(boolean isAvailable); + } + + public static class RemainingValueBuilder { + private final Bundle bundle = new Bundle(); + + public static RemainingValueBuilder createBuilder() { + return new RemainingValueBuilder(); + } + + public RemainingValueBuilder setRemainingSizeInKB(int size) { + Preconditions.checkArgument( + size >= 0, "The remainingSize should be positive integer or zero."); + bundle.putInt(RemainingValues.REMAINING_SIZE_TO_BE_DOWNLOAD_IN_KB, size); + return this; + } + + public Bundle build() { + return bundle; + } + + private RemainingValueBuilder() {} + } + + private PortalHelper() {} +} + + diff --git a/main/java/com/google/android/setupcompat/portal/PortalResultHelper.java b/main/java/com/google/android/setupcompat/portal/PortalResultHelper.java new file mode 100644 index 0000000..cec2990 --- /dev/null +++ b/main/java/com/google/android/setupcompat/portal/PortalResultHelper.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.setupcompat.portal; + +import android.os.Bundle; + +public class PortalResultHelper { + + public static final String RESULT_BUNDLE_KEY_RESULT = "Result"; + public static final String RESULT_BUNDLE_KEY_ERROR = "Error"; + + public static boolean isSuccess(Bundle bundle) { + return bundle.getBoolean(RESULT_BUNDLE_KEY_RESULT, false); + } + + public static String getErrorMessage(Bundle bundle) { + return bundle.getString(RESULT_BUNDLE_KEY_ERROR, null); + } + + public static Bundle createSuccessBundle() { + Bundle resultBundle = new Bundle(); + resultBundle.putBoolean(RESULT_BUNDLE_KEY_RESULT, true); + return resultBundle; + } + + public static Bundle createFailureBundle(String errorMessage) { + Bundle resultBundle = new Bundle(); + resultBundle.putBoolean(RESULT_BUNDLE_KEY_RESULT, false); + resultBundle.putString(RESULT_BUNDLE_KEY_ERROR, errorMessage); + return resultBundle; + } + + private PortalResultHelper() {} + ; +} diff --git a/main/java/com/google/android/setupcompat/portal/ProgressServiceComponent.java b/main/java/com/google/android/setupcompat/portal/ProgressServiceComponent.java new file mode 100644 index 0000000..be11239 --- /dev/null +++ b/main/java/com/google/android/setupcompat/portal/ProgressServiceComponent.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.setupcompat.portal; + +import android.content.Intent; +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import com.google.android.setupcompat.internal.Preconditions; + +/** + * A class that represents how a progress service to be registered to {@link + * com.google.android.setupcompat.portal.ISetupNotificationService}. + */ +public class ProgressServiceComponent implements Parcelable { + private final String packageName; + private final String taskName; + private final boolean isSilent; + private final boolean autoRebind; + private final long timeoutForReRegister; + @StringRes private final int displayNameResId; + @DrawableRes private final int displayIconResId; + private final Intent serviceIntent; + private final Intent itemClickIntent; + + private ProgressServiceComponent( + String packageName, + String taskName, + boolean isSilent, + boolean autoRebind, + long timeoutForReRegister, + @StringRes int displayNameResId, + @DrawableRes int displayIconResId, + Intent serviceIntent, + Intent itemClickIntent) { + this.packageName = packageName; + this.taskName = taskName; + this.isSilent = isSilent; + this.autoRebind = autoRebind; + this.timeoutForReRegister = timeoutForReRegister; + this.displayNameResId = displayNameResId; + this.displayIconResId = displayIconResId; + this.serviceIntent = serviceIntent; + this.itemClickIntent = itemClickIntent; + } + + /** Returns a new instance of {@link Builder}. */ + public static Builder newBuilder() { + return new ProgressServiceComponent.Builder(); + } + + /** Returns the package name where the service exist. */ + @NonNull + public String getPackageName() { + return packageName; + } + + /** Returns the service class name */ + @NonNull + public String getTaskName() { + return taskName; + } + + /** Returns the whether the service is silent or not */ + public boolean isSilent() { + return isSilent; + } + + /** Auto rebind progress service while service connection disconnect. Default: true */ + public boolean isAutoRebind() { + return autoRebind; + } + + /** The timeout period waiting for client register progress service again. */ + public long getTimeoutForReRegister() { + return timeoutForReRegister; + } + + /** Returns the string resource id of display name. */ + @StringRes + public int getDisplayName() { + return displayNameResId; + } + + /** Returns the drawable resource id of display icon. */ + @DrawableRes + public int getDisplayIcon() { + return displayIconResId; + } + + /** Returns the Intent used to bind progress service. */ + public Intent getServiceIntent() { + return serviceIntent; + } + + /** Returns the Intent to start the user interface while progress item click. */ + public Intent getItemClickIntent() { + return itemClickIntent; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getPackageName()); + dest.writeString(getTaskName()); + dest.writeInt(isSilent() ? 1 : 0); + dest.writeInt(getDisplayName()); + dest.writeInt(getDisplayIcon()); + dest.writeParcelable(getServiceIntent(), 0); + dest.writeParcelable(getItemClickIntent(), 0); + dest.writeInt(isAutoRebind() ? 1 : 0); + dest.writeLong(getTimeoutForReRegister()); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<ProgressServiceComponent> CREATOR = + new Creator<ProgressServiceComponent>() { + @Override + public ProgressServiceComponent createFromParcel(Parcel in) { + return ProgressServiceComponent.newBuilder() + .setPackageName(in.readString()) + .setTaskName(in.readString()) + .setSilentMode(in.readInt() == 1) + .setDisplayName(in.readInt()) + .setDisplayIcon(in.readInt()) + .setServiceIntent(in.readParcelable(Intent.class.getClassLoader())) + .setItemClickIntent(in.readParcelable(Intent.class.getClassLoader())) + .setAutoRebind(in.readInt() == 1) + .setTimeoutForReRegister(in.readLong()) + .build(); + } + + @Override + public ProgressServiceComponent[] newArray(int size) { + return new ProgressServiceComponent[size]; + } + }; + + /** Builder class for {@link ProgressServiceComponent} objects */ + public static class Builder { + private String packageName; + private String taskName; + private boolean isSilent = false; + private boolean autoRebind = true; + private long timeoutForReRegister = 0L; + @StringRes private int displayNameResId; + @DrawableRes private int displayIconResId; + private Intent serviceIntent; + private Intent itemClickIntent; + + /** Sets the packages name which is the service exists */ + public Builder setPackageName(@NonNull String packageName) { + this.packageName = packageName; + return this; + } + + /** Sets a name to identify what task this progress is. */ + public Builder setTaskName(@NonNull String taskName) { + this.taskName = taskName; + return this; + } + + /** Sets the service as silent mode, it executes without UI on PortalActivity. */ + public Builder setSilentMode(boolean isSilent) { + this.isSilent = isSilent; + return this; + } + + /** Sets the service need auto rebind or not when service connection disconnected. */ + public Builder setAutoRebind(boolean autoRebind) { + this.autoRebind = autoRebind; + return this; + } + + /** + * Sets the timeout period waiting for the client register again, only works when auto-rebind + * disabled. When 0 is set, will read default configuration from SUW. + */ + public Builder setTimeoutForReRegister(long timeoutForReRegister) { + this.timeoutForReRegister = timeoutForReRegister; + return this; + } + + /** Sets the name which is displayed on PortalActivity */ + public Builder setDisplayName(@StringRes int displayNameResId) { + this.displayNameResId = displayNameResId; + return this; + } + + /** Sets the icon which is display on PortalActivity */ + public Builder setDisplayIcon(@DrawableRes int displayIconResId) { + this.displayIconResId = displayIconResId; + return this; + } + + public Builder setServiceIntent(Intent serviceIntent) { + this.serviceIntent = serviceIntent; + return this; + } + + public Builder setItemClickIntent(Intent itemClickIntent) { + this.itemClickIntent = itemClickIntent; + return this; + } + + public ProgressServiceComponent build() { + Preconditions.checkNotNull(packageName, "packageName cannot be null."); + Preconditions.checkNotNull(taskName, "serviceClass cannot be null."); + Preconditions.checkNotNull(serviceIntent, "Service intent cannot be null."); + Preconditions.checkNotNull(itemClickIntent, "Item click intent cannot be null"); + if (!isSilent) { + Preconditions.checkArgument( + displayNameResId != 0, "Invalidate resource id of display name"); + Preconditions.checkArgument( + displayIconResId != 0, "Invalidate resource id of display icon"); + } + return new ProgressServiceComponent( + packageName, + taskName, + isSilent, + autoRebind, + timeoutForReRegister, + displayNameResId, + displayIconResId, + serviceIntent, + itemClickIntent); + } + + private Builder() {} + } +} diff --git a/main/java/com/google/android/setupcompat/template/FooterActionButton.java b/main/java/com/google/android/setupcompat/template/FooterActionButton.java index bb26d19..86a06d9 100644 --- a/main/java/com/google/android/setupcompat/template/FooterActionButton.java +++ b/main/java/com/google/android/setupcompat/template/FooterActionButton.java @@ -18,11 +18,11 @@ package com.google.android.setupcompat.template; import android.annotation.SuppressLint; import android.content.Context; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.Button; +import androidx.annotation.Nullable; /** Button that can react to touch when disabled. */ public class FooterActionButton extends Button { diff --git a/main/java/com/google/android/setupcompat/template/FooterBarMixin.java b/main/java/com/google/android/setupcompat/template/FooterBarMixin.java index bc9e5c1..b75d972 100644 --- a/main/java/com/google/android/setupcompat/template/FooterBarMixin.java +++ b/main/java/com/google/android/setupcompat/template/FooterBarMixin.java @@ -24,16 +24,17 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Color; -import android.graphics.PorterDuff.Mode; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.InsetDrawable; -import android.graphics.drawable.LayerDrawable; -import android.graphics.drawable.RippleDrawable; import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.PersistableBundle; +import android.util.AttributeSet; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewStub; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.LinearLayout.LayoutParams; import androidx.annotation.AttrRes; import androidx.annotation.CallSuper; import androidx.annotation.ColorInt; @@ -44,25 +45,15 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StyleRes; import androidx.annotation.VisibleForTesting; -import android.util.AttributeSet; -import android.util.StateSet; -import android.util.TypedValue; -import android.view.ContextThemeWrapper; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewStub; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.LinearLayout.LayoutParams; import com.google.android.setupcompat.PartnerCustomizationLayout; import com.google.android.setupcompat.R; import com.google.android.setupcompat.internal.FooterButtonPartnerConfig; -import com.google.android.setupcompat.internal.Preconditions; import com.google.android.setupcompat.internal.TemplateLayout; import com.google.android.setupcompat.logging.internal.FooterBarMixinMetrics; import com.google.android.setupcompat.partnerconfig.PartnerConfig; import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper; import com.google.android.setupcompat.template.FooterButton.ButtonType; +import java.util.Locale; import java.util.concurrent.atomic.AtomicInteger; /** @@ -77,6 +68,8 @@ public class FooterBarMixin implements Mixin { @Nullable private final ViewStub footerStub; @VisibleForTesting final boolean applyPartnerResources; + @VisibleForTesting final boolean applyDynamicColor; + @VisibleForTesting final boolean useFullDynamicColor; private LinearLayout buttonContainer; private FooterButton primaryButton; @@ -94,8 +87,8 @@ public class FooterBarMixin implements Mixin { @ColorInt private final int footerBarPrimaryBackgroundColor; @ColorInt private final int footerBarSecondaryBackgroundColor; private boolean removeFooterBarWhenEmpty = true; + private boolean isSecondaryButtonInPrimaryStyle = false; - private static final float DEFAULT_DISABLED_ALPHA = 0.26f; private static final AtomicInteger nextGeneratedId = new AtomicInteger(1); @VisibleForTesting public final FooterBarMixinMetrics metrics = new FooterBarMixinMetrics(); @@ -110,10 +103,10 @@ public class FooterBarMixin implements Mixin { Button button = buttonContainer.findViewById(id); if (button != null) { button.setEnabled(enabled); - if (applyPartnerResources) { - updateButtonTextColorWithPartnerConfig( + if (applyPartnerResources && !applyDynamicColor) { + updateButtonTextColorWithEnabledState( button, - (id == primaryButtonId) + (id == primaryButtonId || isSecondaryButtonInPrimaryStyle) ? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR : PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR); } @@ -141,6 +134,25 @@ public class FooterBarMixin implements Mixin { } } } + + @Override + @TargetApi(VERSION_CODES.JELLY_BEAN_MR1) + public void onLocaleChanged(Locale locale) { + if (buttonContainer != null) { + Button button = buttonContainer.findViewById(id); + if (button != null && locale != null) { + button.setTextLocale(locale); + } + } + } + + @Override + @TargetApi(VERSION_CODES.JELLY_BEAN_MR1) + public void onDirectionChanged(int direction) { + if (buttonContainer != null && direction != -1) { + buttonContainer.setLayoutDirection(direction); + } + } }; } @@ -159,6 +171,14 @@ public class FooterBarMixin implements Mixin { layout instanceof PartnerCustomizationLayout && ((PartnerCustomizationLayout) layout).shouldApplyPartnerResource(); + applyDynamicColor = + layout instanceof PartnerCustomizationLayout + && ((PartnerCustomizationLayout) layout).shouldApplyDynamicColor(); + + useFullDynamicColor = + layout instanceof PartnerCustomizationLayout + && ((PartnerCustomizationLayout) layout).useFullDynamicColor(); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SucFooterBarMixin, defStyleAttr, 0); defaultPadding = @@ -253,11 +273,14 @@ public class FooterBarMixin implements Mixin { return; } - @ColorInt - int color = - PartnerConfigHelper.get(context) - .getColor(context, PartnerConfig.CONFIG_FOOTER_BAR_BG_COLOR); - buttonContainer.setBackgroundColor(color); + // skip apply partner resources on footerbar background if dynamic color enabled + if (!useFullDynamicColor) { + @ColorInt + int color = + PartnerConfigHelper.get(context) + .getColor(context, PartnerConfig.CONFIG_FOOTER_BAR_BG_COLOR); + buttonContainer.setBackgroundColor(color); + } footerBarPaddingTop = (int) @@ -273,6 +296,17 @@ public class FooterBarMixin implements Mixin { footerBarPaddingTop, buttonContainer.getPaddingRight(), footerBarPaddingBottom); + + if (PartnerConfigHelper.get(context) + .isPartnerConfigAvailable(PartnerConfig.CONFIG_FOOTER_BAR_MIN_HEIGHT)) { + int minHeight = + (int) + PartnerConfigHelper.get(context) + .getDimension(context, PartnerConfig.CONFIG_FOOTER_BAR_MIN_HEIGHT); + if (minHeight > 0) { + buttonContainer.setMinimumHeight(minHeight); + } + } } /** @@ -310,7 +344,9 @@ public class FooterBarMixin implements Mixin { .setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA) .setTextColorConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR) .setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE) + .setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT) .setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY) + .setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE) .build(); FooterActionButton button = inflateButton(footerButton, footerButtonPartnerConfig); @@ -346,7 +382,14 @@ public class FooterBarMixin implements Mixin { /** Sets secondary button for footer. */ @MainThread public void setSecondaryButton(FooterButton footerButton) { + setSecondaryButton(footerButton, /*usePrimaryStyle= */ false); + } + + /** Sets secondary button for footer. Allow to use the primary button style. */ + @MainThread + public void setSecondaryButton(FooterButton footerButton, boolean usePrimaryStyle) { ensureOnMainThread("setSecondaryButton"); + isSecondaryButtonInPrimaryStyle = usePrimaryStyle; ensureFooterInflated(); // Setup button partner config @@ -355,18 +398,29 @@ public class FooterBarMixin implements Mixin { .setPartnerTheme( getPartnerTheme( footerButton, - /* defaultPartnerTheme= */ R.style.SucPartnerCustomizationButton_Secondary, - /* buttonBackgroundColorConfig= */ PartnerConfig - .CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR)) - .setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR) + /* defaultPartnerTheme= */ usePrimaryStyle + ? R.style.SucPartnerCustomizationButton_Primary + : R.style.SucPartnerCustomizationButton_Secondary, + /* buttonBackgroundColorConfig= */ usePrimaryStyle + ? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR + : PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR)) + .setButtonBackgroundConfig( + usePrimaryStyle + ? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR + : PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR) .setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA) .setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR) .setButtonIconConfig(getDrawablePartnerConfig(footerButton.getButtonType())) .setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS) .setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA) - .setTextColorConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR) + .setTextColorConfig( + usePrimaryStyle + ? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR + : PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR) .setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE) + .setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT) .setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY) + .setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE) .build(); FooterActionButton button = inflateButton(footerButton, footerButtonPartnerConfig); @@ -395,6 +449,16 @@ public class FooterBarMixin implements Mixin { buttonContainer.removeAllViews(); if (tempSecondaryButton != null) { + if (isSecondaryButtonInPrimaryStyle) { + // Since the secondary button has the same style (with background) as the primary button, + // we need to have the left padding equal to the right padding. + updateFooterBarPadding( + buttonContainer, + buttonContainer.getPaddingRight(), + buttonContainer.getPaddingTop(), + buttonContainer.getPaddingRight(), + buttonContainer.getPaddingBottom()); + } buttonContainer.addView(tempSecondaryButton); } addSpace(); @@ -411,7 +475,7 @@ public class FooterBarMixin implements Mixin { protected void onFooterButtonInflated(Button button, @ColorInt int defaultButtonBackgroundColor) { // Try to set default background if (defaultButtonBackgroundColor != 0) { - updateButtonBackground(button, defaultButtonBackgroundColor); + FooterButtonStyleUtils.updateButtonBackground(button, defaultButtonBackgroundColor); } else { // TODO: get button background color from activity theme } @@ -544,187 +608,30 @@ public class FooterBarMixin implements Mixin { if (!applyPartnerResources) { return; } - updateButtonTextColorWithPartnerConfig( - button, footerButtonPartnerConfig.getButtonTextColorConfig()); - updateButtonTextSizeWithPartnerConfig( - button, footerButtonPartnerConfig.getButtonTextSizeConfig()); - updateButtonTypeFaceWithPartnerConfig( - button, footerButtonPartnerConfig.getButtonTextTypeFaceConfig()); - updateButtonBackgroundWithPartnerConfig( + FooterButtonStyleUtils.applyButtonPartnerResources( + context, button, - footerButtonPartnerConfig.getButtonBackgroundConfig(), - footerButtonPartnerConfig.getButtonDisableAlphaConfig(), - footerButtonPartnerConfig.getButtonDisableBackgroundConfig()); - updateButtonRadiusWithPartnerConfig(button, footerButtonPartnerConfig.getButtonRadiusConfig()); - updateButtonIconWithPartnerConfig(button, footerButtonPartnerConfig.getButtonIconConfig()); - updateButtonRippleColorWithPartnerConfig(button, footerButtonPartnerConfig); + applyDynamicColor, + /* isButtonIconAtEnd= */ (button.getId() == primaryButtonId), + footerButtonPartnerConfig); + if (!applyDynamicColor) { + // adjust text color based on enabled state + updateButtonTextColorWithEnabledState( + button, footerButtonPartnerConfig.getButtonTextColorConfig()); + } } - private void updateButtonTextColorWithPartnerConfig( + private void updateButtonTextColorWithEnabledState( Button button, PartnerConfig buttonTextColorConfig) { if (button.isEnabled()) { - @ColorInt - int color = PartnerConfigHelper.get(context).getColor(context, buttonTextColorConfig); - if (color != Color.TRANSPARENT) { - button.setTextColor(ColorStateList.valueOf(color)); - } + FooterButtonStyleUtils.updateButtonTextEnabledColorWithPartnerConfig( + context, button, buttonTextColorConfig); } else { - // disable state will use the default disable state color - button.setTextColor( - button.getId() == primaryButtonId ? primaryDefaultTextColor : secondaryDefaultTextColor); - } - } - - private void updateButtonTextSizeWithPartnerConfig( - Button button, PartnerConfig buttonTextSizeConfig) { - float size = PartnerConfigHelper.get(context).getDimension(context, buttonTextSizeConfig); - if (size > 0) { - button.setTextSize(TypedValue.COMPLEX_UNIT_PX, size); - } - } - - private void updateButtonTypeFaceWithPartnerConfig( - Button button, PartnerConfig buttonTextTypeFaceConfig) { - String fontFamilyName = - PartnerConfigHelper.get(context).getString(context, buttonTextTypeFaceConfig); - Typeface font = Typeface.create(fontFamilyName, Typeface.NORMAL); - if (font != null) { - button.setTypeface(font); - } - } - - @TargetApi(VERSION_CODES.Q) - private void updateButtonBackgroundWithPartnerConfig( - Button button, - PartnerConfig buttonBackgroundConfig, - PartnerConfig buttonDisableAlphaConfig, - PartnerConfig buttonDisableBackgroundConfig) { - Preconditions.checkArgument( - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q, - "Update button background only support on sdk Q or higher"); - @ColorInt int color; - @ColorInt int disabledColor; - float disabledAlpha; - int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled}; - int[] ENABLED_STATE_SET = {}; - color = PartnerConfigHelper.get(context).getColor(context, buttonBackgroundConfig); - disabledAlpha = - PartnerConfigHelper.get(context).getFraction(context, buttonDisableAlphaConfig, 0f); - disabledColor = - PartnerConfigHelper.get(context).getColor(context, buttonDisableBackgroundConfig); - - if (color != Color.TRANSPARENT) { - if (disabledAlpha <= 0f) { - // if no partner resource, fallback to theme disable alpha - float alpha; - TypedArray a = context.obtainStyledAttributes(new int[] {android.R.attr.disabledAlpha}); - alpha = a.getFloat(0, DEFAULT_DISABLED_ALPHA); - a.recycle(); - disabledAlpha = alpha; - } - if (disabledColor == Color.TRANSPARENT) { - // if no partner resource, fallback to button background color - disabledColor = color; - } - - // Set text color for ripple. - ColorStateList colorStateList = - new ColorStateList( - new int[][] {DISABLED_STATE_SET, ENABLED_STATE_SET}, - new int[] {convertRgbToArgb(disabledColor, disabledAlpha), color}); - - // b/129482013: When a LayerDrawable is mutated, a new clone of its children drawables are - // created, but without copying the state from the parent drawable. So even though the - // parent is getting the correct drawable state from the view, the children won't get those - // states until a state change happens. - // As a workaround, we mutate the drawable and forcibly set the state to empty, and then - // refresh the state so the children will have the updated states. - button.getBackground().mutate().setState(new int[0]); - button.refreshDrawableState(); - button.setBackgroundTintList(colorStateList); - } - } - - private void updateButtonBackground(Button button, @ColorInt int color) { - button.getBackground().mutate().setColorFilter(color, Mode.SRC_ATOP); - } - - private void updateButtonRadiusWithPartnerConfig( - Button button, PartnerConfig buttonRadiusConfig) { - if (Build.VERSION.SDK_INT >= VERSION_CODES.N) { - float radius = PartnerConfigHelper.get(context).getDimension(context, buttonRadiusConfig); - GradientDrawable gradientDrawable = getGradientDrawable(button); - if (gradientDrawable != null) { - gradientDrawable.setCornerRadius(radius); - } - } - } - - private void updateButtonRippleColorWithPartnerConfig( - Button button, FooterButtonPartnerConfig footerButtonPartnerConfig) { - // RippleDrawable is available after sdk 21. And because on lower sdk the RippleDrawable is - // unavailable. Since Stencil customization provider only works on Q+, there is no need to - // perform any customization for versions 21. - if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - RippleDrawable rippleDrawable = getRippleDrawable(button); - if (rippleDrawable == null) { - return; - } - - int[] pressedState = {android.R.attr.state_pressed}; - @ColorInt int color; - // Get partner text color. - color = - PartnerConfigHelper.get(context) - .getColor(context, footerButtonPartnerConfig.getButtonTextColorConfig()); - - float alpha = - PartnerConfigHelper.get(context) - .getFraction(context, footerButtonPartnerConfig.getButtonRippleColorAlphaConfig()); - - // Set text color for ripple. - ColorStateList colorStateList = - new ColorStateList( - new int[][] {pressedState, StateSet.NOTHING}, - new int[] {convertRgbToArgb(color, alpha), Color.TRANSPARENT}); - rippleDrawable.setColor(colorStateList); - } - } - - private void updateButtonIconWithPartnerConfig(Button button, PartnerConfig buttonIconConfig) { - if (button == null) { - return; - } - Drawable icon = null; - if (buttonIconConfig != null) { - icon = PartnerConfigHelper.get(context).getDrawable(context, buttonIconConfig); - } - setButtonIcon(button, icon); - } - - private void setButtonIcon(Button button, Drawable icon) { - if (button == null) { - return; - } - - if (icon != null) { - // TODO: restrict the icons to a reasonable size - int h = icon.getIntrinsicHeight(); - int w = icon.getIntrinsicWidth(); - icon.setBounds(0, 0, w, h); - } - - Drawable iconStart = null; - Drawable iconEnd = null; - if (button.getId() == primaryButtonId) { - iconEnd = icon; - } else if (button.getId() == secondaryButtonId) { - iconStart = icon; - } - if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { - button.setCompoundDrawablesRelative(iconStart, null, iconEnd, null); - } else { - button.setCompoundDrawables(iconStart, null, iconEnd, null); + FooterButtonStyleUtils.updateButtonTextDisableColor( + button, + /* is Primary= */ (primaryButtonId == button.getId() || isSecondaryButtonInPrimaryStyle) + ? primaryDefaultTextColor + : secondaryDefaultTextColor); } } @@ -763,43 +670,6 @@ public class FooterBarMixin implements Mixin { return result; } - GradientDrawable getGradientDrawable(Button button) { - // RippleDrawable is available after sdk 21, InsetDrawable#getDrawable is available after - // sdk 19. So check the sdk is higher than sdk 21 and since Stencil customization provider only - // works on Q+, there is no need to perform any customization for versions 21. - if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - Drawable drawable = button.getBackground(); - if (drawable instanceof InsetDrawable) { - LayerDrawable layerDrawable = (LayerDrawable) ((InsetDrawable) drawable).getDrawable(); - return (GradientDrawable) layerDrawable.getDrawable(0); - } else if (drawable instanceof RippleDrawable) { - InsetDrawable insetDrawable = (InsetDrawable) ((RippleDrawable) drawable).getDrawable(0); - return (GradientDrawable) insetDrawable.getDrawable(); - } - } - return null; - } - - RippleDrawable getRippleDrawable(Button button) { - // RippleDrawable is available after sdk 21. And because on lower sdk the RippleDrawable is - // unavailable. Since Stencil customization provider only works on Q+, there is no need to - // perform any customization for versions 21. - if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - Drawable drawable = button.getBackground(); - if (drawable instanceof InsetDrawable) { - return (RippleDrawable) ((InsetDrawable) drawable).getDrawable(); - } else if (drawable instanceof RippleDrawable) { - return (RippleDrawable) drawable; - } - } - return null; - } - - @ColorInt - private static int convertRgbToArgb(@ColorInt int color, float alpha) { - return Color.argb((int) (alpha * 255), Color.red(color), Color.green(color), Color.blue(color)); - } - protected View inflateFooter(@LayoutRes int footer) { if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { LayoutInflater inflater = diff --git a/main/java/com/google/android/setupcompat/template/FooterButton.java b/main/java/com/google/android/setupcompat/template/FooterButton.java index 2fa8c7c..90c13ec 100644 --- a/main/java/com/google/android/setupcompat/template/FooterButton.java +++ b/main/java/com/google/android/setupcompat/template/FooterButton.java @@ -23,17 +23,18 @@ import android.content.Context; import android.content.res.TypedArray; import android.os.Build.VERSION_CODES; import android.os.PersistableBundle; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.StyleRes; -import android.util.AttributeSet; -import android.view.View; -import android.view.View.OnClickListener; import com.google.android.setupcompat.R; import com.google.android.setupcompat.logging.CustomEvent; import java.lang.annotation.Retention; +import java.util.Locale; /** * Definition of a footer button. Clients can use this class to customize attributes like text, @@ -53,6 +54,8 @@ public final class FooterButton implements OnClickListener { private OnClickListener onClickListenerWhenDisabled; private OnButtonEventListener buttonListener; private int clickCount = 0; + private Locale locale; + private int direction; public FooterButton(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SucFooterButton); @@ -78,11 +81,15 @@ public final class FooterButton implements OnClickListener { CharSequence text, @Nullable OnClickListener listener, @ButtonType int buttonType, - @StyleRes int theme) { + @StyleRes int theme, + Locale locale, + int direction) { this.text = text; onClickListener = listener; this.buttonType = buttonType; this.theme = theme; + this.locale = locale; + this.direction = direction; } /** Returns the text that this footer button is displaying. */ @@ -142,6 +149,16 @@ public final class FooterButton implements OnClickListener { return enabled; } + /** Returns the layout direction for this footer button. */ + public int getLayoutDirection() { + return direction; + } + + /** Returns the text locale for this footer button. */ + public Locale getTextLocale() { + return locale; + } + /** * Sets the visibility state of this footer button. * @@ -172,6 +189,22 @@ public final class FooterButton implements OnClickListener { } } + /** Sets the text locale to be displayed on footer button. */ + public void setTextLocale(Locale locale) { + this.locale = locale; + if (buttonListener != null) { + buttonListener.onLocaleChanged(locale); + } + } + + /** Sets the layout direction to be displayed on footer button. */ + public void setLayoutDirection(int direction) { + this.direction = direction; + if (buttonListener != null) { + buttonListener.onDirectionChanged(direction); + } + } + /** * Registers a callback to be invoked when footer button API has set. * @@ -201,6 +234,10 @@ public final class FooterButton implements OnClickListener { void onVisibilityChanged(int visibility); void onTextChanged(CharSequence text); + + void onLocaleChanged(Locale locale); + + void onDirectionChanged(int direction); } /** Maximum valid value of ButtonType */ @@ -308,12 +345,16 @@ public final class FooterButton implements OnClickListener { * .setListener(primaryButton) * .setButtonType(ButtonType.NEXT) * .setTheme(R.style.SuwGlifButton_Primary) + * .setTextLocale(Locale.CANADA) + * .setLayoutDirection(View.LAYOUT_DIRECTION_LTR) * .build(); * </pre> */ public static class Builder { private final Context context; private String text = ""; + private Locale locale = null; + private int direction = -1; private OnClickListener onClickListener = null; @ButtonType private int buttonType = ButtonType.OTHER; private int theme = 0; @@ -334,6 +375,18 @@ public final class FooterButton implements OnClickListener { return this; } + /** Sets the {@code locale} of FooterButton. */ + public Builder setTextLocale(Locale locale) { + this.locale = locale; + return this; + } + + /** Sets the {@code direction} of FooterButton. */ + public Builder setLayoutDirection(int direction) { + this.direction = direction; + return this; + } + /** Sets the {@code listener} of FooterButton. */ public Builder setListener(@Nullable OnClickListener listener) { onClickListener = listener; @@ -353,7 +406,7 @@ public final class FooterButton implements OnClickListener { } public FooterButton build() { - return new FooterButton(text, onClickListener, buttonType, theme); + return new FooterButton(text, onClickListener, buttonType, theme, locale, direction); } } } diff --git a/main/java/com/google/android/setupcompat/template/FooterButtonInflater.java b/main/java/com/google/android/setupcompat/template/FooterButtonInflater.java index fe2538b..10aa052 100644 --- a/main/java/com/google/android/setupcompat/template/FooterButtonInflater.java +++ b/main/java/com/google/android/setupcompat/template/FooterButtonInflater.java @@ -19,10 +19,10 @@ package com.google.android.setupcompat.template; import android.content.Context; import android.content.res.Resources; import android.content.res.XmlResourceParser; -import androidx.annotation.NonNull; import android.util.AttributeSet; import android.util.Xml; import android.view.InflateException; +import androidx.annotation.NonNull; import java.io.IOException; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; diff --git a/main/java/com/google/android/setupcompat/template/FooterButtonStyleUtils.java b/main/java/com/google/android/setupcompat/template/FooterButtonStyleUtils.java new file mode 100644 index 0000000..ef45b5c --- /dev/null +++ b/main/java/com/google/android/setupcompat/template/FooterButtonStyleUtils.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.setupcompat.template; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.PorterDuff.Mode; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.InsetDrawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.RippleDrawable; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.util.StateSet; +import android.util.TypedValue; +import android.widget.Button; +import androidx.annotation.ColorInt; +import androidx.annotation.VisibleForTesting; +import com.google.android.setupcompat.R; +import com.google.android.setupcompat.internal.FooterButtonPartnerConfig; +import com.google.android.setupcompat.internal.Preconditions; +import com.google.android.setupcompat.partnerconfig.PartnerConfig; +import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper; + +/** Utils for updating the button style. */ +public class FooterButtonStyleUtils { + private static final float DEFAULT_DISABLED_ALPHA = 0.26f; + + /** Apply the partner primary button style to given {@code button}. */ + public static void applyPrimaryButtonPartnerResource( + Context context, Button button, boolean applyDynamicColor) { + + FooterButtonPartnerConfig footerButtonPartnerConfig = + new FooterButtonPartnerConfig.Builder(null) + .setPartnerTheme(R.style.SucPartnerCustomizationButton_Primary) + .setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR) + .setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA) + .setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR) + .setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS) + .setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA) + .setTextColorConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR) + .setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE) + .setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT) + .setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY) + .setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE) + .build(); + applyButtonPartnerResources( + context, + button, + applyDynamicColor, + /* isButtonIconAtEnd= */ true, + footerButtonPartnerConfig); + } + + /** Apply the partner secondary button style to given {@code button}. */ + public static void applySecondaryButtonPartnerResource( + Context context, Button button, boolean applyDynamicColor) { + + int defaultTheme = R.style.SucPartnerCustomizationButton_Secondary; + int color = + PartnerConfigHelper.get(context) + .getColor(context, PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR); + if (color != Color.TRANSPARENT) { + defaultTheme = R.style.SucPartnerCustomizationButton_Primary; + } + // Setup button partner config + FooterButtonPartnerConfig footerButtonPartnerConfig = + new FooterButtonPartnerConfig.Builder(null) + .setPartnerTheme(defaultTheme) + .setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR) + .setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA) + .setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR) + .setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS) + .setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA) + .setTextColorConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR) + .setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE) + .setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT) + .setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY) + .setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE) + .build(); + applyButtonPartnerResources( + context, + button, + applyDynamicColor, + /* isButtonIconAtEnd= */ false, + footerButtonPartnerConfig); + } + + static void applyButtonPartnerResources( + Context context, + Button button, + boolean applyDynamicColor, + boolean isButtonIconAtEnd, + FooterButtonPartnerConfig footerButtonPartnerConfig) { + + // If dynamic color enabled, these colors won't be overrode by partner config. + // Instead, these colors align with the current theme colors. + if (!applyDynamicColor) { + // use default disable color util we support the partner disable text color + if (button.isEnabled()) { + FooterButtonStyleUtils.updateButtonTextEnabledColorWithPartnerConfig( + context, button, footerButtonPartnerConfig.getButtonTextColorConfig()); + } + FooterButtonStyleUtils.updateButtonBackgroundWithPartnerConfig( + context, + button, + footerButtonPartnerConfig.getButtonBackgroundConfig(), + footerButtonPartnerConfig.getButtonDisableAlphaConfig(), + footerButtonPartnerConfig.getButtonDisableBackgroundConfig()); + } + FooterButtonStyleUtils.updateButtonRippleColorWithPartnerConfig( + context, + button, + applyDynamicColor, + footerButtonPartnerConfig.getButtonTextColorConfig(), + footerButtonPartnerConfig.getButtonRippleColorAlphaConfig()); + FooterButtonStyleUtils.updateButtonTextSizeWithPartnerConfig( + context, button, footerButtonPartnerConfig.getButtonTextSizeConfig()); + FooterButtonStyleUtils.updateButtonMinHeightWithPartnerConfig( + context, button, footerButtonPartnerConfig.getButtonMinHeightConfig()); + FooterButtonStyleUtils.updateButtonTypeFaceWithPartnerConfig( + context, + button, + footerButtonPartnerConfig.getButtonTextTypeFaceConfig(), + footerButtonPartnerConfig.getButtonTextStyleConfig()); + FooterButtonStyleUtils.updateButtonRadiusWithPartnerConfig( + context, button, footerButtonPartnerConfig.getButtonRadiusConfig()); + FooterButtonStyleUtils.updateButtonIconWithPartnerConfig( + context, button, footerButtonPartnerConfig.getButtonIconConfig(), isButtonIconAtEnd); + } + + static void updateButtonTextEnabledColorWithPartnerConfig( + Context context, Button button, PartnerConfig buttonEnableTextColorConfig) { + @ColorInt + int color = PartnerConfigHelper.get(context).getColor(context, buttonEnableTextColorConfig); + updateButtonTextEnabledColor(button, color); + } + + static void updateButtonTextEnabledColor(Button button, @ColorInt int textColor) { + if (textColor != Color.TRANSPARENT) { + button.setTextColor(ColorStateList.valueOf(textColor)); + } + } + + static void updateButtonTextDisableColor(Button button, ColorStateList disabledTextColor) { + // TODO : add disable footer button text color partner config + + // disable state will use the default disable state color + button.setTextColor(disabledTextColor); + } + + @TargetApi(VERSION_CODES.Q) + static void updateButtonBackgroundWithPartnerConfig( + Context context, + Button button, + PartnerConfig buttonBackgroundConfig, + PartnerConfig buttonDisableAlphaConfig, + PartnerConfig buttonDisableBackgroundConfig) { + Preconditions.checkArgument( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q, + "Update button background only support on sdk Q or higher"); + @ColorInt + int color = PartnerConfigHelper.get(context).getColor(context, buttonBackgroundConfig); + float disabledAlpha = + PartnerConfigHelper.get(context).getFraction(context, buttonDisableAlphaConfig, 0f); + @ColorInt + int disabledColor = + PartnerConfigHelper.get(context).getColor(context, buttonDisableBackgroundConfig); + + updateButtonBackgroundTintList(context, button, color, disabledAlpha, disabledColor); + } + + @TargetApi(VERSION_CODES.Q) + static void updateButtonBackgroundTintList( + Context context, + Button button, + @ColorInt int color, + float disabledAlpha, + @ColorInt int disabledColor) { + int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled}; + int[] ENABLED_STATE_SET = {}; + + if (color != Color.TRANSPARENT) { + if (disabledAlpha <= 0f) { + // if no partner resource, fallback to theme disable alpha + TypedArray a = context.obtainStyledAttributes(new int[] {android.R.attr.disabledAlpha}); + float alpha = a.getFloat(0, DEFAULT_DISABLED_ALPHA); + a.recycle(); + disabledAlpha = alpha; + } + if (disabledColor == Color.TRANSPARENT) { + // if no partner resource, fallback to button background color + disabledColor = color; + } + + // Set text color for ripple. + ColorStateList colorStateList = + new ColorStateList( + new int[][] {DISABLED_STATE_SET, ENABLED_STATE_SET}, + new int[] {convertRgbToArgb(disabledColor, disabledAlpha), color}); + + // b/129482013: When a LayerDrawable is mutated, a new clone of its children drawables are + // created, but without copying the state from the parent drawable. So even though the + // parent is getting the correct drawable state from the view, the children won't get those + // states until a state change happens. + // As a workaround, we mutate the drawable and forcibly set the state to empty, and then + // refresh the state so the children will have the updated states. + button.getBackground().mutate().setState(new int[0]); + button.refreshDrawableState(); + button.setBackgroundTintList(colorStateList); + } + } + + @TargetApi(VERSION_CODES.Q) + static void updateButtonRippleColorWithPartnerConfig( + Context context, + Button button, + boolean applyDynamicColor, + PartnerConfig buttonTextColorConfig, + PartnerConfig buttonRippleColorAlphaConfig) { + if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + + @ColorInt int textDefaultColor; + if (applyDynamicColor) { + // Get dynamic text color + textDefaultColor = button.getTextColors().getDefaultColor(); + } else { + // Get partner text color. + textDefaultColor = + PartnerConfigHelper.get(context).getColor(context, buttonTextColorConfig); + } + float alpha = + PartnerConfigHelper.get(context).getFraction(context, buttonRippleColorAlphaConfig); + updateButtonRippleColor(button, textDefaultColor, alpha); + } + } + + private static void updateButtonRippleColor( + Button button, @ColorInt int textColor, float rippleAlpha) { + // RippleDrawable is available after sdk 21. And because on lower sdk the RippleDrawable is + // unavailable. Since Stencil customization provider only works on Q+, there is no need to + // perform any customization for versions 21. + if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + RippleDrawable rippleDrawable = getRippleDrawable(button); + if (rippleDrawable == null) { + return; + } + + int[] pressedState = {android.R.attr.state_pressed}; + + // Set text color for ripple. + ColorStateList colorStateList = + new ColorStateList( + new int[][] {pressedState, StateSet.NOTHING}, + new int[] {convertRgbToArgb(textColor, rippleAlpha), Color.TRANSPARENT}); + rippleDrawable.setColor(colorStateList); + } + } + + static void updateButtonTextSizeWithPartnerConfig( + Context context, Button button, PartnerConfig buttonTextSizeConfig) { + float size = PartnerConfigHelper.get(context).getDimension(context, buttonTextSizeConfig); + if (size > 0) { + button.setTextSize(TypedValue.COMPLEX_UNIT_PX, size); + } + } + + static void updateButtonMinHeightWithPartnerConfig( + Context context, Button button, PartnerConfig buttonMinHeightConfig) { + if (PartnerConfigHelper.get(context).isPartnerConfigAvailable(buttonMinHeightConfig)) { + float size = PartnerConfigHelper.get(context).getDimension(context, buttonMinHeightConfig); + if (size > 0) { + button.setMinHeight((int) size); + } + } + } + + static void updateButtonTypeFaceWithPartnerConfig( + Context context, + Button button, + PartnerConfig buttonTextTypeFaceConfig, + PartnerConfig buttonTextStyleConfig) { + String fontFamilyName = + PartnerConfigHelper.get(context).getString(context, buttonTextTypeFaceConfig); + + int textStyleValue = Typeface.NORMAL; + if (PartnerConfigHelper.get(context).isPartnerConfigAvailable(buttonTextStyleConfig)) { + textStyleValue = + PartnerConfigHelper.get(context) + .getInteger(context, buttonTextStyleConfig, Typeface.NORMAL); + } + Typeface font = Typeface.create(fontFamilyName, textStyleValue); + if (font != null) { + button.setTypeface(font); + } + } + + static void updateButtonRadiusWithPartnerConfig( + Context context, Button button, PartnerConfig buttonRadiusConfig) { + if (Build.VERSION.SDK_INT >= VERSION_CODES.N) { + float radius = PartnerConfigHelper.get(context).getDimension(context, buttonRadiusConfig); + GradientDrawable gradientDrawable = getGradientDrawable(button); + if (gradientDrawable != null) { + gradientDrawable.setCornerRadius(radius); + } + } + } + + static void updateButtonIconWithPartnerConfig( + Context context, Button button, PartnerConfig buttonIconConfig, boolean isButtonIconAtEnd) { + if (button == null) { + return; + } + Drawable icon = null; + if (buttonIconConfig != null) { + icon = PartnerConfigHelper.get(context).getDrawable(context, buttonIconConfig); + } + setButtonIcon(button, icon, isButtonIconAtEnd); + } + + private static void setButtonIcon(Button button, Drawable icon, boolean isButtonIconAtEnd) { + if (button == null) { + return; + } + + if (icon != null) { + // TODO: restrict the icons to a reasonable size + int h = icon.getIntrinsicHeight(); + int w = icon.getIntrinsicWidth(); + icon.setBounds(0, 0, w, h); + } + + Drawable iconStart = null; + Drawable iconEnd = null; + if (isButtonIconAtEnd) { + iconEnd = icon; + } else { + iconStart = icon; + } + if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { + button.setCompoundDrawablesRelative(iconStart, null, iconEnd, null); + } else { + button.setCompoundDrawables(iconStart, null, iconEnd, null); + } + } + + static void updateButtonBackground(Button button, @ColorInt int color) { + button.getBackground().mutate().setColorFilter(color, Mode.SRC_ATOP); + } + + @VisibleForTesting + public static GradientDrawable getGradientDrawable(Button button) { + // RippleDrawable is available after sdk 21, InsetDrawable#getDrawable is available after + // sdk 19. So check the sdk is higher than sdk 21 and since Stencil customization provider only + // works on Q+, there is no need to perform any customization for versions 21. + if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + Drawable drawable = button.getBackground(); + if (drawable instanceof InsetDrawable) { + LayerDrawable layerDrawable = (LayerDrawable) ((InsetDrawable) drawable).getDrawable(); + return (GradientDrawable) layerDrawable.getDrawable(0); + } else if (drawable instanceof RippleDrawable) { + if (((RippleDrawable) drawable).getDrawable(0) instanceof GradientDrawable) { + return (GradientDrawable) ((RippleDrawable) drawable).getDrawable(0); + } + InsetDrawable insetDrawable = (InsetDrawable) ((RippleDrawable) drawable).getDrawable(0); + return (GradientDrawable) insetDrawable.getDrawable(); + } + } + return null; + } + + static RippleDrawable getRippleDrawable(Button button) { + // RippleDrawable is available after sdk 21. And because on lower sdk the RippleDrawable is + // unavailable. Since Stencil customization provider only works on Q+, there is no need to + // perform any customization for versions 21. + if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + Drawable drawable = button.getBackground(); + if (drawable instanceof InsetDrawable) { + return (RippleDrawable) ((InsetDrawable) drawable).getDrawable(); + } else if (drawable instanceof RippleDrawable) { + return (RippleDrawable) drawable; + } + } + return null; + } + + @ColorInt + private static int convertRgbToArgb(@ColorInt int color, float alpha) { + return Color.argb((int) (alpha * 255), Color.red(color), Color.green(color), Color.blue(color)); + } + + private FooterButtonStyleUtils() {} +} diff --git a/main/java/com/google/android/setupcompat/template/StatusBarMixin.java b/main/java/com/google/android/setupcompat/template/StatusBarMixin.java index 1bd6949..c0f1c45 100644 --- a/main/java/com/google/android/setupcompat/template/StatusBarMixin.java +++ b/main/java/com/google/android/setupcompat/template/StatusBarMixin.java @@ -25,13 +25,13 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Build.VERSION_CODES; -import androidx.annotation.AttrRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.View; import android.view.Window; import android.widget.LinearLayout; +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.setupcompat.PartnerCustomizationLayout; import com.google.android.setupcompat.R; import com.google.android.setupcompat.partnerconfig.PartnerConfig; @@ -112,10 +112,14 @@ public class StatusBarMixin implements Mixin { */ public void setStatusBarBackground(Drawable background) { if (partnerCustomizationLayout.shouldApplyPartnerResource()) { + // If full dynamic color enabled which means this activity is running outside of setup + // flow, the colors should refer to R.style.SudFullDynamicColorThemeGlifV3. + if (!partnerCustomizationLayout.useFullDynamicColor()) { Context context = partnerCustomizationLayout.getContext(); background = PartnerConfigHelper.get(context) .getDrawable(context, PartnerConfig.CONFIG_STATUS_BAR_BACKGROUND); + } } if (statusBarLayout == null) { diff --git a/main/java/com/google/android/setupcompat/template/SystemNavBarMixin.java b/main/java/com/google/android/setupcompat/template/SystemNavBarMixin.java index e055d28..32c708c 100644 --- a/main/java/com/google/android/setupcompat/template/SystemNavBarMixin.java +++ b/main/java/com/google/android/setupcompat/template/SystemNavBarMixin.java @@ -24,13 +24,13 @@ import android.graphics.Color; import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; +import android.util.AttributeSet; +import android.view.View; +import android.view.Window; import androidx.annotation.AttrRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import android.util.AttributeSet; -import android.view.View; -import android.view.Window; import com.google.android.setupcompat.PartnerCustomizationLayout; import com.google.android.setupcompat.R; import com.google.android.setupcompat.internal.TemplateLayout; @@ -47,6 +47,7 @@ public class SystemNavBarMixin implements Mixin { private final TemplateLayout templateLayout; @Nullable private final Window windowOfActivity; @VisibleForTesting final boolean applyPartnerResources; + @VisibleForTesting final boolean useFullDynamicColor; private int sucSystemNavBarBackgroundColor = 0; /** @@ -61,6 +62,10 @@ public class SystemNavBarMixin implements Mixin { this.applyPartnerResources = layout instanceof PartnerCustomizationLayout && ((PartnerCustomizationLayout) layout).shouldApplyPartnerResource(); + + this.useFullDynamicColor = + layout instanceof PartnerCustomizationLayout + && ((PartnerCustomizationLayout) layout).useFullDynamicColor(); } /** @@ -83,6 +88,19 @@ public class SystemNavBarMixin implements Mixin { setLightSystemNavBar( a.getBoolean( R.styleable.SucSystemNavBarMixin_sucLightSystemNavBar, isLightSystemNavBar())); + + // Support updating system navigation bar divider color from P. + if (VERSION.SDK_INT >= VERSION_CODES.P) { + // get fallback value from theme + int[] navBarDividerColorAttr = new int[] {android.R.attr.navigationBarDividerColor}; + TypedArray typedArray = + templateLayout.getContext().obtainStyledAttributes(navBarDividerColorAttr); + int defaultColor = typedArray.getColor(/* index= */ 0, /* defValue= */ 0); + int sucSystemNavBarDividerColor = + a.getColor(R.styleable.SucSystemNavBarMixin_sucSystemNavBarDividerColor, defaultColor); + setSystemNavBarDividerColor(sucSystemNavBarDividerColor); + typedArray.recycle(); + } a.recycle(); } } @@ -96,10 +114,14 @@ public class SystemNavBarMixin implements Mixin { public void setSystemNavBarBackground(int color) { if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && windowOfActivity != null) { if (applyPartnerResources) { - Context context = templateLayout.getContext(); - color = - PartnerConfigHelper.get(context) - .getColor(context, PartnerConfig.CONFIG_NAVIGATION_BAR_BG_COLOR); + // If full dynamic color enabled which means this activity is running outside of setup + // flow, the colors should refer to R.style.SudFullDynamicColorThemeGlifV3. + if (!useFullDynamicColor) { + Context context = templateLayout.getContext(); + color = + PartnerConfigHelper.get(context) + .getColor(context, PartnerConfig.CONFIG_NAVIGATION_BAR_BG_COLOR); + } } windowOfActivity.setNavigationBarColor(color); } @@ -120,6 +142,7 @@ public class SystemNavBarMixin implements Mixin { * * @param isLight true means compatible with light theme, otherwise compatible with dark theme */ + public void setLightSystemNavBar(boolean isLight) { if (Build.VERSION.SDK_INT >= VERSION_CODES.O && windowOfActivity != null) { if (applyPartnerResources) { @@ -158,6 +181,28 @@ public class SystemNavBarMixin implements Mixin { } /** + * Sets the divider color of navigation bar. The color will be overridden by partner resource if + * the activity is running in setup wizard flow. + * + * @param color the default divider color of navigation bar + */ + public void setSystemNavBarDividerColor(int color) { + if (Build.VERSION.SDK_INT >= VERSION_CODES.P && windowOfActivity != null) { + if (applyPartnerResources) { + Context context = templateLayout.getContext(); + // Do nothing if the old version partner provider did not contain the new config. + if (PartnerConfigHelper.get(context) + .isPartnerConfigAvailable(PartnerConfig.CONFIG_NAVIGATION_BAR_DIVIDER_COLOR)) { + color = + PartnerConfigHelper.get(context) + .getColor(context, PartnerConfig.CONFIG_NAVIGATION_BAR_DIVIDER_COLOR); + } + } + windowOfActivity.setNavigationBarDividerColor(color); + } + } + + /** * Hides the navigation bar, make the color of the status and navigation bars transparent, and * specify {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} flag so that the content is laid-out * behind the transparent status bar. This is commonly used with {@link diff --git a/main/java/com/google/android/setupcompat/util/BuildCompatUtils.java b/main/java/com/google/android/setupcompat/util/BuildCompatUtils.java new file mode 100644 index 0000000..ea54745 --- /dev/null +++ b/main/java/com/google/android/setupcompat/util/BuildCompatUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.setupcompat.util; + +import android.os.Build; + +/** + * An util class to check whether the current OS version is higher or equal to sdk version of + * device. + */ +public final class BuildCompatUtils { + + /** + * Implementation of BuildCompat.isAtLeast*() suitable for use in Setup + * + * <p>BuildCompat.isAtLeast*() can be changed by Android Release team, and once that is changed it + * may take weeks for that to propagate to stable/prerelease/experimental SDKs in Google3. Also it + * can be different in all these channels. This can cause random issues, especially with sidecars + * (i.e., the code running on R may not know that it runs on R). + * + * <p>This still should try using BuildCompat.isAtLeastR() as source of truth, but also checking + * for VERSION_SDK_INT and VERSION.CODENAME in case when BuildCompat implementation returned + * false. Note that both checks should be >= and not = to make sure that when Android version + * increases (i.e., from R to S), this does not stop working. + * + * <p>Supported configurations: + * + * <ul> + * <li>For current Android release: while new API is not finalized yet (CODENAME = "S", SDK_INT + * = 30|31) + * <li>For current Android release: when new API is finalized (CODENAME = "REL", SDK_INT = 31) + * <li>For next Android release (CODENAME = "T", SDK_INT = 30+) + * </ul> + * + * <p>Note that Build.VERSION_CODES.S cannot be used here until final SDK is available in all + * Google3 channels, because it is equal to Build.VERSION_CODES.CUR_DEVELOPMENT before API + * finalization. + * + * @return Whether the current OS version is higher or equal to S. + */ + public static boolean isAtLeastS() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return false; + } + return (Build.VERSION.CODENAME.equals("REL") && Build.VERSION.SDK_INT >= 31) + || (Build.VERSION.CODENAME.length() == 1 + && Build.VERSION.CODENAME.charAt(0) >= 'S' + && Build.VERSION.CODENAME.charAt(0) <= 'Z'); + } + + private BuildCompatUtils() {} +} diff --git a/main/java/com/google/android/setupcompat/util/Logger.java b/main/java/com/google/android/setupcompat/util/Logger.java new file mode 100644 index 0000000..3f8dfd1 --- /dev/null +++ b/main/java/com/google/android/setupcompat/util/Logger.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.setupcompat.util; + +import android.util.Log; + +/** + * Helper class that wraps {@link Log} to log messages to logcat. This class consolidate the log + * {@link #TAG} in both SetupCompat and SetupDesign library. + * + * <p>When logging verbose and debug logs, the logs should either be guarded by {@code if + * (logger.isV())}, or a constant if (DEBUG). That DEBUG constant should be false on any submitted + * code. + */ +public final class Logger { + + public static final String TAG = "SetupLibrary"; + + private final String prefix; + + public Logger(Class<?> cls) { + this(cls.getSimpleName()); + } + + public Logger(String prefix) { + this.prefix = "[" + prefix + "] "; + } + + public boolean isV() { + return Log.isLoggable(TAG, Log.VERBOSE); + } + + public boolean isD() { + return Log.isLoggable(TAG, Log.DEBUG); + } + + public boolean isI() { + return Log.isLoggable(TAG, Log.INFO); + } + + public void atVerbose(String message) { + if (isV()) { + Log.v(TAG, prefix.concat(message)); + } + } + + public void atDebug(String message) { + if (isD()) { + Log.d(TAG, prefix.concat(message)); + } + } + + public void atInfo(String message) { + if (isI()) { + Log.i(TAG, prefix.concat(message)); + } + } + + public void w(String message) { + Log.w(TAG, prefix.concat(message)); + } + + public void e(String message) { + Log.e(TAG, prefix.concat(message)); + } + + public void e(String message, Throwable throwable) { + Log.e(TAG, prefix.concat(message), throwable); + } +} diff --git a/main/java/com/google/android/setupcompat/util/SystemBarHelper.java b/main/java/com/google/android/setupcompat/util/SystemBarHelper.java index 75e5dd3..dd92501 100644 --- a/main/java/com/google/android/setupcompat/util/SystemBarHelper.java +++ b/main/java/com/google/android/setupcompat/util/SystemBarHelper.java @@ -24,13 +24,12 @@ import android.content.res.TypedArray; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Handler; -import androidx.annotation.RequiresPermission; -import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; +import androidx.annotation.RequiresPermission; /** * A helper class to manage the system navigation bar and status bar. This will add various @@ -44,7 +43,7 @@ import android.view.WindowManager; */ public final class SystemBarHelper { - private static final String TAG = "SystemBarHelper"; + private static final Logger LOG = new Logger("SystemBarHelper"); /** Needs to be equal to View.STATUS_BAR_DISABLE_BACK */ private static final int STATUS_BAR_DISABLE_BACK = 0x00400000; @@ -329,7 +328,7 @@ public final class SystemBarHelper { // If the decor view is not installed yet, try again in the next loop. handler.post(checkDecorViewRunnable); } else { - Log.w(TAG, "Cannot get decor view of window: " + window); + LOG.e("Cannot get decor view of window: " + window); } } } diff --git a/main/java/com/google/android/setupcompat/util/WizardManagerHelper.java b/main/java/com/google/android/setupcompat/util/WizardManagerHelper.java index bfe1dbb..79976bc 100644 --- a/main/java/com/google/android/setupcompat/util/WizardManagerHelper.java +++ b/main/java/com/google/android/setupcompat/util/WizardManagerHelper.java @@ -34,7 +34,7 @@ import java.util.Arrays; */ public final class WizardManagerHelper { - private static final String ACTION_NEXT = "com.android.wizard.NEXT"; + @VisibleForTesting public static final String ACTION_NEXT = "com.android.wizard.NEXT"; // EXTRA_SCRIPT_URI and EXTRA_ACTION_ID are used in setup wizard in versions before M and are // kept for backwards compatibility. @@ -43,10 +43,27 @@ public final class WizardManagerHelper { @VisibleForTesting static final String EXTRA_WIZARD_BUNDLE = "wizardBundle"; private static final String EXTRA_RESULT_CODE = "com.android.setupwizard.ResultCode"; - @VisibleForTesting public static final String EXTRA_IS_FIRST_RUN = "firstRun"; - @VisibleForTesting static final String EXTRA_IS_DEFERRED_SETUP = "deferredSetup"; - @VisibleForTesting static final String EXTRA_IS_PRE_DEFERRED_SETUP = "preDeferredSetup"; - @VisibleForTesting public static final String EXTRA_IS_SETUP_FLOW = "isSetupFlow"; + + /** Extra for notifying an Activity that it is inside the first SetupWizard flow or not. */ + public static final String EXTRA_IS_FIRST_RUN = "firstRun"; + + /** Extra for notifying an Activity that it is inside the Deferred SetupWizard flow or not. */ + public static final String EXTRA_IS_DEFERRED_SETUP = "deferredSetup"; + + /** Extra for notifying an Activity that it is inside the "Pre-Deferred Setup" flow. */ + public static final String EXTRA_IS_PRE_DEFERRED_SETUP = "preDeferredSetup"; + + /** Extra for notifying an Activity that it is inside the "Portal Setup" flow. */ + public static final String EXTRA_IS_PORTAL_SETUP = "portalSetup"; + + /** + * Extra for notifying an Activity that it is inside the any setup flow. + * + * <p>Apps that target API levels below {@link android.os.Build.VERSION_CODES#Q} is able to + * determine whether Activity is inside the any setup flow by one of {@link #EXTRA_IS_FIRST_RUN}, + * {@link #EXTRA_IS_DEFERRED_SETUP}, and {@link #EXTRA_IS_PRE_DEFERRED_SETUP} is true. + */ + public static final String EXTRA_IS_SETUP_FLOW = "isSetupFlow"; public static final String EXTRA_THEME = "theme"; public static final String EXTRA_USE_IMMERSIVE_MODE = "useImmersiveMode"; @@ -104,6 +121,7 @@ public final class WizardManagerHelper { EXTRA_IS_FIRST_RUN, EXTRA_IS_DEFERRED_SETUP, EXTRA_IS_PRE_DEFERRED_SETUP, + EXTRA_IS_PORTAL_SETUP, EXTRA_IS_SETUP_FLOW)) { dstIntent.putExtra(key, srcIntent.getBooleanExtra(key, false)); } diff --git a/main/res/values/attrs.xml b/main/res/values/attrs.xml index 1a5342c..07f87ed 100644 --- a/main/res/values/attrs.xml +++ b/main/res/values/attrs.xml @@ -32,6 +32,7 @@ This attribute will be ignored and use partner resource when inside setup wizard flow. The default value is true. --> <attr name="sucUsePartnerResource" format="boolean" /> + <attr name="sucFullDynamicColor" format="boolean" /> </declare-styleable> <!-- Status bar attributes; only takes effect on M or above --> @@ -52,6 +53,10 @@ "android:windowTranslucentNavigation" should be set to false. --> <attr name="sucSystemNavBarBackgroundColor" format="color" /> <attr name="sucLightSystemNavBar" format="boolean" /> + <!-- The color for the system navigation bar divider. For this to take effect, + "android:windowDrawsSystemBarBackgrounds" should be set to true and + "android:windowTranslucentNavigation" should be set to false. --> + <attr name="sucSystemNavBarDividerColor" format="color" /> </declare-styleable> <!-- FooterButton attributes --> @@ -70,6 +75,8 @@ <enum name="skip" value="7" /> <enum name="stop" value="8" /> </attr> + <attr name="sucFooterButtonPaddingStart" format="dimension" /> + <attr name="sucFooterButtonPaddingEnd" format="dimension" /> </declare-styleable> <!-- Button of footer attributes --> @@ -87,11 +94,18 @@ <attr name="sucFooterBarButtonColorControlHighlight" format="color" /> <attr name="sucFooterBarButtonColorControlHighlightRipple" format="color" /> <attr name="sucFooterBarPaddingVertical" format="dimension" /> + <attr name="sucFooterBarPaddingStart" format="dimension" /> + <attr name="sucFooterBarPaddingEnd" format="dimension" /> + <attr name="sucFooterBarMinHeight" format="dimension" /> </declare-styleable> <declare-styleable name="SucHeaderMixin"> <attr name="sucHeaderText" format="string" localization="suggested" /> <attr name="sucHeaderTextColor" format="reference|color" /> + <attr name="sucGlifHeaderMarginTop" format="dimension" /> + <attr name="sucGlifHeaderMarginBottom" format="dimension" /> + <attr name="sucGlifIconMarginTop" format="dimension" /> + <attr name="sucHeaderContainerMarginBottom" format="dimension" /> </declare-styleable> </resources> diff --git a/main/res/values/dimens.xml b/main/res/values/dimens.xml deleted file mode 100644 index ef3d98d..0000000 --- a/main/res/values/dimens.xml +++ /dev/null @@ -1,29 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2018 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<resources> - - <!-- TODO: Remove default values from setup compat, use from theme --> - <!-- Footer button bar style padding attributes--> - <dimen name="suc_customization_footer_min_height">72dp</dimen> - <dimen name="suc_customization_button_margin_start">8dp</dimen> - <dimen name="suc_customization_button_margin_end">20dp</dimen> - - <!-- Footer button style padding attributes --> - <dimen name="suc_customization_button_padding">16dp</dimen> - -</resources> diff --git a/main/res/values/styles.xml b/main/res/values/styles.xml index 48fcddf..6474426 100644 --- a/main/res/values/styles.xml +++ b/main/res/values/styles.xml @@ -27,14 +27,14 @@ <item name="android:clipChildren">false</item> <item name="android:clipToPadding">false</item> <item name="android:gravity">center_vertical</item> - <item name="android:minHeight">@dimen/suc_customization_footer_min_height</item> + <item name="android:minHeight">?attr/sucFooterBarMinHeight</item> <item name="android:orientation">horizontal</item> <item name="android:paddingTop">?attr/sucFooterBarPaddingVertical</item> <item name="android:paddingBottom">?attr/sucFooterBarPaddingVertical</item> - <item name="android:paddingEnd" tools:ignore="NewApi">@dimen/suc_customization_button_margin_end</item> - <item name="android:paddingLeft">@dimen/suc_customization_button_margin_start</item> - <item name="android:paddingRight">@dimen/suc_customization_button_margin_end</item> - <item name="android:paddingStart" tools:ignore="NewApi">@dimen/suc_customization_button_margin_start</item> + <item name="android:paddingEnd" tools:ignore="NewApi">?attr/sucFooterBarPaddingEnd</item> + <item name="android:paddingLeft">?attr/sucFooterBarPaddingStart</item> + <item name="android:paddingRight">?attr/sucFooterBarPaddingEnd</item> + <item name="android:paddingStart" tools:ignore="NewApi">?attr/sucFooterBarPaddingStart</item> </style> <style name="SucPartnerCustomizationButton.Primary" parent="android:Widget.Material.Button.Colored"> @@ -46,8 +46,10 @@ <!-- Values used in styles --> <item name="android:fontFamily">?attr/sucFooterBarButtonFontFamily</item> - <item name="android:paddingLeft">@dimen/suc_customization_button_padding</item> - <item name="android:paddingRight">@dimen/suc_customization_button_padding</item> + <item name="android:paddingLeft">?attr/sucFooterButtonPaddingStart</item> + <item name="android:paddingStart" tools:ignore="NewApi">?attr/sucFooterButtonPaddingStart</item> + <item name="android:paddingRight">?attr/sucFooterButtonPaddingEnd</item> + <item name="android:paddingEnd" tools:ignore="NewApi">?attr/sucFooterButtonPaddingEnd</item> <item name="android:textAllCaps">?attr/sucFooterBarButtonAllCaps</item> <item name="android:stateListAnimator">@null</item> @@ -65,8 +67,10 @@ <!-- Values used in styles --> <item name="android:fontFamily">?attr/sucFooterBarButtonFontFamily</item> <item name="android:minWidth">0dp</item> - <item name="android:paddingLeft">@dimen/suc_customization_button_padding</item> - <item name="android:paddingRight">@dimen/suc_customization_button_padding</item> + <item name="android:paddingLeft">?attr/sucFooterButtonPaddingStart</item> + <item name="android:paddingStart" tools:ignore="NewApi">?attr/sucFooterButtonPaddingStart</item> + <item name="android:paddingRight">?attr/sucFooterButtonPaddingEnd</item> + <item name="android:paddingEnd" tools:ignore="NewApi">?attr/sucFooterButtonPaddingEnd</item> <item name="android:textAllCaps">?attr/sucFooterBarButtonAllCaps</item> <!-- Values used in themes --> diff --git a/partnerconfig/AndroidManifest.xml b/partnerconfig/AndroidManifest.xml index c95a4dd..43e2041 100644 --- a/partnerconfig/AndroidManifest.xml +++ b/partnerconfig/AndroidManifest.xml @@ -16,9 +16,16 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" package="com.google.android.setupcompat.partnerconfig"> - <uses-sdk - android:minSdkVersion="14" - android:targetSdkVersion="28" /> + <uses-sdk /> + + <!-- after SDK 30, package need to declare its visible packages. --> + <queries tools:node="merge"> + <intent> + <action android:name="com.android.setupwizard.action.PARTNER_CUSTOMIZATION" /> + </intent> + <provider android:authorities="com.google.android.setupwizard.partner" /> + </queries> </manifest> diff --git a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfig.java b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfig.java index 56256c9..280ab81 100644 --- a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfig.java +++ b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfig.java @@ -30,9 +30,16 @@ public enum PartnerConfig { // Navigation bar background color CONFIG_NAVIGATION_BAR_BG_COLOR(PartnerConfigKey.KEY_NAVIGATION_BAR_BG_COLOR, ResourceType.COLOR), + // Navigation bar divider color + CONFIG_NAVIGATION_BAR_DIVIDER_COLOR( + PartnerConfigKey.KEY_NAVIGATION_BAR_DIVIDER_COLOR, ResourceType.COLOR), + // Background color of the footer bar. CONFIG_FOOTER_BAR_BG_COLOR(PartnerConfigKey.KEY_FOOTER_BAR_BG_COLOR, ResourceType.COLOR), + // The min height of the footer buttons + CONFIG_FOOTER_BAR_MIN_HEIGHT(PartnerConfigKey.KEY_FOOTER_BAR_MIN_HEIGHT, ResourceType.DIMENSION), + // The same as "windowLightNavigationBar". If set true, the navigation bar icons will be drawn // such that it is compatible with a light navigation bar background. CONFIG_LIGHT_NAVIGATION_BAR(PartnerConfigKey.KEY_LIGHT_NAVIGATION_BAR, ResourceType.BOOL), @@ -89,10 +96,18 @@ public enum PartnerConfig { CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA( PartnerConfigKey.KEY_FOOTER_BUTTON_RIPPLE_ALPHA, ResourceType.FRACTION), - // Text size of the primary footer button + // Text size of the footer buttons CONFIG_FOOTER_BUTTON_TEXT_SIZE( PartnerConfigKey.KEY_FOOTER_BUTTON_TEXT_SIZE, ResourceType.DIMENSION), + // The text style of footer buttons {0 = NORMAL}, {1 = BOLD}, {2 = ITALIC}, {3 = BOLD_ITALIC} + CONFIG_FOOTER_BUTTON_TEXT_STYLE( + PartnerConfigKey.KEY_FOOTER_BUTTON_TEXT_STYLE, ResourceType.INTEGER), + + // The min height of the footer buttons + CONFIG_FOOTER_BUTTON_MIN_HEIGHT( + PartnerConfigKey.KEY_FOOTER_BUTTON_MIN_HEIGHT, ResourceType.DIMENSION), + // Disabled background alpha of the footer buttons CONFIG_FOOTER_BUTTON_DISABLED_ALPHA( PartnerConfigKey.KEY_FOOTER_BUTTON_DISABLED_ALPHA, ResourceType.FRACTION), @@ -120,6 +135,12 @@ public enum PartnerConfig { // Background color of layout CONFIG_LAYOUT_BACKGROUND_COLOR(PartnerConfigKey.KEY_LAYOUT_BACKGROUND_COLOR, ResourceType.COLOR), + // Margin start of the layout + CONFIG_LAYOUT_MARGIN_START(PartnerConfigKey.KEY_LAYOUT_MARGIN_START, ResourceType.DIMENSION), + + // Margin end of the layout + CONFIG_LAYOUT_MARGIN_END(PartnerConfigKey.KEY_LAYOUT_MARGIN_END, ResourceType.DIMENSION), + // Text color of the header CONFIG_HEADER_TEXT_COLOR(PartnerConfigKey.KEY_HEADER_TEXT_COLOR, ResourceType.COLOR), @@ -129,13 +150,50 @@ public enum PartnerConfig { // Font family of the header CONFIG_HEADER_FONT_FAMILY(PartnerConfigKey.KEY_HEADER_FONT_FAMILY, ResourceType.STRING), + // Margin top of the header text + CONFIG_HEADER_TEXT_MARGIN_TOP( + PartnerConfigKey.KEY_HEADER_TEXT_MARGIN_TOP, ResourceType.DIMENSION), + + // Margin bottom of the header text + CONFIG_HEADER_TEXT_MARGIN_BOTTOM( + PartnerConfigKey.KEY_HEADER_TEXT_MARGIN_BOTTOM, ResourceType.DIMENSION), + // Gravity of the header, icon and description CONFIG_LAYOUT_GRAVITY(PartnerConfigKey.KEY_LAYOUT_GRAVITY, ResourceType.STRING), + // Margin top of the icon + CONFIG_ICON_MARGIN_TOP(PartnerConfigKey.KEY_ICON_MARGIN_TOP, ResourceType.DIMENSION), + + // Size of the icon + CONFIG_ICON_SIZE(PartnerConfigKey.KEY_ICON_SIZE, ResourceType.DIMENSION), + // Background color of the header area CONFIG_HEADER_AREA_BACKGROUND_COLOR( PartnerConfigKey.KEY_HEADER_AREA_BACKGROUND_COLOR, ResourceType.COLOR), + // Margin bottom of the header container + CONFIG_HEADER_CONTAINER_MARGIN_BOTTOM( + PartnerConfigKey.KEY_HEADER_CONTAINER_MARGIN_BOTTOM, ResourceType.DIMENSION), + + // Auto text size enabled status + CONFIG_HEADER_AUTO_SIZE_ENABLED(PartnerConfigKey.KEY_HEADER_AUTO_SIZE_ENABLED, ResourceType.BOOL), + + // Max text size of header when auto size enabled. Ignored if auto size is false. + CONFIG_HEADER_AUTO_SIZE_MAX_TEXT_SIZE( + PartnerConfigKey.KEY_HEADER_AUTO_SIZE_MAX_TEXT_SIZE, ResourceType.DIMENSION), + + // Min text size of header when auto size enabled. Ignored if auto size is false. + CONFIG_HEADER_AUTO_SIZE_MIN_TEXT_SIZE( + PartnerConfigKey.KEY_HEADER_AUTO_SIZE_MIN_TEXT_SIZE, ResourceType.DIMENSION), + + // The max lines of the max text size when auto size enabled. Ignored if auto size is false. + CONFIG_HEADER_AUTO_SIZE_MAX_LINE_OF_MAX_SIZE( + PartnerConfigKey.KEY_HEADER_AUTO_SIZE_MAX_LINE_OF_MAX_SIZE, ResourceType.INTEGER), + + // Extra line spacing of header when auto size enabled. Ignored if auto size is false. + CONFIG_HEADER_AUTO_SIZE_LINE_SPACING_EXTRA( + PartnerConfigKey.KEY_HEADER_AUTO_SIZE_LINE_SPACING_EXTRA, ResourceType.DIMENSION), + // Text size of the description CONFIG_DESCRIPTION_TEXT_SIZE(PartnerConfigKey.KEY_DESCRIPTION_TEXT_SIZE, ResourceType.DIMENSION), @@ -149,6 +207,14 @@ public enum PartnerConfig { // Font family of the description CONFIG_DESCRIPTION_FONT_FAMILY(PartnerConfigKey.KEY_DESCRIPTION_FONT_FAMILY, ResourceType.STRING), + // Margin top of the description text + CONFIG_DESCRIPTION_TEXT_MARGIN_TOP( + PartnerConfigKey.KEY_DESCRIPTION_TEXT_MARGIN_TOP, ResourceType.DIMENSION), + + // Margin bottom of the description text + CONFIG_DESCRIPTION_TEXT_MARGIN_BOTTOM( + PartnerConfigKey.KEY_DESCRIPTION_TEXT_MARGIN_BOTTOM, ResourceType.DIMENSION), + // Text size of the body content text CONFIG_CONTENT_TEXT_SIZE(PartnerConfigKey.KEY_CONTENT_TEXT_SIZE, ResourceType.DIMENSION), @@ -164,6 +230,75 @@ public enum PartnerConfig { // Gravity of the body content text CONFIG_CONTENT_LAYOUT_GRAVITY(PartnerConfigKey.KEY_CONTENT_LAYOUT_GRAVITY, ResourceType.STRING), + // The padding top of the content + CONFIG_CONTENT_PADDING_TOP(PartnerConfigKey.KEY_CONTENT_PADDING_TOP, ResourceType.DIMENSION), + + // The text size of the content info. + CONFIG_CONTENT_INFO_TEXT_SIZE( + PartnerConfigKey.KEY_CONTENT_INFO_TEXT_SIZE, ResourceType.DIMENSION), + + // The font family of the content info. + CONFIG_CONTENT_INFO_FONT_FAMILY( + PartnerConfigKey.KEY_CONTENT_INFO_FONT_FAMILY, ResourceType.STRING), + + // The text line spacing extra of the content info. + CONFIG_CONTENT_INFO_LINE_SPACING_EXTRA( + PartnerConfigKey.KEY_CONTENT_INFO_LINE_SPACING_EXTRA, ResourceType.DIMENSION), + + // The icon size of the content info. + CONFIG_CONTENT_INFO_ICON_SIZE( + PartnerConfigKey.KEY_CONTENT_INFO_ICON_SIZE, ResourceType.DIMENSION), + + // The icon margin end of the content info. + CONFIG_CONTENT_INFO_ICON_MARGIN_END( + PartnerConfigKey.KEY_CONTENT_INFO_ICON_MARGIN_END, ResourceType.DIMENSION), + + // The padding top of the content info. + CONFIG_CONTENT_INFO_PADDING_TOP( + PartnerConfigKey.KEY_CONTENT_INFO_PADDING_TOP, ResourceType.DIMENSION), + + // The padding bottom of the content info. + CONFIG_CONTENT_INFO_PADDING_BOTTOM( + PartnerConfigKey.KEY_CONTENT_INFO_PADDING_BOTTOM, ResourceType.DIMENSION), + + // The title text size of list items. + CONFIG_ITEMS_TITLE_TEXT_SIZE(PartnerConfigKey.KEY_ITEMS_TITLE_TEXT_SIZE, ResourceType.DIMENSION), + + // The summary text size of list items. + CONFIG_ITEMS_SUMMARY_TEXT_SIZE( + PartnerConfigKey.KEY_ITEMS_SUMMARY_TEXT_SIZE, ResourceType.DIMENSION), + + // The summary margin top of list items. + CONFIG_ITEMS_SUMMARY_MARGIN_TOP( + PartnerConfigKey.KEY_ITEMS_SUMMARY_MARGIN_TOP, ResourceType.DIMENSION), + + // The title font family of list items. + CONFIG_ITEMS_TITLE_FONT_FAMILY(PartnerConfigKey.KEY_ITEMS_TITLE_FONT_FAMILY, ResourceType.STRING), + + // The summary font family of list items. + CONFIG_ITEMS_SUMMARY_FONT_FAMILY( + PartnerConfigKey.KEY_ITEMS_SUMMARY_FONT_FAMILY, ResourceType.STRING), + + // The padding top of list items. + CONFIG_ITEMS_PADDING_TOP(PartnerConfigKey.KEY_ITEMS_PADDING_TOP, ResourceType.DIMENSION), + + // The padding bottom of list items. + CONFIG_ITEMS_PADDING_BOTTOM(PartnerConfigKey.KEY_ITEMS_PADDING_BOTTOM, ResourceType.DIMENSION), + + // The minimum height of list items. + CONFIG_ITEMS_MIN_HEIGHT(PartnerConfigKey.KEY_ITEMS_MIN_HEIGHT, ResourceType.DIMENSION), + + // The divider of list items are showing on the pages. + CONFIG_ITEMS_DIVIDER_SHOWN(PartnerConfigKey.KEY_ITEMS_DIVIDER_SHOWN, ResourceType.BOOL), + + // The intrinsic width of the card view for foldabe/tablet. + CONFIG_CARD_VIEW_INTRINSIC_WIDTH( + PartnerConfigKey.KEY_CARD_VIEW_INTRINSIC_WIDTH, ResourceType.DIMENSION), + + // The intrinsic height of the card view for foldabe/tablet. + CONFIG_CARD_VIEW_INTRINSIC_HEIGHT( + PartnerConfigKey.KEY_CARD_VIEW_INTRINSIC_HEIGHT, ResourceType.DIMENSION), + // The animation of loading screen used in those activities which is non of below type. CONFIG_PROGRESS_ILLUSTRATION_DEFAULT( PartnerConfigKey.KEY_PROGRESS_ILLUSTRATION_DEFAULT, ResourceType.ILLUSTRATION), @@ -184,9 +319,104 @@ public enum PartnerConfig { CONFIG_PROGRESS_ILLUSTRATION_UPDATE( PartnerConfigKey.KEY_PROGRESS_ILLUSTRATION_UPDATE, ResourceType.ILLUSTRATION), + // The animation of loading screen used in those activities which is finishing setup. + // For example:com.google.android.setupwizard.FINAL_HOLD + CONFIG_PROGRESS_ILLUSTRATION_FINAL_HOLD( + PartnerConfigKey.KEY_PROGRESS_ILLUSTRATION_FINAL_HOLD, ResourceType.ILLUSTRATION), + + // The animation of loading screen to define how long showing on the pages. CONFIG_PROGRESS_ILLUSTRATION_DISPLAY_MINIMUM_MS( - PartnerConfigKey.KEY_PROGRESS_ILLUSTRATION_DISPLAY_MINIMUM_MS, ResourceType.INTEGER); + PartnerConfigKey.KEY_PROGRESS_ILLUSTRATION_DISPLAY_MINIMUM_MS, ResourceType.INTEGER), + + // The animation for S+ devices used in those screens waiting for non of below type. + CONFIG_LOADING_LOTTIE_DEFAULT( + PartnerConfigKey.KEY_LOADING_LOTTIE_DEFAULT, ResourceType.ILLUSTRATION), + + // The animation for S+ devices used in those screens which is processing account info or related + // functions. + // For example:com.google.android.setupwizard.LOAD_ADD_ACCOUNT_INTENT + CONFIG_LOADING_LOTTIE_ACCOUNT( + PartnerConfigKey.KEY_LOADING_LOTTIE_ACCOUNT, ResourceType.ILLUSTRATION), + + // The animation for S+ devices used in those screens which is processing data connection. + // For example:com.android.setupwizard.CAPTIVE_PORTAL + CONFIG_LOADING_LOTTIE_CONNECTION( + PartnerConfigKey.KEY_LOADING_LOTTIE_CONNECTION, ResourceType.ILLUSTRATION), + + // The animation for S+ devices used in those screens which is updating devices. + // For example:com.google.android.setupwizard.COMPAT_EARLY_UPDATE + CONFIG_LOADING_LOTTIE_UPDATE( + PartnerConfigKey.KEY_LOADING_LOTTIE_UPDATE, ResourceType.ILLUSTRATION), + + // The animation for S+ devices used in those screens which is updating devices. + // For example:com.google.android.setupwizard.COMPAT_EARLY_UPDATE + CONFIG_LOADING_LOTTIE_FINAL_HOLD( + PartnerConfigKey.KEY_LOADING_LOTTIE_FINAL_HOLD, ResourceType.ILLUSTRATION), + + // The transition type to decide the transition between activities or fragments. + CONFIG_TRANSITION_TYPE(PartnerConfigKey.KEY_TRANSITION_TYPE, ResourceType.INTEGER), + + // The list of keypath and color map, applied to default animation when light theme. + CONFIG_LOTTIE_LIGHT_THEME_CUSTOMIZATION_DEFAULT( + PartnerConfigKey.KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_DEFAULT, ResourceType.STRING_ARRAY), + + // The list of keypath and color map, applied to account animation when light theme. + CONFIG_LOTTIE_LIGHT_THEME_CUSTOMIZATION_ACCOUNT( + PartnerConfigKey.KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_ACCOUNT, ResourceType.STRING_ARRAY), + + // The list of keypath and color map, applied to connection animation when light theme. + CONFIG_LOTTIE_LIGHT_THEME_CUSTOMIZATION_CONNECTION( + PartnerConfigKey.KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_CONNECTION, ResourceType.STRING_ARRAY), + + // The list of keypath and color map, applied to update animation when light theme. + CONFIG_LOTTIE_LIGHT_THEME_CUSTOMIZATION_UPDATE( + PartnerConfigKey.KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_UPDATE, ResourceType.STRING_ARRAY), + + // The list of keypath and color map, applied to update animation when light theme. + CONFIG_LOTTIE_LIGHT_THEME_CUSTOMIZATION_FINAL_HOLD( + PartnerConfigKey.KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_FINAL_HOLD, ResourceType.STRING_ARRAY), + + // The list of keypath and color map, applied to default animation when dark theme. + CONFIG_LOTTIE_DARK_THEME_CUSTOMIZATION_DEFAULT( + PartnerConfigKey.KEY_LOADING_DARK_THEME_CUSTOMIZATION_DEFAULT, ResourceType.STRING_ARRAY), + + // The list of keypath and color map, applied to account animation when dark theme. + CONFIG_LOTTIE_DARK_THEME_CUSTOMIZATION_ACCOUNT( + PartnerConfigKey.KEY_LOADING_DARK_THEME_CUSTOMIZATION_ACCOUNT, ResourceType.STRING_ARRAY), + + // The list of keypath and color map, applied to connection animation when dark theme. + CONFIG_LOTTIE_DARK_THEME_CUSTOMIZATION_CONNECTION( + PartnerConfigKey.KEY_LOADING_DARK_THEME_CUSTOMIZATION_CONNECTION, ResourceType.STRING_ARRAY), + + // The list of keypath and color map, applied to update animation when dark theme. + CONFIG_LOTTIE_DARK_THEME_CUSTOMIZATION_UPDATE( + PartnerConfigKey.KEY_LOADING_DARK_THEME_CUSTOMIZATION_UPDATE, ResourceType.STRING_ARRAY), + + // The list of keypath and color map, applied to final hold animation when dark theme. + CONFIG_LOTTIE_DARK_THEME_CUSTOMIZATION_FINAL_HOLD( + PartnerConfigKey.KEY_LOADING_DARK_THEME_CUSTOMIZATION_FINAL_HOLD, ResourceType.STRING_ARRAY), + + // The padding top of the content frame of loading layout. + CONFIG_LOADING_LAYOUT_PADDING_TOP( + PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_TOP, ResourceType.DIMENSION), + + // The padding start of the content frame of loading layout. + CONFIG_LOADING_LAYOUT_PADDING_START( + PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_START, ResourceType.DIMENSION), + + // The padding end of the content frame of loading layout. + CONFIG_LOADING_LAYOUT_PADDING_END( + PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_END, ResourceType.DIMENSION), + + // The padding bottom of the content frame of loading layout. + CONFIG_LOADING_LAYOUT_PADDING_BOTTOM( + PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_BOTTOM, ResourceType.DIMENSION), + + // The height of the header of the loading layout. + CONFIG_LOADING_LAYOUT_HEADER_HEIGHT( + PartnerConfigKey.KEY_LOADING_LAYOUT_HEADER_HEIGHT, ResourceType.DIMENSION); + /** Resource type of the partner resources type. */ public enum ResourceType { INTEGER, BOOL, @@ -195,7 +425,8 @@ public enum PartnerConfig { STRING, DIMENSION, FRACTION, - ILLUSTRATION; + ILLUSTRATION, + STRING_ARRAY } private final String resourceName; diff --git a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigHelper.java b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigHelper.java index 7b9f65b..2ca8876 100644 --- a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigHelper.java +++ b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigHelper.java @@ -1,11 +1,11 @@ /* - * Copyright 2018 The Android Open Source Project + * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -18,22 +18,27 @@ package com.google.android.setupcompat.partnerconfig; import android.content.ContentResolver; import android.content.Context; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; +import android.database.ContentObserver; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.Bundle; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.TypedValue; import com.google.android.setupcompat.partnerconfig.PartnerConfig.ResourceType; +import java.util.ArrayList; +import java.util.Collections; import java.util.EnumMap; +import java.util.List; /** The helper reads and caches the partner configurations from SUW. */ public class PartnerConfigHelper { @@ -47,6 +52,22 @@ public class PartnerConfigHelper { @VisibleForTesting public static final String KEY_FALLBACK_CONFIG = "fallbackConfig"; + @VisibleForTesting + public static final String IS_SUW_DAY_NIGHT_ENABLED_METHOD = "isSuwDayNightEnabled"; + + @VisibleForTesting + public static final String IS_EXTENDED_PARTNER_CONFIG_ENABLED_METHOD = + "isExtendedPartnerConfigEnabled"; + + @VisibleForTesting + public static final String IS_DYNAMIC_COLOR_ENABLED_METHOD = "isDynamicColorEnabled"; + + @VisibleForTesting static Bundle suwDayNightEnabledBundle = null; + + @VisibleForTesting public static Bundle applyExtendedPartnerConfigBundle = null; + + @VisibleForTesting public static Bundle applyDynamicColorBundle = null; + private static PartnerConfigHelper instance = null; @VisibleForTesting Bundle resultBundle = null; @@ -54,15 +75,44 @@ public class PartnerConfigHelper { @VisibleForTesting final EnumMap<PartnerConfig, Object> partnerResourceCache = new EnumMap<>(PartnerConfig.class); + private static ContentObserver contentObserver; + + private static int savedConfigUiMode; + + private static int savedOrientation = Configuration.ORIENTATION_PORTRAIT; + public static synchronized PartnerConfigHelper get(@NonNull Context context) { - if (instance == null) { + if (!isValidInstance(context)) { instance = new PartnerConfigHelper(context); } return instance; } + private static boolean isValidInstance(@NonNull Context context) { + Configuration currentConfig = context.getResources().getConfiguration(); + if (instance == null) { + savedConfigUiMode = currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK; + savedOrientation = currentConfig.orientation; + return false; + } else { + if (isSetupWizardDayNightEnabled(context) + && (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) != savedConfigUiMode) { + savedConfigUiMode = currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK; + resetInstance(); + return false; + } else if (currentConfig.orientation != savedOrientation) { + savedOrientation = currentConfig.orientation; + resetInstance(); + return false; + } + } + return true; + } + private PartnerConfigHelper(Context context) { getPartnerConfigBundle(context); + + registerContentObserver(context); } /** @@ -75,12 +125,22 @@ public class PartnerConfigHelper { } /** + * Returns whether the given {@code resourceConfig} are available. This is true if setup wizard's + * content provider returns us a non-empty bundle, and this result bundle includes the given + * {@code resourceConfig} even if all the values are default, and none are customized by the + * overlay APK. + */ + public boolean isPartnerConfigAvailable(PartnerConfig resourceConfig) { + return isAvailable() && resultBundle.containsKey(resourceConfig.getResourceName()); + } + + /** * Returns the color of given {@code resourceConfig}, or 0 if the given {@code resourceConfig} is * not found. If the {@code ResourceType} of the given {@code resourceConfig} is not color, * IllegalArgumentException will be thrown. * * @param context The context of client activity - * @param resourceConfig The {@code PartnerConfig} of target resource + * @param resourceConfig The {@link PartnerConfig} of target resource */ @ColorInt public int getColor(@NonNull Context context, PartnerConfig resourceConfig) { @@ -99,6 +159,13 @@ public class PartnerConfigHelper { Resources resource = resourceEntry.getResources(); int resId = resourceEntry.getResourceId(); + // for @null + TypedValue outValue = new TypedValue(); + resource.getValue(resId, outValue, true); + if (outValue.type == TypedValue.TYPE_REFERENCE && outValue.data == 0) { + return result; + } + if (Build.VERSION.SDK_INT >= VERSION_CODES.M) { result = resource.getColor(resId, null); } else { @@ -189,6 +256,38 @@ public class PartnerConfigHelper { } /** + * Returns the string array of the given {@code resourceConfig}, or {@code null} if the given + * {@code resourceConfig} is not found. If the {@code ResourceType} of the given {@code + * resourceConfig} is not string, IllegalArgumentException will be thrown. + * + * @param context The context of client activity + * @param resourceConfig The {@code PartnerConfig} of target resource + */ + @NonNull + public List<String> getStringArray(@NonNull Context context, PartnerConfig resourceConfig) { + if (resourceConfig.getResourceType() != ResourceType.STRING_ARRAY) { + throw new IllegalArgumentException("Not a string array resource"); + } + + String[] result; + List<String> listResult = new ArrayList<>(); + + try { + ResourceEntry resourceEntry = + getResourceEntryFromKey(context, resourceConfig.getResourceName()); + Resources resource = resourceEntry.getResources(); + int resId = resourceEntry.getResourceId(); + + result = resource.getStringArray(resId); + Collections.addAll(listResult, result); + } catch (NullPointerException exception) { + // fall through + } + + return listResult; + } + + /** * Returns the boolean of given {@code resourceConfig}, or {@code defaultValue} if the given * {@code resourceName} is not found. If the {@code ResourceType} of the given {@code * resourceConfig} is not boolean, IllegalArgumentException will be thrown. @@ -233,8 +332,8 @@ public class PartnerConfigHelper { } /** - * Returns the dimension of given {@code resourceConfig}. If the given {@code resourceConfig} not - * found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code + * Returns the dimension of given {@code resourceConfig}. If the given {@code resourceConfig} is + * not found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code * resourceConfig} is not dimension, will throw IllegalArgumentException. * * @param context The context of client activity @@ -316,6 +415,39 @@ public class PartnerConfigHelper { } /** + * Returns the integer of given {@code resourceConfig}. If the given {@code resourceConfig} is not + * found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code + * resourceConfig} is not dimension, will throw IllegalArgumentException. + * + * @param context The context of client activity + * @param resourceConfig The {@code PartnerConfig} of target resource + * @param defaultValue The default value + */ + public int getInteger(@NonNull Context context, PartnerConfig resourceConfig, int defaultValue) { + if (resourceConfig.getResourceType() != ResourceType.INTEGER) { + throw new IllegalArgumentException("Not a integer resource"); + } + + if (partnerResourceCache.containsKey(resourceConfig)) { + return (int) partnerResourceCache.get(resourceConfig); + } + + int result = defaultValue; + try { + ResourceEntry resourceEntry = + getResourceEntryFromKey(context, resourceConfig.getResourceName()); + Resources resource = resourceEntry.getResources(); + int resId = resourceEntry.getResourceId(); + + result = resource.getInteger(resId); + partnerResourceCache.put(resourceConfig, result); + } catch (NullPointerException exception) { + // fall through + } + return result; + } + + /** * Returns the {@link ResourceEntry} of given {@code resourceConfig}, or {@code null} if the given * {@code resourceConfig} is not found. If the {@link ResourceType} of the given {@code * resourceConfig} is not illustration, IllegalArgumentException will be thrown. @@ -362,17 +494,14 @@ public class PartnerConfigHelper { private void getPartnerConfigBundle(Context context) { if (resultBundle == null || resultBundle.isEmpty()) { try { - Uri contentUri = - new Uri.Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(SUW_AUTHORITY) - .appendPath(SUW_GET_PARTNER_CONFIG_METHOD) - .build(); resultBundle = context .getContentResolver() .call( - contentUri, SUW_GET_PARTNER_CONFIG_METHOD, /* arg= */ null, /* extras= */ null); + getContentUri(), + SUW_GET_PARTNER_CONFIG_METHOD, + /* arg= */ null, + /* extras= */ null); partnerResourceCache.clear(); } catch (IllegalArgumentException | SecurityException exception) { Log.w(TAG, "Fail to get config from suw provider"); @@ -381,21 +510,134 @@ public class PartnerConfigHelper { } @Nullable - private ResourceEntry getResourceEntryFromKey(Context context, String resourceName) { + @VisibleForTesting + ResourceEntry getResourceEntryFromKey(Context context, String resourceName) { Bundle resourceEntryBundle = resultBundle.getBundle(resourceName); Bundle fallbackBundle = resultBundle.getBundle(KEY_FALLBACK_CONFIG); if (fallbackBundle != null) { resourceEntryBundle.putBundle(KEY_FALLBACK_CONFIG, fallbackBundle.getBundle(resourceName)); } - return ResourceEntry.fromBundle(context, resourceEntryBundle); + + return adjustResourceEntryDayNightMode( + context, ResourceEntry.fromBundle(context, resourceEntryBundle)); + } + + /** + * Force to day mode if setup wizard does not support day/night mode and current system is in + * night mode. + */ + private static ResourceEntry adjustResourceEntryDayNightMode( + Context context, ResourceEntry resourceEntry) { + Resources resource = resourceEntry.getResources(); + Configuration configuration = resource.getConfiguration(); + if (!isSetupWizardDayNightEnabled(context) && Util.isNightMode(configuration)) { + if (resourceEntry == null) { + Log.w(TAG, "resourceEntry is null, skip to force day mode."); + return resourceEntry; + } + configuration.uiMode = + Configuration.UI_MODE_NIGHT_NO + | (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK); + resource.updateConfiguration(configuration, resource.getDisplayMetrics()); + } + + return resourceEntry; } @VisibleForTesting - public static synchronized void resetForTesting() { + public static synchronized void resetInstance() { instance = null; + suwDayNightEnabledBundle = null; + applyExtendedPartnerConfigBundle = null; + applyDynamicColorBundle = null; } - private TypedValue getTypedValueFromResource(Resources resource, int resId, int type) { + /** + * Checks whether SetupWizard supports the DayNight theme during setup flow; if return false setup + * flow should force to light theme. + * + * <p>Returns true if the setupwizard is listening to system DayNight theme setting. + */ + public static boolean isSetupWizardDayNightEnabled(@NonNull Context context) { + if (suwDayNightEnabledBundle == null) { + try { + suwDayNightEnabledBundle = + context + .getContentResolver() + .call( + getContentUri(), + IS_SUW_DAY_NIGHT_ENABLED_METHOD, + /* arg= */ null, + /* extras= */ null); + } catch (IllegalArgumentException | SecurityException exception) { + Log.w(TAG, "SetupWizard DayNight supporting status unknown; return as false."); + suwDayNightEnabledBundle = null; + return false; + } + } + + return (suwDayNightEnabledBundle != null + && suwDayNightEnabledBundle.getBoolean(IS_SUW_DAY_NIGHT_ENABLED_METHOD, false)); + } + + /** Returns true if the SetupWizard supports the extended partner configs during setup flow. */ + public static boolean shouldApplyExtendedPartnerConfig(@NonNull Context context) { + if (applyExtendedPartnerConfigBundle == null) { + try { + applyExtendedPartnerConfigBundle = + context + .getContentResolver() + .call( + getContentUri(), + IS_EXTENDED_PARTNER_CONFIG_ENABLED_METHOD, + /* arg= */ null, + /* extras= */ null); + } catch (IllegalArgumentException | SecurityException exception) { + Log.w( + TAG, + "SetupWizard extended partner configs supporting status unknown; return as false."); + applyExtendedPartnerConfigBundle = null; + return false; + } + } + + return (applyExtendedPartnerConfigBundle != null + && applyExtendedPartnerConfigBundle.getBoolean( + IS_EXTENDED_PARTNER_CONFIG_ENABLED_METHOD, false)); + } + + /** Returns true if the SetupWizard supports the dynamic color during setup flow. */ + public static boolean isSetupWizardDynamicColorEnabled(@NonNull Context context) { + if (applyDynamicColorBundle == null) { + try { + applyDynamicColorBundle = + context + .getContentResolver() + .call( + getContentUri(), + IS_DYNAMIC_COLOR_ENABLED_METHOD, + /* arg= */ null, + /* extras= */ null); + } catch (IllegalArgumentException | SecurityException exception) { + Log.w(TAG, "SetupWizard dynamic color supporting status unknown; return as false."); + applyDynamicColorBundle = null; + return false; + } + } + + return (applyDynamicColorBundle != null + && applyDynamicColorBundle.getBoolean(IS_DYNAMIC_COLOR_ENABLED_METHOD, false)); + } + + @VisibleForTesting + static Uri getContentUri() { + return new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SUW_AUTHORITY) + .build(); + } + + private static TypedValue getTypedValueFromResource(Resources resource, int resId, int type) { TypedValue value = new TypedValue(); resource.getValue(resId, value, true); if (value.type != type) { @@ -409,8 +651,42 @@ public class PartnerConfigHelper { return value; } - private float getDimensionFromTypedValue(Context context, TypedValue value) { + private static float getDimensionFromTypedValue(Context context, TypedValue value) { DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); return value.getDimension(displayMetrics); } + + private static void registerContentObserver(Context context) { + if (isSetupWizardDayNightEnabled(context)) { + if (contentObserver != null) { + unregisterContentObserver(context); + } + + Uri contentUri = getContentUri(); + try { + contentObserver = + new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + resetInstance(); + } + }; + context + .getContentResolver() + .registerContentObserver(contentUri, /* notifyForDescendants= */ true, contentObserver); + } catch (SecurityException | NullPointerException | IllegalArgumentException e) { + Log.w(TAG, "Failed to register content observer for " + contentUri + ": " + e); + } + } + } + + private static void unregisterContentObserver(Context context) { + try { + context.getContentResolver().unregisterContentObserver(contentObserver); + contentObserver = null; + } catch (SecurityException | NullPointerException | IllegalArgumentException e) { + Log.w(TAG, "Failed to unregister content observer: " + e); + } + } } diff --git a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigKey.java b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigKey.java index e5c5442..c7444a5 100644 --- a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigKey.java +++ b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigKey.java @@ -28,7 +28,9 @@ import java.lang.annotation.RetentionPolicy; PartnerConfigKey.KEY_LIGHT_STATUS_BAR, PartnerConfigKey.KEY_NAVIGATION_BAR_BG_COLOR, PartnerConfigKey.KEY_LIGHT_NAVIGATION_BAR, + PartnerConfigKey.KEY_NAVIGATION_BAR_DIVIDER_COLOR, PartnerConfigKey.KEY_FOOTER_BAR_BG_COLOR, + PartnerConfigKey.KEY_FOOTER_BAR_MIN_HEIGHT, PartnerConfigKey.KEY_FOOTER_BUTTON_FONT_FAMILY, PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_ADD_ANOTHER, PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_CANCEL, @@ -43,6 +45,8 @@ import java.lang.annotation.RetentionPolicy; PartnerConfigKey.KEY_FOOTER_BUTTON_RADIUS, PartnerConfigKey.KEY_FOOTER_BUTTON_RIPPLE_ALPHA, PartnerConfigKey.KEY_FOOTER_BUTTON_TEXT_SIZE, + PartnerConfigKey.KEY_FOOTER_BUTTON_TEXT_STYLE, + PartnerConfigKey.KEY_FOOTER_BUTTON_MIN_HEIGHT, PartnerConfigKey.KEY_FOOTER_BUTTON_DISABLED_ALPHA, PartnerConfigKey.KEY_FOOTER_BUTTON_DISABLED_BG_COLOR, PartnerConfigKey.KEY_FOOTER_PRIMARY_BUTTON_BG_COLOR, @@ -50,25 +54,80 @@ import java.lang.annotation.RetentionPolicy; PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_BG_COLOR, PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_TEXT_COLOR, PartnerConfigKey.KEY_LAYOUT_BACKGROUND_COLOR, + PartnerConfigKey.KEY_LAYOUT_MARGIN_START, + PartnerConfigKey.KEY_LAYOUT_MARGIN_END, PartnerConfigKey.KEY_HEADER_TEXT_SIZE, PartnerConfigKey.KEY_HEADER_TEXT_COLOR, PartnerConfigKey.KEY_HEADER_FONT_FAMILY, PartnerConfigKey.KEY_HEADER_AREA_BACKGROUND_COLOR, + PartnerConfigKey.KEY_HEADER_TEXT_MARGIN_TOP, + PartnerConfigKey.KEY_HEADER_TEXT_MARGIN_BOTTOM, + PartnerConfigKey.KEY_HEADER_CONTAINER_MARGIN_BOTTOM, + PartnerConfigKey.KEY_HEADER_AUTO_SIZE_ENABLED, + PartnerConfigKey.KEY_HEADER_AUTO_SIZE_MAX_TEXT_SIZE, + PartnerConfigKey.KEY_HEADER_AUTO_SIZE_MIN_TEXT_SIZE, + PartnerConfigKey.KEY_HEADER_AUTO_SIZE_MAX_LINE_OF_MAX_SIZE, + PartnerConfigKey.KEY_HEADER_AUTO_SIZE_LINE_SPACING_EXTRA, PartnerConfigKey.KEY_LAYOUT_GRAVITY, + PartnerConfigKey.KEY_ICON_MARGIN_TOP, + PartnerConfigKey.KEY_ICON_SIZE, PartnerConfigKey.KEY_DESCRIPTION_TEXT_SIZE, PartnerConfigKey.KEY_DESCRIPTION_TEXT_COLOR, PartnerConfigKey.KEY_DESCRIPTION_LINK_TEXT_COLOR, PartnerConfigKey.KEY_DESCRIPTION_FONT_FAMILY, + PartnerConfigKey.KEY_DESCRIPTION_TEXT_MARGIN_TOP, + PartnerConfigKey.KEY_DESCRIPTION_TEXT_MARGIN_BOTTOM, PartnerConfigKey.KEY_CONTENT_TEXT_SIZE, PartnerConfigKey.KEY_CONTENT_TEXT_COLOR, PartnerConfigKey.KEY_CONTENT_LINK_TEXT_COLOR, PartnerConfigKey.KEY_CONTENT_FONT_FAMILY, PartnerConfigKey.KEY_CONTENT_LAYOUT_GRAVITY, + PartnerConfigKey.KEY_CONTENT_PADDING_TOP, + PartnerConfigKey.KEY_CONTENT_INFO_TEXT_SIZE, + PartnerConfigKey.KEY_CONTENT_INFO_FONT_FAMILY, + PartnerConfigKey.KEY_CONTENT_INFO_LINE_SPACING_EXTRA, + PartnerConfigKey.KEY_CONTENT_INFO_ICON_SIZE, + PartnerConfigKey.KEY_CONTENT_INFO_ICON_MARGIN_END, + PartnerConfigKey.KEY_CONTENT_INFO_PADDING_TOP, + PartnerConfigKey.KEY_CONTENT_INFO_PADDING_BOTTOM, + PartnerConfigKey.KEY_CARD_VIEW_INTRINSIC_WIDTH, + PartnerConfigKey.KEY_CARD_VIEW_INTRINSIC_HEIGHT, + PartnerConfigKey.KEY_ITEMS_TITLE_TEXT_SIZE, + PartnerConfigKey.KEY_ITEMS_SUMMARY_TEXT_SIZE, + PartnerConfigKey.KEY_ITEMS_SUMMARY_MARGIN_TOP, + PartnerConfigKey.KEY_ITEMS_TITLE_FONT_FAMILY, + PartnerConfigKey.KEY_ITEMS_SUMMARY_FONT_FAMILY, + PartnerConfigKey.KEY_ITEMS_PADDING_TOP, + PartnerConfigKey.KEY_ITEMS_PADDING_BOTTOM, + PartnerConfigKey.KEY_ITEMS_MIN_HEIGHT, + PartnerConfigKey.KEY_ITEMS_DIVIDER_SHOWN, PartnerConfigKey.KEY_PROGRESS_ILLUSTRATION_DEFAULT, PartnerConfigKey.KEY_PROGRESS_ILLUSTRATION_ACCOUNT, PartnerConfigKey.KEY_PROGRESS_ILLUSTRATION_CONNECTION, PartnerConfigKey.KEY_PROGRESS_ILLUSTRATION_UPDATE, + PartnerConfigKey.KEY_PROGRESS_ILLUSTRATION_FINAL_HOLD, PartnerConfigKey.KEY_PROGRESS_ILLUSTRATION_DISPLAY_MINIMUM_MS, + PartnerConfigKey.KEY_LOADING_LOTTIE_ACCOUNT, + PartnerConfigKey.KEY_LOADING_LOTTIE_CONNECTION, + PartnerConfigKey.KEY_LOADING_LOTTIE_DEFAULT, + PartnerConfigKey.KEY_LOADING_LOTTIE_UPDATE, + PartnerConfigKey.KEY_LOADING_LOTTIE_FINAL_HOLD, + PartnerConfigKey.KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_DEFAULT, + PartnerConfigKey.KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_ACCOUNT, + PartnerConfigKey.KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_CONNECTION, + PartnerConfigKey.KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_UPDATE, + PartnerConfigKey.KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_FINAL_HOLD, + PartnerConfigKey.KEY_LOADING_DARK_THEME_CUSTOMIZATION_DEFAULT, + PartnerConfigKey.KEY_LOADING_DARK_THEME_CUSTOMIZATION_ACCOUNT, + PartnerConfigKey.KEY_LOADING_DARK_THEME_CUSTOMIZATION_CONNECTION, + PartnerConfigKey.KEY_LOADING_DARK_THEME_CUSTOMIZATION_UPDATE, + PartnerConfigKey.KEY_LOADING_DARK_THEME_CUSTOMIZATION_FINAL_HOLD, + PartnerConfigKey.KEY_TRANSITION_TYPE, + PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_TOP, + PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_START, + PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_END, + PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_BOTTOM, + PartnerConfigKey.KEY_LOADING_LAYOUT_HEADER_HEIGHT, }) // TODO: can be removed and always reference PartnerConfig.getResourceName()? @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) @@ -87,9 +146,15 @@ public @interface PartnerConfigKey { // such that it is compatible with a light navigation bar background. String KEY_LIGHT_NAVIGATION_BAR = "setup_compat_light_navigation_bar"; + // Navigation bar divider color + String KEY_NAVIGATION_BAR_DIVIDER_COLOR = "setup_compat_navigation_bar_divider_color"; + // Background color of the footer bar. String KEY_FOOTER_BAR_BG_COLOR = "setup_compat_footer_bar_bg_color"; + // The min height of the footer bar + String KEY_FOOTER_BAR_MIN_HEIGHT = "setup_compat_footer_bar_min_height"; + // The font face used in footer buttons. This must be a string reference to a font that is // available in the system. Font references (@font or @xml) are not allowed. String KEY_FOOTER_BUTTON_FONT_FAMILY = "setup_compat_footer_button_font_family"; @@ -127,12 +192,18 @@ public @interface PartnerConfigKey { // Corner radius of the footer buttons String KEY_FOOTER_BUTTON_RADIUS = "setup_compat_footer_button_radius"; - // Ripple color alpha of the footer button + // Ripple color alpha of the footer buttons String KEY_FOOTER_BUTTON_RIPPLE_ALPHA = "setup_compat_footer_button_ripple_alpha"; - // Text size of the footer button + // Text size of the footer buttons String KEY_FOOTER_BUTTON_TEXT_SIZE = "setup_compat_footer_button_text_size"; + // The font face used in footer buttons {0 = NORMAL}, {1 = BOLD}, {2 = ITALIC}, {3 = BOLD_ITALIC} + String KEY_FOOTER_BUTTON_TEXT_STYLE = "setup_compat_footer_button_text_style"; + + // The min height of the footer buttons + String KEY_FOOTER_BUTTON_MIN_HEIGHT = "setup_compat_footer_button_min_height"; + // Disabled background alpha of the footer buttons String KEY_FOOTER_BUTTON_DISABLED_ALPHA = "setup_compat_footer_button_disabled_alpha"; @@ -154,6 +225,12 @@ public @interface PartnerConfigKey { // Background color of layout String KEY_LAYOUT_BACKGROUND_COLOR = "setup_design_layout_bg_color"; + // Margin start of the layout + String KEY_LAYOUT_MARGIN_START = "setup_design_layout_margin_start"; + + // Margin end of the layout + String KEY_LAYOUT_MARGIN_END = "setup_design_layout_margin_end"; + // Text size of the header String KEY_HEADER_TEXT_SIZE = "setup_design_header_text_size"; @@ -163,12 +240,44 @@ public @interface PartnerConfigKey { // Font family of the header String KEY_HEADER_FONT_FAMILY = "setup_design_header_font_family"; + // Margin top of the header text + String KEY_HEADER_TEXT_MARGIN_TOP = "setup_design_header_text_margin_top"; + + // Margin bottom of the header text + String KEY_HEADER_TEXT_MARGIN_BOTTOM = "setup_design_header_text_margin_bottom"; + // Gravity of the header, icon and description String KEY_LAYOUT_GRAVITY = "setup_design_layout_gravity"; + // Margin top of the icon + String KEY_ICON_MARGIN_TOP = "setup_design_icon_margin_top"; + + // Size of the icon + String KEY_ICON_SIZE = "setup_design_icon_size"; + // Background color of the header area String KEY_HEADER_AREA_BACKGROUND_COLOR = "setup_design_header_area_background_color"; + // Margin bottom of the header container + String KEY_HEADER_CONTAINER_MARGIN_BOTTOM = "setup_design_header_container_margin_bottom"; + + // Auto text size enabled status + String KEY_HEADER_AUTO_SIZE_ENABLED = "setup_design_header_auto_size_enabled"; + + // Max text size of header when auto size enabled. Ignored if auto size is false. + String KEY_HEADER_AUTO_SIZE_MAX_TEXT_SIZE = "setup_design_header_auto_size_max_text_size"; + + // Min text size of header when auto size enabled. Ignored if auto size is false. + String KEY_HEADER_AUTO_SIZE_MIN_TEXT_SIZE = "setup_design_header_auto_size_min_text_size"; + + // The max lines of the max text size when auto size enabled. Ignored if auto size is false. + String KEY_HEADER_AUTO_SIZE_MAX_LINE_OF_MAX_SIZE = + "setup_design_header_auto_size_max_line_of_max_size"; + + // Extra line spacing of header when auto size enabled. Ignored if auto size is false. + String KEY_HEADER_AUTO_SIZE_LINE_SPACING_EXTRA = + "setup_design_header_auto_size_line_spacing_extra"; + // Text size of the description String KEY_DESCRIPTION_TEXT_SIZE = "setup_design_description_text_size"; @@ -181,6 +290,12 @@ public @interface PartnerConfigKey { // Font family of the description String KEY_DESCRIPTION_FONT_FAMILY = "setup_design_description_font_family"; + // Margin top of the header text + String KEY_DESCRIPTION_TEXT_MARGIN_TOP = "setup_design_description_text_margin_top"; + + // Margin bottom of the header text + String KEY_DESCRIPTION_TEXT_MARGIN_BOTTOM = "setup_design_description_text_margin_bottom"; + // Text size of the body content text String KEY_CONTENT_TEXT_SIZE = "setup_design_content_text_size"; @@ -196,6 +311,63 @@ public @interface PartnerConfigKey { // Gravity of the body content text String KEY_CONTENT_LAYOUT_GRAVITY = "setup_design_content_layout_gravity"; + // The padding top of the content + String KEY_CONTENT_PADDING_TOP = "setup_design_content_padding_top"; + + // The text size of the content info. + String KEY_CONTENT_INFO_TEXT_SIZE = "setup_design_content_info_text_size"; + + // The font family of the content info. + String KEY_CONTENT_INFO_FONT_FAMILY = "setup_design_content_info_font_family"; + + // The text line spacing extra of the content info. + String KEY_CONTENT_INFO_LINE_SPACING_EXTRA = "setup_design_content_info_line_spacing_extra"; + + // The icon size of the content info. + String KEY_CONTENT_INFO_ICON_SIZE = "setup_design_content_info_icon_size"; + + // The icon margin end of the content info. + String KEY_CONTENT_INFO_ICON_MARGIN_END = "setup_design_content_info_icon_margin_end"; + + // The padding top of the content info. + String KEY_CONTENT_INFO_PADDING_TOP = "setup_design_content_info_padding_top"; + + // The padding bottom of the content info. + String KEY_CONTENT_INFO_PADDING_BOTTOM = "setup_design_content_info_padding_bottom"; + + // The title text size of list items. + String KEY_ITEMS_TITLE_TEXT_SIZE = "setup_design_items_title_text_size"; + + // The summary text size of list items. + String KEY_ITEMS_SUMMARY_TEXT_SIZE = "setup_design_items_summary_text_size"; + + // The summary margin top of list items. + String KEY_ITEMS_SUMMARY_MARGIN_TOP = "setup_design_items_summary_margin_top"; + + // The title font family of list items. + String KEY_ITEMS_TITLE_FONT_FAMILY = "setup_design_items_title_font_family"; + + // The summary font family of list items. + String KEY_ITEMS_SUMMARY_FONT_FAMILY = "setup_design_items_summary_font_family"; + + // The padding top of list items. + String KEY_ITEMS_PADDING_TOP = "setup_design_items_padding_top"; + + // The padding bottom of list items. + String KEY_ITEMS_PADDING_BOTTOM = "setup_design_items_padding_bottom"; + + // The minimum height of list items. + String KEY_ITEMS_MIN_HEIGHT = "setup_design_items_min_height"; + + // The divider of list items are showing. + String KEY_ITEMS_DIVIDER_SHOWN = "setup_design_items_divider_shown"; + + // The intrinsic width of the card view for foldabe/tablet. + String KEY_CARD_VIEW_INTRINSIC_WIDTH = "setup_design_card_view_intrinsic_width"; + + // The intrinsic height of the card view for foldabe/tablet. + String KEY_CARD_VIEW_INTRINSIC_HEIGHT = "setup_design_card_view_intrinsic_height"; + // The animation of loading screen used in those activities which is non of below type. String KEY_PROGRESS_ILLUSTRATION_DEFAULT = "progress_illustration_custom_default"; @@ -212,6 +384,94 @@ public @interface PartnerConfigKey { // For example:com.google.android.setupwizard.COMPAT_EARLY_UPDATE String KEY_PROGRESS_ILLUSTRATION_UPDATE = "progress_illustration_custom_update"; + // The animation of loading screen used in those activities which is updating device. + // For example:com.google.android.setupwizard.FINAL_HOLD + String KEY_PROGRESS_ILLUSTRATION_FINAL_HOLD = "final_hold_custom_illustration"; + // The minimum illustration display time, set to 0 may cause the illustration stuck String KEY_PROGRESS_ILLUSTRATION_DISPLAY_MINIMUM_MS = "progress_illustration_display_minimum_ms"; + + // The animation for S+ devices used in those screens waiting for non of below type. + String KEY_LOADING_LOTTIE_DEFAULT = "loading_animation_custom_default"; + + // The animation for S+ devices used in those screens which is processing account info or related + // functions. + // For example:com.google.android.setupwizard.LOAD_ADD_ACCOUNT_INTENT + String KEY_LOADING_LOTTIE_ACCOUNT = "loading_animation_custom_account"; + + // The animation for S+ devices used in those screens which is processing data connection. + // For example:com.android.setupwizard.CAPTIVE_PORTAL + String KEY_LOADING_LOTTIE_CONNECTION = "loading_animation_custom_connection"; + + // The animation for S+ devices used in those screens which is updating devices. + // For example:com.google.android.setupwizard.COMPAT_EARLY_UPDATE + String KEY_LOADING_LOTTIE_UPDATE = "loading_animation_custom_update"; + + // The animation for S+ devices used in those screens which is updating devices. + // For example:com.google.android.setupwizard.FINAL_HOLD + String KEY_LOADING_LOTTIE_FINAL_HOLD = "loading_animation_custom_final_hold"; + + // A string-array to list all the key path and color map for default animation for light theme. + // For example: background:#FFFFFF + String KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_DEFAULT = + "loading_light_theme_customization_default"; + + // A string-array to list all the key path and color map for account animation for light theme. + // For example: background:#FFFFFF + String KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_ACCOUNT = + "loading_light_theme_customization_account"; + + // A string-array to list all the key path and color map for connection animation for light theme. + // For example: background:#FFFFFF + String KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_CONNECTION = + "loading_light_theme_customization_connection"; + + // A string-array to list all the key path and color map for update animation for light theme. + // For example: background:#FFFFFF + String KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_UPDATE = "loading_light_theme_customization_update"; + + // A string-array to list all the key path and color map for final hold animation for light theme. + // For example: background:#FFFFFF + String KEY_LOADING_LIGHT_THEME_CUSTOMIZATION_FINAL_HOLD = + "loading_light_theme_customization_final_hold"; + + // A string-array to list all the key path and color map for default animation for dark theme. + // For example: background:#000000 + String KEY_LOADING_DARK_THEME_CUSTOMIZATION_DEFAULT = "loading_dark_theme_customization_default"; + + // A string-array to list all the key path and color map for account animation for dark theme. + // For example: background:#000000 + String KEY_LOADING_DARK_THEME_CUSTOMIZATION_ACCOUNT = "loading_dark_theme_customization_account"; + + // A string-array to list all the key path and color map for connection animation for dark theme. + // For example: background:#000000 + String KEY_LOADING_DARK_THEME_CUSTOMIZATION_CONNECTION = + "loading_dark_theme_customization_connection"; + + // A string-array to list all the key path and color map for update animation for dark theme. + // For example: background:#000000 + String KEY_LOADING_DARK_THEME_CUSTOMIZATION_UPDATE = "loading_dark_theme_customization_update"; + + // A string-array to list all the key path and color map for final hold animation for dark theme. + // For example: background:#000000 + String KEY_LOADING_DARK_THEME_CUSTOMIZATION_FINAL_HOLD = + "loading_dark_theme_customization_final_hold"; + + // The transition type between activities + String KEY_TRANSITION_TYPE = "setup_design_transition_type"; + + // A padding top of the content frame of loading layout. + String KEY_LOADING_LAYOUT_CONTENT_PADDING_TOP = "loading_layout_content_padding_top"; + + // A padding start of the content frame of loading layout. + String KEY_LOADING_LAYOUT_CONTENT_PADDING_START = "loading_layout_content_padding_start"; + + // A padding end of the content frame of loading layout. + String KEY_LOADING_LAYOUT_CONTENT_PADDING_END = "loading_layout_content_padding_end"; + + // A padding bottom of the content frame of loading layout. + String KEY_LOADING_LAYOUT_CONTENT_PADDING_BOTTOM = "loading_layout_content_padding_bottom"; + + // A height of the header of loading layout. + String KEY_LOADING_LAYOUT_HEADER_HEIGHT = "loading_layout_header_height"; } diff --git a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/ResourceEntry.java b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/ResourceEntry.java index 8f7c9d8..c8b8623 100644 --- a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/ResourceEntry.java +++ b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/ResourceEntry.java @@ -23,10 +23,10 @@ import android.content.res.Resources; import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.Bundle; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import android.util.Log; /** * A potentially cross-package resource entry, which can then be retrieved using {@link diff --git a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/Util.java b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/Util.java new file mode 100644 index 0000000..8ac4c2d --- /dev/null +++ b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/Util.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.setupcompat.partnerconfig; + +import android.content.res.Configuration; + +/** The utility to help partner to config. */ +public final class Util { + + public static boolean isNightMode(Configuration configuration) { + return (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK) + == Configuration.UI_MODE_NIGHT_YES; + } + + private Util() {} +} |