1 /* 2 * Copyright (C) 2020 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 17 package com.android.documentsui; 18 19 import static androidx.core.util.Preconditions.checkNotNull; 20 21 import static com.android.documentsui.DevicePolicyResources.Strings.PERSONAL_TAB; 22 import static com.android.documentsui.DevicePolicyResources.Strings.WORK_TAB; 23 24 import android.app.admin.DevicePolicyManager; 25 import android.os.Build; 26 import android.os.UserManager; 27 import android.view.View; 28 import android.view.ViewGroup; 29 30 import androidx.annotation.Nullable; 31 import androidx.annotation.RequiresApi; 32 33 import com.android.documentsui.base.RootInfo; 34 import com.android.documentsui.base.State; 35 import com.android.documentsui.base.UserId; 36 import com.android.modules.utils.build.SdkLevel; 37 38 import com.google.android.material.tabs.TabLayout; 39 import com.google.common.base.Objects; 40 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.List; 44 import java.util.Map; 45 46 /** 47 * A manager class to control UI on a {@link TabLayout} for cross-profile purpose. 48 */ 49 public class ProfileTabs implements ProfileTabsAddons { 50 private static final float DISABLED_TAB_OPACITY = 0.38f; 51 52 private final View mTabsContainer; 53 private final TabLayout mTabs; 54 private final State mState; 55 private final NavigationViewManager.Environment mEnv; 56 private final AbstractActionHandler.CommonAddons mCommonAddons; 57 @Nullable 58 private final UserIdManager mUserIdManager; 59 @Nullable 60 private final UserManagerState mUserManagerState; 61 private final ConfigStore mConfigStore; 62 private List<UserId> mUserIds; 63 @Nullable 64 private Listener mListener; 65 private TabLayout.OnTabSelectedListener mOnTabSelectedListener; 66 private View mTabSeparator; 67 ProfileTabs(View tabLayoutContainer, State state, UserIdManager userIdManager, NavigationViewManager.Environment env, AbstractActionHandler.CommonAddons commonAddons, ConfigStore configStore)68 public ProfileTabs(View tabLayoutContainer, State state, UserIdManager userIdManager, 69 NavigationViewManager.Environment env, 70 AbstractActionHandler.CommonAddons commonAddons, ConfigStore configStore) { 71 this(tabLayoutContainer, state, userIdManager, null, env, commonAddons, configStore); 72 } 73 ProfileTabs(View tabLayoutContainer, State state, UserManagerState userManagerState, NavigationViewManager.Environment env, AbstractActionHandler.CommonAddons commonAddons, ConfigStore configStore)74 public ProfileTabs(View tabLayoutContainer, State state, UserManagerState userManagerState, 75 NavigationViewManager.Environment env, 76 AbstractActionHandler.CommonAddons commonAddons, ConfigStore configStore) { 77 this(tabLayoutContainer, state, null, userManagerState, env, commonAddons, configStore); 78 } 79 ProfileTabs(View tabLayoutContainer, State state, @Nullable UserIdManager userIdManager, @Nullable UserManagerState userManagerState, NavigationViewManager.Environment env, AbstractActionHandler.CommonAddons commonAddons, ConfigStore configStore)80 public ProfileTabs(View tabLayoutContainer, State state, @Nullable UserIdManager userIdManager, 81 @Nullable UserManagerState userManagerState, NavigationViewManager.Environment env, 82 AbstractActionHandler.CommonAddons commonAddons, ConfigStore configStore) { 83 mTabsContainer = checkNotNull(tabLayoutContainer); 84 mTabs = tabLayoutContainer.findViewById(R.id.tabs); 85 mState = checkNotNull(state); 86 mEnv = checkNotNull(env); 87 mCommonAddons = checkNotNull(commonAddons); 88 mConfigStore = configStore; 89 if (mConfigStore.isPrivateSpaceInDocsUIEnabled()) { 90 mUserIdManager = userIdManager; 91 mUserManagerState = checkNotNull(userManagerState); 92 } else { 93 mUserIdManager = checkNotNull(userIdManager); 94 mUserManagerState = userManagerState; 95 } 96 mTabs.removeAllTabs(); 97 mUserIds = Collections.singletonList(UserId.CURRENT_USER); 98 mTabSeparator = tabLayoutContainer.findViewById(R.id.tab_separator); 99 100 mOnTabSelectedListener = new TabLayout.OnTabSelectedListener() { 101 @Override 102 public void onTabSelected(TabLayout.Tab tab) { 103 if (mListener != null) { 104 // find a way to identify user iteraction 105 mListener.onUserSelected((UserId) tab.getTag()); 106 } 107 } 108 109 @Override 110 public void onTabUnselected(TabLayout.Tab tab) { 111 } 112 113 @Override 114 public void onTabReselected(TabLayout.Tab tab) { 115 } 116 }; 117 mTabs.addOnTabSelectedListener(mOnTabSelectedListener); 118 } 119 120 /** 121 * Update the tab layout based on status of availability of the hidden profile. 122 */ updateView()123 public void updateView() { 124 updateTabsIfNeeded(); 125 RootInfo currentRoot = mCommonAddons.getCurrentRoot(); 126 if (mTabs.getSelectedTabPosition() == -1 127 || !Objects.equal(currentRoot.userId, getSelectedUser())) { 128 // Update the layout according to the current root if necessary. 129 // Make sure we do not invoke callback. Otherwise, it is likely to cause infinite loop. 130 mTabs.removeOnTabSelectedListener(mOnTabSelectedListener); 131 mTabs.selectTab(mTabs.getTabAt(mUserIds.indexOf(currentRoot.userId))); 132 mTabs.addOnTabSelectedListener(mOnTabSelectedListener); 133 } 134 mTabsContainer.setVisibility(shouldShow() ? View.VISIBLE : View.GONE); 135 136 // Material next changes apply only for version S or greater 137 if (SdkLevel.isAtLeastS()) { 138 mTabSeparator.setVisibility(View.GONE); 139 int tabContainerHeightInDp = (int) mTabsContainer.getContext().getResources() 140 .getDimension(R.dimen.tab_container_height); 141 mTabsContainer.getLayoutParams().height = tabContainerHeightInDp; 142 ViewGroup.MarginLayoutParams tabContainerMarginLayoutParams = 143 (ViewGroup.MarginLayoutParams) mTabsContainer.getLayoutParams(); 144 int tabContainerMarginTop = (int) mTabsContainer.getContext().getResources() 145 .getDimension(R.dimen.profile_tab_margin_top); 146 tabContainerMarginLayoutParams.setMargins(0, tabContainerMarginTop, 0, 0); 147 mTabsContainer.requestLayout(); 148 for (int i = 0; i < mTabs.getTabCount(); i++) { 149 150 // Tablayout holds a view that contains the individual tab 151 View tab = ((ViewGroup) mTabs.getChildAt(0)).getChildAt(i); 152 153 // Get individual tab to set the style 154 ViewGroup.MarginLayoutParams marginLayoutParams = 155 (ViewGroup.MarginLayoutParams) tab.getLayoutParams(); 156 int tabMarginSide = (int) mTabsContainer.getContext().getResources() 157 .getDimension(R.dimen.profile_tab_margin_side); 158 marginLayoutParams.setMargins(tabMarginSide, 0, tabMarginSide, 0); 159 int tabHeightInDp = (int) mTabsContainer.getContext().getResources() 160 .getDimension(R.dimen.tab_height); 161 tab.getLayoutParams().height = tabHeightInDp; 162 tab.requestLayout(); 163 tab.setBackgroundResource(R.drawable.tab_border_rounded); 164 } 165 166 } 167 } 168 setListener(@ullable Listener listener)169 public void setListener(@Nullable Listener listener) { 170 mListener = listener; 171 } 172 updateTabsIfNeeded()173 private void updateTabsIfNeeded() { 174 List<UserId> userIds = getUserIds(); 175 // Add tabs if the userIds is not equals to cached mUserIds. 176 // Given that mUserIds was initialized with only the current user, if getUserIds() 177 // returns just the current user, we don't need to do anything on the tab layout. 178 if (!userIds.equals(mUserIds)) { 179 mUserIds = new ArrayList<>(); 180 mUserIds.addAll(userIds); 181 mTabs.removeAllTabs(); 182 if (mUserIds.size() > 1) { 183 if (mConfigStore.isPrivateSpaceInDocsUIEnabled() && SdkLevel.isAtLeastS()) { 184 addTabsPrivateSpaceEnabled(); 185 } else { 186 addTabsPrivateSpaceDisabled(); 187 } 188 } 189 } 190 } 191 getUserIds()192 private List<UserId> getUserIds() { 193 if (mConfigStore.isPrivateSpaceInDocsUIEnabled() && SdkLevel.isAtLeastS()) { 194 assert mUserManagerState != null; 195 return mUserManagerState.getUserIds(); 196 } 197 assert mUserIdManager != null; 198 return mUserIdManager.getUserIds(); 199 } 200 201 @RequiresApi(Build.VERSION_CODES.S) addTabsPrivateSpaceEnabled()202 private void addTabsPrivateSpaceEnabled() { 203 // set setSelected to false otherwise it will trigger callback. 204 assert mUserManagerState != null; 205 Map<UserId, String> userIdToLabelMap = mUserManagerState.getUserIdToLabelMap(); 206 UserManager userManager = mTabsContainer.getContext().getSystemService(UserManager.class); 207 assert userManager != null; 208 for (UserId userId : mUserIds) { 209 mTabs.addTab(createTab(userIdToLabelMap.get(userId), userId), /* setSelected= */false); 210 } 211 } 212 addTabsPrivateSpaceDisabled()213 private void addTabsPrivateSpaceDisabled() { 214 // set setSelected to false otherwise it will trigger callback. 215 assert mUserIdManager != null; 216 mTabs.addTab(createTab( 217 getEnterpriseString(PERSONAL_TAB, R.string.personal_tab), 218 mUserIdManager.getSystemUser()), /* setSelected= */false); 219 mTabs.addTab(createTab( 220 getEnterpriseString(WORK_TAB, R.string.work_tab), 221 mUserIdManager.getManagedUser()), /* setSelected= */false); 222 } 223 getEnterpriseString(String updatableStringId, int defaultStringId)224 private String getEnterpriseString(String updatableStringId, int defaultStringId) { 225 if (SdkLevel.isAtLeastT()) { 226 return getUpdatableEnterpriseString(updatableStringId, defaultStringId); 227 } else { 228 return mTabsContainer.getContext().getString(defaultStringId); 229 } 230 } 231 232 @RequiresApi(Build.VERSION_CODES.TIRAMISU) getUpdatableEnterpriseString(String updatableStringId, int defaultStringId)233 private String getUpdatableEnterpriseString(String updatableStringId, int defaultStringId) { 234 DevicePolicyManager dpm = mTabsContainer.getContext().getSystemService( 235 DevicePolicyManager.class); 236 return dpm.getResources().getString( 237 updatableStringId, 238 () -> mTabsContainer.getContext().getString(defaultStringId)); 239 } 240 241 /** 242 * Returns the user represented by the selected tab. If there is no tab, return the 243 * current user. 244 */ getSelectedUser()245 public UserId getSelectedUser() { 246 if (mTabs.getTabCount() > 1 && mTabs.getSelectedTabPosition() >= 0) { 247 return (UserId) mTabs.getTabAt(mTabs.getSelectedTabPosition()).getTag(); 248 } 249 return UserId.CURRENT_USER; 250 } 251 shouldShow()252 private boolean shouldShow() { 253 // Only show tabs when: 254 // 1. state supports cross profile, and 255 // 2. more than one tab, and 256 // 3. not in search mode, and 257 // 4. not in sub-folder, and 258 // 5. the root supports cross profile. 259 return mState.supportsCrossProfile() 260 && mTabs.getTabCount() > 1 261 && !mEnv.isSearchExpanded() 262 && mState.stack.size() <= 1 263 && mState.stack.getRoot() != null && mState.stack.getRoot().supportsCrossProfile(); 264 } 265 createTab(String text, UserId userId)266 private TabLayout.Tab createTab(String text, UserId userId) { 267 return mTabs.newTab().setText(text).setTag(userId); 268 } 269 270 @Override setEnabled(boolean enabled)271 public void setEnabled(boolean enabled) { 272 if (mTabs.getChildCount() > 0) { 273 View view = mTabs.getChildAt(0); 274 if (view instanceof ViewGroup) { 275 ViewGroup tabs = (ViewGroup) view; 276 for (int i = 0; i < tabs.getChildCount(); i++) { 277 View tabView = tabs.getChildAt(i); 278 tabView.setEnabled(enabled); 279 tabView.setAlpha((enabled || mTabs.getSelectedTabPosition() == i) ? 1f 280 : DISABLED_TAB_OPACITY); 281 } 282 } 283 } 284 } 285 286 /** 287 * Interface definition for a callback to be invoked. 288 */ 289 interface Listener { 290 /** 291 * Called when a user tab has been selected. 292 */ onUserSelected(UserId userId)293 void onUserSelected(UserId userId); 294 } 295 } 296