1 /*
2  * Copyright (C) 2023 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 com.android.systemui.car.distantdisplay.util;
18 
19 import android.content.pm.ApplicationInfo;
20 import android.content.pm.PackageManager;
21 import android.content.res.Resources;
22 import android.content.res.XmlResourceParser;
23 import android.text.TextUtils;
24 import android.util.Log;
25 
26 import org.xmlpull.v1.XmlPullParser;
27 import org.xmlpull.v1.XmlPullParserException;
28 
29 import java.io.IOException;
30 import java.util.ArrayDeque;
31 import java.util.ArrayList;
32 import java.util.List;
33 
34 /**
35  * Util class that contains helper method used by System Bar button.
36  */
37 public class AppCategoryDetector {
38     private static final String TAG = AppCategoryDetector.class.getSimpleName();
39     private static final String TAG_AUTOMOTIVE_APP = "automotiveApp";
40     private static final String TAG_USES = "uses";
41     private static final String ATTRIBUTE_NAME = "name";
42     private static final String TYPE_VIDEO = "video";
43     // Max no. of uses tags in automotiveApp XML. This is an arbitrary limit to be defensive
44     // to bad input.
45     private static final int MAX_APP_TYPES = 64;
46 
AppCategoryDetector()47     private AppCategoryDetector() {
48     }
49 
50     /**
51      * Returns whether app identified by {@code packageName} declares itself as a video app.
52      */
isVideoApp(PackageManager packageManager, String packageName)53     public static boolean isVideoApp(PackageManager packageManager, String packageName) {
54         return getAutomotiveAppTypes(packageManager, packageName).contains(TYPE_VIDEO);
55     }
56 
57     /**
58      * Queries an app manifest and resources to determine the types of AAOS app it declares itself
59      * as.
60      *
61      * @param packageManager {@link PackageManager} to query.
62      * @param packageName App package.
63      * @return List of AAOS app-types from XML resources.
64      */
getAutomotiveAppTypes(PackageManager packageManager, String packageName)65     private static List<String> getAutomotiveAppTypes(PackageManager packageManager,
66             String packageName) {
67         ApplicationInfo appInfo;
68         Resources appResources;
69         try {
70             appInfo = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
71             appResources = packageManager.getResourcesForApplication(appInfo);
72         } catch (PackageManager.NameNotFoundException e) {
73             Log.w(TAG, "Unexpected package not found for: " + packageName, e);
74             return new ArrayList<>();
75         }
76 
77         int resourceId =
78                 appInfo.metaData != null
79                         ? appInfo.metaData.getInt("com.android.automotive", -1) : -1;
80         Log.d(TAG, "Is package automotive type: " + resourceId);
81         if (resourceId == -1) {
82             Log.d(TAG, "Returning empty automotive app types");
83             return new ArrayList<>();
84         }
85         try (XmlResourceParser parser = appResources.getXml(resourceId)) {
86             return parseAutomotiveAppTypes(parser);
87         }
88     }
89 
parseAutomotiveAppTypes(XmlPullParser parser)90     static List<String> parseAutomotiveAppTypes(XmlPullParser parser) {
91         try {
92             // This pattern for parsing can be seen in Javadocs for XmlPullParser.
93             List<String> appTypes = new ArrayList<>();
94             ArrayDeque<String> tagStack = new ArrayDeque<>();
95             int eventType = parser.getEventType();
96             while (eventType != XmlPullParser.END_DOCUMENT) {
97                 if (eventType == XmlPullParser.START_TAG) {
98                     String tag = parser.getName();
99                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
100                         Log.v(TAG, "Start tag " + tag);
101                     }
102                     tagStack.addFirst(tag);
103                     if (!validTagStack(tagStack)) {
104                         Log.w(TAG, "Invalid XML; tagStack: " + tagStack);
105                         return new ArrayList<>();
106                     }
107                     if (TAG_USES.equals(tag)) {
108                         String nameValue =
109                                 parser.getAttributeValue(/* namespace= */ null , ATTRIBUTE_NAME);
110                         if (TextUtils.isEmpty(nameValue)) {
111                             Log.w(TAG, "Invalid XML; uses tag with missing/empty name attribute");
112                             return new ArrayList<>();
113                         }
114                         appTypes.add(nameValue);
115                         if (appTypes.size() > MAX_APP_TYPES) {
116                             Log.w(TAG, "Too many uses tags in automotiveApp tag");
117                             return new ArrayList<>();
118                         }
119                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
120                             Log.v(TAG, "Found appType: " + nameValue);
121                         }
122                     }
123                 } else if (eventType == XmlPullParser.END_TAG) {
124                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
125                         Log.v(TAG, "End tag " + parser.getName());
126                     }
127                     tagStack.removeFirst();
128                 }
129                 eventType = parser.next();
130             }
131             return appTypes;
132         } catch (XmlPullParserException | IOException e) {
133             Log.w(TAG, "Unexpected exception whiling parsing XML resource", e);
134             return new ArrayList<>();
135         }
136     }
137 
validTagStack(ArrayDeque<String> tagStack)138     private static boolean validTagStack(ArrayDeque<String> tagStack) {
139         // Expected to be called after a new tag is pushed on this stack.
140         // Ensures that XML is of form:
141         // <automotiveApp>
142         //     <uses/>
143         //     <uses/>
144         //     ....
145         // </automotiveApp>
146         switch (tagStack.size()) {
147             case 1:
148                 return TAG_AUTOMOTIVE_APP.equals(tagStack.peekFirst());
149             case 2:
150                 return TAG_USES.equals(tagStack.peekFirst());
151             default:
152                 return false;
153         }
154     }
155 
156 }
157