Merge "Implement save-to-USB for bug reports." into qt-dev
am: be4a27abd5
Change-Id: If548dbecb13ff32037e166d3a8526ff2fc675658
diff --git a/tests/BugReportApp/res/layout/bug_info_view.xml b/tests/BugReportApp/res/layout/bug_info_view.xml
index a02df20..3736c00 100644
--- a/tests/BugReportApp/res/layout/bug_info_view.xml
+++ b/tests/BugReportApp/res/layout/bug_info_view.xml
@@ -60,6 +60,14 @@
android:textColor="@android:color/holo_blue_dark"
android:textSize="28sp" />
+ <Button
+ android:id="@+id/bug_info_upload_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/bug_report_user_action_button_padding"
+ android:textSize="@dimen/bug_report_button_text_size"
+ android:text="@string/bugreport_upload_button_text" />
+
</LinearLayout>
<LinearLayout
@@ -103,6 +111,14 @@
android:layout_height="wrap_content"
android:textSize="28sp" />
+ <Button
+ android:id="@+id/bug_info_move_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/bug_report_user_action_button_padding"
+ android:textSize="@dimen/bug_report_button_text_size"
+ android:text="@string/bugreport_move_button_text" />
+
</LinearLayout>
</LinearLayout>
\ No newline at end of file
diff --git a/tests/BugReportApp/res/values/dimens.xml b/tests/BugReportApp/res/values/dimens.xml
index ebddcfe..4a5f270 100644
--- a/tests/BugReportApp/res/values/dimens.xml
+++ b/tests/BugReportApp/res/values/dimens.xml
@@ -28,6 +28,8 @@
<dimen name="bug_report_primary_button_padding">52dp</dimen>
<dimen name="bug_report_secondary_button_padding">30dp</dimen>
<dimen name="bug_report_small_button_padding">16dp</dimen>
+ <dimen name="bug_report_button_text_size">28dp</dimen>
+ <dimen name="bug_report_user_action_button_padding">5dp</dimen>
<!-- ProgressBar dimensions -->
<dimen name="bug_report_progress_bar_margin_top">32dp</dimen>
diff --git a/tests/BugReportApp/res/values/strings.xml b/tests/BugReportApp/res/values/strings.xml
index 491bbc2..814b2a4 100644
--- a/tests/BugReportApp/res/values/strings.xml
+++ b/tests/BugReportApp/res/values/strings.xml
@@ -28,6 +28,8 @@
<string name="bugreport_dialog_recording_finished" translatable="false">Recording finished</string>
<string name="bugreport_dialog_in_progress_title" translatable="false">A bug report is already being collected</string>
<string name="bugreport_dialog_in_progress_title_finished" translatable="false">A bug report has been collected</string>
+ <string name="bugreport_move_button_text" translatable="false">Move</string>
+ <string name="bugreport_upload_button_text" translatable="false">Upload</string>
<string name="toast_permissions_denied" translatable="false">Please grant permissions</string>
<string name="toast_bug_report_in_progress" translatable="false">Bug report already being collected</string>
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugInfoAdapter.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugInfoAdapter.java
index fb7c3fa..b9ce6f8 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugInfoAdapter.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugInfoAdapter.java
@@ -18,22 +18,29 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.Button;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
-/**
- * Adapter class for bug report information
- */
public class BugInfoAdapter extends RecyclerView.Adapter<BugInfoAdapter.BugInfoViewHolder> {
+ static final int BUTTON_TYPE_UPLOAD = 0;
+ static final int BUTTON_TYPE_MOVE = 1;
+
+ /** Provides a handler for click events*/
+ interface ItemClickedListener {
+ /** onItemClicked handles click events differently depending on provided buttonType and
+ * uses additional information provided in metaBugReport. */
+ void onItemClicked(int buttonType, MetaBugReport metaBugReport);
+ }
+
/**
* Reference to each bug report info views.
*/
public static class BugInfoViewHolder extends RecyclerView.ViewHolder {
-
/** Title view */
public TextView titleView;
@@ -49,6 +56,12 @@
/** Message View */
public TextView messageView;
+ /** Move Button */
+ public Button moveButton;
+
+ /** Upload Button */
+ public Button uploadButton;
+
BugInfoViewHolder(View v) {
super(v);
titleView = itemView.findViewById(R.id.bug_info_row_title);
@@ -56,13 +69,18 @@
timestampView = itemView.findViewById(R.id.bug_info_row_timestamp);
statusView = itemView.findViewById(R.id.bug_info_row_status);
messageView = itemView.findViewById(R.id.bug_info_row_message);
+ moveButton = itemView.findViewById(R.id.bug_info_move_button);
+ uploadButton = itemView.findViewById(R.id.bug_info_upload_button);
}
}
- private List<MetaBugReport> mDataset;
+ private final List<MetaBugReport> mDataset;
+ private final ItemClickedListener mItemClickedListener;
- BugInfoAdapter(List<MetaBugReport> dataSet) {
+
+ BugInfoAdapter(List<MetaBugReport> dataSet, ItemClickedListener itemClickedListener) {
mDataset = dataSet;
+ mItemClickedListener = itemClickedListener;
}
@Override
@@ -75,15 +93,31 @@
@Override
public void onBindViewHolder(BugInfoViewHolder holder, int position) {
+ MetaBugReport bugreport = mDataset.get(position);
holder.titleView.setText(mDataset.get(position).getTitle());
holder.userView.setText(mDataset.get(position).getUsername());
holder.timestampView.setText(mDataset.get(position).getTimestamp());
holder.statusView.setText(Status.toString(mDataset.get(position).getStatus()));
holder.messageView.setText(mDataset.get(position).getStatusMessage());
+ if (bugreport.getStatus() == Status.STATUS_PENDING_USER_ACTION.getValue()
+ || bugreport.getStatus() == Status.STATUS_MOVE_FAILED.getValue()
+ || bugreport.getStatus() == Status.STATUS_UPLOAD_FAILED.getValue()) {
+ holder.moveButton.setOnClickListener(
+ view -> mItemClickedListener.onItemClicked(BUTTON_TYPE_MOVE, bugreport));
+ holder.uploadButton.setOnClickListener(
+ view -> mItemClickedListener.onItemClicked(BUTTON_TYPE_UPLOAD, bugreport));
+ } else {
+ holder.moveButton.setEnabled(false);
+ holder.uploadButton.setEnabled(false);
+ }
}
@Override
public int getItemCount() {
return mDataset.size();
}
+
+ public MetaBugReport getDataItemAt(int position) {
+ return mDataset.get(position);
+ }
}
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportInfoActivity.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportInfoActivity.java
index d9ca7ea..0469bb1 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportInfoActivity.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportInfoActivity.java
@@ -19,9 +19,12 @@
import android.app.Activity;
import android.app.NotificationManager;
+import android.content.ContentResolver;
import android.content.Intent;
+import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
+import android.provider.DocumentsContract;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
@@ -30,6 +33,10 @@
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
@@ -40,12 +47,74 @@
public class BugReportInfoActivity extends Activity {
public static final String TAG = BugReportInfoActivity.class.getSimpleName();
+ private static final int SELECT_DIRECTORY_REQUEST_CODE = 1;
+
private RecyclerView mRecyclerView;
private RecyclerView.Adapter mAdapter;
private RecyclerView.LayoutManager mLayoutManager;
private NotificationManager mNotificationManager;
+ private MetaBugReport mLastSelectedBugReport;
- private static class BugReportInfoTask extends AsyncTask<Void, Void, List<MetaBugReport>> {
+ private static final class AsyncMoveFilesTask extends AsyncTask<Void, Void, Boolean> {
+ private final BugReportInfoActivity mActivity;
+ private final MetaBugReport mBugReport;
+ private final Uri mDestinationDirUri;
+
+ AsyncMoveFilesTask(BugReportInfoActivity activity, MetaBugReport bugReport,
+ Uri destinationDir) {
+ mActivity = activity;
+ mBugReport = bugReport;
+ mDestinationDirUri = destinationDir;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ Uri sourceUri = BugStorageProvider.buildUriWithBugId(mBugReport.getId());
+ ContentResolver resolver = mActivity.getContentResolver();
+ String documentId = DocumentsContract.getTreeDocumentId(mDestinationDirUri);
+ Uri parentDocumentUri =
+ DocumentsContract.buildDocumentUriUsingTree(mDestinationDirUri, documentId);
+ String mimeType = resolver.getType(sourceUri);
+ try {
+ Uri newFileUri = DocumentsContract.createDocument(resolver, parentDocumentUri,
+ mimeType,
+ new File(mBugReport.getFilePath()).toPath().getFileName().toString());
+ if (newFileUri == null) {
+ Log.e(TAG, "Unable to create a new file.");
+ return false;
+ }
+ try (InputStream input = resolver.openInputStream(sourceUri);
+ OutputStream output = resolver.openOutputStream(newFileUri)) {
+ byte[] buffer = new byte[4096];
+ int len;
+ while ((len = input.read(buffer)) > 0) {
+ output.write(buffer, 0, len);
+ }
+ }
+ BugStorageUtils.setBugReportStatus(
+ mActivity, mBugReport,
+ com.google.android.car.bugreport.Status.STATUS_MOVE_SUCCESSFUL, "");
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to create the bug report in the location.", e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean moveSuccessful) {
+ if (!moveSuccessful) {
+ BugStorageUtils.setBugReportStatus(
+ mActivity, mBugReport,
+ com.google.android.car.bugreport.Status.STATUS_MOVE_FAILED, "");
+ }
+ // Refresh the UI to reflect the new status.
+ new BugReportInfoTask(mActivity).execute();
+ }
+ }
+
+ private static final class BugReportInfoTask extends
+ AsyncTask<Void, Void, List<MetaBugReport>> {
private final WeakReference<BugReportInfoActivity> mBugReportInfoActivityWeakReference;
BugReportInfoTask(BugReportInfoActivity activity) {
@@ -69,7 +138,7 @@
Log.w(TAG, "Activity is gone, cancelling onPostExecute.");
return;
}
- activity.mAdapter = new BugInfoAdapter(result);
+ activity.mAdapter = new BugInfoAdapter(result, activity::onBugReportItemClicked);
activity.mRecyclerView.setAdapter(activity.mAdapter);
activity.mRecyclerView.getAdapter().notifyDataSetChanged();
}
@@ -91,7 +160,7 @@
DividerItemDecoration.VERTICAL));
// specify an adapter (see also next example)
- mAdapter = new BugInfoAdapter(new ArrayList<>());
+ mAdapter = new BugInfoAdapter(new ArrayList<>(), this::onBugReportItemClicked);
mRecyclerView.setAdapter(mAdapter);
findViewById(R.id.quit_button).setOnClickListener(this::onQuitButtonClick);
@@ -117,6 +186,39 @@
mNotificationManager.cancel(BugReportService.BUGREPORT_FINISHED_NOTIF_ID);
}
+ private void onBugReportItemClicked(int buttonType, MetaBugReport bugReport) {
+ if (buttonType == BugInfoAdapter.BUTTON_TYPE_UPLOAD) {
+ Log.i(TAG, "Uploading " + bugReport.getFilePath());
+ BugStorageUtils.setBugReportStatus(this, bugReport, Status.STATUS_UPLOAD_PENDING, "");
+ // Refresh the UI to reflect the new status.
+ new BugReportInfoTask(this).execute();
+ } else if (buttonType == BugInfoAdapter.BUTTON_TYPE_MOVE) {
+ Log.i(TAG, "Moving " + bugReport.getFilePath());
+ mLastSelectedBugReport = bugReport;
+ startActivityForResult(new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE),
+ SELECT_DIRECTORY_REQUEST_CODE);
+ } else {
+ throw new IllegalStateException("unreachable");
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == SELECT_DIRECTORY_REQUEST_CODE && resultCode == RESULT_OK) {
+ int takeFlags =
+ data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ Uri destDirUri = data.getData();
+ getContentResolver().takePersistableUriPermission(destDirUri, takeFlags);
+ if (mLastSelectedBugReport == null) {
+ Log.w(TAG, "No bug report is selected.");
+ return;
+ }
+ new AsyncMoveFilesTask(this, mLastSelectedBugReport, destDirUri).execute();
+ }
+ }
+
private void onQuitButtonClick(View view) {
finish();
}
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java
index 8e10f8c..3d07acf 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java
@@ -173,12 +173,6 @@
@Nullable
@Override
- public String getType(@NonNull Uri uri) {
- return null;
- }
-
- @Nullable
- @Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
String table;
if (values == null) {
@@ -206,6 +200,16 @@
return null;
}
+ @Nullable
+ @Override
+ public String getType(@NonNull Uri uri) {
+ if (mUriMatcher.match(uri) != URL_MATCHED_BUG_REPORT_ID_URI) {
+ throw new IllegalArgumentException("unknown uri:" + uri);
+ }
+ // We only store zip files in this provider.
+ return "application/zip";
+ }
+
@Override
public int delete(
@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
@@ -291,11 +295,20 @@
int modeBits = ParcelFileDescriptor.parseMode(mode);
try {
return ParcelFileDescriptor.open(new File(path), modeBits, mHandler, e -> {
+ if (mode.equals("r")) {
+ Log.i(TAG, "File " + path + " opened in read-only mode.");
+ return;
+ } else if (!mode.equals("w")) {
+ Log.e(TAG, "Only read-only or write-only mode supported; mode=" + mode);
+ return;
+ }
+ Log.i(TAG, "File " + path + " opened in write-only mode.");
Status status;
if (e == null) {
// success writing the file. Update the field to indicate bugreport
// is ready for upload
- status = Status.STATUS_UPLOAD_PENDING;
+ status = JobSchedulingUtils.uploadByDefault() ? Status.STATUS_UPLOAD_PENDING
+ : Status.STATUS_PENDING_USER_ACTION;
JobSchedulingUtils.scheduleUploadJob(BugStorageProvider.this.getContext());
} else {
// We log it and ignore it
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java b/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java
index 7e8c933..7b9cefb 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java
@@ -58,4 +58,16 @@
.setBackoffCriteria(RETRY_DELAY_IN_MS, JobInfo.BACKOFF_POLICY_LINEAR)
.build());
}
+
+ /** uploadByDefault switches app behavior between two workflows.
+ *
+ * Returns true if collected bugreports are uploaded automatically.
+ *
+ * Otherwise, it maps to an alternative workflow that requires user action after bugreport
+ * is successfully written. A user then has an option to choose whether to upload the bugreport
+ * or copy it to an external drive.
+ */
+ static boolean uploadByDefault() {
+ return true;
+ }
}
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java b/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java
index f18baae..9142b91 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java
@@ -33,7 +33,16 @@
STATUS_UPLOAD_FAILED(4),
// Bugreport is cancelled by user
- STATUS_USER_CANCELLED(5);
+ STATUS_USER_CANCELLED(5),
+
+ // Bugreport is pending user choice on whether to upload or copy.
+ STATUS_PENDING_USER_ACTION(6),
+
+ // Bugreport was moved successfully.
+ STATUS_MOVE_SUCCESSFUL(7),
+
+ // Bugreport move has failed.
+ STATUS_MOVE_FAILED(8);
private final int mValue;
@@ -50,17 +59,23 @@
public static String toString(int value) {
switch (value) {
case 0:
- return "Write Pending";
+ return "Write pending";
case 1:
return "Write failed";
case 2:
return "Upload pending";
case 3:
- return "Upload success";
+ return "Upload successful";
case 4:
return "Upload failed";
case 5:
return "User cancelled";
+ case 6:
+ return "Pending user action";
+ case 7:
+ return "Move successful";
+ case 8:
+ return "Move failed";
}
return "unknown";
}