• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 package com.android.car.bugreport;
17 
18 import static com.android.car.bugreport.BugStorageProvider.COLUMN_AUDIO_FILENAME;
19 import static com.android.car.bugreport.BugStorageProvider.COLUMN_BUGREPORT_FILENAME;
20 import static com.android.car.bugreport.BugStorageProvider.COLUMN_FILEPATH;
21 import static com.android.car.bugreport.BugStorageProvider.COLUMN_ID;
22 import static com.android.car.bugreport.BugStorageProvider.COLUMN_STATUS;
23 import static com.android.car.bugreport.BugStorageProvider.COLUMN_STATUS_MESSAGE;
24 import static com.android.car.bugreport.BugStorageProvider.COLUMN_TIMESTAMP;
25 import static com.android.car.bugreport.BugStorageProvider.COLUMN_TITLE;
26 import static com.android.car.bugreport.BugStorageProvider.COLUMN_TTL_POINTS;
27 import static com.android.car.bugreport.BugStorageProvider.COLUMN_TYPE;
28 import static com.android.car.bugreport.BugStorageProvider.COLUMN_USERNAME;
29 
30 import android.annotation.NonNull;
31 import android.annotation.Nullable;
32 import android.content.ContentResolver;
33 import android.content.ContentValues;
34 import android.content.Context;
35 import android.database.Cursor;
36 import android.net.Uri;
37 import android.util.Log;
38 
39 import com.google.api.client.auth.oauth2.TokenResponseException;
40 import com.google.common.base.Preconditions;
41 import com.google.common.base.Strings;
42 import com.google.common.collect.ImmutableList;
43 
44 import java.io.FileNotFoundException;
45 import java.io.InputStream;
46 import java.io.OutputStream;
47 import java.text.DateFormat;
48 import java.text.SimpleDateFormat;
49 import java.util.ArrayList;
50 import java.util.Date;
51 import java.util.List;
52 import java.util.Objects;
53 import java.util.Optional;
54 
55 /**
56  * A class that hides details when communicating with the bug storage provider.
57  */
58 final class BugStorageUtils {
59     private static final String TAG = BugStorageUtils.class.getSimpleName();
60 
61     /**
62      * When time/time-zone set incorrectly, Google API returns "400: invalid_grant" error with
63      * description containing this text.
64      */
65     private static final String CLOCK_SKEW_ERROR = "clock with skew to account";
66 
67     /** When time/time-zone set incorrectly, Google API returns this error. */
68     private static final String INVALID_GRANT = "invalid_grant";
69 
70     private static final DateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
71 
72     /**
73      * List of {@link Status}-es that will be expired after certain period by
74      * {@link ExpireOldBugReportsJob}.
75      */
76     private static final ImmutableList<Integer> EXPIRATION_STATUSES = ImmutableList.of(
77             Status.STATUS_WRITE_FAILED.getValue(),
78             Status.STATUS_UPLOAD_PENDING.getValue(),
79             Status.STATUS_UPLOAD_FAILED.getValue(),
80             Status.STATUS_PENDING_USER_ACTION.getValue(),
81             Status.STATUS_MOVE_FAILED.getValue(),
82             Status.STATUS_MOVE_IN_PROGRESS.getValue(),
83             Status.STATUS_AUDIO_PENDING.getValue(),
84             Status.STATUS_UPLOADED_BEFORE.getValue());
85 
86     /**
87      * Creates a new {@link Status#STATUS_WRITE_PENDING} bug report record in a local sqlite
88      * database.
89      *
90      * @param context   - an application context.
91      * @param title     - title of the bug report.
92      * @param timestamp - timestamp when the bug report was initiated.
93      * @param username  - current user name. Note, it's a user name, not an account name.
94      * @param type      - bug report type, {@link MetaBugReport.BugReportType}.
95      * @return an instance of {@link MetaBugReport} that was created in a database.
96      */
97     @NonNull
createBugReport( @onNull Context context, @NonNull String title, @NonNull String timestamp, @NonNull String username, @MetaBugReport.BugReportType int type)98     static MetaBugReport createBugReport(
99             @NonNull Context context,
100             @NonNull String title,
101             @NonNull String timestamp,
102             @NonNull String username,
103             @MetaBugReport.BugReportType int type) {
104         // insert bug report username and title
105         ContentValues values = new ContentValues();
106         values.put(COLUMN_TITLE, title);
107         values.put(COLUMN_TIMESTAMP, timestamp);
108         values.put(COLUMN_USERNAME, username);
109         values.put(COLUMN_TYPE, type);
110 
111         ContentResolver r = context.getContentResolver();
112         Uri uri = r.insert(BugStorageProvider.BUGREPORT_CONTENT_URI, values);
113         return findBugReport(context, Integer.parseInt(uri.getLastPathSegment())).get();
114     }
115 
116     /** Returns an output stream to write the zipped file to. */
117     @NonNull
openBugReportFileToWrite( @onNull Context context, @NonNull MetaBugReport metaBugReport)118     static OutputStream openBugReportFileToWrite(
119             @NonNull Context context, @NonNull MetaBugReport metaBugReport)
120             throws FileNotFoundException {
121         ContentResolver r = context.getContentResolver();
122         return r.openOutputStream(BugStorageProvider.buildUriWithSegment(
123                 metaBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_BUGREPORT_FILE));
124     }
125 
126     /** Returns an output stream to write the audio message file to. */
openAudioMessageFileToWrite( @onNull Context context, @NonNull MetaBugReport metaBugReport)127     static OutputStream openAudioMessageFileToWrite(
128             @NonNull Context context, @NonNull MetaBugReport metaBugReport)
129             throws FileNotFoundException {
130         ContentResolver r = context.getContentResolver();
131         return r.openOutputStream(BugStorageProvider.buildUriWithSegment(
132                 metaBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_AUDIO_FILE));
133     }
134 
135     /**
136      * Returns an input stream to read the final zip file from.
137      *
138      * <p>NOTE: This is the old way of storing final zipped bugreport. See
139      * {@link BugStorageProvider#URL_SEGMENT_OPEN_FILE} for more info.
140      */
openFileToRead(Context context, MetaBugReport bug)141     static InputStream openFileToRead(Context context, MetaBugReport bug)
142             throws FileNotFoundException {
143         return context.getContentResolver().openInputStream(
144                 BugStorageProvider.buildUriWithSegment(
145                         bug.getId(), BugStorageProvider.URL_SEGMENT_OPEN_FILE));
146     }
147 
148     /** Returns an input stream to read the bug report zip file from. */
openBugReportFileToRead(Context context, MetaBugReport bug)149     static InputStream openBugReportFileToRead(Context context, MetaBugReport bug)
150             throws FileNotFoundException {
151         return context.getContentResolver().openInputStream(
152                 BugStorageProvider.buildUriWithSegment(
153                         bug.getId(), BugStorageProvider.URL_SEGMENT_OPEN_BUGREPORT_FILE));
154     }
155 
156     /** Returns an input stream to read the audio file from. */
openAudioFileToRead(Context context, MetaBugReport bug)157     static InputStream openAudioFileToRead(Context context, MetaBugReport bug)
158             throws FileNotFoundException {
159         return context.getContentResolver().openInputStream(
160                 BugStorageProvider.buildUriWithSegment(
161                         bug.getId(), BugStorageProvider.URL_SEGMENT_OPEN_AUDIO_FILE));
162     }
163 
164     /**
165      * Deletes {@link MetaBugReport} record from a local database and deletes the associated file.
166      *
167      * <p>WARNING: destructive operation.
168      *
169      * @param context     - an application context.
170      * @param bugReportId - a bug report id.
171      * @return true if the record was deleted.
172      */
completeDeleteBugReport(@onNull Context context, int bugReportId)173     static boolean completeDeleteBugReport(@NonNull Context context, int bugReportId) {
174         ContentResolver r = context.getContentResolver();
175         return r.delete(BugStorageProvider.buildUriWithSegment(
176                 bugReportId, BugStorageProvider.URL_SEGMENT_COMPLETE_DELETE), null, null) == 1;
177     }
178 
179     /** Deletes all files for given bugreport id; doesn't delete sqlite3 record. */
deleteBugReportFiles(@onNull Context context, int bugReportId)180     static boolean deleteBugReportFiles(@NonNull Context context, int bugReportId) {
181         ContentResolver r = context.getContentResolver();
182         return r.delete(BugStorageProvider.buildUriWithSegment(
183                 bugReportId, BugStorageProvider.URL_SEGMENT_DELETE_FILES), null, null) == 1;
184     }
185 
186     /**
187      * Deletes the associated zip file from disk and then sets status {@link Status#STATUS_EXPIRED}.
188      *
189      * @return true if succeeded.
190      */
expireBugReport(@onNull Context context, int bugReportId)191     static boolean expireBugReport(@NonNull Context context, int bugReportId) {
192         ContentResolver r = context.getContentResolver();
193         return r.delete(BugStorageProvider.buildUriWithSegment(
194                 bugReportId, BugStorageProvider.URL_SEGMENT_EXPIRE), null, null) == 1;
195     }
196 
197     /**
198      * Returns all the bugreports that are waiting to be uploaded.
199      */
200     @NonNull
getUploadPendingBugReports(@onNull Context context)201     public static List<MetaBugReport> getUploadPendingBugReports(@NonNull Context context) {
202         String selection = COLUMN_STATUS + "=?";
203         String[] selectionArgs = new String[]{
204                 Integer.toString(Status.STATUS_UPLOAD_PENDING.getValue())};
205         return getBugreports(context, selection, selectionArgs, null);
206     }
207 
208     /**
209      * Returns all bugreports in descending order by the ID field. ID is the index in the
210      * database.
211      */
212     @NonNull
getAllBugReportsDescending(@onNull Context context)213     public static List<MetaBugReport> getAllBugReportsDescending(@NonNull Context context) {
214         return getBugreports(context, null, null, COLUMN_ID + " DESC");
215     }
216 
217     /**
218      * Returns list of bugreports with zip files (with the best possible guess).
219      *
220      * @param context A context.
221      * @param ttlPointsReachedZero if true it returns bugreports with
222      *                             {@link BugStorageProvider#COLUMN_TTL_POINTS} equal 0; if false
223      *                             {@link BugStorageProvider#COLUMN_TTL_POINTS} more than 0.
224      */
225     @NonNull
getUnexpiredBugReportsWithZipFile( @onNull Context context, boolean ttlPointsReachedZero)226     static List<MetaBugReport> getUnexpiredBugReportsWithZipFile(
227             @NonNull Context context, boolean ttlPointsReachedZero) {
228         // Number of question marks should be the same as the size of EXPIRATION_STATUSES.
229         String selection = COLUMN_STATUS + " IN (?, ?, ?, ?, ?, ?, ?, ?)";
230         Preconditions.checkState(EXPIRATION_STATUSES.size() == 8, "Invalid EXPIRATION_STATUSES");
231         if (ttlPointsReachedZero) {
232             selection += " AND " + COLUMN_TTL_POINTS + " = 0";
233         } else {
234             selection += " AND " + COLUMN_TTL_POINTS + " > 0";
235         }
236         String[] selectionArgs = EXPIRATION_STATUSES.stream()
237                 .map(i -> Integer.toString(i)).toArray(String[]::new);
238         return getBugreports(context, selection, selectionArgs, null);
239     }
240 
241     /** Return true if bugreport with given status can be expired. */
canBugReportBeExpired(int status)242     static boolean canBugReportBeExpired(int status) {
243         return EXPIRATION_STATUSES.contains(status);
244     }
245 
246     /** Returns {@link MetaBugReport} for given bugreport id. */
findBugReport(Context context, int bugreportId)247     static Optional<MetaBugReport> findBugReport(Context context, int bugreportId) {
248         String selection = COLUMN_ID + " = ?";
249         String[] selectionArgs = new String[]{Integer.toString(bugreportId)};
250         List<MetaBugReport> bugs = BugStorageUtils.getBugreports(
251                 context, selection, selectionArgs, null);
252         if (bugs.isEmpty()) {
253             return Optional.empty();
254         }
255         return Optional.of(bugs.get(0));
256     }
257 
getBugreports( Context context, String selection, String[] selectionArgs, String order)258     private static List<MetaBugReport> getBugreports(
259             Context context, String selection, String[] selectionArgs, String order) {
260         ArrayList<MetaBugReport> bugReports = new ArrayList<>();
261         String[] projection = {
262                 COLUMN_ID,
263                 COLUMN_USERNAME,
264                 COLUMN_TITLE,
265                 COLUMN_TIMESTAMP,
266                 COLUMN_BUGREPORT_FILENAME,
267                 COLUMN_AUDIO_FILENAME,
268                 COLUMN_FILEPATH,
269                 COLUMN_STATUS,
270                 COLUMN_STATUS_MESSAGE,
271                 COLUMN_TYPE,
272                 COLUMN_TTL_POINTS};
273         ContentResolver r = context.getContentResolver();
274         Cursor c = r.query(BugStorageProvider.BUGREPORT_CONTENT_URI, projection,
275                 selection, selectionArgs, order);
276 
277         int count = (c != null) ? c.getCount() : 0;
278 
279         if (count > 0) c.moveToFirst();
280         for (int i = 0; i < count; i++) {
281             MetaBugReport meta = MetaBugReport.builder()
282                     .setId(getInt(c, COLUMN_ID))
283                     .setTimestamp(getString(c, COLUMN_TIMESTAMP))
284                     .setUserName(getString(c, COLUMN_USERNAME))
285                     .setTitle(getString(c, COLUMN_TITLE))
286                     .setBugReportFileName(getString(c, COLUMN_BUGREPORT_FILENAME))
287                     .setAudioFileName(getString(c, COLUMN_AUDIO_FILENAME))
288                     .setFilePath(getString(c, COLUMN_FILEPATH))
289                     .setStatus(getInt(c, COLUMN_STATUS))
290                     .setStatusMessage(getString(c, COLUMN_STATUS_MESSAGE))
291                     .setType(getInt(c, COLUMN_TYPE))
292                     .setTtlPoints(getInt(c, COLUMN_TTL_POINTS))
293                     .build();
294             bugReports.add(meta);
295             c.moveToNext();
296         }
297         if (c != null) c.close();
298         return bugReports;
299     }
300 
301     /**
302      * returns 0 if the column is not found. Otherwise returns the column value.
303      */
getInt(Cursor c, String colName)304     private static int getInt(Cursor c, String colName) {
305         int colIndex = c.getColumnIndex(colName);
306         if (colIndex == -1) {
307             Log.w(TAG, "Column " + colName + " not found.");
308             return 0;
309         }
310         return c.getInt(colIndex);
311     }
312 
313     /**
314      * Returns the column value. If the column is not found returns empty string.
315      */
getString(Cursor c, String colName)316     private static String getString(Cursor c, String colName) {
317         int colIndex = c.getColumnIndex(colName);
318         if (colIndex == -1) {
319             Log.w(TAG, "Column " + colName + " not found.");
320             return "";
321         }
322         return Strings.nullToEmpty(c.getString(colIndex));
323     }
324 
325     /**
326      * Sets bugreport status to uploaded successfully.
327      */
setUploadSuccess(Context context, MetaBugReport bugReport)328     public static void setUploadSuccess(Context context, MetaBugReport bugReport) {
329         setBugReportStatus(context, bugReport, Status.STATUS_UPLOAD_SUCCESS,
330                 "Upload time: " + currentTimestamp());
331     }
332 
333     /**
334      * Sets bugreport status to uploaded before.
335      */
setUploadedBefore(Context context, MetaBugReport bugReport, Exception e)336     public static void setUploadedBefore(Context context, MetaBugReport bugReport, Exception e) {
337         setBugReportStatus(context, bugReport, Status.STATUS_UPLOADED_BEFORE,
338                 "Already uploaded, new attempt failed:  " + getRootCauseMessage(e));
339     }
340 
341     /**
342      * Sets bugreport status pending, and update the message to last exception message.
343      *
344      * <p>Used when a transient error has occurred.
345      */
setUploadRetry(Context context, MetaBugReport bugReport, Exception e)346     public static void setUploadRetry(Context context, MetaBugReport bugReport, Exception e) {
347         setBugReportStatus(context, bugReport, Status.STATUS_UPLOAD_PENDING,
348                 getRootCauseMessage(e));
349     }
350 
351     /**
352      * Sets bugreport status pending and update the message to last message.
353      *
354      * <p>Used when a transient error has occurred.
355      */
setUploadRetry(Context context, MetaBugReport bugReport, String msg)356     public static void setUploadRetry(Context context, MetaBugReport bugReport, String msg) {
357         setBugReportStatus(context, bugReport, Status.STATUS_UPLOAD_PENDING, msg);
358     }
359 
360     /** Gets the root cause of the error. */
361     @NonNull
getRootCauseMessage(@ullable Throwable t)362     private static String getRootCauseMessage(@Nullable Throwable t) {
363         if (t == null) {
364             return "No error";
365         } else if (t instanceof TokenResponseException) {
366             TokenResponseException ex = (TokenResponseException) t;
367             if (Objects.equals(ex.getDetails().getError(), INVALID_GRANT)
368                     && ex.getDetails().getErrorDescription().contains(CLOCK_SKEW_ERROR)) {
369                 return "Auth error. Check if time & time-zone is correct.";
370             }
371         }
372         while (t.getCause() != null) t = t.getCause();
373         return t.getMessage();
374     }
375 
376     /**
377      * Updates bug report record status.
378      *
379      * <p>NOTE: When status is set to STATUS_UPLOAD_PENDING, BugStorageProvider automatically
380      * schedules the bugreport to be uploaded.
381      *
382      * @return Updated {@link MetaBugReport}.
383      */
setBugReportStatus( Context context, MetaBugReport bugReport, Status status, String message)384     static MetaBugReport setBugReportStatus(
385             Context context, MetaBugReport bugReport, Status status, String message) {
386         return update(context, bugReport.toBuilder()
387                 .setStatus(status.getValue())
388                 .setStatusMessage(message)
389                 .build());
390     }
391 
392     /**
393      * Updates bug report record status.
394      *
395      * <p>NOTE: When status is set to STATUS_UPLOAD_PENDING, BugStorageProvider automatically
396      * schedules the bugreport to be uploaded.
397      *
398      * @return Updated {@link MetaBugReport}.
399      */
setBugReportStatus( Context context, MetaBugReport bugReport, Status status, Exception e)400     static MetaBugReport setBugReportStatus(
401             Context context, MetaBugReport bugReport, Status status, Exception e) {
402         return setBugReportStatus(context, bugReport, status, getRootCauseMessage(e));
403     }
404 
405     /**
406      * Updates the bugreport and returns the updated version.
407      *
408      * <p>NOTE: doesn't update all the fields.
409      */
update(Context context, MetaBugReport bugReport)410     static MetaBugReport update(Context context, MetaBugReport bugReport) {
411         // Update only necessary fields.
412         ContentValues values = new ContentValues();
413         values.put(COLUMN_BUGREPORT_FILENAME, bugReport.getBugReportFileName());
414         values.put(COLUMN_AUDIO_FILENAME, bugReport.getAudioFileName());
415         values.put(COLUMN_STATUS, bugReport.getStatus());
416         values.put(COLUMN_STATUS_MESSAGE, bugReport.getStatusMessage());
417         String where = COLUMN_ID + "=" + bugReport.getId();
418         context.getContentResolver().update(
419                 BugStorageProvider.BUGREPORT_CONTENT_URI, values, where, null);
420         return findBugReport(context, bugReport.getId()).orElseThrow(
421                 () -> new IllegalArgumentException("Bug " + bugReport.getId() + " not found"));
422     }
423 
currentTimestamp()424     private static String currentTimestamp() {
425         return TIMESTAMP_FORMAT.format(new Date());
426     }
427 }
428