1 /*
2  * Copyright (C) 2016 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.tradefed.util;
18 
19 import com.android.tradefed.log.LogUtil.CLog;
20 
21 import java.io.BufferedReader;
22 import java.io.File;
23 import java.io.FileReader;
24 import java.io.FilenameFilter;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.List;
28 import java.util.regex.Pattern;
29 
30 /**
31  * Utility class to download files from x20 to the corp TF environment. In particular, it is used in
32  * conjunction with the <a href="http://go/app-compatibility-readme">App Compatibility pipeline</a>.
33  */
34 public class PublicApkUtil {
35     private static final Pattern DATE_FORMAT = Pattern.compile("\\d{8}");
36     private static final long DOWNLOAD_TIMEOUT_MS = 60 * 1000;
37     private static final int DOWNLOAD_RETRIES = 3;
38     private static final String LATEST_FILE = "latest.txt";
39 
40     /**
41      * Helper method which constructs the dated CNS directory from the base directory and either the
42      * supplied date option or the most recent directory.
43      *
44      * @param baseDir The base directory with the "latest" file and dated subdirectories.
45      * @param subDir A specific target directory, or null if using the latest file.
46      * @return The {@link File} of the x20 dir where the APKs are stored.
47      * @throws IOException
48      */
constructApkDir(String baseDir, String subDir)49     public static File constructApkDir(String baseDir, String subDir) throws IOException {
50         if (subDir != null) {
51             return new File(baseDir, subDir);
52         }
53         File latestFile = null;
54         try {
55             latestFile =
56                     downloadFile(
57                             new File(baseDir, LATEST_FILE), DOWNLOAD_TIMEOUT_MS, DOWNLOAD_RETRIES);
58             String date = FileUtil.readStringFromFile(latestFile).trim();
59             if (DATE_FORMAT.matcher(date).matches()) {
60                 return new File(baseDir, date);
61             }
62             return null;
63         } finally {
64             FileUtil.deleteFile(latestFile);
65         }
66     }
67 
68     /**
69      * A configurable helper method for downloading a remote file.
70      *
71      * @param remoteFile The remote {@link File} location.
72      * @param downloadTimeout The download timeout in milliseconds.
73      * @param downloadRetries The download retry count, in case of failure.
74      * @return The local {@link File} that was downloaded.
75      * @throws IOException
76      */
downloadFile(File remoteFile, long downloadTimeout, int downloadRetries)77     public static File downloadFile(File remoteFile, long downloadTimeout, int downloadRetries)
78             throws IOException {
79         CLog.i("Attempting to download %s", remoteFile);
80         File tmpFile = FileUtil.createTempFile(remoteFile.getName(), null);
81         FileUtil.copyFile(remoteFile, tmpFile);
82         return tmpFile;
83     }
84 
85     /**
86      * Helper method which downloads the ranking file and returns the list of apks.
87      *
88      * @param flavor The APK variant to pick.
89      * @param dir The {@link File} of the dated x20 dir.
90      * @param fallbackToApkScan fallback to scan for apk files in folder if no ranking csv file
91      * @return The list of {@link ApkInfo} objects.
92      * @throws IOException
93      */
getApkList(String flavor, File dir, boolean fallbackToApkScan)94     public static List<ApkInfo> getApkList(String flavor, File dir, boolean fallbackToApkScan)
95             throws IOException {
96         File apkFile = new File(dir, String.format("%s_ranking.csv", flavor));
97         if (!apkFile.exists() && fallbackToApkScan) {
98             return getApkListFromDirectory(dir);
99         } else {
100             return getApkListFromRankingInfo(apkFile);
101         }
102     }
103 
104     /**
105      * Constructs a list of degenerate {@link ApkInfo} based on apks files found in provided base
106      * directory. The {@link ApkInfo} instance only contains relative filename, without the ranking
107      * information.
108      *
109      * @param baseDir
110      * @return
111      * @throws IOException
112      */
getApkListFromDirectory(File baseDir)113     private static List<ApkInfo> getApkListFromDirectory(File baseDir) throws IOException {
114         List<ApkInfo> apkList = new ArrayList<>();
115         File[] apks =
116                 baseDir.listFiles(
117                         new FilenameFilter() {
118                             @Override
119                             public boolean accept(File dir, String name) {
120                                 // filters out all apk files
121                                 return name.endsWith(".apk");
122                             }
123                         });
124         for (File apk : apks) {
125             AaptParser parser = AaptParser.parse(apk);
126             if (parser == null) {
127                 throw new IOException(
128                         String.format("Failed to parse apk file %s", apk.getCanonicalPath()));
129             }
130             ApkInfo apkInfo =
131                     new ApkInfo(
132                             -1,
133                             parser.getPackageName(),
134                             parser.getVersionName(),
135                             parser.getVersionCode(),
136                             apk.getName());
137             apkList.add(apkInfo);
138         }
139         return apkList;
140     }
141 
142     /**
143      * Parses ranking information csv file into the data structure representing a list of apks with
144      * ranking and package information
145      *
146      * @param rankingInfo the path to ranking csv file
147      * @return
148      * @throws IOException
149      */
getApkListFromRankingInfo(File rankingInfo)150     private static List<ApkInfo> getApkListFromRankingInfo(File rankingInfo) throws IOException {
151         List<ApkInfo> apkList = new ArrayList<>();
152         File copiedFile = null;
153         BufferedReader br = null;
154         try {
155             copiedFile = downloadFile(rankingInfo, DOWNLOAD_TIMEOUT_MS, DOWNLOAD_RETRIES);
156             br = new BufferedReader(new FileReader(copiedFile));
157             String line;
158             boolean firstLine = true;
159             while ((line = br.readLine()) != null) {
160                 if (firstLine) {
161                     firstLine = false;
162                 } else {
163                     try {
164                         apkList.add(ApkInfo.fromCsvLine(line));
165                     } catch (IllegalArgumentException e) {
166                         CLog.e("Ranking file not formatted properly, skipping.");
167                         CLog.e(e);
168                     }
169                 }
170             }
171         } finally {
172             StreamUtil.close(br);
173             FileUtil.deleteFile(copiedFile);
174         }
175         return apkList;
176     }
177 
178     /**
179      * Helper class which holds information about the ranking list such as rank, package name, etc.
180      */
181     public static class ApkInfo {
182         public final int rank;
183         public final String packageName;
184         public final String versionString;
185         public final String versionCode;
186         public final String fileName;
187 
ApkInfo( int rank, String packageName, String versionString, String versionCode, String fileName)188         public ApkInfo(
189                 int rank,
190                 String packageName,
191                 String versionString,
192                 String versionCode,
193                 String fileName) {
194             this.rank = rank;
195             this.packageName = packageName;
196             this.versionString = versionString;
197             this.versionCode = versionCode;
198             this.fileName = fileName;
199         }
200 
fromCsvLine(String line)201         public static ApkInfo fromCsvLine(String line) {
202             String[] cols = QuotationAwareTokenizer.tokenizeLine(line, ",");
203             int rank = -1;
204             try {
205                 rank = Integer.parseInt(cols[0]);
206             } catch (NumberFormatException e) {
207                 // rethrow as IAE with content of problematic line
208                 throw new IllegalArgumentException(
209                         String.format("Invalid line (rank field not a number): %s", line), e);
210             }
211             if (cols.length != 5) {
212                 throw new IllegalArgumentException(
213                         String.format("Invalid line (expected 5 data columns): %s", line));
214             }
215             return new ApkInfo(rank, cols[1], cols[2], cols[3], cols[4]);
216         }
217 
218         @Override
toString()219         public String toString() {
220             return String.format(
221                     "Package: %s v%s (%s), rank: %d, file: %s",
222                     packageName, versionCode, versionString, rank, fileName);
223         }
224     }
225 }
226