Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e4eb991b authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Make the logic of QSSecurityFooter reusable

This CL makes the logic of the QSSecurityFooter reusable by extracting
the logic of refreshState() that maps the current security info into the
security button configuration. This is going to be used by ag/19678215.

This CL also introduces the generic Icon class to model an icon, which
either can be a loaded Drawable or a reference to a Resource. This model
will be shared by all layers of ag/19678215.

The SecurityInfo class is going to be used by the data layer of
ag/19678215, and SecurityButtonConfig by the the domain layer.

Bug: 242040009
Test: atest QSSecurityFooterTest
Change-Id: I0cd229b257a3eafd4c07a965abe536835724ecef
parent e496b54c
Loading
Loading
Loading
Loading
+34 −0
Original line number Diff line number Diff line
/*
 * 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()
}
+91 −59
Original line number Diff line number Diff line
@@ -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);
            }
        }
    };

+26 −0
Original line number Diff line number Diff line
/*
 * 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,
)
+89 −0
Original line number Diff line number Diff line
/*
 * 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),
            )
        }
    }
}
+3 −0
Original line number Diff line number Diff line
@@ -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();