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