Make the logic of QSSecurityFooter reusable am: e4eb991ba6

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19678345

Change-Id: I23240c90c75f52de8b6c47f732dedfc4c5fafef6
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Jordan Demeulenaere
2022-08-26 07:09:20 +00:00
committed by Automerger Merge Worker
5 changed files with 243 additions and 59 deletions

View File

@@ -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()
}

View File

@@ -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<View>
public class QSSecurityFooter extends ViewController<View>
implements OnClickListener, DialogInterface.OnClickListener,
VisibilityChangedDispatcher {
protected static final String TAG = "QSSecurityFooter";
@@ -130,11 +134,10 @@ class QSSecurityFooter extends ViewController<View>
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<View>
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<View>
.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<View>
}
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<View>
}
}
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<View>
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<View>
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);
}
}
};

View File

@@ -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,
)

View File

@@ -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),
)
}
}
}

View File

@@ -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();