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.IntDef;
20import android.annotation.NonNull;
21import android.annotation.StringRes;
22import android.annotation.SystemApi;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.PackageManager;
27import android.content.pm.PackageManager.NameNotFoundException;
28import android.content.pm.ResolveInfo;
29import android.content.pm.ServiceInfo;
30import android.content.res.Resources;
31import android.content.res.TypedArray;
32import android.content.res.XmlResourceParser;
33import android.graphics.drawable.Drawable;
34import android.graphics.drawable.Icon;
35import android.hardware.hdmi.HdmiDeviceInfo;
36import android.net.Uri;
37import android.os.Bundle;
38import android.os.Parcel;
39import android.os.Parcelable;
40import android.os.UserHandle;
41import android.provider.Settings;
42import android.text.TextUtils;
43import android.util.AttributeSet;
44import android.util.Log;
45import android.util.SparseIntArray;
46import android.util.Xml;
47
48import org.xmlpull.v1.XmlPullParser;
49import org.xmlpull.v1.XmlPullParserException;
50
51import java.io.FileNotFoundException;
52import java.io.IOException;
53import java.io.InputStream;
54import java.lang.annotation.Retention;
55import java.lang.annotation.RetentionPolicy;
56import java.util.HashMap;
57import java.util.HashSet;
58import java.util.Locale;
59import java.util.Map;
60import java.util.Objects;
61import java.util.Set;
62
63/**
64 * This class is used to specify meta information of a TV input.
65 */
66public final class TvInputInfo implements Parcelable {
67    private static final boolean DEBUG = false;
68    private static final String TAG = "TvInputInfo";
69
70    /** @hide */
71    @Retention(RetentionPolicy.SOURCE)
72    @IntDef({TYPE_TUNER, TYPE_OTHER, TYPE_COMPOSITE, TYPE_SVIDEO, TYPE_SCART, TYPE_COMPONENT,
73            TYPE_VGA, TYPE_DVI, TYPE_HDMI, TYPE_DISPLAY_PORT})
74    public @interface Type {}
75
76    // Should be in sync with frameworks/base/core/res/res/values/attrs.xml
77    /**
78     * TV input type: the TV input service is a tuner which provides channels.
79     */
80    public static final int TYPE_TUNER = 0;
81    /**
82     * TV input type: a generic hardware TV input type.
83     */
84    public static final int TYPE_OTHER = 1000;
85    /**
86     * TV input type: the TV input service represents a composite port.
87     */
88    public static final int TYPE_COMPOSITE = 1001;
89    /**
90     * TV input type: the TV input service represents a SVIDEO port.
91     */
92    public static final int TYPE_SVIDEO = 1002;
93    /**
94     * TV input type: the TV input service represents a SCART port.
95     */
96    public static final int TYPE_SCART = 1003;
97    /**
98     * TV input type: the TV input service represents a component port.
99     */
100    public static final int TYPE_COMPONENT = 1004;
101    /**
102     * TV input type: the TV input service represents a VGA port.
103     */
104    public static final int TYPE_VGA = 1005;
105    /**
106     * TV input type: the TV input service represents a DVI port.
107     */
108    public static final int TYPE_DVI = 1006;
109    /**
110     * TV input type: the TV input service is HDMI. (e.g. HDMI 1)
111     */
112    public static final int TYPE_HDMI = 1007;
113    /**
114     * TV input type: the TV input service represents a display port.
115     */
116    public static final int TYPE_DISPLAY_PORT = 1008;
117
118    /**
119     * Used as a String extra field in setup intents created by {@link #createSetupIntent()} to
120     * supply the ID of a specific TV input to set up.
121     */
122    public static final String EXTRA_INPUT_ID = "android.media.tv.extra.INPUT_ID";
123
124    private final ResolveInfo mService;
125
126    private final String mId;
127    private final int mType;
128    private final boolean mIsHardwareInput;
129
130    // TODO: Remove mIconUri when createTvInputInfo() is removed.
131    private Uri mIconUri;
132
133    private final CharSequence mLabel;
134    private final int mLabelResId;
135    private final Icon mIcon;
136    private final Icon mIconStandby;
137    private final Icon mIconDisconnected;
138
139    // Attributes from XML meta data.
140    private final String mSetupActivity;
141    private final boolean mCanRecord;
142    private final int mTunerCount;
143
144    // Attributes specific to HDMI
145    private final HdmiDeviceInfo mHdmiDeviceInfo;
146    private final boolean mIsConnectedToHdmiSwitch;
147    private final String mParentId;
148
149    private final Bundle mExtras;
150
151    /**
152     * Create a new instance of the TvInputInfo class, instantiating it from the given Context,
153     * ResolveInfo, and HdmiDeviceInfo.
154     *
155     * @param service The ResolveInfo returned from the package manager about this TV input service.
156     * @param hdmiDeviceInfo The HdmiDeviceInfo for a HDMI CEC logical device.
157     * @param parentId The ID of this TV input's parent input. {@code null} if none exists.
158     * @param label The label of this TvInputInfo. If it is {@code null} or empty, {@code service}
159     *            label will be loaded.
160     * @param iconUri The {@link android.net.Uri} to load the icon image. See
161     *            {@link android.content.ContentResolver#openInputStream}. If it is {@code null},
162     *            the application icon of {@code service} will be loaded.
163     * @hide
164     * @deprecated Use {@link Builder} instead.
165     */
166    @Deprecated
167    @SystemApi
168    public static TvInputInfo createTvInputInfo(Context context, ResolveInfo service,
169            HdmiDeviceInfo hdmiDeviceInfo, String parentId, String label, Uri iconUri)
170                    throws XmlPullParserException, IOException {
171        TvInputInfo info = new TvInputInfo.Builder(context, service)
172                .setHdmiDeviceInfo(hdmiDeviceInfo)
173                .setParentId(parentId)
174                .setLabel(label)
175                .build();
176        info.mIconUri = iconUri;
177        return info;
178    }
179
180    /**
181     * Create a new instance of the TvInputInfo class, instantiating it from the given Context,
182     * ResolveInfo, and HdmiDeviceInfo.
183     *
184     * @param service The ResolveInfo returned from the package manager about this TV input service.
185     * @param hdmiDeviceInfo The HdmiDeviceInfo for a HDMI CEC logical device.
186     * @param parentId The ID of this TV input's parent input. {@code null} if none exists.
187     * @param labelRes The label resource ID of this TvInputInfo. If it is {@code 0},
188     *            {@code service} label will be loaded.
189     * @param icon The {@link android.graphics.drawable.Icon} to load the icon image. If it is
190     *            {@code null}, the application icon of {@code service} will be loaded.
191     * @hide
192     * @deprecated Use {@link Builder} instead.
193     */
194    @Deprecated
195    @SystemApi
196    public static TvInputInfo createTvInputInfo(Context context, ResolveInfo service,
197            HdmiDeviceInfo hdmiDeviceInfo, String parentId, int labelRes, Icon icon)
198            throws XmlPullParserException, IOException {
199        return new TvInputInfo.Builder(context, service)
200                .setHdmiDeviceInfo(hdmiDeviceInfo)
201                .setParentId(parentId)
202                .setLabel(labelRes)
203                .setIcon(icon)
204                .build();
205    }
206
207    /**
208     * Create a new instance of the TvInputInfo class, instantiating it from the given Context,
209     * ResolveInfo, and TvInputHardwareInfo.
210     *
211     * @param service The ResolveInfo returned from the package manager about this TV input service.
212     * @param hardwareInfo The TvInputHardwareInfo for a TV input hardware device.
213     * @param label The label of this TvInputInfo. If it is {@code null} or empty, {@code service}
214     *            label will be loaded.
215     * @param iconUri The {@link android.net.Uri} to load the icon image. See
216     *            {@link android.content.ContentResolver#openInputStream}. If it is {@code null},
217     *            the application icon of {@code service} will be loaded.
218     * @hide
219     * @deprecated Use {@link Builder} instead.
220     */
221    @Deprecated
222    @SystemApi
223    public static TvInputInfo createTvInputInfo(Context context, ResolveInfo service,
224            TvInputHardwareInfo hardwareInfo, String label, Uri iconUri)
225                    throws XmlPullParserException, IOException {
226        TvInputInfo info = new TvInputInfo.Builder(context, service)
227                .setTvInputHardwareInfo(hardwareInfo)
228                .setLabel(label)
229                .build();
230        info.mIconUri = iconUri;
231        return info;
232    }
233
234    /**
235     * Create a new instance of the TvInputInfo class, instantiating it from the given Context,
236     * ResolveInfo, and TvInputHardwareInfo.
237     *
238     * @param service The ResolveInfo returned from the package manager about this TV input service.
239     * @param hardwareInfo The TvInputHardwareInfo for a TV input hardware device.
240     * @param labelRes The label resource ID of this TvInputInfo. If it is {@code 0},
241     *            {@code service} label will be loaded.
242     * @param icon The {@link android.graphics.drawable.Icon} to load the icon image. If it is
243     *            {@code null}, the application icon of {@code service} will be loaded.
244     * @hide
245     * @deprecated Use {@link Builder} instead.
246     */
247    @Deprecated
248    @SystemApi
249    public static TvInputInfo createTvInputInfo(Context context, ResolveInfo service,
250            TvInputHardwareInfo hardwareInfo, int labelRes, Icon icon)
251            throws XmlPullParserException, IOException {
252        return new TvInputInfo.Builder(context, service)
253                .setTvInputHardwareInfo(hardwareInfo)
254                .setLabel(labelRes)
255                .setIcon(icon)
256                .build();
257    }
258
259    private TvInputInfo(ResolveInfo service, String id, int type, boolean isHardwareInput,
260            CharSequence label, int labelResId, Icon icon, Icon iconStandby, Icon iconDisconnected,
261            String setupActivity, boolean canRecord, int tunerCount, HdmiDeviceInfo hdmiDeviceInfo,
262            boolean isConnectedToHdmiSwitch, String parentId, Bundle extras) {
263        mService = service;
264        mId = id;
265        mType = type;
266        mIsHardwareInput = isHardwareInput;
267        mLabel = label;
268        mLabelResId = labelResId;
269        mIcon = icon;
270        mIconStandby = iconStandby;
271        mIconDisconnected = iconDisconnected;
272        mSetupActivity = setupActivity;
273        mCanRecord = canRecord;
274        mTunerCount = tunerCount;
275        mHdmiDeviceInfo = hdmiDeviceInfo;
276        mIsConnectedToHdmiSwitch = isConnectedToHdmiSwitch;
277        mParentId = parentId;
278        mExtras = extras;
279    }
280
281    /**
282     * Returns a unique ID for this TV input. The ID is generated from the package and class name
283     * implementing the TV input service.
284     */
285    public String getId() {
286        return mId;
287    }
288
289    /**
290     * Returns the parent input ID.
291     *
292     * <p>A TV input may have a parent input if the TV input is actually a logical representation of
293     * a device behind the hardware port represented by the parent input.
294     * For example, a HDMI CEC logical device, connected to a HDMI port, appears as another TV
295     * input. In this case, the parent input of this logical device is the HDMI port.
296     *
297     * <p>Applications may group inputs by parent input ID to provide an easier access to inputs
298     * sharing the same physical port. In the example of HDMI CEC, logical HDMI CEC devices behind
299     * the same HDMI port have the same parent ID, which is the ID representing the port. Thus
300     * applications can group the hardware HDMI port and the logical HDMI CEC devices behind it
301     * together using this method.
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     * @deprecated Use {@link #createSetupIntent()} instead. Settings activity is deprecated.
342     *             Use setup activity instead to provide settings.
343     */
344    @Deprecated
345    public Intent createSettingsIntent() {
346        return null;
347    }
348
349    /**
350     * Returns the type of this TV input.
351     */
352    @Type
353    public int getType() {
354        return mType;
355    }
356
357    /**
358     * Returns the number of tuners this TV input has.
359     *
360     * <p>This method is valid only for inputs of type {@link #TYPE_TUNER}. For inputs of other
361     * types, it returns 0.
362     *
363     * <p>Tuners correspond to physical/logical resources that allow reception of TV signal. Having
364     * <i>N</i> tuners means that the TV input is capable of receiving <i>N</i> different channels
365     * concurrently.
366     */
367    public int getTunerCount() {
368        return mTunerCount;
369    }
370
371    /**
372     * Returns {@code true} if this TV input can record TV programs, {@code false} otherwise.
373     */
374    public boolean canRecord() {
375        return mCanRecord;
376    }
377
378    /**
379     * Returns domain-specific extras associated with this TV input.
380     */
381    public Bundle getExtras() {
382        return mExtras;
383    }
384
385    /**
386     * Returns the HDMI device information of this TV input.
387     * @hide
388     */
389    @SystemApi
390    public HdmiDeviceInfo getHdmiDeviceInfo() {
391        if (mType == TYPE_HDMI) {
392            return mHdmiDeviceInfo;
393        }
394        return null;
395    }
396
397    /**
398     * Returns {@code true} if this TV input is pass-though which does not have any real channels in
399     * TvProvider. {@code false} otherwise.
400     *
401     * @see TvContract#buildChannelUriForPassthroughInput(String)
402     */
403    public boolean isPassthroughInput() {
404        return mType != TYPE_TUNER;
405    }
406
407    /**
408     * Returns {@code true} if this TV input represents a hardware device. (e.g. built-in tuner,
409     * HDMI1) {@code false} otherwise.
410     * @hide
411     */
412    @SystemApi
413    public boolean isHardwareInput() {
414        return mIsHardwareInput;
415    }
416
417    /**
418     * Returns {@code true}, if a CEC device for this TV input is connected to an HDMI switch, i.e.,
419     * the device isn't directly connected to a HDMI port.
420     * @hide
421     */
422    @SystemApi
423    public boolean isConnectedToHdmiSwitch() {
424        return mIsConnectedToHdmiSwitch;
425    }
426
427    /**
428     * Checks if this TV input is marked hidden by the user in the settings.
429     *
430     * @param context Supplies a {@link Context} used to check if this TV input is hidden.
431     * @return {@code true} if the user marked this TV input hidden in settings. {@code false}
432     *         otherwise.
433     */
434    public boolean isHidden(Context context) {
435        return TvInputSettings.isHidden(context, mId, UserHandle.myUserId());
436    }
437
438    /**
439     * Loads the user-displayed label for this TV input.
440     *
441     * @param context Supplies a {@link Context} used to load the label.
442     * @return a CharSequence containing the TV input's label. If the TV input does not have
443     *         a label, its name is returned.
444     */
445    public CharSequence loadLabel(@NonNull Context context) {
446        if (mLabelResId != 0) {
447            return context.getPackageManager().getText(mService.serviceInfo.packageName,
448                    mLabelResId, null);
449        } else if (!TextUtils.isEmpty(mLabel)) {
450            return mLabel;
451        }
452        return mService.loadLabel(context.getPackageManager());
453    }
454
455    /**
456     * Loads the custom label set by user in settings.
457     *
458     * @param context Supplies a {@link Context} used to load the custom label.
459     * @return a CharSequence containing the TV input's custom label. {@code null} if there is no
460     *         custom label.
461     */
462    public CharSequence loadCustomLabel(Context context) {
463        return TvInputSettings.getCustomLabel(context, mId, UserHandle.myUserId());
464    }
465
466    /**
467     * Loads the user-displayed icon for this TV input.
468     *
469     * @param context Supplies a {@link Context} used to load the icon.
470     * @return a Drawable containing the TV input's icon. If the TV input does not have an icon,
471     *         application's icon is returned. If it's unavailable too, {@code null} is returned.
472     */
473    public Drawable loadIcon(@NonNull Context context) {
474        if (mIcon != null) {
475            return mIcon.loadDrawable(context);
476        } else if (mIconUri != null) {
477            try (InputStream is = context.getContentResolver().openInputStream(mIconUri)) {
478                Drawable drawable = Drawable.createFromStream(is, null);
479                if (drawable != null) {
480                    return drawable;
481                }
482            } catch (IOException e) {
483                Log.w(TAG, "Loading the default icon due to a failure on loading " + mIconUri, e);
484                // Falls back.
485            }
486        }
487        return loadServiceIcon(context);
488    }
489
490    /**
491     * Loads the user-displayed icon for this TV input per input state.
492     *
493     * @param context Supplies a {@link Context} used to load the icon.
494     * @param state The input state. Should be one of the followings.
495     *              {@link TvInputManager#INPUT_STATE_CONNECTED},
496     *              {@link TvInputManager#INPUT_STATE_CONNECTED_STANDBY} and
497     *              {@link TvInputManager#INPUT_STATE_DISCONNECTED}.
498     * @return a Drawable containing the TV input's icon for the given state or {@code null} if such
499     *         an icon is not defined.
500     * @hide
501     */
502    @SystemApi
503    public Drawable loadIcon(@NonNull Context context, int state) {
504        if (state == TvInputManager.INPUT_STATE_CONNECTED) {
505            return loadIcon(context);
506        } else if (state == TvInputManager.INPUT_STATE_CONNECTED_STANDBY) {
507            if (mIconStandby != null) {
508                return mIconStandby.loadDrawable(context);
509            }
510        } else if (state == TvInputManager.INPUT_STATE_DISCONNECTED) {
511            if (mIconDisconnected != null) {
512                return mIconDisconnected.loadDrawable(context);
513            }
514        } else {
515            throw new IllegalArgumentException("Unknown state: " + state);
516        }
517        return null;
518    }
519
520    @Override
521    public int describeContents() {
522        return 0;
523    }
524
525    @Override
526    public int hashCode() {
527        return mId.hashCode();
528    }
529
530    @Override
531    public boolean equals(Object o) {
532        if (o == this) {
533            return true;
534        }
535
536        if (!(o instanceof TvInputInfo)) {
537            return false;
538        }
539
540        TvInputInfo obj = (TvInputInfo) o;
541        return Objects.equals(mService, obj.mService)
542                && TextUtils.equals(mId, obj.mId)
543                && mType == obj.mType
544                && mIsHardwareInput == obj.mIsHardwareInput
545                && TextUtils.equals(mLabel, obj.mLabel)
546                && Objects.equals(mIconUri, obj.mIconUri)
547                && mLabelResId == obj.mLabelResId
548                && Objects.equals(mIcon, obj.mIcon)
549                && Objects.equals(mIconStandby, obj.mIconStandby)
550                && Objects.equals(mIconDisconnected, obj.mIconDisconnected)
551                && TextUtils.equals(mSetupActivity, obj.mSetupActivity)
552                && mCanRecord == obj.mCanRecord
553                && mTunerCount == obj.mTunerCount
554                && Objects.equals(mHdmiDeviceInfo, obj.mHdmiDeviceInfo)
555                && mIsConnectedToHdmiSwitch == obj.mIsConnectedToHdmiSwitch
556                && TextUtils.equals(mParentId, obj.mParentId)
557                && Objects.equals(mExtras, obj.mExtras);
558    }
559
560    @Override
561    public String toString() {
562        return "TvInputInfo{id=" + mId
563                + ", pkg=" + mService.serviceInfo.packageName
564                + ", service=" + mService.serviceInfo.name + "}";
565    }
566
567    /**
568     * Used to package this object into a {@link Parcel}.
569     *
570     * @param dest The {@link Parcel} to be written.
571     * @param flags The flags used for parceling.
572     */
573    @Override
574    public void writeToParcel(@NonNull Parcel dest, int flags) {
575        mService.writeToParcel(dest, flags);
576        dest.writeString(mId);
577        dest.writeInt(mType);
578        dest.writeByte(mIsHardwareInput ? (byte) 1 : 0);
579        TextUtils.writeToParcel(mLabel, dest, flags);
580        dest.writeParcelable(mIconUri, flags);
581        dest.writeInt(mLabelResId);
582        dest.writeParcelable(mIcon, flags);
583        dest.writeParcelable(mIconStandby, flags);
584        dest.writeParcelable(mIconDisconnected, flags);
585        dest.writeString(mSetupActivity);
586        dest.writeByte(mCanRecord ? (byte) 1 : 0);
587        dest.writeInt(mTunerCount);
588        dest.writeParcelable(mHdmiDeviceInfo, flags);
589        dest.writeByte(mIsConnectedToHdmiSwitch ? (byte) 1 : 0);
590        dest.writeString(mParentId);
591        dest.writeBundle(mExtras);
592    }
593
594    private Drawable loadServiceIcon(Context context) {
595        if (mService.serviceInfo.icon == 0
596                && mService.serviceInfo.applicationInfo.icon == 0) {
597            return null;
598        }
599        return mService.serviceInfo.loadIcon(context.getPackageManager());
600    }
601
602    public static final Parcelable.Creator<TvInputInfo> CREATOR =
603            new Parcelable.Creator<TvInputInfo>() {
604        @Override
605        public TvInputInfo createFromParcel(Parcel in) {
606            return new TvInputInfo(in);
607        }
608
609        @Override
610        public TvInputInfo[] newArray(int size) {
611            return new TvInputInfo[size];
612        }
613    };
614
615    private TvInputInfo(Parcel in) {
616        mService = ResolveInfo.CREATOR.createFromParcel(in);
617        mId = in.readString();
618        mType = in.readInt();
619        mIsHardwareInput = in.readByte() == 1;
620        mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
621        mIconUri = in.readParcelable(null);
622        mLabelResId = in.readInt();
623        mIcon = in.readParcelable(null);
624        mIconStandby = in.readParcelable(null);
625        mIconDisconnected = in.readParcelable(null);
626        mSetupActivity = in.readString();
627        mCanRecord = in.readByte() == 1;
628        mTunerCount = in.readInt();
629        mHdmiDeviceInfo = in.readParcelable(null);
630        mIsConnectedToHdmiSwitch = in.readByte() == 1;
631        mParentId = in.readString();
632        mExtras = in.readBundle();
633    }
634
635    /**
636     * A convenience builder for creating {@link TvInputInfo} objects.
637     */
638    public static final class Builder {
639        private static final int LENGTH_HDMI_PHYSICAL_ADDRESS = 4;
640        private static final int LENGTH_HDMI_DEVICE_ID = 2;
641
642        private static final String XML_START_TAG_NAME = "tv-input";
643        private static final String DELIMITER_INFO_IN_ID = "/";
644        private static final String PREFIX_HDMI_DEVICE = "HDMI";
645        private static final String PREFIX_HARDWARE_DEVICE = "HW";
646
647        private static final SparseIntArray sHardwareTypeToTvInputType = new SparseIntArray();
648        static {
649            sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_OTHER_HARDWARE,
650                    TYPE_OTHER);
651            sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_TUNER, TYPE_TUNER);
652            sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_COMPOSITE,
653                    TYPE_COMPOSITE);
654            sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_SVIDEO, TYPE_SVIDEO);
655            sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_SCART, TYPE_SCART);
656            sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_COMPONENT,
657                    TYPE_COMPONENT);
658            sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_VGA, TYPE_VGA);
659            sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_DVI, TYPE_DVI);
660            sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_HDMI, TYPE_HDMI);
661            sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_DISPLAY_PORT,
662                    TYPE_DISPLAY_PORT);
663        }
664
665        private final Context mContext;
666        private final ResolveInfo mResolveInfo;
667        private CharSequence mLabel;
668        private int mLabelResId;
669        private Icon mIcon;
670        private Icon mIconStandby;
671        private Icon mIconDisconnected;
672        private String mSetupActivity;
673        private Boolean mCanRecord;
674        private Integer mTunerCount;
675        private TvInputHardwareInfo mTvInputHardwareInfo;
676        private HdmiDeviceInfo mHdmiDeviceInfo;
677        private String mParentId;
678        private Bundle mExtras;
679
680        /**
681         * Constructs a new builder for {@link TvInputInfo}.
682         *
683         * @param context A Context of the application package implementing this class.
684         * @param component The name of the application component to be used for the
685         *            {@link TvInputService}.
686         */
687        public Builder(Context context, ComponentName component) {
688            if (context == null) {
689                throw new IllegalArgumentException("context cannot be null.");
690            }
691            Intent intent = new Intent(TvInputService.SERVICE_INTERFACE).setComponent(component);
692            mResolveInfo = context.getPackageManager().resolveService(intent,
693                    PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
694            if (mResolveInfo == null) {
695                throw new IllegalArgumentException("Invalid component. Can't find the service.");
696            }
697            mContext = context;
698        }
699
700        /**
701         * Constructs a new builder for {@link TvInputInfo}.
702         *
703         * @param resolveInfo The ResolveInfo returned from the package manager about this TV input
704         *            service.
705         * @hide
706         */
707        public Builder(Context context, ResolveInfo resolveInfo) {
708            if (context == null) {
709                throw new IllegalArgumentException("context cannot be null");
710            }
711            if (resolveInfo == null) {
712                throw new IllegalArgumentException("resolveInfo cannot be null");
713            }
714            mContext = context;
715            mResolveInfo = resolveInfo;
716        }
717
718        /**
719         * Sets the icon.
720         *
721         * @param icon The icon that represents this TV input.
722         * @return This Builder object to allow for chaining of calls to builder methods.
723         * @hide
724         */
725        @SystemApi
726        public Builder setIcon(Icon icon) {
727            this.mIcon = icon;
728            return this;
729        }
730
731        /**
732         * Sets the icon for a given input state.
733         *
734         * @param icon The icon that represents this TV input for the given state.
735         * @param state The input state. Should be one of the followings.
736         *              {@link TvInputManager#INPUT_STATE_CONNECTED},
737         *              {@link TvInputManager#INPUT_STATE_CONNECTED_STANDBY} and
738         *              {@link TvInputManager#INPUT_STATE_DISCONNECTED}.
739         * @return This Builder object to allow for chaining of calls to builder methods.
740         * @hide
741         */
742        @SystemApi
743        public Builder setIcon(Icon icon, int state) {
744            if (state == TvInputManager.INPUT_STATE_CONNECTED) {
745                this.mIcon = icon;
746            } else if (state == TvInputManager.INPUT_STATE_CONNECTED_STANDBY) {
747                this.mIconStandby = icon;
748            } else if (state == TvInputManager.INPUT_STATE_DISCONNECTED) {
749                this.mIconDisconnected = icon;
750            } else {
751                throw new IllegalArgumentException("Unknown state: " + state);
752            }
753            return this;
754        }
755
756        /**
757         * Sets the label.
758         *
759         * @param label The text to be used as label.
760         * @return This Builder object to allow for chaining of calls to builder methods.
761         * @hide
762         */
763        @SystemApi
764        public Builder setLabel(CharSequence label) {
765            if (mLabelResId != 0) {
766                throw new IllegalStateException("Resource ID for label is already set.");
767            }
768            this.mLabel = label;
769            return this;
770        }
771
772        /**
773         * Sets the label.
774         *
775         * @param resId The resource ID of the text to use.
776         * @return This Builder object to allow for chaining of calls to builder methods.
777         * @hide
778         */
779        @SystemApi
780        public Builder setLabel(@StringRes int resId) {
781            if (mLabel != null) {
782                throw new IllegalStateException("Label text is already set.");
783            }
784            this.mLabelResId = resId;
785            return this;
786        }
787
788        /**
789         * Sets the HdmiDeviceInfo.
790         *
791         * @param hdmiDeviceInfo The HdmiDeviceInfo for a HDMI CEC logical device.
792         * @return This Builder object to allow for chaining of calls to builder methods.
793         * @hide
794         */
795        @SystemApi
796        public Builder setHdmiDeviceInfo(HdmiDeviceInfo hdmiDeviceInfo) {
797            if (mTvInputHardwareInfo != null) {
798                Log.w(TAG, "TvInputHardwareInfo will not be used to build this TvInputInfo");
799                mTvInputHardwareInfo = null;
800            }
801            this.mHdmiDeviceInfo = hdmiDeviceInfo;
802            return this;
803        }
804
805        /**
806         * Sets the parent ID.
807         *
808         * @param parentId The parent ID.
809         * @return This Builder object to allow for chaining of calls to builder methods.
810         * @hide
811         */
812        @SystemApi
813        public Builder setParentId(String parentId) {
814            this.mParentId = parentId;
815            return this;
816        }
817
818        /**
819         * Sets the TvInputHardwareInfo.
820         *
821         * @param tvInputHardwareInfo
822         * @return This Builder object to allow for chaining of calls to builder methods.
823         * @hide
824         */
825        @SystemApi
826        public Builder setTvInputHardwareInfo(TvInputHardwareInfo tvInputHardwareInfo) {
827            if (mHdmiDeviceInfo != null) {
828                Log.w(TAG, "mHdmiDeviceInfo will not be used to build this TvInputInfo");
829                mHdmiDeviceInfo = null;
830            }
831            this.mTvInputHardwareInfo = tvInputHardwareInfo;
832            return this;
833        }
834
835        /**
836         * Sets the tuner count. Valid only for {@link #TYPE_TUNER}.
837         *
838         * @param tunerCount The number of tuners this TV input has.
839         * @return This Builder object to allow for chaining of calls to builder methods.
840         */
841        public Builder setTunerCount(int tunerCount) {
842            this.mTunerCount = tunerCount;
843            return this;
844        }
845
846        /**
847         * Sets whether this TV input can record TV programs or not.
848         *
849         * @param canRecord Whether this TV input can record TV programs.
850         * @return This Builder object to allow for chaining of calls to builder methods.
851         */
852        public Builder setCanRecord(boolean canRecord) {
853            this.mCanRecord = canRecord;
854            return this;
855        }
856
857        /**
858         * Sets domain-specific extras associated with this TV input.
859         *
860         * @param extras Domain-specific extras associated with this TV input. Keys <em>must</em> be
861         *            a scoped name, i.e. prefixed with a package name you own, so that different
862         *            developers will not create conflicting keys.
863         * @return This Builder object to allow for chaining of calls to builder methods.
864         */
865        public Builder setExtras(Bundle extras) {
866            this.mExtras = extras;
867            return this;
868        }
869
870        /**
871         * Creates a {@link TvInputInfo} instance with the specified fields. Most of the information
872         * is obtained by parsing the AndroidManifest and {@link TvInputService#SERVICE_META_DATA}
873         * for the {@link TvInputService} this TV input implements.
874         *
875         * @return TvInputInfo containing information about this TV input.
876         */
877        public TvInputInfo build() {
878            ComponentName componentName = new ComponentName(mResolveInfo.serviceInfo.packageName,
879                    mResolveInfo.serviceInfo.name);
880            String id;
881            int type;
882            boolean isHardwareInput = false;
883            boolean isConnectedToHdmiSwitch = false;
884
885            if (mHdmiDeviceInfo != null) {
886                id = generateInputId(componentName, mHdmiDeviceInfo);
887                type = TYPE_HDMI;
888                isHardwareInput = true;
889                isConnectedToHdmiSwitch = (mHdmiDeviceInfo.getPhysicalAddress() & 0x0FFF) != 0;
890            } else if (mTvInputHardwareInfo != null) {
891                id = generateInputId(componentName, mTvInputHardwareInfo);
892                type = sHardwareTypeToTvInputType.get(mTvInputHardwareInfo.getType(), TYPE_TUNER);
893                isHardwareInput = true;
894            } else {
895                id = generateInputId(componentName);
896                type = TYPE_TUNER;
897            }
898            parseServiceMetadata(type);
899            return new TvInputInfo(mResolveInfo, id, type, isHardwareInput, mLabel, mLabelResId,
900                    mIcon, mIconStandby, mIconDisconnected, mSetupActivity,
901                    mCanRecord == null ? false : mCanRecord, mTunerCount == null ? 0 : mTunerCount,
902                    mHdmiDeviceInfo, isConnectedToHdmiSwitch, mParentId, mExtras);
903        }
904
905        private static String generateInputId(ComponentName name) {
906            return name.flattenToShortString();
907        }
908
909        private static String generateInputId(ComponentName name, HdmiDeviceInfo hdmiDeviceInfo) {
910            // Example of the format : "/HDMI%04X%02X"
911            String format = DELIMITER_INFO_IN_ID + PREFIX_HDMI_DEVICE
912                    + "%0" + LENGTH_HDMI_PHYSICAL_ADDRESS + "X"
913                    + "%0" + LENGTH_HDMI_DEVICE_ID + "X";
914            return name.flattenToShortString() + String.format(Locale.ENGLISH, format,
915                    hdmiDeviceInfo.getPhysicalAddress(), hdmiDeviceInfo.getId());
916        }
917
918        private static String generateInputId(ComponentName name,
919                TvInputHardwareInfo tvInputHardwareInfo) {
920            return name.flattenToShortString() + DELIMITER_INFO_IN_ID + PREFIX_HARDWARE_DEVICE
921                    + tvInputHardwareInfo.getDeviceId();
922        }
923
924        private void parseServiceMetadata(int inputType) {
925            ServiceInfo si = mResolveInfo.serviceInfo;
926            PackageManager pm = mContext.getPackageManager();
927            try (XmlResourceParser parser =
928                         si.loadXmlMetaData(pm, TvInputService.SERVICE_META_DATA)) {
929                if (parser == null) {
930                    throw new IllegalStateException("No " + TvInputService.SERVICE_META_DATA
931                            + " meta-data found for " + si.name);
932                }
933
934                Resources res = pm.getResourcesForApplication(si.applicationInfo);
935                AttributeSet attrs = Xml.asAttributeSet(parser);
936
937                int type;
938                while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
939                        && type != XmlPullParser.START_TAG) {
940                }
941
942                String nodeName = parser.getName();
943                if (!XML_START_TAG_NAME.equals(nodeName)) {
944                    throw new IllegalStateException("Meta-data does not start with "
945                            + XML_START_TAG_NAME + " tag for " + si.name);
946                }
947
948                TypedArray sa = res.obtainAttributes(attrs,
949                        com.android.internal.R.styleable.TvInputService);
950                mSetupActivity = sa.getString(
951                        com.android.internal.R.styleable.TvInputService_setupActivity);
952                if (mCanRecord == null) {
953                    mCanRecord = sa.getBoolean(
954                            com.android.internal.R.styleable.TvInputService_canRecord, false);
955                }
956                if (mTunerCount == null && inputType == TYPE_TUNER) {
957                    mTunerCount = sa.getInt(
958                            com.android.internal.R.styleable.TvInputService_tunerCount, 1);
959                }
960                sa.recycle();
961            } catch (IOException | XmlPullParserException e) {
962                throw new IllegalStateException("Failed reading meta-data for " + si.packageName, e);
963            } catch (NameNotFoundException e) {
964                throw new IllegalStateException("No resources found for " + si.packageName, e);
965            }
966        }
967    }
968
969    /**
970     * Utility class for putting and getting settings for TV input.
971     *
972     * @hide
973     */
974    @SystemApi
975    public static final class TvInputSettings {
976        private static final String TV_INPUT_SEPARATOR = ":";
977        private static final String CUSTOM_NAME_SEPARATOR = ",";
978
979        private TvInputSettings() { }
980
981        private static boolean isHidden(Context context, String inputId, int userId) {
982            return getHiddenTvInputIds(context, userId).contains(inputId);
983        }
984
985        private static String getCustomLabel(Context context, String inputId, int userId) {
986            return getCustomLabels(context, userId).get(inputId);
987        }
988
989        /**
990         * Returns a set of TV input IDs which are marked as hidden by user in the settings.
991         *
992         * @param context The application context
993         * @param userId The user ID for the stored hidden input set
994         * @hide
995         */
996        @SystemApi
997        public static Set<String> getHiddenTvInputIds(Context context, int userId) {
998            String hiddenIdsString = Settings.Secure.getStringForUser(
999                    context.getContentResolver(), Settings.Secure.TV_INPUT_HIDDEN_INPUTS, userId);
1000            Set<String> set = new HashSet<>();
1001            if (TextUtils.isEmpty(hiddenIdsString)) {
1002                return set;
1003            }
1004            String[] ids = hiddenIdsString.split(TV_INPUT_SEPARATOR);
1005            for (String id : ids) {
1006                set.add(Uri.decode(id));
1007            }
1008            return set;
1009        }
1010
1011        /**
1012         * Returns a map of TV input ID/custom label pairs set by the user in the settings.
1013         *
1014         * @param context The application context
1015         * @param userId The user ID for the stored hidden input map
1016         * @hide
1017         */
1018        @SystemApi
1019        public static Map<String, String> getCustomLabels(Context context, int userId) {
1020            String labelsString = Settings.Secure.getStringForUser(
1021                    context.getContentResolver(), Settings.Secure.TV_INPUT_CUSTOM_LABELS, userId);
1022            Map<String, String> map = new HashMap<>();
1023            if (TextUtils.isEmpty(labelsString)) {
1024                return map;
1025            }
1026            String[] pairs = labelsString.split(TV_INPUT_SEPARATOR);
1027            for (String pairString : pairs) {
1028                String[] pair = pairString.split(CUSTOM_NAME_SEPARATOR);
1029                map.put(Uri.decode(pair[0]), Uri.decode(pair[1]));
1030            }
1031            return map;
1032        }
1033
1034        /**
1035         * Stores a set of TV input IDs which are marked as hidden by user. This is expected to
1036         * be called from the settings app.
1037         *
1038         * @param context The application context
1039         * @param hiddenInputIds A set including all the hidden TV input IDs
1040         * @param userId The user ID for the stored hidden input set
1041         * @hide
1042         */
1043        @SystemApi
1044        public static void putHiddenTvInputs(Context context, Set<String> hiddenInputIds,
1045                int userId) {
1046            StringBuilder builder = new StringBuilder();
1047            boolean firstItem = true;
1048            for (String inputId : hiddenInputIds) {
1049                ensureValidField(inputId);
1050                if (firstItem) {
1051                    firstItem = false;
1052                } else {
1053                    builder.append(TV_INPUT_SEPARATOR);
1054                }
1055                builder.append(Uri.encode(inputId));
1056            }
1057            Settings.Secure.putStringForUser(context.getContentResolver(),
1058                    Settings.Secure.TV_INPUT_HIDDEN_INPUTS, builder.toString(), userId);
1059
1060            // Notify of the TvInputInfo changes.
1061            TvInputManager tm = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
1062            for (String inputId : hiddenInputIds) {
1063                TvInputInfo info = tm.getTvInputInfo(inputId);
1064                if (info != null) {
1065                    tm.updateTvInputInfo(info);
1066                }
1067            }
1068        }
1069
1070        /**
1071         * Stores a map of TV input ID/custom label set by user. This is expected to be
1072         * called from the settings app.
1073         *
1074         * @param context The application context.
1075         * @param customLabels A map of TV input ID/custom label pairs
1076         * @param userId The user ID for the stored hidden input map
1077         * @hide
1078         */
1079        @SystemApi
1080        public static void putCustomLabels(Context context,
1081                Map<String, String> customLabels, int userId) {
1082            StringBuilder builder = new StringBuilder();
1083            boolean firstItem = true;
1084            for (Map.Entry<String, String> entry: customLabels.entrySet()) {
1085                ensureValidField(entry.getKey());
1086                ensureValidField(entry.getValue());
1087                if (firstItem) {
1088                    firstItem = false;
1089                } else {
1090                    builder.append(TV_INPUT_SEPARATOR);
1091                }
1092                builder.append(Uri.encode(entry.getKey()));
1093                builder.append(CUSTOM_NAME_SEPARATOR);
1094                builder.append(Uri.encode(entry.getValue()));
1095            }
1096            Settings.Secure.putStringForUser(context.getContentResolver(),
1097                    Settings.Secure.TV_INPUT_CUSTOM_LABELS, builder.toString(), userId);
1098
1099            // Notify of the TvInputInfo changes.
1100            TvInputManager tm = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
1101            for (String inputId : customLabels.keySet()) {
1102                TvInputInfo info = tm.getTvInputInfo(inputId);
1103                if (info != null) {
1104                    tm.updateTvInputInfo(info);
1105                }
1106            }
1107        }
1108
1109        private static void ensureValidField(String value) {
1110            if (TextUtils.isEmpty(value)) {
1111                throw new IllegalArgumentException(value + " should not empty ");
1112            }
1113        }
1114    }
1115}
1116