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