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