1/*
2 * Copyright (C) 2014 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 android.media.tv;
18
19import android.annotation.SystemApi;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.pm.PackageManager;
24import android.content.pm.PackageManager.NameNotFoundException;
25import android.content.pm.ResolveInfo;
26import android.content.pm.ServiceInfo;
27import android.content.res.Resources;
28import android.content.res.TypedArray;
29import android.content.res.XmlResourceParser;
30import android.graphics.drawable.Drawable;
31import android.hardware.hdmi.HdmiDeviceInfo;
32import android.net.Uri;
33import android.os.Parcel;
34import android.os.Parcelable;
35import android.os.UserHandle;
36import android.provider.Settings;
37import android.text.TextUtils;
38import android.util.AttributeSet;
39import android.util.Log;
40import android.util.SparseIntArray;
41import android.util.Xml;
42
43import org.xmlpull.v1.XmlPullParser;
44import org.xmlpull.v1.XmlPullParserException;
45
46import java.io.IOException;
47import java.io.InputStream;
48import java.util.HashMap;
49import java.util.HashSet;
50import java.util.Map;
51import java.util.Set;
52
53/**
54 * This class is used to specify meta information of a TV input.
55 */
56public final class TvInputInfo implements Parcelable {
57    private static final boolean DEBUG = false;
58    private static final String TAG = "TvInputInfo";
59
60    // Should be in sync with frameworks/base/core/res/res/values/attrs.xml
61    /**
62     * TV input type: the TV input service is a tuner which provides channels.
63     */
64    public static final int TYPE_TUNER = 0;
65    /**
66     * TV input type: a generic hardware TV input type.
67     */
68    public static final int TYPE_OTHER = 1000;
69    /**
70     * TV input type: the TV input service represents a composite port.
71     */
72    public static final int TYPE_COMPOSITE = 1001;
73    /**
74     * TV input type: the TV input service represents a SVIDEO port.
75     */
76    public static final int TYPE_SVIDEO = 1002;
77    /**
78     * TV input type: the TV input service represents a SCART port.
79     */
80    public static final int TYPE_SCART = 1003;
81    /**
82     * TV input type: the TV input service represents a component port.
83     */
84    public static final int TYPE_COMPONENT = 1004;
85    /**
86     * TV input type: the TV input service represents a VGA port.
87     */
88    public static final int TYPE_VGA = 1005;
89    /**
90     * TV input type: the TV input service represents a DVI port.
91     */
92    public static final int TYPE_DVI = 1006;
93    /**
94     * TV input type: the TV input service is HDMI. (e.g. HDMI 1)
95     */
96    public static final int TYPE_HDMI = 1007;
97    /**
98     * TV input type: the TV input service represents a display port.
99     */
100    public static final int TYPE_DISPLAY_PORT = 1008;
101
102    /**
103     * The ID of the TV input to provide to the setup activity and settings activity.
104     */
105    public static final String EXTRA_INPUT_ID = "android.media.tv.extra.INPUT_ID";
106
107    private static SparseIntArray sHardwareTypeToTvInputType = new SparseIntArray();
108
109    private static final String XML_START_TAG_NAME = "tv-input";
110    private static final String DELIMITER_INFO_IN_ID = "/";
111    private static final String PREFIX_HDMI_DEVICE = "HDMI";
112    private static final String PREFIX_HARDWARE_DEVICE = "HW";
113    private static final int LENGTH_HDMI_PHYSICAL_ADDRESS = 4;
114    private static final int LENGTH_HDMI_DEVICE_ID = 2;
115
116    private final ResolveInfo mService;
117    private final String mId;
118    private final String mParentId;
119
120    // Attributes from XML meta data.
121    private String mSetupActivity;
122    private String mSettingsActivity;
123
124    private int mType = TYPE_TUNER;
125    private HdmiDeviceInfo mHdmiDeviceInfo;
126    private String mLabel;
127    private Uri mIconUri;
128    private boolean mIsConnectedToHdmiSwitch;
129
130    static {
131        sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_OTHER_HARDWARE,
132                TYPE_OTHER);
133        sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_TUNER, TYPE_TUNER);
134        sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_COMPOSITE, TYPE_COMPOSITE);
135        sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_SVIDEO, TYPE_SVIDEO);
136        sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_SCART, TYPE_SCART);
137        sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_COMPONENT, TYPE_COMPONENT);
138        sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_VGA, TYPE_VGA);
139        sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_DVI, TYPE_DVI);
140        sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_HDMI, TYPE_HDMI);
141        sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_DISPLAY_PORT,
142                TYPE_DISPLAY_PORT);
143    }
144
145    /**
146     * Create a new instance of the TvInputInfo class,
147     * instantiating it from the given Context and ResolveInfo.
148     *
149     * @param service The ResolveInfo returned from the package manager about this TV input service.
150     * @hide
151     */
152    public static TvInputInfo createTvInputInfo(Context context, ResolveInfo service)
153            throws XmlPullParserException, IOException {
154        return createTvInputInfo(context, service, generateInputIdForComponentName(
155                new ComponentName(service.serviceInfo.packageName, service.serviceInfo.name)),
156                null, TYPE_TUNER, null, null, false);
157    }
158
159    /**
160     * Create a new instance of the TvInputInfo class, instantiating it from the given Context,
161     * ResolveInfo, and HdmiDeviceInfo.
162     *
163     * @param service The ResolveInfo returned from the package manager about this TV input service.
164     * @param hdmiDeviceInfo The HdmiDeviceInfo for a HDMI CEC logical device.
165     * @param parentId The ID of this TV input's parent input. {@code null} if none exists.
166     * @param iconUri The {@link android.net.Uri} to load the icon image. See
167     *            {@link android.content.ContentResolver#openInputStream}. If it is {@code null},
168     *            the application icon of {@code service} will be loaded.
169     * @param label The label of this TvInputInfo. If it is {@code null} or empty, {@code service}
170     *            label will be loaded.
171     * @hide
172     */
173    @SystemApi
174    public static TvInputInfo createTvInputInfo(Context context, ResolveInfo service,
175            HdmiDeviceInfo hdmiDeviceInfo, String parentId, String label, Uri iconUri)
176                    throws XmlPullParserException, IOException {
177        boolean isConnectedToHdmiSwitch = (hdmiDeviceInfo.getPhysicalAddress() & 0x0FFF) != 0;
178        TvInputInfo input = createTvInputInfo(context, service, generateInputIdForHdmiDevice(
179                new ComponentName(service.serviceInfo.packageName, service.serviceInfo.name),
180                hdmiDeviceInfo), parentId, TYPE_HDMI, label, iconUri, isConnectedToHdmiSwitch);
181        input.mHdmiDeviceInfo = hdmiDeviceInfo;
182        return input;
183    }
184
185    /**
186     * Create a new instance of the TvInputInfo class, instantiating it from the given Context,
187     * ResolveInfo, and TvInputHardwareInfo.
188     *
189     * @param service The ResolveInfo returned from the package manager about this TV input service.
190     * @param hardwareInfo The TvInputHardwareInfo for a TV input hardware device.
191     * @param iconUri The {@link android.net.Uri} to load the icon image. See
192     *            {@link android.content.ContentResolver#openInputStream}. If it is {@code null},
193     *            the application icon of {@code service} will be loaded.
194     * @param label The label of this TvInputInfo. If it is {@code null} or empty, {@code service}
195     *            label will be loaded.
196     * @hide
197     */
198    @SystemApi
199    public static TvInputInfo createTvInputInfo(Context context, ResolveInfo service,
200            TvInputHardwareInfo hardwareInfo, String label, Uri iconUri)
201                    throws XmlPullParserException, IOException {
202        int inputType = sHardwareTypeToTvInputType.get(hardwareInfo.getType(), TYPE_TUNER);
203        return createTvInputInfo(context, service, generateInputIdForHardware(
204                new ComponentName(service.serviceInfo.packageName, service.serviceInfo.name),
205                hardwareInfo), null, inputType, label, iconUri, false);
206    }
207
208    private static TvInputInfo createTvInputInfo(Context context, ResolveInfo service,
209            String id, String parentId, int inputType, String label, Uri iconUri,
210            boolean isConnectedToHdmiSwitch)
211                    throws XmlPullParserException, IOException {
212        ServiceInfo si = service.serviceInfo;
213        PackageManager pm = context.getPackageManager();
214        XmlResourceParser parser = null;
215        try {
216            parser = si.loadXmlMetaData(pm, TvInputService.SERVICE_META_DATA);
217            if (parser == null) {
218                throw new XmlPullParserException("No " + TvInputService.SERVICE_META_DATA
219                        + " meta-data for " + si.name);
220            }
221
222            Resources res = pm.getResourcesForApplication(si.applicationInfo);
223            AttributeSet attrs = Xml.asAttributeSet(parser);
224
225            int type;
226            while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
227                    && type != XmlPullParser.START_TAG) {
228            }
229
230            String nodeName = parser.getName();
231            if (!XML_START_TAG_NAME.equals(nodeName)) {
232                throw new XmlPullParserException(
233                        "Meta-data does not start with tv-input-service tag in " + si.name);
234            }
235
236            TvInputInfo input = new TvInputInfo(service, id, parentId, inputType);
237            TypedArray sa = res.obtainAttributes(attrs,
238                    com.android.internal.R.styleable.TvInputService);
239            input.mSetupActivity = sa.getString(
240                    com.android.internal.R.styleable.TvInputService_setupActivity);
241            if (DEBUG) {
242                Log.d(TAG, "Setup activity loaded. [" + input.mSetupActivity + "] for " + si.name);
243            }
244            input.mSettingsActivity = sa.getString(
245                    com.android.internal.R.styleable.TvInputService_settingsActivity);
246            if (DEBUG) {
247                Log.d(TAG, "Settings activity loaded. [" + input.mSettingsActivity + "] for "
248                        + si.name);
249            }
250            sa.recycle();
251
252            input.mLabel = label;
253            input.mIconUri = iconUri;
254            input.mIsConnectedToHdmiSwitch = isConnectedToHdmiSwitch;
255            return input;
256        } catch (NameNotFoundException e) {
257            throw new XmlPullParserException("Unable to create context for: " + si.packageName);
258        } finally {
259            if (parser != null) {
260                parser.close();
261            }
262        }
263    }
264
265    /**
266     * Constructor.
267     *
268     * @param service The ResolveInfo returned from the package manager about this TV input service.
269     * @param id ID of this TV input. Should be generated via generateInputId*().
270     * @param parentId ID of this TV input's parent input. {@code null} if none exists.
271     * @param type The type of this TV input service.
272     */
273    private TvInputInfo(ResolveInfo service, String id, String parentId, int type) {
274        mService = service;
275        mId = id;
276        mParentId = parentId;
277        mType = type;
278    }
279
280    /**
281     * Returns a unique ID for this TV input. The ID is generated from the package and class name
282     * implementing the TV input service.
283     */
284    public String getId() {
285        return mId;
286    }
287
288    /**
289     * Returns the parent input ID.
290     * <p>
291     * A TV input may have a parent input if the TV input is actually a logical representation of
292     * a device behind the hardware port represented by the parent input.
293     * For example, a HDMI CEC logical device, connected to a HDMI port, appears as another TV
294     * input. In this case, the parent input of this logical device is the HDMI port.
295     * </p><p>
296     * Applications may group inputs by parent input ID to provide an easier access to inputs
297     * sharing the same physical port. In the example of HDMI CEC, logical HDMI CEC devices behind
298     * the same HDMI port have the same parent ID, which is the ID representing the port. Thus
299     * applications can group the hardware HDMI port and the logical HDMI CEC devices behind it
300     * together using this method.
301     * </p>
302     *
303     * @return the ID of the parent input, if exists. Returns {@code null} if the parent input is
304     *         not specified.
305     */
306    public String getParentId() {
307        return mParentId;
308    }
309
310    /**
311     * Returns the information of the service that implements this TV input.
312     */
313    public ServiceInfo getServiceInfo() {
314        return mService.serviceInfo;
315    }
316
317    /**
318     * Returns the component of the service that implements this TV input.
319     * @hide
320     */
321    public ComponentName getComponent() {
322        return new ComponentName(mService.serviceInfo.packageName, mService.serviceInfo.name);
323    }
324
325    /**
326     * Returns an intent to start the setup activity for this TV input.
327     */
328    public Intent createSetupIntent() {
329        if (!TextUtils.isEmpty(mSetupActivity)) {
330            Intent intent = new Intent(Intent.ACTION_MAIN);
331            intent.setClassName(mService.serviceInfo.packageName, mSetupActivity);
332            intent.putExtra(EXTRA_INPUT_ID, getId());
333            return intent;
334        }
335        return null;
336    }
337
338    /**
339     * Returns an intent to start the settings activity for this TV input.
340     */
341    public Intent createSettingsIntent() {
342        if (!TextUtils.isEmpty(mSettingsActivity)) {
343            Intent intent = new Intent(Intent.ACTION_MAIN);
344            intent.setClassName(mService.serviceInfo.packageName, mSettingsActivity);
345            intent.putExtra(EXTRA_INPUT_ID, getId());
346            return intent;
347        }
348        return null;
349    }
350
351    /**
352     * Returns the type of this TV input.
353     */
354    public int getType() {
355        return mType;
356    }
357
358    /**
359     * Returns the HDMI device information of this TV input.
360     * @hide
361     */
362    @SystemApi
363    public HdmiDeviceInfo getHdmiDeviceInfo() {
364        if (mType == TYPE_HDMI) {
365            return mHdmiDeviceInfo;
366        }
367        return null;
368    }
369
370    /**
371     * Returns {@code true} if this TV input is pass-though which does not have any real channels in
372     * TvProvider. {@code false} otherwise.
373     *
374     * @see TvContract#buildChannelUriForPassthroughInput(String)
375     */
376    public boolean isPassthroughInput() {
377        return mType != TYPE_TUNER;
378    }
379
380    /**
381     * Returns {@code true}, if a CEC device for this TV input is connected to an HDMI switch, i.e.,
382     * the device isn't directly connected to a HDMI port.
383     * @hide
384     */
385    @SystemApi
386    public boolean isConnectedToHdmiSwitch() {
387        return mIsConnectedToHdmiSwitch;
388    }
389
390    /**
391     * Checks if this TV input is marked hidden by the user in the settings.
392     *
393     * @param context Supplies a {@link Context} used to check if this TV input is hidden.
394     * @return {@code true} if the user marked this TV input hidden in settings. {@code false}
395     *         otherwise.
396     * @hide
397     */
398    @SystemApi
399    public boolean isHidden(Context context) {
400        return TvInputSettings.isHidden(context, mId, UserHandle.myUserId());
401    }
402
403    /**
404     * Loads the user-displayed label for this TV input.
405     *
406     * @param context Supplies a {@link Context} used to load the label.
407     * @return a CharSequence containing the TV input's label. If the TV input does not have
408     *         a label, its name is returned.
409     */
410    public CharSequence loadLabel(Context context) {
411        if (TextUtils.isEmpty(mLabel)) {
412            return mService.loadLabel(context.getPackageManager());
413        } else {
414            return mLabel;
415        }
416    }
417
418    /**
419     * Loads the custom label set by user in settings.
420     *
421     * @param context Supplies a {@link Context} used to load the custom label.
422     * @return a CharSequence containing the TV input's custom label. {@code null} if there is no
423     *         custom label.
424     * @hide
425     */
426    @SystemApi
427    public CharSequence loadCustomLabel(Context context) {
428        return TvInputSettings.getCustomLabel(context, mId, UserHandle.myUserId());
429    }
430
431    /**
432     * Loads the user-displayed icon for this TV input.
433     *
434     * @param context Supplies a {@link Context} used to load the icon.
435     * @return a Drawable containing the TV input's icon. If the TV input does not have an icon,
436     *         application's icon is returned. If it's unavailable too, {@code null} is returned.
437     */
438    public Drawable loadIcon(Context context) {
439        if (mIconUri == null) {
440            return loadServiceIcon(context);
441        }
442        try (InputStream is = context.getContentResolver().openInputStream(mIconUri)) {
443            Drawable drawable = Drawable.createFromStream(is, null);
444            if (drawable == null) {
445                return loadServiceIcon(context);
446            }
447            return drawable;
448        } catch (IOException e) {
449            Log.w(TAG, "Loading the default icon due to a failure on loading " + mIconUri, e);
450            return loadServiceIcon(context);
451        }
452    }
453
454    @Override
455    public int describeContents() {
456        return 0;
457    }
458
459    @Override
460    public int hashCode() {
461        return mId.hashCode();
462    }
463
464    @Override
465    public boolean equals(Object o) {
466        if (o == this) {
467            return true;
468        }
469
470        if (!(o instanceof TvInputInfo)) {
471            return false;
472        }
473
474        TvInputInfo obj = (TvInputInfo) o;
475        return mId.equals(obj.mId);
476    }
477
478    @Override
479    public String toString() {
480        return "TvInputInfo{id=" + mId
481                + ", pkg=" + mService.serviceInfo.packageName
482                + ", service=" + mService.serviceInfo.name + "}";
483    }
484
485    /**
486     * Used to package this object into a {@link Parcel}.
487     *
488     * @param dest The {@link Parcel} to be written.
489     * @param flags The flags used for parceling.
490     */
491    @Override
492    public void writeToParcel(Parcel dest, int flags) {
493        dest.writeString(mId);
494        dest.writeString(mParentId);
495        mService.writeToParcel(dest, flags);
496        dest.writeString(mSetupActivity);
497        dest.writeString(mSettingsActivity);
498        dest.writeInt(mType);
499        dest.writeParcelable(mHdmiDeviceInfo, flags);
500        dest.writeParcelable(mIconUri, flags);
501        dest.writeString(mLabel);
502        dest.writeByte(mIsConnectedToHdmiSwitch ? (byte) 1 : 0);
503    }
504
505    private Drawable loadServiceIcon(Context context) {
506        if (mService.serviceInfo.icon == 0
507                && mService.serviceInfo.applicationInfo.icon == 0) {
508            return null;
509        }
510        return mService.serviceInfo.loadIcon(context.getPackageManager());
511    }
512
513    /**
514     * Used to generate an input id from a ComponentName.
515     *
516     * @param name the component name for generating an input id.
517     * @return the generated input id for the given {@code name}.
518     */
519    private static final String generateInputIdForComponentName(ComponentName name) {
520        return name.flattenToShortString();
521    }
522
523    /**
524     * Used to generate an input id from a ComponentName and HdmiDeviceInfo.
525     *
526     * @param name the component name for generating an input id.
527     * @param deviceInfo HdmiDeviceInfo describing this TV input.
528     * @return the generated input id for the given {@code name} and {@code deviceInfo}.
529     */
530    private static final String generateInputIdForHdmiDevice(
531            ComponentName name, HdmiDeviceInfo deviceInfo) {
532        // Example of the format : "/HDMI%04X%02X"
533        String format = String.format("%s%s%%0%sX%%0%sX", DELIMITER_INFO_IN_ID, PREFIX_HDMI_DEVICE,
534                LENGTH_HDMI_PHYSICAL_ADDRESS, LENGTH_HDMI_DEVICE_ID);
535        return name.flattenToShortString() + String.format(format,
536                deviceInfo.getPhysicalAddress(), deviceInfo.getId());
537    }
538
539    /**
540     * Used to generate an input id from a ComponentName and TvInputHardwareInfo
541     *
542     * @param name the component name for generating an input id.
543     * @param hardwareInfo TvInputHardwareInfo describing this TV input.
544     * @return the generated input id for the given {@code name} and {@code hardwareInfo}.
545     */
546    private static final String generateInputIdForHardware(
547            ComponentName name, TvInputHardwareInfo hardwareInfo) {
548        return name.flattenToShortString() + String.format("%s%s%d",
549                DELIMITER_INFO_IN_ID, PREFIX_HARDWARE_DEVICE, hardwareInfo.getDeviceId());
550    }
551
552    public static final Parcelable.Creator<TvInputInfo> CREATOR =
553            new Parcelable.Creator<TvInputInfo>() {
554        @Override
555        public TvInputInfo createFromParcel(Parcel in) {
556            return new TvInputInfo(in);
557        }
558
559        @Override
560        public TvInputInfo[] newArray(int size) {
561            return new TvInputInfo[size];
562        }
563    };
564
565    private TvInputInfo(Parcel in) {
566        mId = in.readString();
567        mParentId = in.readString();
568        mService = ResolveInfo.CREATOR.createFromParcel(in);
569        mSetupActivity = in.readString();
570        mSettingsActivity = in.readString();
571        mType = in.readInt();
572        mHdmiDeviceInfo = in.readParcelable(null);
573        mIconUri = in.readParcelable(null);
574        mLabel = in.readString();
575        mIsConnectedToHdmiSwitch = in.readByte() == 1 ? true : false;
576    }
577
578    /**
579     * Utility class for putting and getting settings for TV input.
580     *
581     * @hide
582     */
583    @SystemApi
584    public static final class TvInputSettings {
585        private static final String TV_INPUT_SEPARATOR = ":";
586        private static final String CUSTOM_NAME_SEPARATOR = ",";
587
588        private TvInputSettings() { }
589
590        private static boolean isHidden(Context context, String inputId, int userId) {
591            return getHiddenTvInputIds(context, userId).contains(inputId);
592        }
593
594        private static String getCustomLabel(Context context, String inputId, int userId) {
595            return getCustomLabels(context, userId).get(inputId);
596        }
597
598        /**
599         * Returns a set of TV input IDs which are marked as hidden by user in the settings.
600         *
601         * @param context The application context
602         * @param userId The user ID for the stored hidden input set
603         * @hide
604         */
605        @SystemApi
606        public static Set<String> getHiddenTvInputIds(Context context, int userId) {
607            String hiddenIdsString = Settings.Secure.getStringForUser(
608                    context.getContentResolver(), Settings.Secure.TV_INPUT_HIDDEN_INPUTS, userId);
609            Set<String> set = new HashSet<String>();
610            if (TextUtils.isEmpty(hiddenIdsString)) {
611                return set;
612            }
613            String[] ids = hiddenIdsString.split(TV_INPUT_SEPARATOR);
614            for (String id : ids) {
615                set.add(Uri.decode(id));
616            }
617            return set;
618        }
619
620        /**
621         * Returns a map of TV input ID/custom label pairs set by the user in the settings.
622         *
623         * @param context The application context
624         * @param userId The user ID for the stored hidden input map
625         * @hide
626         */
627        @SystemApi
628        public static Map<String, String> getCustomLabels(Context context, int userId) {
629            String labelsString = Settings.Secure.getStringForUser(
630                    context.getContentResolver(), Settings.Secure.TV_INPUT_CUSTOM_LABELS, userId);
631            Map<String, String> map = new HashMap<String, String>();
632            if (TextUtils.isEmpty(labelsString)) {
633                return map;
634            }
635            String[] pairs = labelsString.split(TV_INPUT_SEPARATOR);
636            for (String pairString : pairs) {
637                String[] pair = pairString.split(CUSTOM_NAME_SEPARATOR);
638                map.put(Uri.decode(pair[0]), Uri.decode(pair[1]));
639            }
640            return map;
641        }
642
643        /**
644         * Stores a set of TV input IDs which are marked as hidden by user. This is expected to
645         * be called from the settings app.
646         *
647         * @param context The application context
648         * @param hiddenInputIds A set including all the hidden TV input IDs
649         * @param userId The user ID for the stored hidden input set
650         * @hide
651         */
652        @SystemApi
653        public static void putHiddenTvInputs(Context context, Set<String> hiddenInputIds,
654                int userId) {
655            StringBuilder builder = new StringBuilder();
656            boolean firstItem = true;
657            for (String inputId : hiddenInputIds) {
658                ensureValidField(inputId);
659                if (firstItem) {
660                    firstItem = false;
661                } else {
662                    builder.append(TV_INPUT_SEPARATOR);
663                }
664                builder.append(Uri.encode(inputId));
665            }
666            Settings.Secure.putStringForUser(context.getContentResolver(),
667                    Settings.Secure.TV_INPUT_HIDDEN_INPUTS, builder.toString(), userId);
668        }
669
670        /**
671         * Stores a map of TV input ID/custom label set by user. This is expected to be
672         * called from the settings app.
673         *
674         * @param context The application context.
675         * @param customLabels A map of TV input ID/custom label pairs
676         * @param userId The user ID for the stored hidden input map
677         * @hide
678         */
679        @SystemApi
680        public static void putCustomLabels(Context context,
681                Map<String, String> customLabels, int userId) {
682            StringBuilder builder = new StringBuilder();
683            boolean firstItem = true;
684            for (Map.Entry<String, String> entry: customLabels.entrySet()) {
685                ensureValidField(entry.getKey());
686                ensureValidField(entry.getValue());
687                if (firstItem) {
688                    firstItem = false;
689                } else {
690                    builder.append(TV_INPUT_SEPARATOR);
691                }
692                builder.append(Uri.encode(entry.getKey()));
693                builder.append(CUSTOM_NAME_SEPARATOR);
694                builder.append(Uri.encode(entry.getValue()));
695            }
696            Settings.Secure.putStringForUser(context.getContentResolver(),
697                    Settings.Secure.TV_INPUT_CUSTOM_LABELS, builder.toString(), userId);
698        }
699
700        private static void ensureValidField(String value) {
701            if (TextUtils.isEmpty(value)) {
702                throw new IllegalArgumentException(value + " should not empty ");
703            }
704        }
705    }
706}
707