/* * Copyright (C) 2020 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.documentsui; import static androidx.core.util.Preconditions.checkNotNull; import static com.android.documentsui.DevicePolicyResources.Strings.PERSONAL_TAB; import static com.android.documentsui.DevicePolicyResources.Strings.WORK_TAB; import android.app.admin.DevicePolicyManager; import android.os.Build; import android.os.UserManager; import android.view.View; import android.view.ViewGroup; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.State; import com.android.documentsui.base.UserId; import com.android.modules.utils.build.SdkLevel; import com.google.android.material.tabs.TabLayout; import com.google.common.base.Objects; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; /** * A manager class to control UI on a {@link TabLayout} for cross-profile purpose. */ public class ProfileTabs implements ProfileTabsAddons { private static final float DISABLED_TAB_OPACITY = 0.38f; private final View mTabsContainer; private final TabLayout mTabs; private final State mState; private final NavigationViewManager.Environment mEnv; private final AbstractActionHandler.CommonAddons mCommonAddons; @Nullable private final UserIdManager mUserIdManager; @Nullable private final UserManagerState mUserManagerState; private final ConfigStore mConfigStore; private List mUserIds; @Nullable private Listener mListener; private TabLayout.OnTabSelectedListener mOnTabSelectedListener; private View mTabSeparator; public ProfileTabs(View tabLayoutContainer, State state, UserIdManager userIdManager, NavigationViewManager.Environment env, AbstractActionHandler.CommonAddons commonAddons, ConfigStore configStore) { this(tabLayoutContainer, state, userIdManager, null, env, commonAddons, configStore); } public ProfileTabs(View tabLayoutContainer, State state, UserManagerState userManagerState, NavigationViewManager.Environment env, AbstractActionHandler.CommonAddons commonAddons, ConfigStore configStore) { this(tabLayoutContainer, state, null, userManagerState, env, commonAddons, configStore); } public ProfileTabs(View tabLayoutContainer, State state, @Nullable UserIdManager userIdManager, @Nullable UserManagerState userManagerState, NavigationViewManager.Environment env, AbstractActionHandler.CommonAddons commonAddons, ConfigStore configStore) { mTabsContainer = checkNotNull(tabLayoutContainer); mTabs = tabLayoutContainer.findViewById(R.id.tabs); mState = checkNotNull(state); mEnv = checkNotNull(env); mCommonAddons = checkNotNull(commonAddons); mConfigStore = configStore; if (mConfigStore.isPrivateSpaceInDocsUIEnabled()) { mUserIdManager = userIdManager; mUserManagerState = checkNotNull(userManagerState); } else { mUserIdManager = checkNotNull(userIdManager); mUserManagerState = userManagerState; } mTabs.removeAllTabs(); mUserIds = Collections.singletonList(UserId.CURRENT_USER); mTabSeparator = tabLayoutContainer.findViewById(R.id.tab_separator); mOnTabSelectedListener = new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { if (mListener != null) { // find a way to identify user iteraction mListener.onUserSelected((UserId) tab.getTag()); } } @Override public void onTabUnselected(TabLayout.Tab tab) { } @Override public void onTabReselected(TabLayout.Tab tab) { } }; mTabs.addOnTabSelectedListener(mOnTabSelectedListener); } /** * Update the tab layout based on status of availability of the hidden profile. */ public void updateView() { updateTabsIfNeeded(); RootInfo currentRoot = mCommonAddons.getCurrentRoot(); if (mTabs.getSelectedTabPosition() == -1 || !Objects.equal(currentRoot.userId, getSelectedUser())) { // Update the layout according to the current root if necessary. // Make sure we do not invoke callback. Otherwise, it is likely to cause infinite loop. mTabs.removeOnTabSelectedListener(mOnTabSelectedListener); mTabs.selectTab(mTabs.getTabAt(mUserIds.indexOf(currentRoot.userId))); mTabs.addOnTabSelectedListener(mOnTabSelectedListener); } mTabsContainer.setVisibility(shouldShow() ? View.VISIBLE : View.GONE); // Material next changes apply only for version S or greater if (SdkLevel.isAtLeastS()) { mTabSeparator.setVisibility(View.GONE); int tabContainerHeightInDp = (int) mTabsContainer.getContext().getResources() .getDimension(R.dimen.tab_container_height); mTabsContainer.getLayoutParams().height = tabContainerHeightInDp; ViewGroup.MarginLayoutParams tabContainerMarginLayoutParams = (ViewGroup.MarginLayoutParams) mTabsContainer.getLayoutParams(); int tabContainerMarginTop = (int) mTabsContainer.getContext().getResources() .getDimension(R.dimen.profile_tab_margin_top); tabContainerMarginLayoutParams.setMargins(0, tabContainerMarginTop, 0, 0); mTabsContainer.requestLayout(); for (int i = 0; i < mTabs.getTabCount(); i++) { // Tablayout holds a view that contains the individual tab View tab = ((ViewGroup) mTabs.getChildAt(0)).getChildAt(i); // Get individual tab to set the style ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) tab.getLayoutParams(); int tabMarginSide = (int) mTabsContainer.getContext().getResources() .getDimension(R.dimen.profile_tab_margin_side); marginLayoutParams.setMargins(tabMarginSide, 0, tabMarginSide, 0); int tabHeightInDp = (int) mTabsContainer.getContext().getResources() .getDimension(R.dimen.tab_height); tab.getLayoutParams().height = tabHeightInDp; tab.requestLayout(); tab.setBackgroundResource(R.drawable.tab_border_rounded); } } } public void setListener(@Nullable Listener listener) { mListener = listener; } private void updateTabsIfNeeded() { List userIds = getUserIds(); // Add tabs if the userIds is not equals to cached mUserIds. // Given that mUserIds was initialized with only the current user, if getUserIds() // returns just the current user, we don't need to do anything on the tab layout. if (!userIds.equals(mUserIds)) { mUserIds = new ArrayList<>(); mUserIds.addAll(userIds); mTabs.removeAllTabs(); if (mUserIds.size() > 1) { if (mConfigStore.isPrivateSpaceInDocsUIEnabled() && SdkLevel.isAtLeastS()) { addTabsPrivateSpaceEnabled(); } else { addTabsPrivateSpaceDisabled(); } } } } private List getUserIds() { if (mConfigStore.isPrivateSpaceInDocsUIEnabled() && SdkLevel.isAtLeastS()) { assert mUserManagerState != null; return mUserManagerState.getUserIds(); } assert mUserIdManager != null; return mUserIdManager.getUserIds(); } @RequiresApi(Build.VERSION_CODES.S) private void addTabsPrivateSpaceEnabled() { // set setSelected to false otherwise it will trigger callback. assert mUserManagerState != null; Map userIdToLabelMap = mUserManagerState.getUserIdToLabelMap(); UserManager userManager = mTabsContainer.getContext().getSystemService(UserManager.class); assert userManager != null; for (UserId userId : mUserIds) { mTabs.addTab(createTab(userIdToLabelMap.get(userId), userId), /* setSelected= */false); } } private void addTabsPrivateSpaceDisabled() { // set setSelected to false otherwise it will trigger callback. assert mUserIdManager != null; mTabs.addTab(createTab( getEnterpriseString(PERSONAL_TAB, R.string.personal_tab), mUserIdManager.getSystemUser()), /* setSelected= */false); mTabs.addTab(createTab( getEnterpriseString(WORK_TAB, R.string.work_tab), mUserIdManager.getManagedUser()), /* setSelected= */false); } private String getEnterpriseString(String updatableStringId, int defaultStringId) { if (SdkLevel.isAtLeastT()) { return getUpdatableEnterpriseString(updatableStringId, defaultStringId); } else { return mTabsContainer.getContext().getString(defaultStringId); } } @RequiresApi(Build.VERSION_CODES.TIRAMISU) private String getUpdatableEnterpriseString(String updatableStringId, int defaultStringId) { DevicePolicyManager dpm = mTabsContainer.getContext().getSystemService( DevicePolicyManager.class); return dpm.getResources().getString( updatableStringId, () -> mTabsContainer.getContext().getString(defaultStringId)); } /** * Returns the user represented by the selected tab. If there is no tab, return the * current user. */ public UserId getSelectedUser() { if (mTabs.getTabCount() > 1 && mTabs.getSelectedTabPosition() >= 0) { return (UserId) mTabs.getTabAt(mTabs.getSelectedTabPosition()).getTag(); } return UserId.CURRENT_USER; } private boolean shouldShow() { // Only show tabs when: // 1. state supports cross profile, and // 2. more than one tab, and // 3. not in search mode, and // 4. not in sub-folder, and // 5. the root supports cross profile. return mState.supportsCrossProfile() && mTabs.getTabCount() > 1 && !mEnv.isSearchExpanded() && mState.stack.size() <= 1 && mState.stack.getRoot() != null && mState.stack.getRoot().supportsCrossProfile(); } private TabLayout.Tab createTab(String text, UserId userId) { return mTabs.newTab().setText(text).setTag(userId); } @Override public void setEnabled(boolean enabled) { if (mTabs.getChildCount() > 0) { View view = mTabs.getChildAt(0); if (view instanceof ViewGroup) { ViewGroup tabs = (ViewGroup) view; for (int i = 0; i < tabs.getChildCount(); i++) { View tabView = tabs.getChildAt(i); tabView.setEnabled(enabled); tabView.setAlpha((enabled || mTabs.getSelectedTabPosition() == i) ? 1f : DISABLED_TAB_OPACITY); } } } } /** * Interface definition for a callback to be invoked. */ interface Listener { /** * Called when a user tab has been selected. */ void onUserSelected(UserId userId); } }