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