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