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 android.annotation.NonNull; 19 import android.content.Context; 20 import android.os.AsyncTask; 21 import android.text.TextUtils; 22 import android.util.Log; 23 24 import com.google.api.client.extensions.android.http.AndroidHttp; 25 import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; 26 import com.google.api.client.http.HttpTransport; 27 import com.google.api.client.http.InputStreamContent; 28 import com.google.api.client.json.JsonFactory; 29 import com.google.api.client.json.jackson2.JacksonFactory; 30 import com.google.api.services.storage.Storage; 31 import com.google.api.services.storage.model.StorageObject; 32 import com.google.common.base.Strings; 33 import com.google.common.collect.ImmutableMap; 34 35 import java.io.BufferedOutputStream; 36 import java.io.File; 37 import java.io.FileInputStream; 38 import java.io.FileOutputStream; 39 import java.io.IOException; 40 import java.io.InputStream; 41 import java.util.Collections; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.zip.ZipOutputStream; 45 46 /** 47 * Uploads a bugreport files to GCS using a simple (no-multipart / no-resume) upload policy. 48 * 49 * <p>It merges bugreport zip file and audio file into one final zip file and uploads it. 50 * 51 * <p>Please see {@code res/values/configs.xml} and {@code res/raw/gcs_credentials.json} for the 52 * configuration. 53 */ 54 class SimpleUploaderAsyncTask extends AsyncTask<Void, Void, Boolean> { 55 private static final String TAG = SimpleUploaderAsyncTask.class.getSimpleName(); 56 57 private static final String ACCESS_SCOPE = 58 "https://www.googleapis.com/auth/devstorage.read_write"; 59 60 private static final String STORAGE_METADATA_TITLE = "title"; 61 62 private final Context mContext; 63 private final Result mResult; 64 65 /** 66 * The uploader uploads only one bugreport each time it is called. This interface is 67 * used to reschedule upload job, if there are more bugreports waiting. 68 * 69 * Pass true to reschedule upload job, false not to reschedule. 70 */ 71 interface Result { reschedule(boolean s)72 void reschedule(boolean s); 73 } 74 75 /** Constructs SimpleUploaderAsyncTask. */ SimpleUploaderAsyncTask(@onNull Context context, @NonNull Result result)76 SimpleUploaderAsyncTask(@NonNull Context context, @NonNull Result result) { 77 mContext = context; 78 mResult = result; 79 } 80 uploadSimple( Storage storage, MetaBugReport bugReport, String fileName, InputStream data)81 private StorageObject uploadSimple( 82 Storage storage, MetaBugReport bugReport, String fileName, InputStream data) 83 throws IOException { 84 InputStreamContent mediaContent = new InputStreamContent("application/zip", data); 85 86 String bucket = mContext.getString(R.string.config_gcs_bucket); 87 if (TextUtils.isEmpty(bucket)) { 88 throw new RuntimeException("config_gcs_bucket is empty."); 89 } 90 91 // Create GCS MetaData. 92 Map<String, String> metadata = ImmutableMap.of( 93 STORAGE_METADATA_TITLE, bugReport.getTitle() 94 ); 95 StorageObject object = new StorageObject() 96 .setBucket(bucket) 97 .setName(fileName) 98 .setMetadata(metadata) 99 .setContentDisposition("attachment"); 100 Storage.Objects.Insert insertObject = storage.objects().insert(bucket, object, 101 mediaContent); 102 103 // The media uploader gzips content by default, and alters the Content-Encoding accordingly. 104 // GCS dutifully stores content as-uploaded. This line disables the media uploader behavior, 105 // so the service stores exactly what is in the InputStream, without transformation. 106 insertObject.getMediaHttpUploader().setDisableGZipContent(true); 107 Log.v(TAG, "started uploading object " + fileName + " to bucket " + bucket); 108 return insertObject.execute(); 109 } 110 upload(MetaBugReport bugReport)111 private void upload(MetaBugReport bugReport) throws IOException { 112 GoogleCredential credential = GoogleCredential 113 .fromStream(mContext.getResources().openRawResource(R.raw.gcs_credentials)) 114 .createScoped(Collections.singleton(ACCESS_SCOPE)); 115 Log.v(TAG, "Created credential"); 116 HttpTransport httpTransport = AndroidHttp.newCompatibleTransport(); 117 JsonFactory jsonFactory = JacksonFactory.getDefaultInstance(); 118 119 Storage storage = new Storage.Builder(httpTransport, jsonFactory, credential) 120 .setApplicationName("Bugreportupload/1.0").build(); 121 122 File tmpBugReportFile = zipBugReportFiles(bugReport); 123 Log.d(TAG, "Uploading file " + tmpBugReportFile); 124 try { 125 // Upload filename is bugreport filename, although, now it contains the audio message. 126 String fileName = bugReport.getBugReportFileName(); 127 if (Strings.isNullOrEmpty(fileName)) { 128 // Old bugreports don't contain getBugReportFileName, fallback to getFilePath. 129 fileName = new File(bugReport.getFilePath()).getName(); 130 } 131 try (FileInputStream inputStream = new FileInputStream(tmpBugReportFile)) { 132 StorageObject object = uploadSimple(storage, bugReport, fileName, inputStream); 133 Log.v(TAG, "finished uploading object " + object.getName() + " file " + fileName); 134 } 135 File pendingDir = FileUtils.getPendingDir(mContext); 136 // Delete only after successful upload; the files are needed for retry. 137 if (!Strings.isNullOrEmpty(bugReport.getAudioFileName())) { 138 Log.v(TAG, "Deleting file " + bugReport.getAudioFileName()); 139 new File(pendingDir, bugReport.getAudioFileName()).delete(); 140 } 141 if (!Strings.isNullOrEmpty(bugReport.getBugReportFileName())) { 142 Log.v(TAG, "Deleting file " + bugReport.getBugReportFileName()); 143 new File(pendingDir, bugReport.getBugReportFileName()).delete(); 144 } 145 } finally { 146 // Delete the temp file if it's not a MetaBugReport#getFilePath, because it's needed 147 // for retry. 148 if (Strings.isNullOrEmpty(bugReport.getFilePath())) { 149 Log.v(TAG, "Deleting file " + tmpBugReportFile); 150 tmpBugReportFile.delete(); 151 } 152 } 153 } 154 zipBugReportFiles(MetaBugReport bugReport)155 private File zipBugReportFiles(MetaBugReport bugReport) throws IOException { 156 if (!Strings.isNullOrEmpty(bugReport.getFilePath())) { 157 // Old bugreports still have this field. 158 return new File(bugReport.getFilePath()); 159 } 160 File finalZipFile = 161 File.createTempFile("bugreport", ".zip", mContext.getCacheDir()); 162 File pendingDir = FileUtils.getPendingDir(mContext); 163 try (ZipOutputStream zipStream = new ZipOutputStream( 164 new BufferedOutputStream(new FileOutputStream(finalZipFile)))) { 165 ZipUtils.extractZippedFileToZipStream( 166 new File(pendingDir, bugReport.getBugReportFileName()), zipStream); 167 ZipUtils.addFileToZipStream( 168 new File(pendingDir, bugReport.getAudioFileName()), zipStream); 169 } 170 return finalZipFile; 171 } 172 173 @Override onPostExecute(Boolean success)174 protected void onPostExecute(Boolean success) { 175 mResult.reschedule(success); 176 } 177 178 /** Returns true is there are more files to upload. */ 179 @Override doInBackground(Void... voids)180 protected Boolean doInBackground(Void... voids) { 181 List<MetaBugReport> bugReports = BugStorageUtils.getUploadPendingBugReports(mContext); 182 183 for (MetaBugReport bugReport : bugReports) { 184 try { 185 if (isCancelled()) { 186 BugStorageUtils.setUploadRetry(mContext, bugReport, "Upload Job Cancelled"); 187 return true; 188 } 189 upload(bugReport); 190 BugStorageUtils.setUploadSuccess(mContext, bugReport); 191 } catch (Exception e) { 192 Log.e(TAG, String.format("Failed uploading %s - likely a transient error", 193 bugReport.getTimestamp()), e); 194 BugStorageUtils.setUploadRetry(mContext, bugReport, e); 195 } 196 } 197 return false; 198 } 199 200 @Override onCancelled(Boolean success)201 protected void onCancelled(Boolean success) { 202 mResult.reschedule(true); 203 } 204 } 205