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