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