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