1 /*
2  * Copyright (C) 2012 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 package com.android.tradefed.util;
17 
18 import com.android.tradefed.log.LogUtil.CLog;
19 
20 import com.google.common.base.Strings;
21 
22 import java.io.File;
23 import java.util.ArrayList;
24 import java.util.List;
25 import java.util.regex.Matcher;
26 import java.util.regex.Pattern;
27 
28 /**
29  * Class that extracts info from apk by parsing output of 'aapt dump badging'.
30  * <p/>
31  * aapt must be on PATH
32  */
33 public class AaptParser {
34     private static final Pattern PKG_PATTERN = Pattern.compile(
35             "^package:\\s+name='(.*?)'\\s+versionCode='(\\d*)'\\s+versionName='(.*?)'.*$",
36             Pattern.MULTILINE);
37     private static final Pattern LABEL_PATTERN = Pattern.compile(
38             "^application-label:'(.+?)'.*$",
39             Pattern.MULTILINE);
40     private static final Pattern SDK_PATTERN = Pattern.compile(
41             "^sdkVersion:'(\\d+)'", Pattern.MULTILINE);
42     private static final Pattern TARGET_SDK_PATTERN =
43             Pattern.compile("^targetSdkVersion:'(\\d+)'", Pattern.MULTILINE);
44     /**
45      * 1. Patterns for native code are not always present, so the list may stay empty; 2. There may
46      * be more than two native-codes for APKs built by Soong; 3. Java regular expressions cannot be
47      * captured for each group in a repeated group, so hard-code it for now to handle up to ten
48      * native-codes.
49      */
50     private static final int MAX_NUM_NATIVE_CODE = 10;
51 
52     private static final Pattern NATIVE_CODE_PATTERN =
53             Pattern.compile(
54                     "native-code: '(.*?)'" + Strings.repeat("( '.*?')?", MAX_NUM_NATIVE_CODE - 1));
55 
56     private static final Pattern REQUEST_LEGACY_STORAGE_PATTERN =
57             Pattern.compile("requestLegacyExternalStorage.*=\\(.*\\)(.*)", Pattern.MULTILINE);
58 
59     private static final Pattern ALT_NATIVE_CODE_PATTERN =
60             Pattern.compile("alt-native-code: '(.*)'");
61     private static final Pattern USES_PERMISSION_MANAGE_EXTERNAL_STORAGE_PATTERN =
62             Pattern.compile(
63                     "uses-permission: name='android\\.permission\\.MANAGE_EXTERNAL_STORAGE'");
64     private static final int AAPT_TIMEOUT_MS = 60000;
65     private static final int INVALID_SDK = -1;
66 
67     /**
68      * Enum of options for AAPT version used to parse APK files.
69      */
70     public static enum AaptVersion {
71         AAPT {
72             @Override
dumpBadgingCommand(File apkFile)73             public String[] dumpBadgingCommand(File apkFile) {
74                 return new String[] {"aapt", "dump", "badging", apkFile.getAbsolutePath()};
75             }
76 
77             @Override
dumpXmlTreeCommand(File apkFile)78             public String[] dumpXmlTreeCommand(File apkFile) {
79                 return new String[] {
80                     "aapt", "dump", "xmltree", apkFile.getAbsolutePath(), "AndroidManifest.xml"
81                 };
82             }
83         },
84 
85         AAPT2 {
86             @Override
dumpBadgingCommand(File apkFile)87             public String[] dumpBadgingCommand(File apkFile) {
88                 return new String[] {"aapt2", "dump", "badging", apkFile.getAbsolutePath()};
89             }
90 
91             @Override
dumpXmlTreeCommand(File apkFile)92             public String[] dumpXmlTreeCommand(File apkFile) {
93                 return new String[] {
94                     "aapt2",
95                     "dump",
96                     "xmltree",
97                     apkFile.getAbsolutePath(),
98                     "--file",
99                     "AndroidManifest.xml"
100                 };
101             }
102         };
103 
dumpBadgingCommand(File apkFile)104         public abstract String[] dumpBadgingCommand(File apkFile);
105 
dumpXmlTreeCommand(File apkFile)106         public abstract String[] dumpXmlTreeCommand(File apkFile);
107     }
108 
109     private String mPackageName;
110     private String mVersionCode;
111     private String mVersionName;
112     private List<String> mNativeCode = new ArrayList<>();
113     private String mLabel;
114     private int mSdkVersion = INVALID_SDK;
115     private int mTargetSdkVersion = 10000;
116     private boolean mRequestLegacyStorage = false;
117     private boolean mUsesPermissionManageExternalStorage;
118 
119     // @VisibleForTesting
AaptParser()120     AaptParser() {
121     }
122 
parse(String aaptOut)123     boolean parse(String aaptOut) {
124         //CLog.e(aaptOut);
125         Matcher m = PKG_PATTERN.matcher(aaptOut);
126         if (m.find()) {
127             mPackageName = m.group(1);
128             mLabel = mPackageName;
129             mVersionCode = m.group(2);
130             mVersionName = m.group(3);
131             m = LABEL_PATTERN.matcher(aaptOut);
132             if (m.find()) {
133                 mLabel = m.group(1);
134             }
135             m = SDK_PATTERN.matcher(aaptOut);
136             if (m.find()) {
137                 mSdkVersion = Integer.parseInt(m.group(1));
138             }
139             m = TARGET_SDK_PATTERN.matcher(aaptOut);
140             if (m.find()) {
141                 mTargetSdkVersion = Integer.parseInt(m.group(1));
142             }
143             m = NATIVE_CODE_PATTERN.matcher(aaptOut);
144             if (m.find()) {
145                 for (int i = 1; i <= m.groupCount(); i++) {
146                     if (m.group(i) != null) {
147                         mNativeCode.add(m.group(i).replace("'", "").trim());
148                     }
149                 }
150             }
151             m = ALT_NATIVE_CODE_PATTERN.matcher(aaptOut);
152             if (m.find()) {
153                 for (int i = 1; i <= m.groupCount(); i++) {
154                     if (m.group(i) != null) {
155                         mNativeCode.add(m.group(i).replace("'", ""));
156                     }
157                 }
158             }
159             m = USES_PERMISSION_MANAGE_EXTERNAL_STORAGE_PATTERN.matcher(aaptOut);
160             mUsesPermissionManageExternalStorage = m.find();
161             return true;
162         }
163         CLog.e("Failed to parse package and version info from 'aapt dump badging'. stdout: '%s'",
164                 aaptOut);
165         return false;
166     }
167 
168     /**
169      * Parse info from the apk.
170      *
171      * @param apkFile the apk file
172      * @return the {@link AaptParser} or <code>null</code> if failed to extract the information
173      */
parse(File apkFile)174     public static AaptParser parse(File apkFile) {
175         return parse(apkFile, AaptVersion.AAPT);
176     }
177 
178     /**
179      * Parse info from the apk.
180      *
181      * @param apkFile the apk file
182      * @param aaptVersion the aapt version
183      * @return the {@link AaptParser} or <code>null</code> if failed to extract the information
184      */
parse(File apkFile, AaptVersion aaptVersion)185     public static AaptParser parse(File apkFile, AaptVersion aaptVersion) {
186         CommandResult result =
187                 RunUtil.getDefault()
188                         .runTimedCmdRetry(
189                                 AAPT_TIMEOUT_MS, 0L, 2, aaptVersion.dumpBadgingCommand(apkFile));
190 
191         String stderr = result.getStderr();
192         if (stderr != null && !stderr.isEmpty()) {
193             CLog.e("%s dump badging stderr: %s", toolName(aaptVersion), stderr);
194         }
195         AaptParser p = new AaptParser();
196         if (!CommandStatus.SUCCESS.equals(result.getStatus()) || !p.parse(result.getStdout())) {
197             CLog.e(
198                     "Failed to run %s on %s. stdout: %s",
199                     toolName(aaptVersion), apkFile.getAbsoluteFile(), result.getStdout());
200             return null;
201         }
202         result =
203                 RunUtil.getDefault()
204                         .runTimedCmdRetry(
205                                 AAPT_TIMEOUT_MS,
206                                 0L,
207                                 2,
208                                 aaptVersion.dumpXmlTreeCommand(apkFile));
209 
210         stderr = result.getStderr();
211         if (stderr != null && !stderr.isEmpty()) {
212             CLog.e("%s dump xmltree AndroidManifest.xml stderr: %s", toolName(aaptVersion), stderr);
213         }
214 
215         if (!CommandStatus.SUCCESS.equals(result.getStatus())
216                 || !p.parseXmlTree(result.getStdout())) {
217             CLog.e(
218                     "Failed to run %s on %s. stdout: %s",
219                     toolName(aaptVersion), apkFile.getAbsoluteFile(), result.getStdout());
220             return null;
221         }
222         return p;
223     }
224 
parseXmlTree(String aaptOut)225     boolean parseXmlTree(String aaptOut) {
226         Matcher m = REQUEST_LEGACY_STORAGE_PATTERN.matcher(aaptOut);
227         if (m.find()) {
228             // 0xffffffff is true and 0x0 is false
229             mRequestLegacyStorage = m.group(1).equals("0xffffffff");
230         }
231         // REQUEST_LEGACY_STORAGE_PATTERN may or may not be present
232         return true;
233     }
234 
getPackageName()235     public String getPackageName() {
236         return mPackageName;
237     }
238 
getVersionCode()239     public String getVersionCode() {
240         return mVersionCode;
241     }
242 
getVersionName()243     public String getVersionName() {
244         return mVersionName;
245     }
246 
getNativeCode()247     public List<String> getNativeCode() {
248         return mNativeCode;
249     }
250 
getLabel()251     public String getLabel() {
252         return mLabel;
253     }
254 
getSdkVersion()255     public int getSdkVersion() {
256         return mSdkVersion;
257     }
258 
getTargetSdkVersion()259     public int getTargetSdkVersion() {
260         return mTargetSdkVersion;
261     }
262 
263     /**
264      * Check if the app is requesting legacy storage.
265      *
266      * @return boolean return true if requestLegacyExternalStorage is true in AndroidManifest.xml
267      */
isRequestingLegacyStorage()268     public boolean isRequestingLegacyStorage() {
269         return mRequestLegacyStorage;
270     }
271 
isUsingPermissionManageExternalStorage()272     public boolean isUsingPermissionManageExternalStorage() {
273         return mUsesPermissionManageExternalStorage;
274     }
275 
toolName(AaptVersion version)276     private static String toolName(AaptVersion version) {
277         return version.name().toLowerCase();
278     }
279 }
280