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