diff --git a/android/app/build.gradle b/android/app/build.gradle index 46de9642d9..44ce64bf9b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,7 +24,8 @@ android { '-DRELEASE_TYPE=' + RELEASE_TYPE, '-DBUILD_BRANCH=' + BUILD_BRANCH, '-DDISABLE_QML=OFF', - '-DDISABLE_KTX_CACHE=OFF' + '-DDISABLE_KTX_CACHE=OFF', + '-DUSE_BREAKPAD=' + (project.hasProperty("BACKTRACE_URL") && project.hasProperty("BACKTRACE_TOKEN") ? 'ON' : 'OFF'); } } signingConfigs { @@ -43,6 +44,10 @@ android { } buildTypes { + debug { + buildConfigField "String", "BACKTRACE_URL", "\"" + (project.hasProperty("BACKTRACE_URL") ? BACKTRACE_URL : '') + "\"" + buildConfigField "String", "BACKTRACE_TOKEN", "\"" + (project.hasProperty("BACKTRACE_TOKEN") ? BACKTRACE_TOKEN : '') + "\"" + } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' @@ -50,6 +55,8 @@ android { project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") && project.hasProperty("HIFI_ANDROID_KEY_ALIAS") && project.hasProperty("HIFI_ANDROID_KEY_PASSWORD")? signingConfigs.release : null + buildConfigField "String", "BACKTRACE_URL", "\"" + (project.hasProperty("BACKTRACE_URL") ? BACKTRACE_URL : '') + "\"" + buildConfigField "String", "BACKTRACE_TOKEN", "\"" + (project.hasProperty("BACKTRACE_TOKEN") ? BACKTRACE_TOKEN : '') + "\"" } } diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java b/android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java index 45060d6d0c..0d2d39db7d 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java @@ -8,22 +8,43 @@ import android.app.Activity; import android.content.DialogInterface; import android.app.AlertDialog; +import android.util.Log; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.json.JSONException; import org.json.JSONObject; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.BufferedWriter; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; import java.io.Writer; +import java.net.URL; +import java.net.URLEncoder; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; + +import javax.net.ssl.HttpsURLConnection; public class PermissionChecker extends Activity { private static final int REQUEST_PERMISSIONS = 20; private static final boolean CHOOSE_AVATAR_ON_STARTUP = false; + private static final String TAG = "Interface"; + private static final String ANNOTATIONS_JSON = "annotations.json"; @Override protected void onCreate(Bundle savedInstanceState) { @@ -31,13 +52,20 @@ public class PermissionChecker extends Activity { if (CHOOSE_AVATAR_ON_STARTUP) { showMenu(); } - this.requestAppPermissions(new - String[]{ - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.RECORD_AUDIO, - Manifest.permission.CAMERA} - ,2,REQUEST_PERMISSIONS); + + Thread networkThread = new Thread(new Runnable() { + public void run() { + UploadCrashReports(); + runOnUiThread(() -> requestAppPermissions(new + String[]{ + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.CAMERA} + ,2,REQUEST_PERMISSIONS)); + } + }); + networkThread.start(); } @@ -125,5 +153,88 @@ public class PermissionChecker extends Activity { } } + public void UploadCrashReports() + { + try + { + String parameters = getAnnotationsAsUrlEncodedParameters(); + URL url = new URL(BuildConfig.BACKTRACE_URL+ "/post?format=minidump&token=" + BuildConfig.BACKTRACE_TOKEN + (parameters.isEmpty() ? "" : ("&" + parameters))); + // Check if a crash .dmp exists + File[] matchingFiles = getFilesByExtension(getObbDir(), "dmp"); + for (File file : matchingFiles) + { + int size = (int) file.length(); + byte[] bytes = new byte[size]; + BufferedInputStream buf = new BufferedInputStream(new FileInputStream(file)); + buf.read(bytes, 0, bytes.length); + buf.close(); + HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); + urlConnection.setRequestMethod("POST"); + urlConnection.setDoOutput(true); + urlConnection.setChunkedStreamingMode(0); + OutputStream ostream = urlConnection.getOutputStream(); + + OutputStream out = new BufferedOutputStream(ostream); + out.write(bytes, 0, size); + + InputStream in = new BufferedInputStream(urlConnection.getInputStream()); + in.read(); + if (urlConnection.getResponseCode() == 200) { + file.delete(); + } + urlConnection.disconnect(); + } + } + catch (Exception e) + { + Log.e(TAG, "Error uploading breakpad dumps", e); + } + } + + private File[] getFilesByExtension(File dir, final String extension) + { + return dir.listFiles(pathName -> getExtension(pathName.getName()).equals(extension)); + } + + private String getExtension(String fileName) + { + String extension = ""; + + int i = fileName.lastIndexOf('.'); + int p = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\')); + + if (i > p) + { + extension = fileName.substring(i+1); + } + + return extension; + } + + + public String getAnnotationsAsUrlEncodedParameters() { + String parameters = ""; + File annotationsFile = new File(getObbDir(), ANNOTATIONS_JSON); + if (annotationsFile.exists()) { + JsonParser parser = new JsonParser(); + try { + JsonObject json = (JsonObject) parser.parse(new FileReader(annotationsFile)); + for (String k: json.keySet()) { + if (!json.get(k).getAsString().isEmpty()) { + String key = k.contains("/") ? k.substring(k.indexOf("/") + 1) : k; + if (!parameters.isEmpty()) { + parameters += "&"; + } + parameters += URLEncoder.encode(key, "UTF-8") + "=" + URLEncoder.encode(json.get(k).getAsString(), "UTF-8"); + } + } + } catch (FileNotFoundException e) { + Log.e(TAG, "Error reading annotations file", e); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Error reading annotations file", e); + } + } + return parameters; + } } diff --git a/cmake/macros/TargetBreakpad.cmake b/cmake/macros/TargetBreakpad.cmake new file mode 100644 index 0000000000..dac581d6c7 --- /dev/null +++ b/cmake/macros/TargetBreakpad.cmake @@ -0,0 +1,22 @@ +# +# Copyright 2018 High Fidelity, Inc. +# Created by Gabriel Calero & Cristian Duarte on 2018/03/13 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# +macro(TARGET_BREAKPAD) + if (ANDROID) + set(INSTALL_DIR ${HIFI_ANDROID_PRECOMPILED}/breakpad) + set(BREAKPAD_INCLUDE_DIRS "${INSTALL_DIR}/include" CACHE TYPE INTERNAL) + set(LIB_DIR ${INSTALL_DIR}/lib) + list(APPEND BREAKPAD_LIBRARIES ${LIB_DIR}/libbreakpad_client.a) + target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${BREAKPAD_INCLUDE_DIRS}) + if (USE_BREAKPAD) + add_definitions(-DHAS_BREAKPAD) + endif() + endif() + target_link_libraries(${TARGET_NAME} ${BREAKPAD_LIBRARIES}) +endmacro() + + diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index ac9441319b..ee77369548 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -232,6 +232,7 @@ target_openssl() target_bullet() target_opengl() add_crashpad() +target_breakpad() # perform standard include and linking for found externals foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) diff --git a/interface/src/Breakpad.cpp b/interface/src/Breakpad.cpp new file mode 100644 index 0000000000..3d27eee29c --- /dev/null +++ b/interface/src/Breakpad.cpp @@ -0,0 +1,72 @@ +// +// Breakpad.cpp +// interface/src +// +// Created by Gabriel Calero & Cristian Duarte on 06/06/18 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "Crashpad.h" + +#if defined(HAS_BREAKPAD) +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +google_breakpad::ExceptionHandler *gBreakpadHandler; + +std::mutex annotationMutex; +QMap annotations; + +static bool breakpad_dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* context, bool succeeded) { + return succeeded; +} + +QString obbDir() { + QAndroidJniObject mediaDir = QAndroidJniObject::callStaticObjectMethod("android/os/Environment", "getExternalStorageDirectory", "()Ljava/io/File;"); + QAndroidJniObject mediaPath = mediaDir.callObjectMethod( "getAbsolutePath", "()Ljava/lang/String;" ); + QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative", "activity", "()Landroid/app/Activity;"); + QAndroidJniObject package = activity.callObjectMethod("getPackageName", "()Ljava/lang/String;"); + QString dataAbsPath = mediaPath.toString()+"/Android/obb/" + package.toString(); + return dataAbsPath; +} + +bool startCrashHandler() { + + gBreakpadHandler = new google_breakpad::ExceptionHandler( + google_breakpad::MinidumpDescriptor(obbDir().toStdString()), + nullptr, breakpad_dumpCallback, nullptr, true, -1); + return true; +} + +void setCrashAnnotation(std::string name, std::string value) { + std::lock_guard guard(annotationMutex); + QString qName = QString::fromStdString(name); + QString qValue = QString::fromStdString(value); + if(!annotations.contains(qName)) { + annotations.insert(qName, qValue); + } else { + annotations[qName] = qValue; + } + + QSettings settings(obbDir() + "/annotations.json", JSON_FORMAT); + settings.clear(); + settings.beginGroup("Annotations"); + for(auto k : annotations.keys()) { + settings.setValue(k, annotations.value(k)); + } + settings.endGroup(); + settings.sync(); +} + +#endif diff --git a/interface/src/Breakpad.h b/interface/src/Breakpad.h new file mode 100644 index 0000000000..3d4c46025b --- /dev/null +++ b/interface/src/Breakpad.h @@ -0,0 +1,20 @@ +// +// Breakpad.h +// interface/src +// +// Created by Gabriel Calero & Cristian Duarte on 06/06/18 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_Breakpad_h +#define hifi_Breakpad_h + +#include + +bool startCrashHandler(); +void setCrashAnnotation(std::string name, std::string value); + +#endif // hifi_Crashpad_h diff --git a/interface/src/Crashpad.cpp b/interface/src/Crashpad.cpp index 45f1d0778f..6f29d9dd23 100644 --- a/interface/src/Crashpad.cpp +++ b/interface/src/Crashpad.cpp @@ -114,7 +114,7 @@ void setCrashAnnotation(std::string name, std::string value) { crashpadAnnotations->SetKeyValue(name, value); } -#else +#elif !defined(HAS_BREAKPAD) bool startCrashHandler() { qDebug() << "No crash handler available.";