• 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.PackageUtils.getPackageVersion;
19 
20 import android.app.Activity;
21 import android.app.NotificationManager;
22 import android.content.ContentResolver;
23 import android.content.Intent;
24 import android.content.res.AssetFileDescriptor;
25 import android.database.ContentObserver;
26 import android.net.Uri;
27 import android.os.AsyncTask;
28 import android.os.Bundle;
29 import android.os.Handler;
30 import android.os.UserHandle;
31 import android.provider.DocumentsContract;
32 import android.util.Log;
33 import android.view.View;
34 import android.widget.TextView;
35 
36 import androidx.recyclerview.widget.DividerItemDecoration;
37 import androidx.recyclerview.widget.LinearLayoutManager;
38 import androidx.recyclerview.widget.RecyclerView;
39 
40 import com.google.common.base.Preconditions;
41 import com.google.common.base.Strings;
42 import com.google.common.io.ByteStreams;
43 
44 import java.io.BufferedOutputStream;
45 import java.io.File;
46 import java.io.FileDescriptor;
47 import java.io.IOException;
48 import java.io.InputStream;
49 import java.io.OutputStream;
50 import java.io.PrintWriter;
51 import java.lang.ref.WeakReference;
52 import java.util.ArrayList;
53 import java.util.List;
54 import java.util.zip.ZipEntry;
55 import java.util.zip.ZipInputStream;
56 import java.util.zip.ZipOutputStream;
57 
58 /**
59  * Provides an activity that provides information on the bugreports that are filed.
60  */
61 public class BugReportInfoActivity extends Activity {
62     public static final String TAG = BugReportInfoActivity.class.getSimpleName();
63 
64     /** Used for moving bug reports to a new location (e.g. USB drive). */
65     private static final int SELECT_DIRECTORY_REQUEST_CODE = 1;
66 
67     /** Used to start {@link BugReportActivity} to add audio message. */
68     private static final int ADD_AUDIO_MESSAGE_REQUEST_CODE = 2;
69 
70     private RecyclerView mRecyclerView;
71     private BugInfoAdapter mBugInfoAdapter;
72     private RecyclerView.LayoutManager mLayoutManager;
73     private NotificationManager mNotificationManager;
74     private MetaBugReport mLastSelectedBugReport;
75     private BugInfoAdapter.BugInfoViewHolder mLastSelectedBugInfoViewHolder;
76     private BugStorageObserver mBugStorageObserver;
77     private Config mConfig;
78     private boolean mAudioRecordingStarted;
79 
80     @Override
onCreate(Bundle savedInstanceState)81     protected void onCreate(Bundle savedInstanceState) {
82         Preconditions.checkState(Config.isBugReportEnabled(), "BugReport is disabled.");
83 
84         super.onCreate(savedInstanceState);
85         setContentView(R.layout.bug_report_info_activity);
86 
87         mNotificationManager = getSystemService(NotificationManager.class);
88 
89         mRecyclerView = findViewById(R.id.rv_bug_report_info);
90         mRecyclerView.setHasFixedSize(true);
91         // use a linear layout manager
92         mLayoutManager = new LinearLayoutManager(this);
93         mRecyclerView.setLayoutManager(mLayoutManager);
94         mRecyclerView.addItemDecoration(new DividerItemDecoration(mRecyclerView.getContext(),
95                 DividerItemDecoration.VERTICAL));
96 
97         mConfig = Config.create();
98 
99         mBugInfoAdapter = new BugInfoAdapter(this::onBugReportItemClicked, mConfig);
100         mRecyclerView.setAdapter(mBugInfoAdapter);
101 
102         mBugStorageObserver = new BugStorageObserver(this, new Handler());
103 
104         findViewById(R.id.quit_button).setOnClickListener(this::onQuitButtonClick);
105         findViewById(R.id.start_bug_report_button).setOnClickListener(
106                 this::onStartBugReportButtonClick);
107         ((TextView) findViewById(R.id.version_text_view)).setText(
108                 String.format("v%s", getPackageVersion(this)));
109 
110         cancelBugReportFinishedNotification();
111     }
112 
113     @Override
onStart()114     protected void onStart() {
115         super.onStart();
116         new BugReportsLoaderAsyncTask(this).execute();
117         // As BugStorageProvider is running under user0, we register using USER_ALL.
118         getContentResolver().registerContentObserver(BugStorageProvider.BUGREPORT_CONTENT_URI, true,
119                 mBugStorageObserver, UserHandle.USER_ALL);
120     }
121 
122     @Override
onStop()123     protected void onStop() {
124         super.onStop();
125         getContentResolver().unregisterContentObserver(mBugStorageObserver);
126     }
127 
128     /**
129      * Dismisses {@link BugReportService#BUGREPORT_FINISHED_NOTIF_ID}, otherwise the notification
130      * will stay there forever if this activity opened through the App Launcher.
131      */
cancelBugReportFinishedNotification()132     private void cancelBugReportFinishedNotification() {
133         mNotificationManager.cancel(BugReportService.BUGREPORT_FINISHED_NOTIF_ID);
134     }
135 
onBugReportItemClicked( int buttonType, MetaBugReport bugReport, BugInfoAdapter.BugInfoViewHolder holder)136     private void onBugReportItemClicked(
137             int buttonType, MetaBugReport bugReport, BugInfoAdapter.BugInfoViewHolder holder) {
138         if (buttonType == BugInfoAdapter.BUTTON_TYPE_UPLOAD) {
139             Log.i(TAG, "Uploading " + bugReport.getTimestamp());
140             BugStorageUtils.setBugReportStatus(this, bugReport, Status.STATUS_UPLOAD_PENDING, "");
141             // Refresh the UI to reflect the new status.
142             new BugReportsLoaderAsyncTask(this).execute();
143         } else if (buttonType == BugInfoAdapter.BUTTON_TYPE_MOVE) {
144             Log.i(TAG, "Moving " + bugReport.getTimestamp());
145             mLastSelectedBugReport = bugReport;
146             mLastSelectedBugInfoViewHolder = holder;
147             startActivityForResult(new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE),
148                     SELECT_DIRECTORY_REQUEST_CODE);
149         } else if (buttonType == BugInfoAdapter.BUTTON_TYPE_ADD_AUDIO) {
150             // Check mAudioRecordingStarted to prevent double click to BUTTON_TYPE_ADD_AUDIO.
151             if (!mAudioRecordingStarted) {
152                 mAudioRecordingStarted = true;
153                 startActivityForResult(BugReportActivity.buildAddAudioIntent(this, bugReport),
154                         ADD_AUDIO_MESSAGE_REQUEST_CODE);
155             }
156         } else {
157             throw new IllegalStateException("unreachable");
158         }
159     }
160 
161     @Override
onActivityResult(int requestCode, int resultCode, Intent data)162     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
163         super.onActivityResult(requestCode, resultCode, data);
164         if (requestCode == SELECT_DIRECTORY_REQUEST_CODE && resultCode == RESULT_OK) {
165             int takeFlags =
166                     data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION
167                             | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
168             Uri destDirUri = data.getData();
169             getContentResolver().takePersistableUriPermission(destDirUri, takeFlags);
170             if (mLastSelectedBugReport == null || mLastSelectedBugInfoViewHolder == null) {
171                 Log.w(TAG, "No bug report is selected.");
172                 return;
173             }
174             MetaBugReport updatedBugReport = BugStorageUtils.setBugReportStatus(this,
175                     mLastSelectedBugReport, Status.STATUS_MOVE_IN_PROGRESS, "");
176             mBugInfoAdapter.updateBugReportInDataSet(
177                     updatedBugReport, mLastSelectedBugInfoViewHolder.getAdapterPosition());
178             new AsyncMoveFilesTask(
179                 this,
180                     mBugInfoAdapter,
181                     updatedBugReport,
182                     mLastSelectedBugInfoViewHolder,
183                     destDirUri).execute();
184         }
185     }
186 
onQuitButtonClick(View view)187     private void onQuitButtonClick(View view) {
188         finish();
189     }
190 
onStartBugReportButtonClick(View view)191     private void onStartBugReportButtonClick(View view) {
192         startActivity(BugReportActivity.buildStartBugReportIntent(this));
193     }
194 
195     /**
196      * Print the Provider's state into the given stream. This gets invoked if
197      * you run "adb shell dumpsys activity BugReportInfoActivity".
198      *
199      * @param prefix Desired prefix to prepend at each line of output.
200      * @param fd The raw file descriptor that the dump is being sent to.
201      * @param writer The PrintWriter to which you should dump your state.  This will be
202      * closed for you after you return.
203      * @param args additional arguments to the dump request.
204      */
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)205     public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
206         super.dump(prefix, fd, writer, args);
207         mConfig.dump(prefix, writer);
208     }
209 
210     /**
211      * Moves bugreport zip to USB drive and updates RecyclerView.
212      *
213      * <p>It merges bugreport zip file and audio file into one final zip file and moves it.
214      */
215     private static final class AsyncMoveFilesTask extends AsyncTask<Void, Void, MetaBugReport> {
216         private final BugReportInfoActivity mActivity;
217         private final MetaBugReport mBugReport;
218         private final Uri mDestinationDirUri;
219         /** RecyclerView.Adapter that contains all the bug reports. */
220         private final BugInfoAdapter mBugInfoAdapter;
221         /** ViewHolder for {@link #mBugReport}. */
222         private final BugInfoAdapter.BugInfoViewHolder mBugViewHolder;
223         private final ContentResolver mResolver;
224 
AsyncMoveFilesTask(BugReportInfoActivity activity, BugInfoAdapter bugInfoAdapter, MetaBugReport bugReport, BugInfoAdapter.BugInfoViewHolder holder, Uri destinationDir)225         AsyncMoveFilesTask(BugReportInfoActivity activity, BugInfoAdapter bugInfoAdapter,
226                 MetaBugReport bugReport, BugInfoAdapter.BugInfoViewHolder holder,
227                 Uri destinationDir) {
228             mActivity = activity;
229             mBugInfoAdapter = bugInfoAdapter;
230             mBugReport = bugReport;
231             mBugViewHolder = holder;
232             mDestinationDirUri = destinationDir;
233             mResolver = mActivity.getContentResolver();
234         }
235 
236         /** Moves the bugreport to the USB drive and returns the updated {@link MetaBugReport}. */
237         @Override
doInBackground(Void... params)238         protected MetaBugReport doInBackground(Void... params) {
239             try {
240                 return copyFilesToUsb();
241             } catch (IOException e) {
242                 Log.e(TAG, "Failed to copy bugreport "
243                         + mBugReport.getTimestamp() + " to USB", e);
244                 return BugStorageUtils.setBugReportStatus(
245                     mActivity, mBugReport,
246                     com.android.car.bugreport.Status.STATUS_MOVE_FAILED, e);
247             }
248         }
249 
copyFilesToUsb()250         private MetaBugReport copyFilesToUsb() throws IOException {
251             String documentId = DocumentsContract.getTreeDocumentId(mDestinationDirUri);
252             Uri parentDocumentUri =
253                     DocumentsContract.buildDocumentUriUsingTree(mDestinationDirUri, documentId);
254             if (!Strings.isNullOrEmpty(mBugReport.getFilePath())) {
255                 // There are still old bugreports with deprecated filePath.
256                 Uri sourceUri = BugStorageProvider.buildUriWithSegment(
257                         mBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_FILE);
258                 copyFileToUsb(
259                         new File(mBugReport.getFilePath()).getName(), sourceUri, parentDocumentUri);
260             } else {
261                 mergeFilesAndCopyToUsb(parentDocumentUri);
262             }
263             Log.d(TAG, "Deleting local bug report files.");
264             BugStorageUtils.deleteBugReportFiles(mActivity, mBugReport.getId());
265             return BugStorageUtils.setBugReportStatus(mActivity, mBugReport,
266                     com.android.car.bugreport.Status.STATUS_MOVE_SUCCESSFUL,
267                     "Moved to: " + mDestinationDirUri.getPath());
268         }
269 
mergeFilesAndCopyToUsb(Uri parentDocumentUri)270         private void mergeFilesAndCopyToUsb(Uri parentDocumentUri) throws IOException {
271             Uri sourceBugReport = BugStorageProvider.buildUriWithSegment(
272                     mBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_BUGREPORT_FILE);
273             Uri sourceAudio = BugStorageProvider.buildUriWithSegment(
274                     mBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_AUDIO_FILE);
275             String mimeType = mResolver.getType(sourceBugReport); // It's a zip file.
276             Uri newFileUri = DocumentsContract.createDocument(
277                     mResolver, parentDocumentUri, mimeType, mBugReport.getBugReportFileName());
278             if (newFileUri == null) {
279                 throw new IOException(
280                         "Unable to create a file " + mBugReport.getBugReportFileName() + " in USB");
281             }
282             try (InputStream bugReportInput = mResolver.openInputStream(sourceBugReport);
283                  AssetFileDescriptor fd = mResolver.openAssetFileDescriptor(newFileUri, "w");
284                  OutputStream outputStream = fd.createOutputStream();
285                  ZipOutputStream zipOutStream =
286                          new ZipOutputStream(new BufferedOutputStream(outputStream))) {
287                 // Extract bugreport zip file to the final zip file in USB drive.
288                 try (ZipInputStream zipInStream = new ZipInputStream(bugReportInput)) {
289                     ZipEntry entry;
290                     while ((entry = zipInStream.getNextEntry()) != null) {
291                         ZipUtils.writeInputStreamToZipStream(
292                                 entry.getName(), zipInStream, zipOutStream);
293                     }
294                 }
295                 // Add audio file to the final zip file.
296                 if (!Strings.isNullOrEmpty(mBugReport.getAudioFileName())) {
297                     try (InputStream audioInput = mResolver.openInputStream(sourceAudio)) {
298                         ZipUtils.writeInputStreamToZipStream(
299                                 mBugReport.getAudioFileName(), audioInput, zipOutStream);
300                     }
301                 }
302             }
303             try (AssetFileDescriptor fd = mResolver.openAssetFileDescriptor(newFileUri, "w")) {
304                 // Force sync the written data from memory to the disk.
305                 fd.getFileDescriptor().sync();
306             }
307             Log.d(TAG, "Writing to " + newFileUri + " finished");
308         }
309 
copyFileToUsb(String filename, Uri sourceUri, Uri parentDocumentUri)310         private void copyFileToUsb(String filename, Uri sourceUri, Uri parentDocumentUri)
311                 throws IOException {
312             String mimeType = mResolver.getType(sourceUri);
313             Uri newFileUri = DocumentsContract.createDocument(
314                     mResolver, parentDocumentUri, mimeType, filename);
315             if (newFileUri == null) {
316                 throw new IOException("Unable to create a file " + filename + " in USB");
317             }
318             try (InputStream input = mResolver.openInputStream(sourceUri);
319                  AssetFileDescriptor fd = mResolver.openAssetFileDescriptor(newFileUri, "w")) {
320                 OutputStream output = fd.createOutputStream();
321                 ByteStreams.copy(input, output);
322                 // Force sync the written data from memory to the disk.
323                 fd.getFileDescriptor().sync();
324             }
325         }
326 
327         @Override
onPostExecute(MetaBugReport updatedBugReport)328         protected void onPostExecute(MetaBugReport updatedBugReport) {
329             // Refresh the UI to reflect the new status.
330             mBugInfoAdapter.updateBugReportInDataSet(
331                     updatedBugReport, mBugViewHolder.getAdapterPosition());
332         }
333     }
334 
335     /** Asynchronously loads bugreports from {@link BugStorageProvider}. */
336     private static final class BugReportsLoaderAsyncTask extends
337             AsyncTask<Void, Void, List<MetaBugReport>> {
338         private final WeakReference<BugReportInfoActivity> mBugReportInfoActivityWeakReference;
339 
BugReportsLoaderAsyncTask(BugReportInfoActivity activity)340         BugReportsLoaderAsyncTask(BugReportInfoActivity activity) {
341             mBugReportInfoActivityWeakReference = new WeakReference<>(activity);
342         }
343 
344         @Override
doInBackground(Void... voids)345         protected List<MetaBugReport> doInBackground(Void... voids) {
346             BugReportInfoActivity activity = mBugReportInfoActivityWeakReference.get();
347             if (activity == null) {
348                 Log.w(TAG, "Activity is gone, cancelling BugReportsLoaderAsyncTask.");
349                 return new ArrayList<>();
350             }
351             return BugStorageUtils.getAllBugReportsDescending(activity);
352         }
353 
354         @Override
onPostExecute(List<MetaBugReport> result)355         protected void onPostExecute(List<MetaBugReport> result) {
356             BugReportInfoActivity activity = mBugReportInfoActivityWeakReference.get();
357             if (activity == null) {
358                 Log.w(TAG, "Activity is gone, cancelling onPostExecute.");
359                 return;
360             }
361             activity.mBugInfoAdapter.setDataset(result);
362         }
363     }
364 
365     /** Observer for {@link BugStorageProvider}. */
366     private static class BugStorageObserver extends ContentObserver {
367         private final BugReportInfoActivity mInfoActivity;
368 
369         /**
370          * Creates a content observer.
371          *
372          * @param activity A {@link BugReportInfoActivity} instance.
373          * @param handler The handler to run {@link #onChange} on, or null if none.
374          */
BugStorageObserver(BugReportInfoActivity activity, Handler handler)375         BugStorageObserver(BugReportInfoActivity activity, Handler handler) {
376             super(handler);
377             mInfoActivity = activity;
378         }
379 
380         @Override
onChange(boolean selfChange)381         public void onChange(boolean selfChange) {
382             new BugReportsLoaderAsyncTask(mInfoActivity).execute();
383         }
384     }
385 }
386