diff --git a/res/drawable/bg_ps_header.xml b/res/drawable/bg_ps_header.xml new file mode 100644 index 0000000000..526bb5a9e8 --- /dev/null +++ b/res/drawable/bg_ps_header.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_ps_lock_button.xml b/res/drawable/bg_ps_lock_button.xml new file mode 100644 index 0000000000..aef1e816ef --- /dev/null +++ b/res/drawable/bg_ps_lock_button.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_ps_settings_button.xml b/res/drawable/bg_ps_settings_button.xml new file mode 100644 index 0000000000..c06e0c0249 --- /dev/null +++ b/res/drawable/bg_ps_settings_button.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_ps_transition_image.xml b/res/drawable/bg_ps_transition_image.xml new file mode 100644 index 0000000000..dfad3cf5b3 --- /dev/null +++ b/res/drawable/bg_ps_transition_image.xml @@ -0,0 +1,35 @@ + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_ps_unlock_button.xml b/res/drawable/bg_ps_unlock_button.xml new file mode 100644 index 0000000000..d5eedd293e --- /dev/null +++ b/res/drawable/bg_ps_unlock_button.xml @@ -0,0 +1,29 @@ + + + + + + \ No newline at end of file diff --git a/res/layout/private_space_header.xml b/res/layout/private_space_header.xml new file mode 100644 index 0000000000..24e290d180 --- /dev/null +++ b/res/layout/private_space_header.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/dimens.xml b/res/values/dimens.xml index c0a1e0a605..ac701d653b 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -456,4 +456,17 @@ 300dp + + + 24dp + 64dp + 48dp + 36dp + 24dp + 16dp + 8dp + 16sp + 36dp + 36dp + 89dp diff --git a/res/values/id.xml b/res/values/id.xml index 872ae2fc48..6156c91888 100644 --- a/res/values/id.xml +++ b/res/values/id.xml @@ -45,4 +45,10 @@ + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index f08f8f0ab4..31579cd295 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -454,8 +454,17 @@ Failed: %1$s + Private space + + Private + + Private Space Settings + + Lock/Unlock Private Space + + Private Space Transitioning diff --git a/res/values/styles.xml b/res/values/styles.xml index 82a227a5ba..36991b1f7a 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -435,4 +435,10 @@ @color/arrow_tip_view_bg @color/arrow_tip_view_content + + diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java index 095cfa91fa..7d52cbbef2 100644 --- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java @@ -563,7 +563,7 @@ public class ActivityAllAppsContainerView mainRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(0); workRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(1); mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, mPersonalMatcher); - mAH.get(AdapterHolder.WORK).setup(workRecyclerView, mWorkManager.getMatcher()); + mAH.get(AdapterHolder.WORK).setup(workRecyclerView, mWorkManager.getItemInfoMatcher()); workRecyclerView.setId(R.id.apps_list_view_work); if (enableExpandingPauseWorkButton() || FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) { diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java index 7baf7d3325..bce38a35b4 100644 --- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java @@ -52,13 +52,16 @@ public abstract class BaseAllAppsAdapter ex public static final int VIEW_TYPE_WORK_EDU_CARD = 1 << 4; public static final int VIEW_TYPE_WORK_DISABLED_CARD = 1 << 5; - - public static final int NEXT_ID = 6; + public static final int VIEW_TYPE_PRIVATE_SPACE_HEADER = 1 << 6; + public static final int NEXT_ID = 7; // Common view type masks public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER; public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON; + public static final int VIEW_TYPE_MASK_PRIVATE_SPACE_HEADER = + VIEW_TYPE_PRIVATE_SPACE_HEADER; + protected final SearchAdapterProvider mAdapterProvider; /** @@ -196,6 +199,9 @@ public abstract class BaseAllAppsAdapter ex case VIEW_TYPE_WORK_DISABLED_CARD: return new ViewHolder(mLayoutInflater.inflate( R.layout.work_apps_paused, parent, false)); + case VIEW_TYPE_PRIVATE_SPACE_HEADER: + return new ViewHolder(mLayoutInflater.inflate( + R.layout.private_space_header, parent, false)); default: if (mAdapterProvider.isViewSupported(viewType)) { return mAdapterProvider.onCreateViewHolder(mLayoutInflater, parent, viewType); @@ -223,6 +229,7 @@ public abstract class BaseAllAppsAdapter ex } break; } + case VIEW_TYPE_PRIVATE_SPACE_HEADER: case VIEW_TYPE_ALL_APPS_DIVIDER: case VIEW_TYPE_WORK_DISABLED_CARD: // nothing to do diff --git a/src/com/android/launcher3/allapps/PrivateAppsSectionDecorator.java b/src/com/android/launcher3/allapps/PrivateAppsSectionDecorator.java new file mode 100644 index 0000000000..f7c90583ff --- /dev/null +++ b/src/com/android/launcher3/allapps/PrivateAppsSectionDecorator.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2023 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.launcher3.allapps; + +import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_ICON; +import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.view.View; + +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.launcher3.R; +import com.android.launcher3.pm.UserCache; +import com.android.launcher3.views.ActivityContext; + +/** + * Decorator which changes the background color for Private Space Icon Rows in AllAppsContainer. + */ +public class PrivateAppsSectionDecorator extends RecyclerView.ItemDecoration { + + private final Path mTmpPath = new Path(); + private final RectF mTmpRect = new RectF(); + private final Context mContext; + private final AlphabeticalAppsList mAppsList; + private final PrivateProfileManager mPrivateProfileManager; + private final UserCache mUserCache; + private final Paint mPaint; + + public PrivateAppsSectionDecorator(ActivityAllAppsContainerView appsContainerView, + AlphabeticalAppsList appsList, + PrivateProfileManager privateProfileManager) { + mAppsList = appsList; + mPrivateProfileManager = privateProfileManager; + mContext = appsContainerView.mActivityContext; + mUserCache = UserCache.getInstance(appsContainerView.mActivityContext); + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setColor(ContextCompat.getColor(mContext, + R.color.material_color_surface_container_high)); + } + + /** Decorates Private Space Header and Icon Rows to give the shape of a container. */ + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + mTmpPath.reset(); + mTmpRect.setEmpty(); + int numCol = ActivityContext.lookupContext(mContext).getDeviceProfile() + .numShownAllAppsColumns; + for (int i = 0; i < parent.getChildCount(); i++) { + View view = parent.getChildAt(i); + int position = parent.getChildAdapterPosition(view); + BaseAllAppsAdapter.AdapterItem adapterItem = mAppsList.getAdapterItems().get(position); + // Rectangle that covers the bottom half of the PS Header View when Space is unlocked. + if (adapterItem.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER + && mPrivateProfileManager + .getCurrentState() == PrivateProfileManager.STATE_ENABLED) { + // We flatten the bottom corners of the rectangle, so that it merges with + // the private space app row decorator. + mTmpRect.set( + view.getLeft(), + view.getTop() + (float) (view.getBottom() - view.getTop()) / 2, + view.getRight(), + view.getBottom()); + mTmpPath.addRect(mTmpRect, Path.Direction.CW); + c.drawPath(mTmpPath, mPaint); + } else if (adapterItem.viewType == VIEW_TYPE_ICON + && mUserCache.getUserInfo(adapterItem.itemInfo.user).isPrivate() + // No decoration for any private space app icon other than those at first row. + && adapterItem.rowAppIndex == 0) { + c.drawPath(getPrivateAppRowPath(parent, view, position, numCol), mPaint); + } + } + } + + /** Returns the path to be decorated for Private Space App Row */ + private Path getPrivateAppRowPath(RecyclerView parent, View iconView, int adapterPosition, + int numCol) { + // We always decorate the entire app row here. + // As the iconView just represents the first icon of the row, we get the right margin of + // our decorator using the parent view. + mTmpRect.set(iconView.getLeft(), + iconView.getTop(), + parent.getRight() - parent.getPaddingRight(), + iconView.getBottom()); + // Decorates last app row with rounded bottom corners. + if (adapterPosition + numCol >= mAppsList.getAdapterItems().size()) { + int corner = mContext.getResources().getDimensionPixelSize( + R.dimen.ps_container_corner_radius); + float[] mCornersBot = new float[]{0, 0, 0, 0, corner, corner, corner, corner}; + mTmpPath.addRoundRect(mTmpRect, mCornersBot, Path.Direction.CW); + } else { + // Decorate other rows as a plain rectangle + mTmpPath.addRect(mTmpRect, Path.Direction.CW); + } + return mTmpPath; + } +} diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java new file mode 100644 index 0000000000..ec01aeefaa --- /dev/null +++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2023 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.launcher3.allapps; + +import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN; +import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER; +import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED; + +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.UserHandle; +import android.os.UserManager; + +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.pm.UserCache; +import com.android.launcher3.util.Preconditions; + +import java.util.ArrayList; +import java.util.function.Predicate; + +/** + * Companion class for {@link ActivityAllAppsContainerView} to manage private space section related + * logic in the Personal tab. + */ +public class PrivateProfileManager extends UserProfileManager { + + private static final String SAFETY_CENTER_INTENT = Intent.ACTION_SAFETY_CENTER; + private static final String PS_SETTINGS_FRAGMENT_KEY = ":settings:fragment_args_key"; + private static final String PS_SETTINGS_FRAGMENT_VALUE = "AndroidPrivateSpace_personal"; + private final ActivityAllAppsContainerView mAllApps; + private final Predicate mPrivateProfileMatcher; + + public PrivateProfileManager(UserManager userManager, + UserCache userCache, + ActivityAllAppsContainerView allApps, + StatsLogManager statsLogManager) { + super(userManager, statsLogManager, userCache); + mAllApps = allApps; + mPrivateProfileMatcher = (user) -> userCache.getUserInfo(user).isPrivate(); + } + + /** Adds Private Space Header to the layout. */ + public int addPrivateSpaceHeader(ArrayList adapterItems) { + adapterItems.add(new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_PRIVATE_SPACE_HEADER)); + mAllApps.mAH.get(MAIN).mAdapter.notifyItemInserted(adapterItems.size() - 1); + return adapterItems.size(); + } + + /** Disables quiet mode for Private Space User Profile. */ + public void unlockPrivateProfile() { + // TODO (b/302666597): Log this event to WW. + enableQuietMode(false); + } + + /** Enables quiet mode for Private Space User Profile. */ + public void lockPrivateProfile() { + // TODO (b/302666597): Log this event to WW. + enableQuietMode(true); + } + + /** Whether private profile should be hidden on Launcher. */ + public boolean isPrivateSpaceHidden() { + // TODO (b/289223923): Update this when we are able to read PsSettingsFlag + // from SettingsProvider. + return false; + } + + /** Resets the current state of Private Profile, w.r.t. to Launcher. */ + public void reset() { + boolean isEnabled = !mAllApps.getAppsStore() + .hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED); + int updatedState = isEnabled ? STATE_ENABLED : STATE_DISABLED; + setCurrentState(updatedState); + } + + /** Opens the Private Space Settings Entry Point. */ + public void openPrivateSpaceSettings() { + // TODO (b/302666597): Log this event to WW. + Intent psSettingsIntent = new Intent(SAFETY_CENTER_INTENT); + psSettingsIntent.putExtra(PS_SETTINGS_FRAGMENT_KEY, PS_SETTINGS_FRAGMENT_VALUE); + mAllApps.getContext().startActivity(psSettingsIntent); + } + + /** + * Whether Private Space Settings Entry Point should be made visible. */ + public boolean isPrivateSpaceSettingsButtonVisible() { + Preconditions.assertNonUiThread(); + Intent psSettingsIntent = new Intent(SAFETY_CENTER_INTENT); + psSettingsIntent.putExtra(PS_SETTINGS_FRAGMENT_KEY, PS_SETTINGS_FRAGMENT_VALUE); + ResolveInfo resolveInfo = mAllApps.getContext().getPackageManager() + .resolveActivity(psSettingsIntent, PackageManager.MATCH_SYSTEM_ONLY); + return resolveInfo != null; + } + + /** Posts quiet mode enable/disable call for private profile. */ + private void enableQuietMode(boolean enable) { + setQuietMode(enable); + } + + @Override + public Predicate getUserMatcher() { + return mPrivateProfileMatcher; + } +} diff --git a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java new file mode 100644 index 0000000000..9420b4c038 --- /dev/null +++ b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2023 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.launcher3.allapps; + +import static com.android.launcher3.allapps.PrivateProfileManager.STATE_DISABLED; +import static com.android.launcher3.allapps.PrivateProfileManager.STATE_ENABLED; +import static com.android.launcher3.allapps.PrivateProfileManager.STATE_TRANSITION; + +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +import com.android.launcher3.R; +import com.android.launcher3.allapps.UserProfileManager.UserProfileState; + +/** + * Controller which returns views to be added to Private Space Header based upon + * {@link UserProfileState} + */ +public class PrivateSpaceHeaderViewController { + private final PrivateProfileManager mPrivateProfileManager; + + public PrivateSpaceHeaderViewController(PrivateProfileManager privateProfileManager) { + this.mPrivateProfileManager = privateProfileManager; + } + + /** Add Private Space Header view elements based upon {@link UserProfileState} */ + public void addPrivateSpaceHeaderViewElements(RelativeLayout parent) { + //Add quietMode image and action for lock/unlock button + ImageButton quietModeButton = parent.findViewById(R.id.ps_lock_unlock_button); + assert quietModeButton != null; + addQuietModeButton(quietModeButton); + + //Add image and action for private space settings button + ImageButton settingsButton = parent.findViewById(R.id.ps_settings_button); + assert settingsButton != null; + addPrivateSpaceSettingsButton(settingsButton); + + //Add image for private space transitioning view + ImageView transitionView = parent.findViewById(R.id.ps_transition_image); + assert transitionView != null; + addTransitionImage(transitionView); + } + + private void addQuietModeButton(ImageButton quietModeButton) { + switch (mPrivateProfileManager.getCurrentState()) { + case STATE_ENABLED -> { + quietModeButton.setVisibility(View.VISIBLE); + quietModeButton.setImageResource(R.drawable.bg_ps_lock_button); + quietModeButton.setOnClickListener( + view -> mPrivateProfileManager.lockPrivateProfile()); + } + case STATE_DISABLED -> { + quietModeButton.setVisibility(View.VISIBLE); + quietModeButton.setImageResource(R.drawable.bg_ps_unlock_button); + quietModeButton.setOnClickListener( + view -> mPrivateProfileManager.unlockPrivateProfile()); + } + default -> quietModeButton.setVisibility(View.GONE); + } + } + + private void addPrivateSpaceSettingsButton(ImageButton settingsButton) { + if (mPrivateProfileManager.getCurrentState() == STATE_ENABLED + && mPrivateProfileManager.isPrivateSpaceSettingsButtonVisible()) { + settingsButton.setVisibility(View.VISIBLE); + settingsButton.setOnClickListener(view -> + mPrivateProfileManager.openPrivateSpaceSettings()); + } else { + settingsButton.setVisibility(View.GONE); + } + } + + private void addTransitionImage(ImageView transitionImage) { + if (mPrivateProfileManager.getCurrentState() == STATE_TRANSITION) { + transitionImage.setVisibility(View.VISIBLE); + } else { + transitionImage.setVisibility(View.GONE); + } + } +} diff --git a/src/com/android/launcher3/allapps/UserProfileManager.java b/src/com/android/launcher3/allapps/UserProfileManager.java new file mode 100644 index 0000000000..0261010d8b --- /dev/null +++ b/src/com/android/launcher3/allapps/UserProfileManager.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2023 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.launcher3.allapps; + +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + +import android.os.UserHandle; +import android.os.UserManager; + +import androidx.annotation.IntDef; +import androidx.annotation.VisibleForTesting; + +import com.android.launcher3.Utilities; +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.pm.UserCache; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.function.Predicate; + +/** + * A Generic User Profile Manager which abstract outs the common functionality required + * by user-profiles supported by Launcher + *

+ * Concrete impls are + * {@link WorkProfileManager} which manages work profile state + * {@link PrivateProfileManager} which manages private profile state. + */ +public abstract class UserProfileManager { + public static final int STATE_ENABLED = 1; + public static final int STATE_DISABLED = 2; + public static final int STATE_TRANSITION = 3; + + @IntDef(value = { + STATE_ENABLED, + STATE_DISABLED, + STATE_TRANSITION + }) + @Retention(RetentionPolicy.SOURCE) + public @interface UserProfileState { } + + @UserProfileState + private int mCurrentState; + + private final UserManager mUserManager; + private final StatsLogManager mStatsLogManager; + private final UserCache mUserCache; + + protected UserProfileManager(UserManager userManager, + StatsLogManager statsLogManager, + UserCache userCache) { + mUserManager = userManager; + mStatsLogManager = statsLogManager; + mUserCache = userCache; + } + + /** Sets quiet mode as enabled/disabled for the profile type. */ + protected void setQuietMode(boolean enabled) { + if (Utilities.ATLEAST_P) { + UI_HELPER_EXECUTOR.post(() -> { + mUserCache.getUserProfiles() + .stream() + .filter(getUserMatcher()) + .findFirst() + .ifPresent(userHandle -> + mUserManager.requestQuietModeEnabled(enabled, userHandle)); + }); + } + } + + /** Sets current state for the profile type. */ + protected void setCurrentState(int state) { + mCurrentState = state; + } + + /** Returns current state for the profile type. */ + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + public int getCurrentState() { + return mCurrentState; + } + + /** Logs Event to StatsLogManager. */ + protected void logEvents(StatsLogManager.EventEnum event) { + mStatsLogManager.logger().log(event); + } + + /** Returns the matcher corresponding to profile type. */ + protected abstract Predicate getUserMatcher(); + + /** Returns the matcher corresponding to the profile type associated with ItemInfo. */ + protected Predicate getItemInfoMatcher() { + return itemInfo -> itemInfo != null && getUserMatcher().test(itemInfo.user); + } +} diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java index 61c3d3f76b..c430a36279 100644 --- a/src/com/android/launcher3/allapps/WorkProfileManager.java +++ b/src/com/android/launcher3/allapps/WorkProfileManager.java @@ -26,18 +26,14 @@ import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCU import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_CHANGE_PERMISSION; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED; -import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; -import android.os.Build; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; import android.view.View; -import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.Flags; @@ -46,12 +42,9 @@ import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; import com.android.launcher3.logging.StatsLogManager; -import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.pm.UserCache; import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.function.Predicate; import java.util.stream.Stream; @@ -59,62 +52,29 @@ import java.util.stream.Stream; /** * Companion class for {@link ActivityAllAppsContainerView} to manage work tab and personal tab * related - * logic based on {@link WorkProfileState}? + * logic based on {@link UserProfileState}? */ -public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActivePageChangedListener { +public class WorkProfileManager extends UserProfileManager + implements PersonalWorkSlidingTabStrip.OnActivePageChangedListener { private static final String TAG = "WorkProfileManager"; - - public static final int STATE_ENABLED = 1; - public static final int STATE_DISABLED = 2; - public static final int STATE_TRANSITION = 3; - - /** - * Work profile manager states - */ - @IntDef(value = { - STATE_ENABLED, - STATE_DISABLED, - STATE_TRANSITION - }) - @Retention(RetentionPolicy.SOURCE) - public @interface WorkProfileState { } - - private final UserManager mUserManager; private final ActivityAllAppsContainerView mAllApps; - private final Predicate mMatcher; - private final StatsLogManager mStatsLogManager; - private WorkModeSwitch mWorkModeSwitch; - - private final UserCache mUserCache; - - @WorkProfileState - private int mCurrentState; + private final Predicate mWorkProfileMatcher; public WorkProfileManager( UserManager userManager, ActivityAllAppsContainerView allApps, StatsLogManager statsLogManager, UserCache userCache) { - mUserManager = userManager; + super(userManager, statsLogManager, userCache); mAllApps = allApps; - mStatsLogManager = statsLogManager; - mUserCache = userCache; - mMatcher = info -> info != null && mUserCache.getUserInfo(info.user).isWork(); + mWorkProfileMatcher = (user) -> userCache.getUserInfo(user).isWork(); } /** * Posts quite mode enable/disable call for work profile user */ - @RequiresApi(Build.VERSION_CODES.P) public void setWorkProfileEnabled(boolean enabled) { - updateCurrentState(STATE_TRANSITION); - UI_HELPER_EXECUTOR.post(() -> { - for (UserHandle userProfile : mUserCache.getUserProfiles()) { - if (mUserCache.getUserInfo(userProfile).isWork()) { - mUserManager.requestQuietModeEnabled(!enabled, userProfile); - break; - } - } - }); + setCurrentState(STATE_TRANSITION); + setQuietMode(!enabled); } @Override @@ -126,7 +86,7 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP if (mWorkModeSwitch != null) { if (page == MAIN || page == SEARCH) { mWorkModeSwitch.animateVisibility(false); - } else if (page == WORK && mCurrentState == STATE_ENABLED) { + } else if (page == WORK && getCurrentState() == STATE_ENABLED) { mWorkModeSwitch.animateVisibility(true); } } @@ -151,17 +111,17 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP } } - private void updateCurrentState(@WorkProfileState int currentState) { - mCurrentState = currentState; + private void updateCurrentState(@UserProfileState int currentState) { + setCurrentState(currentState); if (getAH() != null) { getAH().mAppsList.updateAdapterItems(); } if (mWorkModeSwitch != null) { updateWorkFAB(mAllApps.getCurrentPage()); } - if (mCurrentState == STATE_ENABLED) { + if (getCurrentState() == STATE_ENABLED) { attachWorkModeSwitch(); - } else if (mCurrentState == STATE_DISABLED) { + } else if (getCurrentState() == STATE_DISABLED) { detachWorkModeSwitch(); } } @@ -201,10 +161,6 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP mWorkModeSwitch = null; } - public Predicate getMatcher() { - return mMatcher; - } - @Nullable public WorkModeSwitch getWorkModeSwitch() { return mWorkModeSwitch; @@ -214,29 +170,25 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP return mAllApps.mAH.get(WORK); } - public int getCurrentState() { - return mCurrentState; - } - /** * returns whether or not work apps should be visible in work tab. */ public boolean shouldShowWorkApps() { - return mCurrentState != WorkProfileManager.STATE_DISABLED; + return getCurrentState() != WorkProfileManager.STATE_DISABLED; } public boolean hasWorkApps() { - return Stream.of(mAllApps.getAppsStore().getApps()).anyMatch(mMatcher); + return Stream.of(mAllApps.getAppsStore().getApps()).anyMatch(getItemInfoMatcher()); } /** * Adds work profile specific adapter items to adapterItems and returns number of items added */ public int addWorkItems(ArrayList adapterItems) { - if (mCurrentState == WorkProfileManager.STATE_DISABLED) { + if (getCurrentState() == WorkProfileManager.STATE_DISABLED) { //add disabled card here. adapterItems.add(new AdapterItem(VIEW_TYPE_WORK_DISABLED_CARD)); - } else if (mCurrentState == WorkProfileManager.STATE_ENABLED && !isEduSeen()) { + } else if (getCurrentState() == WorkProfileManager.STATE_ENABLED && !isEduSeen()) { adapterItems.add(new AdapterItem(VIEW_TYPE_WORK_EDU_CARD)); } return adapterItems.size(); @@ -247,8 +199,9 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP } private void onWorkFabClicked(View view) { - if (Utilities.ATLEAST_P && mCurrentState == STATE_ENABLED && mWorkModeSwitch.isEnabled()) { - mStatsLogManager.logger().log(LAUNCHER_TURN_OFF_WORK_APPS_TAP); + if (Utilities.ATLEAST_P && getCurrentState() == STATE_ENABLED + && mWorkModeSwitch.isEnabled()) { + logEvents(LAUNCHER_TURN_OFF_WORK_APPS_TAP); setWorkProfileEnabled(false); } } @@ -279,4 +232,9 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP } }; } + + @Override + public Predicate getUserMatcher() { + return mWorkProfileMatcher; + } } diff --git a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java new file mode 100644 index 0000000000..bfa92410c4 --- /dev/null +++ b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2023 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.launcher3.allapps; + +import static com.android.launcher3.allapps.UserProfileManager.STATE_DISABLED; +import static com.android.launcher3.allapps.UserProfileManager.STATE_ENABLED; +import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.Intent; +import android.os.Process; +import android.os.UserHandle; +import android.os.UserManager; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.pm.UserCache; +import com.android.launcher3.util.UserIconInfo; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; + +@RunWith(AndroidJUnit4.class) +public class PrivateProfileManagerTest { + + private static final UserHandle MAIN_HANDLE = Process.myUserHandle(); + private static final UserHandle PRIVATE_HANDLE = new UserHandle(11); + private static final UserIconInfo MAIN_ICON_INFO = + new UserIconInfo(MAIN_HANDLE, UserIconInfo.TYPE_MAIN); + private static final UserIconInfo PRIVATE_ICON_INFO = + new UserIconInfo(PRIVATE_HANDLE, UserIconInfo.TYPE_PRIVATE); + private static final String SAFETY_CENTER_INTENT = Intent.ACTION_SAFETY_CENTER; + private static final String PS_SETTINGS_FRAGMENT_KEY = ":settings:fragment_args_key"; + private static final String PS_SETTINGS_FRAGMENT_VALUE = "AndroidPrivateSpace_personal"; + + private PrivateProfileManager mPrivateProfileManager; + @Mock + private ActivityAllAppsContainerView mActivityAllAppsContainerView; + @Mock + private StatsLogManager mStatsLogManager; + @Mock + private UserCache mUserCache; + @Mock + private UserManager mUserManager; + @Mock + private Context mContext; + @Mock + private AllAppsStore mAllAppsStore; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mUserCache.getUserProfiles()) + .thenReturn(Arrays.asList(MAIN_HANDLE, PRIVATE_HANDLE)); + when(mUserCache.getUserInfo(Process.myUserHandle())).thenReturn(MAIN_ICON_INFO); + when(mUserCache.getUserInfo(PRIVATE_HANDLE)).thenReturn(PRIVATE_ICON_INFO); + when(mActivityAllAppsContainerView.getContext()).thenReturn(mContext); + when(mActivityAllAppsContainerView.getAppsStore()).thenReturn(mAllAppsStore); + mPrivateProfileManager = new PrivateProfileManager(mUserManager, mUserCache, + mActivityAllAppsContainerView, mStatsLogManager); + } + + @Test + public void lockPrivateProfile_requestsQuietModeAsTrue() throws Exception { + when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED)).thenReturn(false); + + mPrivateProfileManager.lockPrivateProfile(); + + awaitTasksCompleted(); + Mockito.verify(mUserManager).requestQuietModeEnabled(true, PRIVATE_HANDLE); + } + + @Test + public void unlockPrivateProfile_requestsQuietModeAsFalse() throws Exception { + when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED)).thenReturn(true); + + mPrivateProfileManager.unlockPrivateProfile(); + + awaitTasksCompleted(); + Mockito.verify(mUserManager).requestQuietModeEnabled(false, PRIVATE_HANDLE); + } + + @Test + public void quietModeFlagPresent_privateSpaceIsResetToDisabled() { + when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED)) + .thenReturn(false, true); + + // In first call the state should be disabled. + mPrivateProfileManager.reset(); + assertEquals(STATE_ENABLED, mPrivateProfileManager.getCurrentState()); + + // In the next call the state should be disabled. + mPrivateProfileManager.reset(); + assertEquals(STATE_DISABLED, mPrivateProfileManager.getCurrentState()); + } + + @Test + public void openPrivateSpaceSettings_triggersSecurityAndPrivacyIntent() { + Intent expectedIntent = new Intent(SAFETY_CENTER_INTENT); + expectedIntent.putExtra(PS_SETTINGS_FRAGMENT_KEY, PS_SETTINGS_FRAGMENT_VALUE); + ArgumentCaptor acIntent = ArgumentCaptor.forClass(Intent.class); + + mPrivateProfileManager.openPrivateSpaceSettings(); + + Mockito.verify(mContext).startActivity(acIntent.capture()); + Intent actualIntent = acIntent.getValue(); + assertEquals(expectedIntent.getAction(), actualIntent.getAction()); + assertEquals(expectedIntent.getStringExtra(PS_SETTINGS_FRAGMENT_KEY), + actualIntent.getStringExtra(PS_SETTINGS_FRAGMENT_KEY)); + } + + private static void awaitTasksCompleted() throws Exception { + UI_HELPER_EXECUTOR.submit(() -> null).get(); + } +} diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java new file mode 100644 index 0000000000..87adaa1dec --- /dev/null +++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2023 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.launcher3.allapps; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; + +import static com.android.launcher3.allapps.UserProfileManager.STATE_DISABLED; +import static com.android.launcher3.allapps.UserProfileManager.STATE_ENABLED; +import static com.android.launcher3.allapps.UserProfileManager.STATE_TRANSITION; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.launcher3.R; +import com.android.launcher3.util.ActivityContextWrapper; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class PrivateSpaceHeaderViewControllerTest { + + private static final int CONTAINER_HEADER_ELEMENT_COUNT = 1; + private static final int LOCK_UNLOCK_BUTTON_COUNT = 1; + private static final int PS_SETTINGS_BUTTON_COUNT_VISIBLE = 1; + private static final int PS_SETTINGS_BUTTON_COUNT_INVISIBLE = 0; + private static final int PS_TRANSITION_IMAGE_COUNT = 1; + + private Context mContext; + private LayoutInflater mLayoutInflater; + private PrivateSpaceHeaderViewController mPsHeaderViewController; + private RelativeLayout mPsHeaderLayout; + @Mock + private PrivateProfileManager mPrivateProfileManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = new ActivityContextWrapper(getApplicationContext()); + mLayoutInflater = LayoutInflater.from(getApplicationContext()); + mPsHeaderViewController = new PrivateSpaceHeaderViewController(mPrivateProfileManager); + mPsHeaderLayout = (RelativeLayout) mLayoutInflater.inflate(R.layout.private_space_header, + null); + } + + @Test + public void privateProfileDisabled_psHeaderContainsLockedView() { + Bitmap unlockButton = getBitmap(mContext.getDrawable(R.drawable.bg_ps_unlock_button)); + when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_DISABLED); + + mPsHeaderViewController.addPrivateSpaceHeaderViewElements(mPsHeaderLayout); + + int totalContainerHeaderView = 0; + int totalLockUnlockButtonView = 0; + for (int i = 0; i < mPsHeaderLayout.getChildCount(); i++) { + View view = mPsHeaderLayout.getChildAt(i); + if (view.getId() == R.id.ps_container_header) { + totalContainerHeaderView += 1; + assertEquals(View.VISIBLE, view.getVisibility()); + } else if (view.getId() == R.id.ps_lock_unlock_button + && view instanceof ImageView imageView) { + totalLockUnlockButtonView += 1; + assertEquals(View.VISIBLE, view.getVisibility()); + getBitmap(imageView.getDrawable()).sameAs(unlockButton); + } else { + assertEquals(View.GONE, view.getVisibility()); + } + } + assertEquals(CONTAINER_HEADER_ELEMENT_COUNT, totalContainerHeaderView); + assertEquals(LOCK_UNLOCK_BUTTON_COUNT, totalLockUnlockButtonView); + } + + @Test + public void privateProfileEnabled_psHeaderContainsUnlockedView() { + Bitmap lockImage = getBitmap(mContext.getDrawable(R.drawable.bg_ps_lock_button)); + Bitmap settingsImage = getBitmap(mContext.getDrawable(R.drawable.bg_ps_settings_button)); + when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED); + when(mPrivateProfileManager.isPrivateSpaceSettingsButtonVisible()).thenReturn(true); + + mPsHeaderViewController.addPrivateSpaceHeaderViewElements(mPsHeaderLayout); + + int totalContainerHeaderView = 0; + int totalLockUnlockButtonView = 0; + int totalSettingsImageView = 0; + for (int i = 0; i < mPsHeaderLayout.getChildCount(); i++) { + View view = mPsHeaderLayout.getChildAt(i); + if (view.getId() == R.id.ps_container_header) { + totalContainerHeaderView += 1; + assertEquals(View.VISIBLE, view.getVisibility()); + } else if (view.getId() == R.id.ps_lock_unlock_button + && view instanceof ImageView imageView) { + totalLockUnlockButtonView += 1; + assertEquals(View.VISIBLE, view.getVisibility()); + getBitmap(imageView.getDrawable()).sameAs(lockImage); + } else if (view.getId() == R.id.ps_settings_button + && view instanceof ImageView imageView) { + totalSettingsImageView += 1; + assertEquals(View.VISIBLE, view.getVisibility()); + getBitmap(imageView.getDrawable()).sameAs(settingsImage); + } else { + assertEquals(View.GONE, view.getVisibility()); + } + } + assertEquals(CONTAINER_HEADER_ELEMENT_COUNT, totalContainerHeaderView); + assertEquals(LOCK_UNLOCK_BUTTON_COUNT, totalLockUnlockButtonView); + assertEquals(PS_SETTINGS_BUTTON_COUNT_VISIBLE, totalSettingsImageView); + } + + @Test + public void privateProfileEnabledAndNoSettingsIntent_psHeaderContainsUnlockedView() { + Bitmap lockImage = getBitmap(mContext.getDrawable(R.drawable.bg_ps_lock_button)); + when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED); + when(mPrivateProfileManager.isPrivateSpaceSettingsButtonVisible()).thenReturn(false); + + mPsHeaderViewController.addPrivateSpaceHeaderViewElements(mPsHeaderLayout); + + int totalContainerHeaderView = 0; + int totalLockUnlockButtonView = 0; + int totalSettingsImageView = 0; + for (int i = 0; i < mPsHeaderLayout.getChildCount(); i++) { + View view = mPsHeaderLayout.getChildAt(i); + if (view.getId() == R.id.ps_container_header) { + totalContainerHeaderView += 1; + assertEquals(View.VISIBLE, view.getVisibility()); + } else if (view.getId() == R.id.ps_lock_unlock_button + && view instanceof ImageView imageView) { + totalLockUnlockButtonView += 1; + assertEquals(View.VISIBLE, view.getVisibility()); + getBitmap(imageView.getDrawable()).sameAs(lockImage); + } else { + assertEquals(View.GONE, view.getVisibility()); + } + } + assertEquals(CONTAINER_HEADER_ELEMENT_COUNT, totalContainerHeaderView); + assertEquals(LOCK_UNLOCK_BUTTON_COUNT, totalLockUnlockButtonView); + assertEquals(PS_SETTINGS_BUTTON_COUNT_INVISIBLE, totalSettingsImageView); + } + + @Test + public void privateProfileTransitioning_psHeaderContainsTransitionView() { + Bitmap transitionImage = getBitmap(mContext.getDrawable(R.drawable.bg_ps_transition_image)); + when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_TRANSITION); + + mPsHeaderViewController.addPrivateSpaceHeaderViewElements(mPsHeaderLayout); + + int totalContainerHeaderView = 0; + int totalLockUnlockButtonView = 0; + for (int i = 0; i < mPsHeaderLayout.getChildCount(); i++) { + View view = mPsHeaderLayout.getChildAt(i); + if (view.getId() == R.id.ps_container_header) { + totalContainerHeaderView += 1; + assertEquals(View.VISIBLE, view.getVisibility()); + } else if (view.getId() == R.id.ps_transition_image + && view instanceof ImageView imageView) { + totalLockUnlockButtonView += 1; + assertEquals(View.VISIBLE, view.getVisibility()); + getBitmap(imageView.getDrawable()).sameAs(transitionImage); + } else { + assertEquals(View.GONE, view.getVisibility()); + } + } + assertEquals(CONTAINER_HEADER_ELEMENT_COUNT, totalContainerHeaderView); + assertEquals(PS_TRANSITION_IMAGE_COUNT, totalLockUnlockButtonView); + } + + private Bitmap getBitmap(Drawable drawable) { + Bitmap result; + if (drawable instanceof BitmapDrawable) { + result = ((BitmapDrawable) drawable).getBitmap(); + } else { + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + // Some drawables have no intrinsic width - e.g. solid colours. + if (width <= 0) { + width = 1; + } + if (height <= 0) { + height = 1; + } + + result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(result); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + } + return result; + } +}