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