1 /* 2 * Copyright (C) 2015 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.tv.common.customization; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.content.pm.PackageInfo; 22 import android.content.pm.PackageManager; 23 import android.content.pm.PackageManager.NameNotFoundException; 24 import android.content.pm.ResolveInfo; 25 import android.content.res.Resources; 26 import android.graphics.drawable.Drawable; 27 import android.support.annotation.IntDef; 28 import android.support.annotation.Nullable; 29 import android.support.annotation.VisibleForTesting; 30 import android.text.TextUtils; 31 import android.util.Log; 32 33 import com.android.tv.common.CommonConstants; 34 35 import com.google.common.collect.Iterables; 36 37 import java.lang.annotation.Retention; 38 import java.lang.annotation.RetentionPolicy; 39 import java.util.ArrayList; 40 import java.util.Collections; 41 import java.util.HashMap; 42 import java.util.List; 43 import java.util.Map; 44 45 public class CustomizationManager { 46 private static final String TAG = "CustomizationManager"; 47 private static final boolean DEBUG = false; 48 49 private static final String[] CUSTOMIZE_PERMISSIONS = { 50 CommonConstants.BASE_PACKAGE + ".permission.CUSTOMIZE_TV_APP" 51 }; 52 53 private static final String CATEGORY_TV_CUSTOMIZATION = 54 CommonConstants.BASE_PACKAGE + ".category"; 55 56 /** Row IDs to share customized actions. Only rows listed below can have customized action. */ 57 public static final String ID_OPTIONS_ROW = "options_row"; 58 59 public static final String ID_PARTNER_ROW = "partner_row"; 60 61 @IntDef({TRICKPLAY_MODE_ENABLED, TRICKPLAY_MODE_DISABLED, TRICKPLAY_MODE_USE_EXTERNAL_STORAGE}) 62 @Retention(RetentionPolicy.SOURCE) 63 public @interface TRICKPLAY_MODE {} 64 65 public static final int TRICKPLAY_MODE_ENABLED = 0; 66 public static final int TRICKPLAY_MODE_DISABLED = 1; 67 public static final int TRICKPLAY_MODE_USE_EXTERNAL_STORAGE = 2; 68 69 private static final String[] TRICKPLAY_MODE_STRINGS = { 70 "enabled", "disabled", "use_external_storage_only" 71 }; 72 73 private static final HashMap<String, String> INTENT_CATEGORY_TO_ROW_ID; 74 75 static { 76 INTENT_CATEGORY_TO_ROW_ID = new HashMap<>(); 77 INTENT_CATEGORY_TO_ROW_ID.put(CATEGORY_TV_CUSTOMIZATION + ".OPTIONS_ROW", ID_OPTIONS_ROW); 78 INTENT_CATEGORY_TO_ROW_ID.put(CATEGORY_TV_CUSTOMIZATION + ".PARTNER_ROW", ID_PARTNER_ROW); 79 } 80 81 private static final String RES_ID_PARTNER_ROW_TITLE = "partner_row_title"; 82 private static final String RES_ID_HAS_LINUX_DVB_BUILT_IN_TUNER = 83 "has_linux_dvb_built_in_tuner"; 84 private static final String RES_ID_TRICKPLAY_MODE = "trickplay_mode"; 85 86 private static final String RES_TYPE_STRING = "string"; 87 private static final String RES_TYPE_BOOLEAN = "bool"; 88 89 private static String sCustomizationPackage; 90 private static Boolean sHasLinuxDvbBuiltInTuner; 91 private static @TRICKPLAY_MODE Integer sTrickplayMode; 92 93 private final Context mContext; 94 private boolean mInitialized; 95 96 private String mPartnerRowTitle; 97 private final Map<String, List<CustomAction>> mRowIdToCustomActionsMap = new HashMap<>(); 98 CustomizationManager(Context context)99 public CustomizationManager(Context context) { 100 mContext = context; 101 mInitialized = false; 102 } 103 104 /** 105 * Returns {@code true} if there's a customization package installed and it specifies built-in 106 * tuner devices are available. The built-in tuner should support DVB API to be recognized by TV 107 * app. 108 */ hasLinuxDvbBuiltInTuner(Context context)109 public static boolean hasLinuxDvbBuiltInTuner(Context context) { 110 if (sHasLinuxDvbBuiltInTuner == null) { 111 if (TextUtils.isEmpty(getCustomizationPackageName(context))) { 112 sHasLinuxDvbBuiltInTuner = false; 113 } else { 114 try { 115 Resources res = 116 context.getPackageManager() 117 .getResourcesForApplication(sCustomizationPackage); 118 int resId = 119 res.getIdentifier( 120 RES_ID_HAS_LINUX_DVB_BUILT_IN_TUNER, 121 RES_TYPE_BOOLEAN, 122 sCustomizationPackage); 123 sHasLinuxDvbBuiltInTuner = resId != 0 && res.getBoolean(resId); 124 } catch (NameNotFoundException e) { 125 sHasLinuxDvbBuiltInTuner = false; 126 } 127 } 128 } 129 return sHasLinuxDvbBuiltInTuner; 130 } 131 getTrickplayMode(Context context)132 public static @TRICKPLAY_MODE int getTrickplayMode(Context context) { 133 if (sTrickplayMode == null) { 134 if (TextUtils.isEmpty(getCustomizationPackageName(context))) { 135 sTrickplayMode = TRICKPLAY_MODE_ENABLED; 136 } else { 137 try { 138 String customization; 139 Resources res = 140 context.getPackageManager() 141 .getResourcesForApplication(sCustomizationPackage); 142 int resId = 143 res.getIdentifier( 144 RES_ID_TRICKPLAY_MODE, RES_TYPE_STRING, sCustomizationPackage); 145 customization = resId == 0 ? null : res.getString(resId); 146 sTrickplayMode = TRICKPLAY_MODE_ENABLED; 147 if (customization != null) { 148 for (int i = 0; i < TRICKPLAY_MODE_STRINGS.length; ++i) { 149 if (TRICKPLAY_MODE_STRINGS[i].equalsIgnoreCase(customization)) { 150 sTrickplayMode = i; 151 break; 152 } 153 } 154 } 155 } catch (NameNotFoundException e) { 156 sTrickplayMode = TRICKPLAY_MODE_ENABLED; 157 } 158 } 159 } 160 return sTrickplayMode; 161 } 162 getCustomizationPackageName(Context context)163 private static String getCustomizationPackageName(Context context) { 164 if (sCustomizationPackage == null) { 165 List<PackageInfo> packageInfos = 166 context.getPackageManager() 167 .getPackagesHoldingPermissions(CUSTOMIZE_PERMISSIONS, 0); 168 sCustomizationPackage = getCustomizationPackageName(packageInfos); 169 } 170 return sCustomizationPackage; 171 } 172 173 @VisibleForTesting getCustomizationPackageName(List<PackageInfo> packageInfos)174 static String getCustomizationPackageName(List<PackageInfo> packageInfos) { 175 Iterable<String> packageNames = 176 Iterables.transform(packageInfos, input -> input.packageName); 177 178 // Find the first vendor customizer 179 return Iterables.find( 180 packageNames, 181 input -> !input.startsWith("com.android"), 182 // else use the first one or blank 183 Iterables.getFirst(packageNames, "")); 184 } 185 186 /** Initialize TV customization options. Run this API only on the main thread. */ initialize()187 public void initialize() { 188 if (mInitialized) { 189 return; 190 } 191 mInitialized = true; 192 if (!TextUtils.isEmpty(getCustomizationPackageName(mContext))) { 193 buildCustomActions(); 194 buildPartnerRow(); 195 } 196 } 197 buildCustomActions()198 private void buildCustomActions() { 199 mRowIdToCustomActionsMap.clear(); 200 PackageManager pm = mContext.getPackageManager(); 201 for (String intentCategory : INTENT_CATEGORY_TO_ROW_ID.keySet()) { 202 Intent customOptionIntent = new Intent(Intent.ACTION_MAIN); 203 customOptionIntent.addCategory(intentCategory); 204 205 List<ResolveInfo> activities = 206 pm.queryIntentActivities( 207 customOptionIntent, 208 PackageManager.GET_RECEIVERS 209 | PackageManager.GET_RESOLVED_FILTER 210 | PackageManager.GET_META_DATA); 211 for (ResolveInfo info : activities) { 212 String packageName = info.activityInfo.packageName; 213 if (!TextUtils.equals(packageName, sCustomizationPackage)) { 214 Log.w( 215 TAG, 216 "A customization package " 217 + sCustomizationPackage 218 + " already exist. Ignoring " 219 + packageName); 220 continue; 221 } 222 223 int position = info.filter.getPriority(); 224 String title = info.loadLabel(pm).toString(); 225 Drawable drawable = info.loadIcon(pm); 226 Intent intent = new Intent(Intent.ACTION_MAIN); 227 intent.addCategory(intentCategory); 228 intent.setClassName(sCustomizationPackage, info.activityInfo.name); 229 230 String rowId = INTENT_CATEGORY_TO_ROW_ID.get(intentCategory); 231 List<CustomAction> actions = mRowIdToCustomActionsMap.get(rowId); 232 if (actions == null) { 233 actions = new ArrayList<>(); 234 mRowIdToCustomActionsMap.put(rowId, actions); 235 } 236 actions.add(new CustomAction(position, title, drawable, intent)); 237 } 238 } 239 // Sort items by position 240 for (List<CustomAction> actions : mRowIdToCustomActionsMap.values()) { 241 Collections.sort(actions); 242 } 243 244 if (DEBUG) { 245 Log.d(TAG, "Dumping custom actions"); 246 for (String id : mRowIdToCustomActionsMap.keySet()) { 247 for (CustomAction action : mRowIdToCustomActionsMap.get(id)) { 248 Log.d( 249 TAG, 250 "Custom row rowId=" 251 + id 252 + " title=" 253 + action.getTitle() 254 + " class=" 255 + action.getIntent()); 256 } 257 } 258 Log.d(TAG, "Dumping custom actions - end of dump"); 259 } 260 } 261 262 /** 263 * Returns custom actions for given row id. 264 * 265 * <p>Row ID is one of ID_OPTIONS_ROW or ID_PARTNER_ROW. 266 */ 267 @Nullable getCustomActions(String rowId)268 public List<CustomAction> getCustomActions(String rowId) { 269 return mRowIdToCustomActionsMap.get(rowId); 270 } 271 buildPartnerRow()272 private void buildPartnerRow() { 273 mPartnerRowTitle = null; 274 Resources res; 275 try { 276 res = mContext.getPackageManager().getResourcesForApplication(sCustomizationPackage); 277 } catch (NameNotFoundException e) { 278 Log.w(TAG, "Could not get resources for package " + sCustomizationPackage); 279 return; 280 } 281 int resId = 282 res.getIdentifier(RES_ID_PARTNER_ROW_TITLE, RES_TYPE_STRING, sCustomizationPackage); 283 if (resId != 0) { 284 mPartnerRowTitle = res.getString(resId); 285 } 286 if (DEBUG) Log.d(TAG, "Partner row title [" + mPartnerRowTitle + "]"); 287 } 288 289 /** Returns partner row title. */ getPartnerRowTitle()290 public String getPartnerRowTitle() { 291 return mPartnerRowTitle; 292 } 293 } 294