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.annotation.Nullable;
21 import android.app.settings.SettingsEnums;
22 import android.content.Context;
23 import android.content.pm.ActivityInfo;
24 import android.content.pm.ApplicationInfo;
25 import android.content.pm.PackageInfo;
26 import android.content.pm.PackageManager;
27 import android.content.pm.UserInfo;
28 import android.os.Bundle;
29 import android.os.UserHandle;
30 import android.os.UserManager;
31 import android.provider.SearchIndexableResource;
32 import android.util.IconDrawableFactory;
33 import android.util.Pair;
34 import android.view.View;
35 
36 import androidx.annotation.VisibleForTesting;
37 import androidx.preference.Preference;
38 import androidx.preference.Preference.OnPreferenceClickListener;
39 import androidx.preference.PreferenceScreen;
40 
41 import com.android.settings.R;
42 import com.android.settings.applications.AppInfoBase;
43 import com.android.settings.search.BaseSearchIndexProvider;
44 import com.android.settings.search.Indexable;
45 import com.android.settings.widget.EmptyTextSettings;
46 import com.android.settingslib.search.SearchIndexable;
47 import com.android.settingslib.widget.apppreference.AppPreference;
48 
49 import java.text.Collator;
50 import java.util.ArrayList;
51 import java.util.Collections;
52 import java.util.Comparator;
53 import java.util.List;
54 
55 @SearchIndexable
56 public class PictureInPictureSettings extends EmptyTextSettings {
57 
58     @VisibleForTesting
59     static final List<String> IGNORE_PACKAGE_LIST = new ArrayList<>();
60 
61     static {
62         IGNORE_PACKAGE_LIST.add("com.android.systemui");
63     }
64 
65     /**
66      * Comparator by name, then user id.
67      * {@see PackageItemInfo#DisplayNameComparator}
68      */
69     static class AppComparator implements Comparator<Pair<ApplicationInfo, Integer>> {
70 
71         private final Collator mCollator = Collator.getInstance();
72         private final PackageManager mPm;
73 
AppComparator(PackageManager pm)74         public AppComparator(PackageManager pm) {
75             mPm = pm;
76         }
77 
compare(Pair<ApplicationInfo, Integer> a, Pair<ApplicationInfo, Integer> b)78         public final int compare(Pair<ApplicationInfo, Integer> a,
79                 Pair<ApplicationInfo, Integer> b) {
80             CharSequence sa = a.first.loadLabel(mPm);
81             if (sa == null) sa = a.first.name;
82             CharSequence sb = b.first.loadLabel(mPm);
83             if (sb == null) sb = b.first.name;
84             int nameCmp = mCollator.compare(sa.toString(), sb.toString());
85             if (nameCmp != 0) {
86                 return nameCmp;
87             } else {
88                 return a.second - b.second;
89             }
90         }
91     }
92 
93     private Context mContext;
94     private PackageManager mPackageManager;
95     private UserManager mUserManager;
96     private IconDrawableFactory mIconDrawableFactory;
97 
98     /**
99      * @return true if the package has any activities that declare that they support
100      * picture-in-picture.
101      */
102 
checkPackageHasPictureInPictureActivities(String packageName, ActivityInfo[] activities)103     public static boolean checkPackageHasPictureInPictureActivities(String packageName,
104             ActivityInfo[] activities) {
105         // Skip if it's in the ignored list
106         if (IGNORE_PACKAGE_LIST.contains(packageName)) {
107             return false;
108         }
109 
110         // Iterate through all the activities and check if it is resizeable and supports
111         // picture-in-picture
112         if (activities != null) {
113             for (int i = activities.length - 1; i >= 0; i--) {
114                 if (activities[i].supportsPictureInPicture()) {
115                     return true;
116                 }
117             }
118         }
119         return false;
120     }
121 
PictureInPictureSettings()122     public PictureInPictureSettings() {
123         // Do nothing
124     }
125 
PictureInPictureSettings(PackageManager pm, UserManager um)126     public PictureInPictureSettings(PackageManager pm, UserManager um) {
127         mPackageManager = pm;
128         mUserManager = um;
129     }
130 
131     @Override
onCreate(Bundle icicle)132     public void onCreate(Bundle icicle) {
133         super.onCreate(icicle);
134 
135         mContext = getActivity();
136         mPackageManager = mContext.getPackageManager();
137         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
138         mIconDrawableFactory = IconDrawableFactory.newInstance(mContext);
139     }
140 
141     @Override
onResume()142     public void onResume() {
143         super.onResume();
144 
145         // Clear the prefs
146         final PreferenceScreen screen = getPreferenceScreen();
147         screen.removeAll();
148 
149         // Fetch the set of applications for each profile which have at least one activity that
150         // declare that they support picture-in-picture
151         final ArrayList<Pair<ApplicationInfo, Integer>> pipApps =
152                 collectPipApps(UserHandle.myUserId());
153         Collections.sort(pipApps, new AppComparator(mPackageManager));
154 
155         // Rebuild the list of prefs
156         final Context prefContext = getPrefContext();
157         for (final Pair<ApplicationInfo, Integer> appData : pipApps) {
158             final ApplicationInfo appInfo = appData.first;
159             final int userId = appData.second;
160             final UserHandle user = UserHandle.of(userId);
161             final String packageName = appInfo.packageName;
162             final CharSequence label = appInfo.loadLabel(mPackageManager);
163 
164             final Preference pref = new AppPreference(prefContext);
165             pref.setIcon(mIconDrawableFactory.getBadgedIcon(appInfo, userId));
166             pref.setTitle(mPackageManager.getUserBadgedLabel(label, user));
167             pref.setSummary(PictureInPictureDetails.getPreferenceSummary(prefContext,
168                     appInfo.uid, packageName));
169             pref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
170                 @Override
171                 public boolean onPreferenceClick(Preference preference) {
172                     AppInfoBase.startAppInfoFragment(PictureInPictureDetails.class,
173                             R.string.picture_in_picture_app_detail_title, 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 Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
223             new BaseSearchIndexProvider() {
224                 @Override
225                 public List<SearchIndexableResource> getXmlResourcesToIndex(Context context,
226                         boolean enabled) {
227                     final ArrayList<SearchIndexableResource> result = new ArrayList<>();
228 
229                     final SearchIndexableResource sir = new SearchIndexableResource(context);
230                     sir.xmlResId = R.xml.picture_in_picture_settings;
231                     result.add(sir);
232                     return result;
233                 }
234             };
235 }
236