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