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