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