1 /* 2 * Copyright (C) 2010 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.cts.verifier; 18 19 import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode; 20 import static com.android.cts.verifier.TestListAdapter.setTestNameSuffix; 21 22 import android.app.backup.BackupManager; 23 import android.content.ContentProvider; 24 import android.content.ContentResolver; 25 import android.content.ContentValues; 26 import android.content.Context; 27 import android.content.UriMatcher; 28 import android.database.Cursor; 29 import android.database.MatrixCursor; 30 import android.database.sqlite.SQLiteDatabase; 31 import android.database.sqlite.SQLiteOpenHelper; 32 import android.database.sqlite.SQLiteQueryBuilder; 33 import android.net.Uri; 34 import android.os.ParcelFileDescriptor; 35 36 import androidx.annotation.NonNull; 37 38 import com.android.compatibility.common.util.ReportLog; 39 import com.android.compatibility.common.util.TestScreenshotsMetadata; 40 41 import java.io.ByteArrayOutputStream; 42 import java.io.File; 43 import java.io.FileInputStream; 44 import java.io.FileNotFoundException; 45 import java.io.IOException; 46 import java.io.ObjectOutputStream; 47 import java.util.Arrays; 48 import java.util.Comparator; 49 50 /** 51 * {@link ContentProvider} that provides read and write access to the test results. 52 */ 53 public class TestResultsProvider extends ContentProvider { 54 55 static final String _ID = "_id"; 56 /** String name of the test like "com.android.cts.verifier.foo.FooTestActivity" */ 57 static final String COLUMN_TEST_NAME = "testname"; 58 /** Integer test result corresponding to constants in {@link TestResult}. */ 59 static final String COLUMN_TEST_RESULT = "testresult"; 60 /** Boolean indicating whether the test info has been seen. */ 61 static final String COLUMN_TEST_INFO_SEEN = "testinfoseen"; 62 /** String containing the test's details. */ 63 static final String COLUMN_TEST_DETAILS = "testdetails"; 64 /** ReportLog containing the test result metrics. */ 65 static final String COLUMN_TEST_METRICS = "testmetrics"; 66 /** TestResultHistory containing the test run histories. */ 67 static final String COLUMN_TEST_RESULT_HISTORY = "testresulthistory"; 68 /** TestScreenshotsMetadata containing the test screenshot metadata. */ 69 static final String COLUMN_TEST_SCREENSHOTS_METADATA = "testscreenshotsmetadata"; 70 71 /** 72 * Report saved location 73 */ 74 private static final String REPORTS_PATH = "reports"; 75 private static final String RESULTS_PATH = "results"; 76 private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); 77 private static final int RESULTS_ALL = 1; 78 private static final int RESULTS_ID = 2; 79 private static final int RESULTS_TEST_NAME = 3; 80 private static final int REPORT = 4; 81 private static final int REPORT_ROW = 5; 82 private static final int REPORT_FILE_NAME = 6; 83 private static final int REPORT_LATEST = 7; 84 private static final String TABLE_NAME = "results"; 85 private SQLiteOpenHelper mOpenHelper; 86 private BackupManager mBackupManager; 87 88 /** 89 * Get the URI from the result content. 90 * 91 * @param context 92 * @return Uri 93 */ getResultContentUri(Context context)94 public static Uri getResultContentUri(Context context) { 95 final String packageName = context.getPackageName(); 96 final Uri contentUri = Uri.parse("content://" + packageName + ".testresultsprovider"); 97 return Uri.withAppendedPath(contentUri, RESULTS_PATH); 98 } 99 100 /** 101 * Get the URI from the test name. 102 * 103 * @param context 104 * @param testName 105 * @return Uri 106 */ getTestNameUri(Context context)107 public static Uri getTestNameUri(Context context) { 108 String name = context.getClass().getName(); 109 name = setTestNameSuffix(sCurrentDisplayMode, name); 110 return getTestNameUri(context, name); 111 } 112 113 /** 114 * Gets the URI from the context and test name. 115 * @param context current context 116 * @param testName name of the test which needs to get the URI 117 * @return the URI for the test result 118 */ getTestNameUri(Context context, String testName)119 public static Uri getTestNameUri(Context context, String testName) { 120 return Uri.withAppendedPath(getResultContentUri(context), testName); 121 } 122 setTestResult(Context context, String testName, int testResult, String testDetails, ReportLog reportLog, TestResultHistoryCollection historyCollection, TestScreenshotsMetadata screenshotsMetadata)123 static void setTestResult(Context context, String testName, int testResult, 124 String testDetails, ReportLog reportLog, TestResultHistoryCollection historyCollection, 125 TestScreenshotsMetadata screenshotsMetadata) { 126 ContentValues values = new ContentValues(2); 127 values.put(TestResultsProvider.COLUMN_TEST_RESULT, testResult); 128 values.put(TestResultsProvider.COLUMN_TEST_NAME, testName); 129 values.put(TestResultsProvider.COLUMN_TEST_DETAILS, testDetails); 130 values.put(TestResultsProvider.COLUMN_TEST_METRICS, serialize(reportLog)); 131 values.put(TestResultsProvider.COLUMN_TEST_RESULT_HISTORY, serialize(historyCollection)); 132 values.put( 133 TestResultsProvider.COLUMN_TEST_SCREENSHOTS_METADATA, 134 serialize(screenshotsMetadata)); 135 136 final Uri uri = getResultContentUri(context); 137 ContentResolver resolver = context.getContentResolver(); 138 int numUpdated = resolver.update(uri, values, 139 TestResultsProvider.COLUMN_TEST_NAME + " = ?", 140 new String[]{testName}); 141 142 if (numUpdated == 0) { 143 resolver.insert(uri, values); 144 } 145 } 146 147 /** 148 * Called by screenshot consumers to provide extra metadata that allows to understand 149 * screenshots better. 150 * 151 * @param context application context 152 * @param testName corresponding test name 153 * @param screenshotsMetadata A {@link TestScreenshotsMetadata} set that contains metadata 154 */ updateColumnTestScreenshotsMetadata( Context context, String testName, TestScreenshotsMetadata screenshotsMetadata)155 public static void updateColumnTestScreenshotsMetadata( 156 Context context, String testName, TestScreenshotsMetadata screenshotsMetadata) { 157 ContentValues values = new ContentValues(2); 158 values.put(TestResultsProvider.COLUMN_TEST_NAME, testName); 159 values.put( 160 TestResultsProvider.COLUMN_TEST_SCREENSHOTS_METADATA, 161 serialize(screenshotsMetadata)); 162 final Uri uri = getResultContentUri(context); 163 ContentResolver resolver = context.getContentResolver(); 164 int numUpdated = resolver.update( 165 uri, values, TestResultsProvider.COLUMN_TEST_NAME + " = ?", 166 new String[]{ testName }); 167 if (numUpdated == 0) { 168 resolver.insert(uri, values); 169 } 170 171 } 172 serialize(Object o)173 private static byte[] serialize(Object o) { 174 ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); 175 ObjectOutputStream objectOutput = null; 176 try { 177 objectOutput = new ObjectOutputStream(byteStream); 178 objectOutput.writeObject(o); 179 return byteStream.toByteArray(); 180 } catch (IOException e) { 181 return null; 182 } finally { 183 try { 184 if (objectOutput != null) { 185 objectOutput.close(); 186 } 187 byteStream.close(); 188 } catch (IOException e) { 189 // Ignore close exception. 190 } 191 } 192 } 193 194 @Override onCreate()195 public boolean onCreate() { 196 final String authority = getContext().getPackageName() + ".testresultsprovider"; 197 198 URI_MATCHER.addURI(authority, RESULTS_PATH, RESULTS_ALL); 199 URI_MATCHER.addURI(authority, RESULTS_PATH + "/#", RESULTS_ID); 200 URI_MATCHER.addURI(authority, RESULTS_PATH + "/*", RESULTS_TEST_NAME); 201 URI_MATCHER.addURI(authority, REPORTS_PATH, REPORT); 202 URI_MATCHER.addURI(authority, REPORTS_PATH + "/latest", REPORT_LATEST); 203 URI_MATCHER.addURI(authority, REPORTS_PATH + "/#", REPORT_ROW); 204 URI_MATCHER.addURI(authority, REPORTS_PATH + "/*", REPORT_FILE_NAME); 205 206 mOpenHelper = new TestResultsOpenHelper(getContext()); 207 mBackupManager = new BackupManager(getContext()); 208 return false; 209 } 210 211 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)212 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 213 String sortOrder) { 214 SQLiteQueryBuilder query = new SQLiteQueryBuilder(); 215 query.setTables(TABLE_NAME); 216 217 int match = URI_MATCHER.match(uri); 218 switch (match) { 219 case RESULTS_ALL: 220 break; 221 222 case RESULTS_ID: 223 query.appendWhere(_ID); 224 query.appendWhere("="); 225 query.appendWhere(uri.getPathSegments().get(1)); 226 break; 227 228 case RESULTS_TEST_NAME: 229 query.appendWhere(COLUMN_TEST_NAME); 230 query.appendWhere("="); 231 query.appendWhere("\"" + uri.getPathSegments().get(1) + "\""); 232 break; 233 234 case REPORT: 235 final MatrixCursor cursor = new MatrixCursor(new String[]{"filename"}); 236 for (String filename : getFileList()) { 237 cursor.addRow(new Object[]{filename}); 238 } 239 return cursor; 240 241 case REPORT_FILE_NAME: 242 case REPORT_ROW: 243 case REPORT_LATEST: 244 throw new IllegalArgumentException( 245 "Report query not supported. Use content read."); 246 247 default: 248 throw new IllegalArgumentException("Unknown URI: " + uri); 249 } 250 251 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 252 return query.query(db, projection, selection, selectionArgs, null, null, sortOrder); 253 } 254 255 @Override insert(Uri uri, ContentValues values)256 public Uri insert(Uri uri, ContentValues values) { 257 int match = URI_MATCHER.match(uri); 258 switch (match) { 259 case REPORT: 260 throw new IllegalArgumentException( 261 "Report insert not supported. Use content query."); 262 case REPORT_FILE_NAME: 263 case REPORT_ROW: 264 case REPORT_LATEST: 265 throw new IllegalArgumentException( 266 "Report insert not supported. Use content read."); 267 default: 268 break; 269 } 270 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 271 long id = db.insert(TABLE_NAME, null, values); 272 getContext().getContentResolver().notifyChange(uri, null); 273 mBackupManager.dataChanged(); 274 return Uri.withAppendedPath(getResultContentUri(getContext()), "" + id); 275 } 276 277 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)278 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 279 int match = URI_MATCHER.match(uri); 280 switch (match) { 281 case RESULTS_ALL: 282 break; 283 284 case RESULTS_ID: 285 String idSelection = _ID + "=" + uri.getPathSegments().get(1); 286 if (selection != null && selection.length() > 0) { 287 selection = idSelection + " AND " + selection; 288 } else { 289 selection = idSelection; 290 } 291 break; 292 293 case RESULTS_TEST_NAME: 294 String testNameSelection = COLUMN_TEST_NAME + "=\"" 295 + uri.getPathSegments().get(1) + "\""; 296 if (selection != null && selection.length() > 0) { 297 selection = testNameSelection + " AND " + selection; 298 } else { 299 selection = testNameSelection; 300 } 301 break; 302 case REPORT: 303 throw new IllegalArgumentException( 304 "Report update not supported. Use content query."); 305 case REPORT_FILE_NAME: 306 case REPORT_ROW: 307 case REPORT_LATEST: 308 throw new IllegalArgumentException( 309 "Report update not supported. Use content read."); 310 default: 311 throw new IllegalArgumentException("Unknown URI: " + uri); 312 } 313 314 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 315 int numUpdated = db.update(TABLE_NAME, values, selection, selectionArgs); 316 if (numUpdated > 0) { 317 getContext().getContentResolver().notifyChange(uri, null); 318 mBackupManager.dataChanged(); 319 } 320 return numUpdated; 321 } 322 323 @Override delete(Uri uri, String selection, String[] selectionArgs)324 public int delete(Uri uri, String selection, String[] selectionArgs) { 325 int match = URI_MATCHER.match(uri); 326 switch (match) { 327 case REPORT: 328 throw new IllegalArgumentException( 329 "Report delete not supported. Use content query."); 330 case REPORT_FILE_NAME: 331 case REPORT_ROW: 332 case REPORT_LATEST: 333 throw new IllegalArgumentException( 334 "Report delete not supported. Use content read."); 335 default: 336 break; 337 } 338 339 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 340 int numDeleted = db.delete(TABLE_NAME, selection, selectionArgs); 341 if (numDeleted > 0) { 342 getContext().getContentResolver().notifyChange(uri, null); 343 mBackupManager.dataChanged(); 344 } 345 return numDeleted; 346 } 347 348 @Override getType(Uri uri)349 public String getType(Uri uri) { 350 return null; 351 } 352 353 @Override openFile(@onNull Uri uri, @NonNull String mode)354 public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) 355 throws FileNotFoundException { 356 String fileName; 357 String[] fileList; 358 File file; 359 int match = URI_MATCHER.match(uri); 360 switch (match) { 361 case REPORT_ROW: 362 int rowId = Integer.parseInt(uri.getPathSegments().get(1)); 363 file = getFileByIndex(rowId); 364 break; 365 366 case REPORT_FILE_NAME: 367 fileName = uri.getPathSegments().get(1); 368 file = getFileByName(fileName); 369 break; 370 371 case REPORT_LATEST: 372 file = getLatestFile(); 373 break; 374 375 case REPORT: 376 throw new IllegalArgumentException("Read not supported. Use content query."); 377 378 case RESULTS_ALL: 379 case RESULTS_ID: 380 case RESULTS_TEST_NAME: 381 throw new IllegalArgumentException("Read not supported for URI: " + uri); 382 383 default: 384 throw new IllegalArgumentException("Unknown URI: " + uri); 385 } 386 try { 387 FileInputStream fis = new FileInputStream(file); 388 return ParcelFileDescriptor.dup(fis.getFD()); 389 } catch (IOException e) { 390 throw new IllegalArgumentException("Cannot open file."); 391 } 392 } 393 394 getFileByIndex(int index)395 private File getFileByIndex(int index) { 396 File[] files = getFiles(); 397 if (files.length == 0) { 398 throw new IllegalArgumentException("No report saved at " + index + "."); 399 } 400 return files[index]; 401 } 402 getFileByName(String fileName)403 private File getFileByName(String fileName) { 404 File[] files = getFiles(); 405 if (files.length == 0) { 406 throw new IllegalArgumentException("No reports saved."); 407 } 408 for (File file : files) { 409 if (fileName.equals(file.getName())) { 410 return file; 411 } 412 } 413 throw new IllegalArgumentException(fileName + " not found."); 414 } 415 getLatestFile()416 private File getLatestFile() { 417 File[] files = getFiles(); 418 if (files.length == 0) { 419 throw new IllegalArgumentException("No reports saved."); 420 } 421 return files[files.length - 1]; 422 } 423 getFileList()424 private String[] getFileList() { 425 return Arrays.stream(getFiles()).map(File::getName).toArray(String[]::new); 426 } 427 getFiles()428 private File[] getFiles() { 429 File dir = getContext().getDir(ReportExporter.REPORT_DIRECTORY, Context.MODE_PRIVATE); 430 File[] files = dir.listFiles(); 431 Arrays.sort(files, Comparator.comparingLong(File::lastModified)); 432 return files; 433 } 434 435 private static class TestResultsOpenHelper extends SQLiteOpenHelper { 436 437 private static final String DATABASE_NAME = "results.db"; 438 439 private static final int DATABASE_VERSION = 6; 440 TestResultsOpenHelper(Context context)441 TestResultsOpenHelper(Context context) { 442 super(context, DATABASE_NAME, null, DATABASE_VERSION); 443 } 444 445 @Override onCreate(SQLiteDatabase db)446 public void onCreate(SQLiteDatabase db) { 447 db.execSQL("CREATE TABLE " + TABLE_NAME + " (" 448 + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " 449 + COLUMN_TEST_NAME + " TEXT, " 450 + COLUMN_TEST_RESULT + " INTEGER," 451 + COLUMN_TEST_INFO_SEEN + " INTEGER DEFAULT 0," 452 + COLUMN_TEST_DETAILS + " TEXT," 453 + COLUMN_TEST_METRICS + " BLOB," 454 + COLUMN_TEST_RESULT_HISTORY + " BLOB," 455 + COLUMN_TEST_SCREENSHOTS_METADATA + " BLOB);"); 456 } 457 458 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)459 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 460 db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); 461 onCreate(db); 462 } 463 } 464 } 465