1 /*
2  * Copyright (C) 2017 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.settings.applications.specialaccess.pictureinpicture;
17 
18 import static android.content.pm.PackageManager.GET_ACTIVITIES;
19 
20 import android.app.settings.SettingsEnums;
21 import android.content.Context;
22 import android.content.pm.ActivityInfo;
23 import android.content.pm.ApplicationInfo;
24 import android.content.pm.PackageInfo;
25 import android.content.pm.PackageManager;
26 import android.content.pm.UserInfo;
27 import android.os.Bundle;
28 import android.os.UserHandle;
29 import android.os.UserManager;
30 import android.util.IconDrawableFactory;
31 import android.util.Pair;
32 import android.view.View;
33 
34 import androidx.annotation.Nullable;
35 import androidx.annotation.VisibleForTesting;
36 import androidx.preference.Preference;
37 import androidx.preference.Preference.OnPreferenceClickListener;
38 import androidx.preference.PreferenceScreen;
39 
40 import com.android.settings.R;
41 import com.android.settings.Utils;
42 import com.android.settings.applications.AppInfoBase;
43 import com.android.settings.search.BaseSearchIndexProvider;
44 import com.android.settings.widget.EmptyTextSettings;
45 import com.android.settingslib.search.SearchIndexable;
46 import com.android.settingslib.widget.AppPreference;
47 
48 import java.text.Collator;
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.Comparator;
52 import java.util.List;
53 
54 @SearchIndexable
55 public class PictureInPictureSettings extends EmptyTextSettings {
56 
57     @VisibleForTesting
58     static final List<String> IGNORE_PACKAGE_LIST = new ArrayList<>();
59 
60     static {
61         IGNORE_PACKAGE_LIST.add(Utils.SYSTEMUI_PACKAGE_NAME);
62     }
63 
64     /**
65      * Comparator by name, then user id.
66      * {@see PackageItemInfo#DisplayNameComparator}
67      */
68     static class AppComparator implements Comparator<Pair<ApplicationInfo, Integer>> {
69 
70         private final Collator mCollator = Collator.getInstance();
71         private final PackageManager mPm;
72 
AppComparator(PackageManager pm)73         public AppComparator(PackageManager pm) {
74             mPm = pm;
75         }
76 
compare(Pair<ApplicationInfo, Integer> a, Pair<ApplicationInfo, Integer> b)77         public final int compare(Pair<ApplicationInfo, Integer> a,
78                 Pair<ApplicationInfo, Integer> b) {
79             CharSequence sa = a.first.loadLabel(mPm);
80             if (sa == null) sa = a.first.name;
81             CharSequence sb = b.first.loadLabel(mPm);
82             if (sb == null) sb = b.first.name;
83             int nameCmp = mCollator.compare(sa.toString(), sb.toString());
84             if (nameCmp != 0) {
85                 return nameCmp;
86             } else {
87                 return a.second - b.second;
88             }
89         }
90     }
91 
92     private Context mContext;
93     private PackageManager mPackageManager;
94     private UserManager mUserManager;
95     private IconDrawableFactory mIconDrawableFactory;
96 
97     /**
98      * @return true if the package has any activities that declare that they support
99      * picture-in-picture.
100      */
101 
checkPackageHasPictureInPictureActivities(String packageName, ActivityInfo[] activities)102     public static boolean checkPackageHasPictureInPictureActivities(String packageName,
103             ActivityInfo[] activities) {
104         // Skip if it's in the ignored list
105         if (IGNORE_PACKAGE_LIST.contains(packageName)) {
106             return false;
107         }
108 
109         // Iterate through all the activities and check if it is resizeable and supports
110         // picture-in-picture
111         if (activities != null) {
112             for (int i = activities.length - 1; i >= 0; i--) {
113                 if (activities[i].supportsPictureInPicture()) {
114                     return true;
115                 }
116             }
117         }
118         return false;
119     }
120 
PictureInPictureSettings()121     public PictureInPictureSettings() {
122         // Do nothing
123     }
124 
PictureInPictureSettings(PackageManager pm, UserManager um)125     public PictureInPictureSettings(PackageManager pm, UserManager um) {
126         mPackageManager = pm;
127         mUserManager = um;
128     }
129 
130     @Override
onCreate(Bundle icicle)131     public void onCreate(Bundle icicle) {
132         super.onCreate(icicle);
133 
134         mContext = getActivity();
135         mPackageManager = mContext.getPackageManager();
136         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
137         mIconDrawableFactory = IconDrawableFactory.newInstance(mContext);
138     }
139 
140     @Override
onResume()141     public void onResume() {
142         super.onResume();
143 
144         // Clear the prefs
145         final PreferenceScreen screen = getPreferenceScreen();
146         screen.removeAll();
147 
148         // Fetch the set of applications for each profile which have at least one activity that
149         // declare that they support picture-in-picture
150         final ArrayList<Pair<ApplicationInfo, Integer>> pipApps =
151                 collectPipApps(UserHandle.myUserId());
152         Collections.sort(pipApps, new AppComparator(mPackageManager));
153 
154         // Rebuild the list of prefs
155         final Context prefContext = getPrefContext();
156         for (final Pair<ApplicationInfo, Integer> appData : pipApps) {
157             final ApplicationInfo appInfo = appData.first;
158             final int userId = appData.second;
159             final UserHandle user = UserHandle.of(userId);
160             final String packageName = appInfo.packageName;
161             final CharSequence label = appInfo.loadLabel(mPackageManager);
162 
163             final Preference pref = new AppPreference(prefContext);
164             pref.setIcon(mIconDrawableFactory.getBadgedIcon(appInfo, userId));
165             pref.setTitle(mPackageManager.getUserBadgedLabel(label, user));
166             pref.setSummary(PictureInPictureDetails.getPreferenceSummary(prefContext,
167                     appInfo.uid, packageName));
168             pref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
169                 @Override
170                 public boolean onPreferenceClick(Preference preference) {
171                     AppInfoBase.startAppInfoFragment(PictureInPictureDetails.class,
172                             getString(R.string.picture_in_picture_app_detail_title),
173                             packageName, appInfo.uid,
174                             PictureInPictureSettings.this, -1, getMetricsCategory());
175                     return true;
176                 }
177             });
178             screen.addPreference(pref);
179         }
180     }
181 
182     @Override
onViewCreated(View view, @Nullable Bundle savedInstanceState)183     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
184         super.onViewCreated(view, savedInstanceState);
185         setEmptyText(R.string.picture_in_picture_empty_text);
186     }
187 
188     @Override
getPreferenceScreenResId()189     protected int getPreferenceScreenResId() {
190         return R.xml.picture_in_picture_settings;
191     }
192 
193     @Override
getMetricsCategory()194     public int getMetricsCategory() {
195         return SettingsEnums.SETTINGS_MANAGE_PICTURE_IN_PICTURE;
196     }
197 
198     /**
199      * @return the list of applications for the given user and all their profiles that have
200      * activities which support PiP.
201      */
collectPipApps(int userId)202     ArrayList<Pair<ApplicationInfo, Integer>> collectPipApps(int userId) {
203         final ArrayList<Pair<ApplicationInfo, Integer>> pipApps = new ArrayList<>();
204         final ArrayList<Integer> userIds = new ArrayList<>();
205         for (UserInfo user : mUserManager.getProfiles(userId)) {
206             userIds.add(user.id);
207         }
208 
209         for (int id : userIds) {
210             final List<PackageInfo> installedPackages = mPackageManager.getInstalledPackagesAsUser(
211                     GET_ACTIVITIES, id);
212             for (PackageInfo packageInfo : installedPackages) {
213                 if (checkPackageHasPictureInPictureActivities(packageInfo.packageName,
214                         packageInfo.activities)) {
215                     pipApps.add(new Pair<>(packageInfo.applicationInfo, id));
216                 }
217             }
218         }
219         return pipApps;
220     }
221 
222     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
223             new BaseSearchIndexProvider(R.xml.picture_in_picture_settings);
224 }
225