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 
17 package android.media.tv;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.StringRes;
22 import android.annotation.SystemApi;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.PackageManager;
27 import android.content.pm.PackageManager.NameNotFoundException;
28 import android.content.pm.ResolveInfo;
29 import android.content.pm.ServiceInfo;
30 import android.content.res.Resources;
31 import android.content.res.TypedArray;
32 import android.content.res.XmlResourceParser;
33 import android.graphics.drawable.Drawable;
34 import android.graphics.drawable.Icon;
35 import android.hardware.hdmi.HdmiDeviceInfo;
36 import android.net.Uri;
37 import android.os.Bundle;
38 import android.os.Parcel;
39 import android.os.Parcelable;
40 import android.os.UserHandle;
41 import android.provider.Settings;
42 import android.text.TextUtils;
43 import android.util.AttributeSet;
44 import android.util.Log;
45 import android.util.SparseIntArray;
46 import android.util.Xml;
47 
48 import org.xmlpull.v1.XmlPullParser;
49 import org.xmlpull.v1.XmlPullParserException;
50 
51 import java.io.FileNotFoundException;
52 import java.io.IOException;
53 import java.io.InputStream;
54 import java.lang.annotation.Retention;
55 import java.lang.annotation.RetentionPolicy;
56 import java.util.HashMap;
57 import java.util.HashSet;
58 import java.util.Locale;
59 import java.util.Map;
60 import java.util.Objects;
61 import java.util.Set;
62 
63 /**
64  * This class is used to specify meta information of a TV input.
65  */
66 public 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
createTvInputInfo(Context context, ResolveInfo service, HdmiDeviceInfo hdmiDeviceInfo, String parentId, String label, Uri iconUri)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
createTvInputInfo(Context context, ResolveInfo service, HdmiDeviceInfo hdmiDeviceInfo, String parentId, int labelRes, Icon icon)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
createTvInputInfo(Context context, ResolveInfo service, TvInputHardwareInfo hardwareInfo, String label, Uri iconUri)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
createTvInputInfo(Context context, ResolveInfo service, TvInputHardwareInfo hardwareInfo, int labelRes, Icon icon)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 
TvInputInfo(ResolveInfo service, String id, int type, boolean isHardwareInput, CharSequence label, int labelResId, Icon icon, Icon iconStandby, Icon iconDisconnected, String setupActivity, boolean canRecord, int tunerCount, HdmiDeviceInfo hdmiDeviceInfo, boolean isConnectedToHdmiSwitch, String parentId, Bundle extras)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      */
getId()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      */
getParentId()306     public String getParentId() {
307         return mParentId;
308     }
309 
310     /**
311      * Returns the information of the service that implements this TV input.
312      */
getServiceInfo()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      */
getComponent()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      */
createSetupIntent()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
createSettingsIntent()345     public Intent createSettingsIntent() {
346         return null;
347     }
348 
349     /**
350      * Returns the type of this TV input.
351      */
352     @Type
getType()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      */
getTunerCount()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      */
canRecord()374     public boolean canRecord() {
375         return mCanRecord;
376     }
377 
378     /**
379      * Returns domain-specific extras associated with this TV input.
380      */
getExtras()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
getHdmiDeviceInfo()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      */
isPassthroughInput()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
isHardwareInput()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
isConnectedToHdmiSwitch()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      */
isHidden(Context context)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      */
loadLabel(@onNull Context context)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      */
loadCustomLabel(Context context)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      */
loadIcon(@onNull Context context)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
loadIcon(@onNull Context context, int state)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
describeContents()521     public int describeContents() {
522         return 0;
523     }
524 
525     @Override
hashCode()526     public int hashCode() {
527         return mId.hashCode();
528     }
529 
530     @Override
equals(Object o)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
toString()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
writeToParcel(@onNull Parcel dest, int flags)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 
loadServiceIcon(Context context)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 
TvInputInfo(Parcel in)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 {
sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_OTHER_HARDWARE, TYPE_OTHER)649             sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_OTHER_HARDWARE,
650                     TYPE_OTHER);
sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_TUNER, TYPE_TUNER)651             sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_TUNER, TYPE_TUNER);
sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_COMPOSITE, TYPE_COMPOSITE)652             sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_COMPOSITE,
653                     TYPE_COMPOSITE);
sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_SVIDEO, TYPE_SVIDEO)654             sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_SVIDEO, TYPE_SVIDEO);
sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_SCART, TYPE_SCART)655             sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_SCART, TYPE_SCART);
sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_COMPONENT, TYPE_COMPONENT)656             sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_COMPONENT,
657                     TYPE_COMPONENT);
sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_VGA, TYPE_VGA)658             sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_VGA, TYPE_VGA);
sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_DVI, TYPE_DVI)659             sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_DVI, TYPE_DVI);
sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_HDMI, TYPE_HDMI)660             sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_HDMI, TYPE_HDMI);
sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_DISPLAY_PORT, TYPE_DISPLAY_PORT)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          */
Builder(Context context, ComponentName component)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          */
Builder(Context context, ResolveInfo resolveInfo)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
setIcon(Icon icon)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
setIcon(Icon icon, int state)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
setLabel(CharSequence label)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
setLabel(@tringRes int resId)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
setHdmiDeviceInfo(HdmiDeviceInfo hdmiDeviceInfo)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
setParentId(String parentId)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
setTvInputHardwareInfo(TvInputHardwareInfo tvInputHardwareInfo)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          */
setTunerCount(int tunerCount)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          */
setCanRecord(boolean canRecord)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          */
setExtras(Bundle extras)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          */
build()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 
generateInputId(ComponentName name)905         private static String generateInputId(ComponentName name) {
906             return name.flattenToShortString();
907         }
908 
generateInputId(ComponentName name, HdmiDeviceInfo hdmiDeviceInfo)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 
generateInputId(ComponentName name, TvInputHardwareInfo tvInputHardwareInfo)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 
parseServiceMetadata(int inputType)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 
TvInputSettings()979         private TvInputSettings() { }
980 
isHidden(Context context, String inputId, int userId)981         private static boolean isHidden(Context context, String inputId, int userId) {
982             return getHiddenTvInputIds(context, userId).contains(inputId);
983         }
984 
getCustomLabel(Context context, String inputId, int userId)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
getHiddenTvInputIds(Context context, int userId)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
getCustomLabels(Context context, int userId)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
putHiddenTvInputs(Context context, Set<String> hiddenInputIds, int userId)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
putCustomLabels(Context context, Map<String, String> customLabels, int userId)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 
ensureValidField(String value)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