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