1 /*
2  * Copyright 2021 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.interactive;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ResolveInfo;
27 import android.content.pm.ServiceInfo;
28 import android.content.res.Resources;
29 import android.content.res.TypedArray;
30 import android.content.res.XmlResourceParser;
31 import android.os.Parcel;
32 import android.os.Parcelable;
33 import android.util.AttributeSet;
34 import android.util.Xml;
35 
36 import org.xmlpull.v1.XmlPullParser;
37 import org.xmlpull.v1.XmlPullParserException;
38 
39 import java.io.IOException;
40 import java.lang.annotation.Retention;
41 import java.lang.annotation.RetentionPolicy;
42 import java.util.ArrayList;
43 import java.util.List;
44 
45 /**
46  * This class is used to specify meta information of a TV interactive app.
47  */
48 public final class TvInteractiveAppServiceInfo implements Parcelable {
49     private static final boolean DEBUG = false;
50     private static final String TAG = "TvInteractiveAppServiceInfo";
51 
52     private static final String XML_START_TAG_NAME = "tv-interactive-app";
53 
54     /** @hide */
55     @Retention(RetentionPolicy.SOURCE)
56     @IntDef(prefix = { "INTERACTIVE_APP_TYPE_" }, value = {
57             INTERACTIVE_APP_TYPE_HBBTV,
58             INTERACTIVE_APP_TYPE_ATSC,
59             INTERACTIVE_APP_TYPE_GINGA,
60             INTERACTIVE_APP_TYPE_TARGETED_AD,
61             INTERACTIVE_APP_TYPE_OTHER
62     })
63     public @interface InteractiveAppType {}
64 
65     /** HbbTV interactive app type */
66     public static final int INTERACTIVE_APP_TYPE_HBBTV = 0x1;
67     /** ATSC interactive app type */
68     public static final int INTERACTIVE_APP_TYPE_ATSC = 0x2;
69     /** Ginga interactive app type */
70     public static final int INTERACTIVE_APP_TYPE_GINGA = 0x4;
71     /** Targeted Advertisement interactive app type */
72     public static final int INTERACTIVE_APP_TYPE_TARGETED_AD = 0x8;
73     /** Other interactive app type */
74     public static final int INTERACTIVE_APP_TYPE_OTHER = 0x80000000;
75 
76     private final ResolveInfo mService;
77     private final String mId;
78     private int mTypes;
79     private final List<String> mExtraTypes = new ArrayList<>();
80 
81     /**
82      * Constructs a TvInteractiveAppServiceInfo object.
83      *
84      * @param context the application context
85      * @param component the component name of the TvInteractiveAppService
86      */
TvInteractiveAppServiceInfo(@onNull Context context, @NonNull ComponentName component)87     public TvInteractiveAppServiceInfo(@NonNull Context context, @NonNull ComponentName component) {
88         if (context == null) {
89             throw new IllegalArgumentException("context cannot be null.");
90         }
91         Intent intent =
92                 new Intent(TvInteractiveAppService.SERVICE_INTERFACE).setComponent(component);
93         ResolveInfo resolveInfo = context.getPackageManager().resolveService(intent,
94                 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
95         if (resolveInfo == null) {
96             throw new IllegalArgumentException("Invalid component. Can't find the service.");
97         }
98 
99         ComponentName componentName = new ComponentName(resolveInfo.serviceInfo.packageName,
100                 resolveInfo.serviceInfo.name);
101         String id;
102         id = generateInteractiveAppServiceId(componentName);
103         List<String> types = new ArrayList<>();
104         parseServiceMetadata(resolveInfo, context, types);
105 
106         mService = resolveInfo;
107         mId = id;
108         toTypesFlag(types);
109     }
TvInteractiveAppServiceInfo( ResolveInfo service, String id, int types, List<String> extraTypes)110     private TvInteractiveAppServiceInfo(
111             ResolveInfo service, String id, int types, List<String> extraTypes) {
112         mService = service;
113         mId = id;
114         mTypes = types;
115         mExtraTypes.addAll(extraTypes);
116     }
117 
TvInteractiveAppServiceInfo(@onNull Parcel in)118     private TvInteractiveAppServiceInfo(@NonNull Parcel in) {
119         mService = ResolveInfo.CREATOR.createFromParcel(in);
120         mId = in.readString();
121         mTypes = in.readInt();
122         in.readStringList(mExtraTypes);
123     }
124 
125     public static final @NonNull Creator<TvInteractiveAppServiceInfo> CREATOR =
126             new Creator<TvInteractiveAppServiceInfo>() {
127                 @Override
128                 public TvInteractiveAppServiceInfo createFromParcel(Parcel in) {
129                     return new TvInteractiveAppServiceInfo(in);
130                 }
131 
132                 @Override
133                 public TvInteractiveAppServiceInfo[] newArray(int size) {
134                     return new TvInteractiveAppServiceInfo[size];
135                 }
136             };
137 
138     @Override
describeContents()139     public int describeContents() {
140         return 0;
141     }
142 
143     @Override
writeToParcel(@onNull Parcel dest, int flags)144     public void writeToParcel(@NonNull Parcel dest, int flags) {
145         mService.writeToParcel(dest, flags);
146         dest.writeString(mId);
147         dest.writeInt(mTypes);
148         dest.writeStringList(mExtraTypes);
149     }
150 
151     /**
152      * Returns a unique ID for this TV interactive app service. The ID is generated from the package
153      * and class name implementing the TV interactive app service.
154      */
155     @NonNull
getId()156     public String getId() {
157         return mId;
158     }
159 
160     /**
161      * Returns the component of the TV Interactive App service.
162      * @hide
163      */
getComponent()164     public ComponentName getComponent() {
165         return new ComponentName(mService.serviceInfo.packageName, mService.serviceInfo.name);
166     }
167 
168     /**
169      * Returns the information of the service that implements this TV Interactive App service.
170      */
171     @Nullable
getServiceInfo()172     public ServiceInfo getServiceInfo() {
173         return mService.serviceInfo;
174     }
175 
176     /**
177      * Gets supported interactive app types.
178      *
179      * <p>The supported interactive app types is in a bit map format. For example:
180      * <pre><code>
181      *   int types = tvInteractiveAppInfo.getSupportedTypes();
182      *   if (types & TvInteractiveAppInfo.INTERACTIVE_APP_TYPE_HBBTV != 0) {
183      *     // HbbTV type is supported. Do something...
184      *   }
185      *   if (types & TvInteractiveAppInfo.INTERACTIVE_APP_TYPE_ATSC == 0) {
186      *     // ATSC type is not supported. Do something...
187      *   }
188      * </code></pre>
189      *
190      * @return An int bit map representing supported types.
191      */
192     @InteractiveAppType
193     @NonNull
getSupportedTypes()194     public int getSupportedTypes() {
195         return mTypes;
196     }
197 
198     /**
199      * Gets custom supported interactive app types which are not listed.
200      *
201      * @see #getSupportedTypes()
202      */
203     @NonNull
getCustomSupportedTypes()204     public List<String> getCustomSupportedTypes() {
205         return mExtraTypes;
206     }
207 
generateInteractiveAppServiceId(ComponentName name)208     private static String generateInteractiveAppServiceId(ComponentName name) {
209         return name.flattenToShortString();
210     }
211 
parseServiceMetadata( ResolveInfo resolveInfo, Context context, List<String> types)212     private static void parseServiceMetadata(
213             ResolveInfo resolveInfo, Context context, List<String> types) {
214         ServiceInfo si = resolveInfo.serviceInfo;
215         PackageManager pm = context.getPackageManager();
216         try (XmlResourceParser parser =
217                      si.loadXmlMetaData(pm, TvInteractiveAppService.SERVICE_META_DATA)) {
218             if (parser == null) {
219                 throw new IllegalStateException(
220                         "No " + TvInteractiveAppService.SERVICE_META_DATA
221                         + " meta-data found for " + si.name);
222             }
223 
224             Resources res = pm.getResourcesForApplication(si.applicationInfo);
225             AttributeSet attrs = Xml.asAttributeSet(parser);
226 
227             int type;
228             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
229                     && type != XmlPullParser.START_TAG) {
230                 // move to the START_TAG
231             }
232 
233             String nodeName = parser.getName();
234             if (!XML_START_TAG_NAME.equals(nodeName)) {
235                 throw new IllegalStateException("Meta-data does not start with "
236                         + XML_START_TAG_NAME + " tag for " + si.name);
237             }
238 
239             TypedArray sa = res.obtainAttributes(attrs,
240                     com.android.internal.R.styleable.TvInteractiveAppService);
241             CharSequence[] textArr = sa.getTextArray(
242                     com.android.internal.R.styleable.TvInteractiveAppService_supportedTypes);
243             for (CharSequence cs : textArr) {
244                 types.add(cs.toString().toLowerCase());
245             }
246 
247             sa.recycle();
248         } catch (IOException | XmlPullParserException e) {
249             throw new IllegalStateException(
250                     "Failed reading meta-data for " + si.packageName, e);
251         } catch (PackageManager.NameNotFoundException e) {
252             throw new IllegalStateException("No resources found for " + si.packageName, e);
253         }
254     }
255 
toTypesFlag(List<String> types)256     private void toTypesFlag(List<String> types) {
257         mTypes = 0;
258         mExtraTypes.clear();
259         for (String type : types) {
260             switch (type) {
261                 case "hbbtv":
262                     mTypes |= INTERACTIVE_APP_TYPE_HBBTV;
263                     break;
264                 case "atsc":
265                     mTypes |= INTERACTIVE_APP_TYPE_ATSC;
266                     break;
267                 case "ginga":
268                     mTypes |= INTERACTIVE_APP_TYPE_GINGA;
269                     break;
270                 case "targeted_ad":
271                     mTypes |= INTERACTIVE_APP_TYPE_TARGETED_AD;
272                 default:
273                     mTypes |= INTERACTIVE_APP_TYPE_OTHER;
274                     mExtraTypes.add(type);
275                     break;
276             }
277         }
278     }
279 }
280