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