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