diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt new file mode 100644 index 0000000000000..0b65966ca1096 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 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.android.systemui.common.shared.model + +import android.annotation.DrawableRes +import android.graphics.drawable.Drawable + +/** + * Models an icon, that can either be already [loaded][Icon.Loaded] or be a [reference] + * [Icon.Resource] to a resource. + */ +sealed class Icon { + data class Loaded( + val drawable: Drawable, + ) : Icon() + + data class Resource( + @DrawableRes val res: Int, + ) : Icon() +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java index 87fcce455ea6d..d983ece8f280a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java @@ -87,10 +87,13 @@ import com.android.systemui.R; import com.android.systemui.animation.DialogCuj; import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.common.shared.model.Icon; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.dagger.QSScope; +import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig; +import com.android.systemui.security.data.model.SecurityModel; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.SecurityController; @@ -102,8 +105,9 @@ import java.util.function.Supplier; import javax.inject.Inject; import javax.inject.Named; +/** Helper class for the configuration of the QS security footer button. */ @QSScope -class QSSecurityFooter extends ViewController +public class QSSecurityFooter extends ViewController implements OnClickListener, DialogInterface.OnClickListener, VisibilityChangedDispatcher { protected static final String TAG = "QSSecurityFooter"; @@ -130,11 +134,10 @@ class QSSecurityFooter extends ViewController protected H mHandler; private boolean mIsVisible; + private boolean mIsClickable; @Nullable private CharSequence mFooterTextContent = null; - private int mFooterIconId; - @Nullable - private Drawable mPrimaryFooterIconDrawable; + private Icon mFooterIcon; @Nullable private VisibilityChangedDispatcher.OnVisibilityChangedListener mVisibilityChangedListener; @@ -215,7 +218,7 @@ class QSSecurityFooter extends ViewController super(rootView); mFooterText = mView.findViewById(R.id.footer_text); mPrimaryFooterIcon = mView.findViewById(R.id.primary_footer_icon); - mFooterIconId = R.drawable.ic_info_outline; + mFooterIcon = new Icon.Resource(R.drawable.ic_info_outline); mContext = rootView.getContext(); mDpm = rootView.getContext().getSystemService(DevicePolicyManager.class); mMainHandler = mainHandler; @@ -287,8 +290,18 @@ class QSSecurityFooter extends ViewController .write(); } + // TODO(b/242040009): Remove this. public void showDeviceMonitoringDialog() { - createDialog(); + showDeviceMonitoringDialog(mContext, mView); + } + + /** + * Show the device monitoring dialog, and expand it from {@code view} if it's not null. + * + * Important: {@code view} must be associated to the same {@link Context} as the QSFragment. + */ + public void showDeviceMonitoringDialog(Context quickSettingsContext, @Nullable View view) { + createDialog(quickSettingsContext, view); } public void refreshState() { @@ -296,75 +309,86 @@ class QSSecurityFooter extends ViewController } private void handleRefreshState() { - final boolean isDeviceManaged = mSecurityController.isDeviceManaged(); + SecurityModel securityModel = SecurityModel.create(mSecurityController); + SecurityButtonConfig buttonConfig = getButtonConfig(securityModel); + + if (buttonConfig == null) { + mIsVisible = false; + } else { + mIsVisible = true; + mIsClickable = buttonConfig.isClickable(); + mFooterTextContent = buttonConfig.getText(); + mFooterIcon = buttonConfig.getIcon(); + } + + // Update the UI. + mMainHandler.post(mUpdatePrimaryIcon); + mMainHandler.post(mUpdateDisplayState); + } + + /** + * Return the {@link SecurityButtonConfig} of the security button, or {@code null} if no + * security button should be shown. + */ + @Nullable + public SecurityButtonConfig getButtonConfig(SecurityModel securityModel) { + final boolean isDeviceManaged = securityModel.isDeviceManaged(); final UserInfo currentUser = mUserTracker.getUserInfo(); final boolean isDemoDevice = UserManager.isDeviceInDemoMode(mContext) && currentUser != null && currentUser.isDemo(); - final boolean hasWorkProfile = mSecurityController.hasWorkProfile(); - final boolean hasCACerts = mSecurityController.hasCACertInCurrentUser(); - final boolean hasCACertsInWorkProfile = mSecurityController.hasCACertInWorkProfile(); - final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled(); - final String vpnName = mSecurityController.getPrimaryVpnName(); - final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName(); - final CharSequence organizationName = mSecurityController.getDeviceOwnerOrganizationName(); + final boolean hasWorkProfile = securityModel.getHasWorkProfile(); + final boolean hasCACerts = securityModel.getHasCACertInCurrentUser(); + final boolean hasCACertsInWorkProfile = securityModel.getHasCACertInWorkProfile(); + final boolean isNetworkLoggingEnabled = securityModel.isNetworkLoggingEnabled(); + final String vpnName = securityModel.getPrimaryVpnName(); + final String vpnNameWorkProfile = securityModel.getWorkProfileVpnName(); + final CharSequence organizationName = securityModel.getDeviceOwnerOrganizationName(); final CharSequence workProfileOrganizationName = - mSecurityController.getWorkProfileOrganizationName(); + securityModel.getWorkProfileOrganizationName(); final boolean isProfileOwnerOfOrganizationOwnedDevice = - mSecurityController.isProfileOwnerOfOrganizationOwnedDevice(); - final boolean isParentalControlsEnabled = mSecurityController.isParentalControlsEnabled(); - final boolean isWorkProfileOn = mSecurityController.isWorkProfileOn(); + securityModel.isProfileOwnerOfOrganizationOwnedDevice(); + final boolean isParentalControlsEnabled = securityModel.isParentalControlsEnabled(); + final boolean isWorkProfileOn = securityModel.isWorkProfileOn(); final boolean hasDisclosableWorkProfilePolicy = hasCACertsInWorkProfile || vpnNameWorkProfile != null || (hasWorkProfile && isNetworkLoggingEnabled); // Update visibility of footer - mIsVisible = (isDeviceManaged && !isDemoDevice) + boolean isVisible = (isDeviceManaged && !isDemoDevice) || hasCACerts || vpnName != null || isProfileOwnerOfOrganizationOwnedDevice || isParentalControlsEnabled || (hasDisclosableWorkProfilePolicy && isWorkProfileOn); + if (!isVisible && !DEBUG_FORCE_VISIBLE) { + return null; + } + // Update the view to be untappable if the device is an organization-owned device with a // managed profile and there is either: // a) no policy set which requires a privacy disclosure. // b) a specific work policy set but the work profile is turned off. - if (mIsVisible && isProfileOwnerOfOrganizationOwnedDevice - && (!hasDisclosableWorkProfilePolicy || !isWorkProfileOn)) { - mView.setClickable(false); - mView.findViewById(R.id.footer_icon).setVisibility(View.GONE); - } else { - mView.setClickable(true); - mView.findViewById(R.id.footer_icon).setVisibility(View.VISIBLE); - } - // Update the string - mFooterTextContent = getFooterText(isDeviceManaged, hasWorkProfile, + boolean isClickable = !(isProfileOwnerOfOrganizationOwnedDevice + && (!hasDisclosableWorkProfilePolicy || !isWorkProfileOn)); + + String text = getFooterText(isDeviceManaged, hasWorkProfile, hasCACerts, hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName, vpnNameWorkProfile, organizationName, workProfileOrganizationName, isProfileOwnerOfOrganizationOwnedDevice, isParentalControlsEnabled, - isWorkProfileOn); - // Update the icon - int footerIconId = R.drawable.ic_info_outline; - if (vpnName != null || vpnNameWorkProfile != null) { - if (mSecurityController.isVpnBranded()) { - footerIconId = R.drawable.stat_sys_branded_vpn; - } else { - footerIconId = R.drawable.stat_sys_vpn_ic; - } - } - if (mFooterIconId != footerIconId) { - mFooterIconId = footerIconId; - } + isWorkProfileOn).toString(); - // Update the primary icon + Icon icon; if (isParentalControlsEnabled) { - if (mPrimaryFooterIconDrawable == null) { - DeviceAdminInfo info = mSecurityController.getDeviceAdminInfo(); - mPrimaryFooterIconDrawable = mSecurityController.getIcon(info); + icon = new Icon.Loaded(securityModel.getDeviceAdminIcon()); + } else if (vpnName != null || vpnNameWorkProfile != null) { + if (securityModel.isVpnBranded()) { + icon = new Icon.Resource(R.drawable.stat_sys_branded_vpn); + } else { + icon = new Icon.Resource(R.drawable.stat_sys_vpn_ic); } } else { - mPrimaryFooterIconDrawable = null; + icon = new Icon.Resource(R.drawable.ic_info_outline); } - mMainHandler.post(mUpdatePrimaryIcon); - mMainHandler.post(mUpdateDisplayState); + return new SecurityButtonConfig(icon, text, isClickable); } @Nullable @@ -547,21 +571,21 @@ class QSSecurityFooter extends ViewController } } - private void createDialog() { + private void createDialog(Context quickSettingsContext, @Nullable View view) { mShouldUseSettingsButton.set(false); mHandler.post(() -> { String settingsButtonText = getSettingsButton(); - final View view = createDialogView(); + final View dialogView = createDialogView(); mMainHandler.post(() -> { - mDialog = new SystemUIDialog(mContext, 0); // Use mContext theme + mDialog = new SystemUIDialog(quickSettingsContext, 0); mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); mDialog.setButton(DialogInterface.BUTTON_POSITIVE, getPositiveButton(), this); mDialog.setButton(DialogInterface.BUTTON_NEGATIVE, mShouldUseSettingsButton.get() ? settingsButtonText : getNegativeButton(), this); - mDialog.setView(view); - if (mView.isAggregatedVisible()) { - mDialogLaunchAnimator.showFromView(mDialog, mView, new DialogCuj( + mDialog.setView(dialogView); + if (view != null && view.isAggregatedVisible()) { + mDialogLaunchAnimator.showFromView(mDialog, view, new DialogCuj( InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)); } else { mDialog.show(); @@ -876,10 +900,10 @@ class QSSecurityFooter extends ViewController private final Runnable mUpdatePrimaryIcon = new Runnable() { @Override public void run() { - if (mPrimaryFooterIconDrawable != null) { - mPrimaryFooterIcon.setImageDrawable(mPrimaryFooterIconDrawable); - } else { - mPrimaryFooterIcon.setImageResource(mFooterIconId); + if (mFooterIcon instanceof Icon.Loaded) { + mPrimaryFooterIcon.setImageDrawable(((Icon.Loaded) mFooterIcon).getDrawable()); + } else if (mFooterIcon instanceof Icon.Resource) { + mPrimaryFooterIcon.setImageResource(((Icon.Resource) mFooterIcon).getRes()); } } }; @@ -894,6 +918,14 @@ class QSSecurityFooter extends ViewController if (mVisibilityChangedListener != null) { mVisibilityChangedListener.onVisibilityChanged(mView.getVisibility()); } + + if (mIsVisible && mIsClickable) { + mView.setClickable(true); + mView.findViewById(R.id.footer_icon).setVisibility(View.VISIBLE); + } else { + mView.setClickable(false); + mView.findViewById(R.id.footer_icon).setVisibility(View.GONE); + } } }; diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/model/SecurityButtonConfig.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/model/SecurityButtonConfig.kt new file mode 100644 index 0000000000000..be9c0c1de799c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/model/SecurityButtonConfig.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 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.android.systemui.qs.footer.domain.model + +import com.android.systemui.common.shared.model.Icon + +/** The config for the security button. */ +data class SecurityButtonConfig( + val icon: Icon, + val text: String, + val isClickable: Boolean, +) diff --git a/packages/SystemUI/src/com/android/systemui/security/data/model/SecurityModel.kt b/packages/SystemUI/src/com/android/systemui/security/data/model/SecurityModel.kt new file mode 100644 index 0000000000000..50af260684f8c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/security/data/model/SecurityModel.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2022 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.android.systemui.security.data.model + +import android.graphics.drawable.Drawable +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.policy.SecurityController +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +/** The security info exposed by [com.android.systemui.statusbar.policy.SecurityController]. */ +// TODO(b/242040009): Consider splitting this model into smaller submodels. +data class SecurityModel( + val isDeviceManaged: Boolean, + val hasWorkProfile: Boolean, + val isWorkProfileOn: Boolean, + val isProfileOwnerOfOrganizationOwnedDevice: Boolean, + val deviceOwnerOrganizationName: String?, + val workProfileOrganizationName: String?, + val isNetworkLoggingEnabled: Boolean, + val isVpnBranded: Boolean, + val primaryVpnName: String?, + val workProfileVpnName: String?, + val hasCACertInCurrentUser: Boolean, + val hasCACertInWorkProfile: Boolean, + val isParentalControlsEnabled: Boolean, + val deviceAdminIcon: Drawable?, +) { + companion object { + /** Create a [SecurityModel] from the current [securityController] state. */ + suspend fun create( + securityController: SecurityController, + @Background bgDispatcher: CoroutineDispatcher, + ): SecurityModel { + return withContext(bgDispatcher) { create(securityController) } + } + + /** + * Create a [SecurityModel] from the current [securityController] state. + * + * Important: This method should be called from a background thread as this will do a lot of + * binder calls. + */ + // TODO(b/242040009): Remove this. + @JvmStatic + fun create(securityController: SecurityController): SecurityModel { + val deviceAdminInfo = + if (securityController.isParentalControlsEnabled) { + securityController.deviceAdminInfo + } else { + null + } + + return SecurityModel( + isDeviceManaged = securityController.isDeviceManaged, + hasWorkProfile = securityController.hasWorkProfile(), + isWorkProfileOn = securityController.isWorkProfileOn, + isProfileOwnerOfOrganizationOwnedDevice = + securityController.isProfileOwnerOfOrganizationOwnedDevice, + deviceOwnerOrganizationName = + securityController.deviceOwnerOrganizationName?.toString(), + workProfileOrganizationName = + securityController.workProfileOrganizationName?.toString(), + isNetworkLoggingEnabled = securityController.isNetworkLoggingEnabled, + isVpnBranded = securityController.isVpnBranded, + primaryVpnName = securityController.primaryVpnName, + workProfileVpnName = securityController.workProfileVpnName, + hasCACertInCurrentUser = securityController.hasCACertInCurrentUser(), + hasCACertInWorkProfile = securityController.hasCACertInWorkProfile(), + isParentalControlsEnabled = securityController.isParentalControlsEnabled, + deviceAdminIcon = securityController.getIcon(deviceAdminInfo), + ) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java index c1c0f78ecd6bc..5d08c18d08d27 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java @@ -690,6 +690,9 @@ public class QSSecurityFooterTest extends SysuiTestCase { @Test public void testParentalControls() { + // Make sure the security footer is visible, so that the images are updated. + when(mSecurityController.isProfileOwnerOfOrganizationOwnedDevice()).thenReturn(true); + when(mSecurityController.isParentalControlsEnabled()).thenReturn(true); Drawable testDrawable = new VectorDrawable();