1 /*
2  * Copyright (C) 2019 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.settings.dashboard.profileselector;
18 
19 import static android.app.admin.DevicePolicyResources.Strings.Settings.PERSONAL_CATEGORY_HEADER;
20 import static android.app.admin.DevicePolicyResources.Strings.Settings.PRIVATE_CATEGORY_HEADER;
21 import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_CATEGORY_HEADER;
22 import static android.content.Intent.EXTRA_USER_ID;
23 
24 import android.annotation.IntDef;
25 import android.app.Activity;
26 import android.app.admin.DevicePolicyManager;
27 import android.content.Context;
28 import android.content.pm.UserInfo;
29 import android.os.Bundle;
30 import android.os.Flags;
31 import android.os.UserHandle;
32 import android.os.UserManager;
33 import android.util.Log;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.widget.FrameLayout;
38 import android.widget.LinearLayout;
39 
40 import androidx.annotation.NonNull;
41 import androidx.annotation.Nullable;
42 import androidx.annotation.VisibleForTesting;
43 import androidx.fragment.app.Fragment;
44 import androidx.fragment.app.FragmentManager;
45 import androidx.lifecycle.Lifecycle;
46 import androidx.recyclerview.widget.RecyclerView;
47 import androidx.viewpager2.adapter.FragmentStateAdapter;
48 import androidx.viewpager2.widget.ViewPager2;
49 
50 import com.android.settings.R;
51 import com.android.settings.SettingsActivity;
52 import com.android.settings.Utils;
53 import com.android.settings.dashboard.DashboardFragment;
54 import com.android.settings.privatespace.PrivateSpaceMaintainer;
55 
56 import com.google.android.material.tabs.TabLayout;
57 import com.google.android.material.tabs.TabLayoutMediator;
58 
59 import java.lang.annotation.Retention;
60 import java.lang.annotation.RetentionPolicy;
61 import java.util.ArrayList;
62 import java.util.List;
63 
64 /**
65  * Base fragment class for profile settings.
66  */
67 public abstract class ProfileSelectFragment extends DashboardFragment {
68 
69     private static final String TAG = "ProfileSelectFragment";
70 
71     /**
72      * Denotes the profile type.
73      */
74     @Retention(RetentionPolicy.SOURCE)
75     @IntDef({
76             ProfileType.PERSONAL,
77             ProfileType.WORK,
78             ProfileType.ALL
79     })
80     public @interface ProfileType {
81         /**
82          * It is personal work profile.
83          */
84         int PERSONAL = 1;
85 
86         /**
87          * It is work profile
88          */
89         int WORK = 1 << 1;
90 
91         /**
92          * It is private profile
93          */
94         int PRIVATE = 1 << 2;
95 
96         /**
97          * It is personal, work, and private profile
98          */
99         int ALL = PERSONAL | WORK | PRIVATE;
100     }
101 
102     /**
103      * Used in fragment argument and pass {@link ProfileType} to it
104      */
105     public static final String EXTRA_PROFILE = "profile";
106 
107     /**
108      * Used in fragment argument with Extra key {@link SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB}
109      */
110     public static final int PERSONAL_TAB = 0;
111 
112     /**
113      * Used in fragment argument with Extra key {@link SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB}
114      */
115     public static final int WORK_TAB = 1;
116 
117     /**
118      * Used in fragment argument with Extra key {@link SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB}
119      */
120     public static final int PRIVATE_TAB = 2;
121 
122     private ViewGroup mContentView;
123 
124     private ViewPager2 mViewPager;
125 
126     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)127     public View onCreateView(LayoutInflater inflater, ViewGroup container,
128             Bundle savedInstanceState) {
129         mContentView = (ViewGroup) super.onCreateView(inflater, container, savedInstanceState);
130         final Activity activity = getActivity();
131         final int titleResId = getTitleResId();
132         if (titleResId > 0) {
133             activity.setTitle(titleResId);
134         }
135 
136         final View tabContainer = mContentView.findViewById(R.id.tab_container);
137         mViewPager = tabContainer.findViewById(R.id.view_pager);
138         mViewPager.setAdapter(new ProfileSelectFragment.ViewPagerAdapter(this));
139         final TabLayout tabs = tabContainer.findViewById(R.id.tabs);
140         new TabLayoutMediator(tabs, mViewPager,
141                 (tab, position) -> tab.setText(getPageTitle(position))
142         ).attach();
143         mViewPager.registerOnPageChangeCallback(
144                 new ViewPager2.OnPageChangeCallback() {
145                     @Override
146                     public void onPageSelected(int position) {
147                         super.onPageSelected(position);
148                         updateHeight(position);
149                     }
150                 }
151         );
152         tabContainer.setVisibility(View.VISIBLE);
153         final int selectedTab = getTabId(activity, getArguments());
154         final TabLayout.Tab tab = tabs.getTabAt(selectedTab);
155         tab.select();
156 
157         final FrameLayout listContainer = mContentView.findViewById(android.R.id.list_container);
158         listContainer.setLayoutParams(new LinearLayout.LayoutParams(
159                 ViewGroup.LayoutParams.MATCH_PARENT,
160                 ViewGroup.LayoutParams.WRAP_CONTENT));
161 
162         final RecyclerView recyclerView = getListView();
163         recyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
164         Utils.setActionBarShadowAnimation(activity, getSettingsLifecycle(), recyclerView);
165 
166         return mContentView;
167     }
168 
forceUpdateHeight()169     protected boolean forceUpdateHeight() {
170         return false;
171     }
172 
updateHeight(int position)173     private void updateHeight(int position) {
174         if (!forceUpdateHeight()) {
175             return;
176         }
177         ViewPagerAdapter adapter = (ViewPagerAdapter) mViewPager.getAdapter();
178         if (adapter == null || adapter.getItemCount() <= position) {
179             return;
180         }
181 
182         Fragment fragment = adapter.createFragment(position);
183         View newPage = fragment.getView();
184         if (newPage != null) {
185             int viewWidth = View.MeasureSpec.makeMeasureSpec(newPage.getWidth(),
186                     View.MeasureSpec.EXACTLY);
187             int viewHeight = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
188             newPage.measure(viewWidth, viewHeight);
189             int currentHeight = mViewPager.getLayoutParams().height;
190             int newHeight = newPage.getMeasuredHeight();
191             if (newHeight != 0 && currentHeight != newHeight) {
192                 ViewGroup.LayoutParams layoutParams = mViewPager.getLayoutParams();
193                 layoutParams.height = newHeight;
194                 mViewPager.setLayoutParams(layoutParams);
195             }
196         }
197     }
198 
199     @Override
getMetricsCategory()200     public int getMetricsCategory() {
201         return METRICS_CATEGORY_UNKNOWN;
202     }
203 
204     /**
205      * Returns an array of {@link Fragment} to display in the
206      * {@link com.google.android.material.tabs.TabLayout}
207      */
getFragments()208     public abstract Fragment[] getFragments();
209 
210     /**
211      * Returns a resource ID of the title
212      * Override this if the title needs to be updated dynamically.
213      */
getTitleResId()214     public int getTitleResId() {
215         return 0;
216     }
217 
218     @Override
getPreferenceScreenResId()219     protected int getPreferenceScreenResId() {
220         return R.xml.placeholder_preference_screen;
221     }
222 
223     @Override
getLogTag()224     protected String getLogTag() {
225         return TAG;
226     }
227 
228     @VisibleForTesting
getTabId(Activity activity, Bundle bundle)229     int getTabId(Activity activity, Bundle bundle) {
230         if (bundle != null) {
231             final int extraTab = bundle.getInt(SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB, -1);
232             if (extraTab != -1) {
233                 return ((ViewPagerAdapter) mViewPager.getAdapter())
234                         .getPositionForProfileTab(extraTab);
235             }
236             final UserManager userManager = getSystemService(UserManager.class);
237             UserHandle mainUser = userManager.getMainUser();
238             if (mainUser == null) {
239                 mainUser = UserHandle.SYSTEM;
240             }
241             final int userId = bundle.getInt(EXTRA_USER_ID, mainUser.getIdentifier());
242             final boolean isWorkProfile = UserManager.get(activity).isManagedProfile(userId);
243             if (isWorkProfile) {
244                 return WORK_TAB;
245             }
246             UserInfo userInfo = UserManager.get(activity).getUserInfo(userId);
247             if (Flags.allowPrivateProfile()
248                     && android.multiuser.Flags.enablePrivateSpaceFeatures()
249                     && userInfo != null && userInfo.isPrivateProfile()) {
250                 return PRIVATE_TAB;
251             }
252         }
253         // Start intent from a specific user eg: adb shell --user 10
254         final int intentUser = activity.getIntent().getContentUserHint();
255         if (UserManager.get(activity).isManagedProfile(intentUser)) {
256             return WORK_TAB;
257         }
258         UserInfo userInfo = UserManager.get(activity).getUserInfo(intentUser);
259         if (Flags.allowPrivateProfile()
260                 && android.multiuser.Flags.enablePrivateSpaceFeatures()
261                 && userInfo != null && userInfo.isPrivateProfile()) {
262             return PRIVATE_TAB;
263         }
264 
265         return PERSONAL_TAB;
266     }
267 
getPageTitle(int position)268     private CharSequence getPageTitle(int position) {
269         final DevicePolicyManager devicePolicyManager =
270                 getContext().getSystemService(DevicePolicyManager.class);
271 
272         if (Flags.allowPrivateProfile() && android.multiuser.Flags.enablePrivateSpaceFeatures()) {
273             int tabForPosition =
274                     ((ViewPagerAdapter) mViewPager.getAdapter()).getTabForPosition(position);
275 
276             if (tabForPosition == WORK_TAB) {
277                 return devicePolicyManager.getResources().getString(WORK_CATEGORY_HEADER,
278                         () -> getContext().getString(
279                                 com.android.settingslib.R.string.category_work));
280             }
281 
282             if (tabForPosition == PRIVATE_TAB) {
283                 return devicePolicyManager.getResources().getString(PRIVATE_CATEGORY_HEADER,
284                         () -> getContext()
285                                 .getString(com.android.settingslib.R.string.category_private));
286             }
287 
288         } else if (position == WORK_TAB) {
289             return devicePolicyManager.getResources().getString(WORK_CATEGORY_HEADER,
290                     () -> getContext().getString(com.android.settingslib.R.string.category_work));
291 
292         }
293         return devicePolicyManager.getResources().getString(PERSONAL_CATEGORY_HEADER,
294                 () -> getContext().getString(
295                         com.android.settingslib.R.string.category_personal));
296     }
297 
298     /** Creates fragments of passed types, and returns them in an array. */
getFragments( Context context, @Nullable Bundle bundle, FragmentConstructor personalFragmentConstructor, FragmentConstructor workFragmentConstructor, FragmentConstructor privateFragmentConstructor)299     @NonNull static Fragment[] getFragments(
300             Context context,
301             @Nullable Bundle bundle,
302             FragmentConstructor personalFragmentConstructor,
303             FragmentConstructor workFragmentConstructor,
304             FragmentConstructor privateFragmentConstructor) {
305         return getFragments(
306                 context,
307                 bundle,
308                 personalFragmentConstructor,
309                 workFragmentConstructor,
310                 privateFragmentConstructor,
311                 new PrivateSpaceInfoProvider() {});
312     }
313 
314     /**
315      * Creates fragments of passed types, and returns them in an array. This overload exists only
316      * for helping with testing.
317      */
getFragments( Context context, @Nullable Bundle bundle, FragmentConstructor personalFragmentConstructor, FragmentConstructor workFragmentConstructor, FragmentConstructor privateFragmentConstructor, PrivateSpaceInfoProvider privateSpaceInfoProvider)318     @NonNull static Fragment[] getFragments(
319             Context context,
320             @Nullable Bundle bundle,
321             FragmentConstructor personalFragmentConstructor,
322             FragmentConstructor workFragmentConstructor,
323             FragmentConstructor privateFragmentConstructor,
324             PrivateSpaceInfoProvider privateSpaceInfoProvider) {
325         Fragment[] result = new Fragment[0];
326         ArrayList<Fragment> fragments = new ArrayList<>();
327 
328         try {
329             UserManager userManager = context.getSystemService(UserManager.class);
330             List<UserInfo> userInfos = userManager.getProfiles(UserHandle.myUserId());
331 
332             for (UserInfo userInfo : userInfos) {
333                 if (userInfo.isMain()) {
334                     fragments.add(createAndGetFragment(
335                             ProfileType.PERSONAL,
336                             bundle != null ? bundle : new Bundle(),
337                             personalFragmentConstructor));
338                 } else if (userInfo.isManagedProfile()) {
339                     fragments.add(createAndGetFragment(
340                             ProfileType.WORK,
341                             bundle != null ? bundle.deepCopy() : new Bundle(),
342                             workFragmentConstructor));
343                 } else if (Flags.allowPrivateProfile()
344                         && android.multiuser.Flags.enablePrivateSpaceFeatures()
345                         && userInfo.isPrivateProfile()) {
346                     if (!privateSpaceInfoProvider.isPrivateSpaceLocked(context)) {
347                         fragments.add(createAndGetFragment(
348                                 ProfileType.PRIVATE,
349                                 bundle != null ? bundle.deepCopy() : new Bundle(),
350                                 privateFragmentConstructor));
351                     }
352                 } else {
353                     Log.d(TAG, "Not showing tab for unsupported user " + userInfo);
354                 }
355             }
356 
357             result = new Fragment[fragments.size()];
358             fragments.toArray(result);
359         } catch (Exception e) {
360             Log.e(TAG, "Failed to create fragment");
361         }
362 
363         return result;
364     }
365 
createAndGetFragment( @rofileType int profileType, Bundle bundle, FragmentConstructor fragmentConstructor)366     private static Fragment createAndGetFragment(
367             @ProfileType int profileType, Bundle bundle, FragmentConstructor fragmentConstructor) {
368         bundle.putInt(EXTRA_PROFILE, profileType);
369         final Fragment fragment = fragmentConstructor.constructAndGetFragment();
370         fragment.setArguments(bundle);
371         return fragment;
372     }
373 
374     @VisibleForTesting
setViewPager(ViewPager2 viewPager)375     void setViewPager(ViewPager2 viewPager) {
376         mViewPager = viewPager;
377     }
378 
379     interface FragmentConstructor {
constructAndGetFragment()380         Fragment constructAndGetFragment();
381     }
382 
383     interface PrivateSpaceInfoProvider {
isPrivateSpaceLocked(Context context)384         default boolean isPrivateSpaceLocked(Context context) {
385             return PrivateSpaceMaintainer.getInstance(context).isPrivateSpaceLocked();
386         }
387     }
388 
389     static class ViewPagerAdapter extends FragmentStateAdapter {
390 
391         private final Fragment[] mChildFragments;
392 
ViewPagerAdapter(ProfileSelectFragment fragment)393         ViewPagerAdapter(ProfileSelectFragment fragment) {
394             super(fragment);
395             mChildFragments = fragment.getFragments();
396         }
397 
398         @VisibleForTesting
ViewPagerAdapter( @onNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle, ProfileSelectFragment profileSelectFragment)399         ViewPagerAdapter(
400                 @NonNull FragmentManager fragmentManager,
401                 @NonNull Lifecycle lifecycle,
402                 ProfileSelectFragment profileSelectFragment) {
403             super(fragmentManager, lifecycle);
404             mChildFragments = profileSelectFragment.getFragments();
405         }
406 
407         @Override
createFragment(int position)408         public Fragment createFragment(int position) {
409             return mChildFragments[position];
410         }
411 
412         @Override
getItemCount()413         public int getItemCount() {
414             return mChildFragments.length;
415         }
416 
417         @VisibleForTesting
getTabForPosition(int position)418         int getTabForPosition(int position) {
419             if (position >= mChildFragments.length) {
420                 Log.e(TAG, "tab requested for out of bound position " + position);
421                 return PERSONAL_TAB;
422             }
423             @ProfileType
424             int profileType = mChildFragments[position].getArguments().getInt(EXTRA_PROFILE);
425             return profileTypeToTab(profileType);
426         }
427 
getPositionForProfileTab(int profileTab)428         private int getPositionForProfileTab(int profileTab) {
429             for (int i = 0; i < mChildFragments.length; ++i) {
430                 Bundle arguments = mChildFragments[i].getArguments();
431                 if (arguments != null
432                         && profileTypeToTab(arguments.getInt(EXTRA_PROFILE)) == profileTab) {
433                     return i;
434                 }
435             }
436             Log.e(TAG, "position requested for an unknown profile tab " + profileTab);
437             return 0;
438         }
439 
profileTypeToTab(@rofileType int profileType)440         private int profileTypeToTab(@ProfileType int profileType) {
441             if (profileType == ProfileType.WORK) {
442                 return WORK_TAB;
443             }
444             if (profileType == ProfileType.PRIVATE) {
445                 return PRIVATE_TAB;
446             }
447             return PERSONAL_TAB;
448         }
449     }
450 }
451