1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.allapps; 17 18 import static com.android.launcher3.LauncherPrefs.WORK_EDU_STEP; 19 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN; 20 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH; 21 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.WORK; 22 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_DISABLED_CARD; 23 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_EDU_CARD; 24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TURN_OFF_WORK_APPS_TAP; 25 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCUT_PERMISSION; 26 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_CHANGE_PERMISSION; 27 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED; 28 import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING; 29 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; 30 31 import android.os.Build; 32 import android.os.Process; 33 import android.os.UserHandle; 34 import android.os.UserManager; 35 import android.util.Log; 36 import android.view.View; 37 38 import androidx.annotation.IntDef; 39 import androidx.annotation.NonNull; 40 import androidx.annotation.Nullable; 41 import androidx.annotation.RequiresApi; 42 import androidx.recyclerview.widget.RecyclerView; 43 44 import com.android.launcher3.LauncherPrefs; 45 import com.android.launcher3.R; 46 import com.android.launcher3.Utilities; 47 import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; 48 import com.android.launcher3.logging.StatsLogManager; 49 import com.android.launcher3.model.data.ItemInfo; 50 import com.android.launcher3.testing.shared.TestProtocol; 51 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip; 52 53 import java.lang.annotation.Retention; 54 import java.lang.annotation.RetentionPolicy; 55 import java.util.ArrayList; 56 import java.util.function.Predicate; 57 58 /** 59 * Companion class for {@link ActivityAllAppsContainerView} to manage work tab and personal tab 60 * related 61 * logic based on {@link WorkProfileState}? 62 */ 63 public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActivePageChangedListener { 64 private static final String TAG = "WorkProfileManager"; 65 66 public static final String KEY_WORK_EDU_STEP = "showed_work_profile_edu"; 67 68 public static final int STATE_ENABLED = 1; 69 public static final int STATE_DISABLED = 2; 70 public static final int STATE_TRANSITION = 3; 71 72 /** 73 * Work profile manager states 74 */ 75 @IntDef(value = { 76 STATE_ENABLED, 77 STATE_DISABLED, 78 STATE_TRANSITION 79 }) 80 @Retention(RetentionPolicy.SOURCE) 81 public @interface WorkProfileState { } 82 83 private final UserManager mUserManager; 84 private final ActivityAllAppsContainerView<?> mAllApps; 85 private final Predicate<ItemInfo> mMatcher; 86 private final StatsLogManager mStatsLogManager; 87 88 private WorkModeSwitch mWorkModeSwitch; 89 90 @WorkProfileState 91 private int mCurrentState; 92 WorkProfileManager( UserManager userManager, ActivityAllAppsContainerView allApps, StatsLogManager statsLogManager)93 public WorkProfileManager( 94 UserManager userManager, ActivityAllAppsContainerView allApps, 95 StatsLogManager statsLogManager) { 96 mUserManager = userManager; 97 mAllApps = allApps; 98 mMatcher = mAllApps.mPersonalMatcher.negate(); 99 mStatsLogManager = statsLogManager; 100 } 101 102 /** 103 * Posts quite mode enable/disable call for work profile user 104 */ 105 @RequiresApi(Build.VERSION_CODES.P) setWorkProfileEnabled(boolean enabled)106 public void setWorkProfileEnabled(boolean enabled) { 107 updateCurrentState(STATE_TRANSITION); 108 UI_HELPER_EXECUTOR.post(() -> { 109 for (UserHandle userProfile : mUserManager.getUserProfiles()) { 110 if (Process.myUserHandle().equals(userProfile)) { 111 continue; 112 } 113 mUserManager.requestQuietModeEnabled(!enabled, userProfile); 114 } 115 }); 116 } 117 118 @Override onActivePageChanged(int page)119 public void onActivePageChanged(int page) { 120 updateWorkFAB(page); 121 } 122 updateWorkFAB(int page)123 private void updateWorkFAB(int page) { 124 if (mWorkModeSwitch != null) { 125 if (page == MAIN || page == SEARCH) { 126 mWorkModeSwitch.animateVisibility(false); 127 } else if (page == WORK && mCurrentState == STATE_ENABLED) { 128 mWorkModeSwitch.animateVisibility(true); 129 } 130 } 131 } 132 133 /** 134 * Requests work profile state from {@link AllAppsStore} and updates work profile related views 135 */ reset()136 public void reset() { 137 boolean isEnabled = !mAllApps.getAppsStore().hasModelFlag(FLAG_QUIET_MODE_ENABLED); 138 updateCurrentState(isEnabled ? STATE_ENABLED : STATE_DISABLED); 139 } 140 updateCurrentState(@orkProfileState int currentState)141 private void updateCurrentState(@WorkProfileState int currentState) { 142 if (TestProtocol.sDebugTracing) { 143 Log.d(WORK_TAB_MISSING, "WorkProfileManager#updateCurrentState: " + 144 currentState, new Throwable()); 145 } 146 mCurrentState = currentState; 147 if (getAH() != null) { 148 getAH().mAppsList.updateAdapterItems(); 149 } 150 if (mWorkModeSwitch != null) { 151 updateWorkFAB(mAllApps.getCurrentPage()); 152 } 153 if (mCurrentState == STATE_ENABLED) { 154 attachWorkModeSwitch(); 155 } else if (mCurrentState == STATE_DISABLED) { 156 detachWorkModeSwitch(); 157 } 158 } 159 160 /** 161 * Creates and attaches for profile toggle button to {@link ActivityAllAppsContainerView} 162 */ attachWorkModeSwitch()163 public boolean attachWorkModeSwitch() { 164 if (TestProtocol.sDebugTracing) { 165 Log.d(WORK_TAB_MISSING, "ActivityAllAppsContainerView#attachWorkModeSwitch " 166 + "mWorkModeSwitch: " + mWorkModeSwitch); 167 } 168 if (!mAllApps.getAppsStore().hasModelFlag( 169 FLAG_HAS_SHORTCUT_PERMISSION | FLAG_QUIET_MODE_CHANGE_PERMISSION)) { 170 Log.e(TAG, "unable to attach work mode switch; Missing required permissions"); 171 return false; 172 } 173 if (mWorkModeSwitch == null) { 174 mWorkModeSwitch = (WorkModeSwitch) mAllApps.getLayoutInflater().inflate( 175 R.layout.work_mode_fab, mAllApps, false); 176 } 177 if (mWorkModeSwitch.getParent() == null) { 178 mAllApps.addView(mWorkModeSwitch); 179 } 180 if (mAllApps.getCurrentPage() != WORK) { 181 mWorkModeSwitch.animateVisibility(false); 182 } 183 if (getAH() != null) { 184 getAH().applyPadding(); 185 } 186 mWorkModeSwitch.setOnClickListener(this::onWorkFabClicked); 187 return true; 188 } 189 /** 190 * Removes work profile toggle button from {@link ActivityAllAppsContainerView} 191 */ detachWorkModeSwitch()192 public void detachWorkModeSwitch() { 193 if (mWorkModeSwitch != null && mWorkModeSwitch.getParent() == mAllApps) { 194 mAllApps.removeView(mWorkModeSwitch); 195 } 196 mWorkModeSwitch = null; 197 } 198 getMatcher()199 public Predicate<ItemInfo> getMatcher() { 200 return mMatcher; 201 } 202 203 @Nullable getWorkModeSwitch()204 public WorkModeSwitch getWorkModeSwitch() { 205 return mWorkModeSwitch; 206 } 207 getAH()208 private ActivityAllAppsContainerView.AdapterHolder getAH() { 209 return mAllApps.mAH.get(WORK); 210 } 211 getCurrentState()212 public int getCurrentState() { 213 return mCurrentState; 214 } 215 216 /** 217 * returns whether or not work apps should be visible in work tab. 218 */ shouldShowWorkApps()219 public boolean shouldShowWorkApps() { 220 return mCurrentState != WorkProfileManager.STATE_DISABLED; 221 } 222 223 /** 224 * Adds work profile specific adapter items to adapterItems and returns number of items added 225 */ addWorkItems(ArrayList<AdapterItem> adapterItems)226 public int addWorkItems(ArrayList<AdapterItem> adapterItems) { 227 if (mCurrentState == WorkProfileManager.STATE_DISABLED) { 228 //add disabled card here. 229 adapterItems.add(new AdapterItem(VIEW_TYPE_WORK_DISABLED_CARD)); 230 } else if (mCurrentState == WorkProfileManager.STATE_ENABLED && !isEduSeen()) { 231 adapterItems.add(new AdapterItem(VIEW_TYPE_WORK_EDU_CARD)); 232 } 233 return adapterItems.size(); 234 } 235 isEduSeen()236 private boolean isEduSeen() { 237 return LauncherPrefs.get(mAllApps.getContext()).get(WORK_EDU_STEP) != 0; 238 } 239 onWorkFabClicked(View view)240 private void onWorkFabClicked(View view) { 241 if (Utilities.ATLEAST_P && mCurrentState == STATE_ENABLED && mWorkModeSwitch.isEnabled()) { 242 mStatsLogManager.logger().log(LAUNCHER_TURN_OFF_WORK_APPS_TAP); 243 setWorkProfileEnabled(false); 244 } 245 } 246 newScrollListener()247 public RecyclerView.OnScrollListener newScrollListener() { 248 return new RecyclerView.OnScrollListener() { 249 int totalDelta = 0; 250 @Override 251 public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState){ 252 if (newState == RecyclerView.SCROLL_STATE_IDLE) { 253 totalDelta = 0; 254 } 255 } 256 @Override 257 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 258 WorkModeSwitch fab = getWorkModeSwitch(); 259 if (fab == null){ 260 return; 261 } 262 totalDelta = Utilities.boundToRange(totalDelta, 263 -fab.getScrollThreshold(), fab.getScrollThreshold()) + dy; 264 boolean isScrollAtTop = recyclerView.computeVerticalScrollOffset() == 0; 265 if ((isScrollAtTop || totalDelta < -fab.getScrollThreshold())) { 266 fab.extend(); 267 } else if (totalDelta > fab.getScrollThreshold()) { 268 fab.shrink(); 269 } 270 } 271 }; 272 } 273 } 274