Merge pull request #13331 from gcalero/breakpad_android

Breakpad integration for android Interface
This commit is contained in:
Elisa Lupin-Jimenez 2018-06-18 11:32:05 -07:00 committed by GitHub
commit 9c488ff05e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 397 additions and 81 deletions

View file

@ -27,7 +27,8 @@ android {
'-DRELEASE_TYPE=' + RELEASE_TYPE,
'-DSTABLE_BUILD=' + STABLE_BUILD,
'-DDISABLE_QML=OFF',
'-DDISABLE_KTX_CACHE=OFF'
'-DDISABLE_KTX_CACHE=OFF',
'-DUSE_BREAKPAD=' + (project.hasProperty("BACKTRACE_URL") && project.hasProperty("BACKTRACE_TOKEN") ? 'ON' : 'OFF');
}
}
signingConfigs {
@ -46,6 +47,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'
@ -53,6 +58,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 : '') + "\""
}
}

View file

@ -67,6 +67,12 @@
android:name=".SplashActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Translucent.NoActionBar" />
<service
android:name=".BreakpadUploaderService"
android:enabled="true"
android:exported="false"
android:process=":breakpad_uploader"/>
</application>
<uses-feature android:name="android.software.vr.mode" android:required="true"/>

View file

@ -0,0 +1,173 @@
package io.highfidelity.hifiinterface;
import android.app.Service;
import android.content.Intent;
import android.os.FileObserver;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Timer;
import java.util.TimerTask;
import javax.net.ssl.HttpsURLConnection;
public class BreakpadUploaderService extends Service {
private static final String ANNOTATIONS_JSON = "annotations.json";
private static final String TAG = "Interface";
public static final String EXT_DMP = "dmp";
private static final long DUMP_DELAY = 5000;
private FileObserver fileObserver;
public BreakpadUploaderService() {
super();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
URL baseUrl;
baseUrl = getUrl();
if (baseUrl == null) {
stopSelf();
return START_NOT_STICKY;
}
new Thread(() -> {
File[] matchingFiles = getFilesByExtension(getObbDir(), EXT_DMP);
for (File file : matchingFiles) {
uploadDumpAndDelete(file, baseUrl);
}
}).start();
fileObserver = new FileObserver(getObbDir().getPath()) {
@Override
public void onEvent(int event, String path) {
if (path == null) {
return;
}
if (FileObserver.CREATE == event && EXT_DMP.equals(getExtension(path))) {
URL baseUrl = getUrl();
if (baseUrl != null) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
uploadDumpAndDelete(new File(getObbDir(), path), baseUrl);
}
}, DUMP_DELAY);
}
}
}
};
fileObserver.startWatching();
return START_STICKY;
}
private URL getUrl() {
String parameters = getAnnotationsAsUrlEncodedParameters();
try {
return new URL(BuildConfig.BACKTRACE_URL+ "/post?format=minidump&token=" + BuildConfig.BACKTRACE_TOKEN + (parameters.isEmpty() ? "" : ("&" + parameters)));
} catch (MalformedURLException e) {
Log.e(TAG, "Could not initialize Breakpad URL", e);
}
return null;
}
private void uploadDumpAndDelete(File file, URL url) {
int size = (int) file.length();
byte[] bytes = new byte[size];
try {
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 (IOException e) {
Log.e(TAG, "Error uploading file " + file.getAbsolutePath(), e);
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
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;
}
}

View file

@ -1,13 +1,13 @@
package io.highfidelity.hifiinterface;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.app.Activity;
import android.content.DialogInterface;
import android.app.AlertDialog;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
@ -24,20 +24,31 @@ 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";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent myIntent = new Intent(this, BreakpadUploaderService.class);
startService(myIntent);
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);
File obbDir = getObbDir();
if (!obbDir.exists()) {
if (obbDir.mkdirs()) {
Log.d(TAG, "Obb dir created");
}
}
requestAppPermissions(new
String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CAMERA}
,2,REQUEST_PERMISSIONS);
}
@ -124,6 +135,4 @@ public class PermissionChecker extends Activity {
launchActivityWithPermissions();
}
}
}

View file

@ -149,6 +149,13 @@ def packages = [
file: 'etc2comp-patched-armv8-libcpp.tgz',
versionId: 'bHhGECRAQR1vkpshBcK6ByNc1BQIM8gU',
checksum: '14b02795d774457a33bbc60e00a786bc'
],
breakpad: [
file: 'breakpad.zip',
versionId: '2OwvCCZrF171wnte5T44AnjTYFhhJsGJ',
checksum: 'a46062a3167dfedd4fb4916136e204d2',
sharedLibFolder: 'lib',
includeLibs: ['libbreakpad_client.a','libbreakpad.a']
]
]
@ -367,6 +374,7 @@ task verifyPolyvox(type: Verify) { def p = packages['polyvox']; src new File(bas
task verifyTBB(type: Verify) { def p = packages['tbb']; src new File(baseFolder, p['file']); checksum p['checksum'] }
task verifyHifiAC(type: Verify) { def p = packages['hifiAC']; src new File(baseFolder, p['file']); checksum p['checksum'] }
task verifyEtc2Comp(type: Verify) { def p = packages['etc2comp']; src new File(baseFolder, p['file']); checksum p['checksum'] }
task verifyBreakpad(type: Verify) { def p = packages['breakpad']; src new File(baseFolder, p['file']); checksum p['checksum'] }
task verifyDependencyDownloads(dependsOn: downloadDependencies) { }
verifyDependencyDownloads.dependsOn verifyQt
@ -378,6 +386,7 @@ verifyDependencyDownloads.dependsOn verifyPolyvox
verifyDependencyDownloads.dependsOn verifyTBB
verifyDependencyDownloads.dependsOn verifyHifiAC
verifyDependencyDownloads.dependsOn verifyEtc2Comp
verifyDependencyDownloads.dependsOn verifyBreakpad
task extractDependencies(dependsOn: verifyDependencyDownloads) {
doLast {
@ -615,4 +624,4 @@ task testElf (dependsOn: 'externalNativeBuildDebug') {
}
}
}
*/
*/

View file

@ -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()

View file

@ -226,6 +226,7 @@ target_openssl()
target_bullet()
target_opengl()
add_crashpad()
target_breakpad()
# perform standard include and linking for found externals
foreach(EXTERNAL ${OPTIONAL_EXTERNALS})

View file

@ -150,8 +150,8 @@
#include "audio/AudioScope.h"
#include "avatar/AvatarManager.h"
#include "avatar/MyHead.h"
#include "CrashRecoveryHandler.h"
#include "CrashHandler.h"
#include "Crashpad.h"
#include "devices/DdeFaceTracker.h"
#include "DiscoverabilityManager.h"
#include "GLCanvas.h"
@ -791,7 +791,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
bool previousSessionCrashed { false };
if (!inTestMode) {
previousSessionCrashed = CrashHandler::checkForResetSettings(runningMarkerExisted, suppressPrompt);
previousSessionCrashed = CrashRecoveryHandler::checkForResetSettings(runningMarkerExisted, suppressPrompt);
}
// get dir to use for cache

View file

@ -2,8 +2,8 @@
// CrashHandler.h
// interface/src
//
// Created by David Rowe on 24 Aug 2015.
// Copyright 2015 High Fidelity, Inc.
// Created by Clement Brisset on 01/19/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
@ -12,22 +12,10 @@
#ifndef hifi_CrashHandler_h
#define hifi_CrashHandler_h
#include <QString>
#include <string>
class CrashHandler {
bool startCrashHandler();
void setCrashAnnotation(std::string name, std::string value);
public:
static bool checkForResetSettings(bool wasLikelyCrash, bool suppressPrompt = false);
private:
enum Action {
DELETE_INTERFACE_INI,
RETAIN_IMPORTANT_INFO,
DO_NOTHING
};
static Action promptUserForAction(bool showCrashMessage);
static void handleCrash(Action action);
};
#endif // hifi_CrashHandler_h
#endif

View file

@ -0,0 +1,70 @@
//
// CrashHandler_Breakpad.cpp
// interface/src
//
// Created by Clement Brisset on 01/19/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 "CrashHandler.h"
#if HAS_BREAKPAD
#include <mutex>
#include <client/linux/handler/exception_handler.h>
#include <client/linux/handler/minidump_descriptor.h>
#include <QDebug>
#include <QMap>
#include <QtCore/QFileInfo>
#include <QtAndroidExtras/QAndroidJniObject>
#include <SettingHelpers.h>
google_breakpad::ExceptionHandler* gBreakpadHandler;
std::mutex annotationMutex;
QMap<QString, QString> 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<std::mutex> guard(annotationMutex);
QString qName = QString::fromStdString(name);
QString qValue = QString::fromStdString(value);
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

View file

@ -1,5 +1,5 @@
//
// Crashpad.cpp
// CrashHandler_Crashpad.cpp
// interface/src
//
// Created by Clement Brisset on 01/19/18.
@ -9,7 +9,7 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "Crashpad.h"
#include "CrashHandler.h"
#include <assert.h>
@ -117,14 +117,4 @@ void setCrashAnnotation(std::string name, std::string value) {
crashpadAnnotations->SetKeyValue(name, value);
}
#else
bool startCrashHandler() {
qDebug() << "No crash handler available.";
return false;
}
void setCrashAnnotation(std::string name, std::string value) {
}
#endif

View file

@ -0,0 +1,27 @@
//
// CrashHandler_None.cpp
// interface/src
//
// Created by Clement Brisset on 01/19/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 "CrashHandler.h"
#include <assert.h>
#include <QDebug>
#if !defined(HAS_CRASHPAD) && !defined(HAS_BREAKPAD)
bool startCrashHandler() {
qDebug() << "No crash handler available.";
return false;
}
void setCrashAnnotation(std::string name, std::string value) {
}
#endif

View file

@ -1,5 +1,5 @@
//
// CrashHandler.cpp
// CrashRecoveryHandler.cpp
// interface/src
//
// Created by David Rowe on 24 Aug 2015.
@ -9,7 +9,7 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "CrashHandler.h"
#include "CrashRecoveryHandler.h"
#include <QCoreApplication>
#include <QDialog>
@ -30,7 +30,7 @@
#include <SettingHelpers.h>
bool CrashHandler::checkForResetSettings(bool wasLikelyCrash, bool suppressPrompt) {
bool CrashRecoveryHandler::checkForResetSettings(bool wasLikelyCrash, bool suppressPrompt) {
QSettings::setDefaultFormat(JSON_FORMAT);
QSettings settings;
settings.beginGroup("Developer");
@ -59,7 +59,7 @@ bool CrashHandler::checkForResetSettings(bool wasLikelyCrash, bool suppressPromp
return wasLikelyCrash;
}
CrashHandler::Action CrashHandler::promptUserForAction(bool showCrashMessage) {
CrashRecoveryHandler::Action CrashRecoveryHandler::promptUserForAction(bool showCrashMessage) {
QDialog crashDialog;
QLabel* label;
if (showCrashMessage) {
@ -94,20 +94,20 @@ CrashHandler::Action CrashHandler::promptUserForAction(bool showCrashMessage) {
if (result == QDialog::Accepted) {
if (option1->isChecked()) {
return CrashHandler::DELETE_INTERFACE_INI;
return CrashRecoveryHandler::DELETE_INTERFACE_INI;
}
if (option2->isChecked()) {
return CrashHandler::RETAIN_IMPORTANT_INFO;
return CrashRecoveryHandler::RETAIN_IMPORTANT_INFO;
}
}
// Dialog cancelled or "do nothing" option chosen
return CrashHandler::DO_NOTHING;
return CrashRecoveryHandler::DO_NOTHING;
}
void CrashHandler::handleCrash(CrashHandler::Action action) {
if (action != CrashHandler::DELETE_INTERFACE_INI && action != CrashHandler::RETAIN_IMPORTANT_INFO) {
// CrashHandler::DO_NOTHING or unexpected value
void CrashRecoveryHandler::handleCrash(CrashRecoveryHandler::Action action) {
if (action != CrashRecoveryHandler::DELETE_INTERFACE_INI && action != CrashRecoveryHandler::RETAIN_IMPORTANT_INFO) {
// CrashRecoveryHandler::DO_NOTHING or unexpected value
return;
}
@ -126,7 +126,7 @@ void CrashHandler::handleCrash(CrashHandler::Action action) {
QUrl address;
bool tutorialComplete = false;
if (action == CrashHandler::RETAIN_IMPORTANT_INFO) {
if (action == CrashRecoveryHandler::RETAIN_IMPORTANT_INFO) {
// Read avatar info
// Location and orientation
@ -151,7 +151,7 @@ void CrashHandler::handleCrash(CrashHandler::Action action) {
settingsFile.remove();
}
if (action == CrashHandler::RETAIN_IMPORTANT_INFO) {
if (action == CrashRecoveryHandler::RETAIN_IMPORTANT_INFO) {
// Write avatar info
// Location and orientation

View file

@ -0,0 +1,33 @@
//
// CrashRecoveryHandler.h
// interface/src
//
// Created by David Rowe on 24 Aug 2015.
// Copyright 2015 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_CrashRecoveryHandler_h
#define hifi_CrashRecoveryHandler_h
#include <QString>
class CrashRecoveryHandler {
public:
static bool checkForResetSettings(bool wasLikelyCrash, bool suppressPrompt = false);
private:
enum Action {
DELETE_INTERFACE_INI,
RETAIN_IMPORTANT_INFO,
DO_NOTHING
};
static Action promptUserForAction(bool showCrashMessage);
static void handleCrash(Action action);
};
#endif // hifi_CrashRecoveryHandler_h

View file

@ -1,20 +0,0 @@
//
// Crashpad.h
// interface/src
//
// Created by Clement Brisset on 01/19/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_Crashpad_h
#define hifi_Crashpad_h
#include <string>
bool startCrashHandler();
void setCrashAnnotation(std::string name, std::string value);
#endif // hifi_Crashpad_h

View file

@ -23,7 +23,7 @@
#include <UserActivityLogger.h>
#include <UUID.h>
#include "Crashpad.h"
#include "CrashHandler.h"
#include "Menu.h"
const Discoverability::Mode DEFAULT_DISCOVERABILITY_MODE = Discoverability::Connections;

View file

@ -26,7 +26,7 @@
#include "AddressManager.h"
#include "Application.h"
#include "Crashpad.h"
#include "CrashHandler.h"
#include "InterfaceLogging.h"
#include "UserActivityLogger.h"
#include "MainWindow.h"

View file

@ -46,6 +46,7 @@ function init() {
function onMuteClicked() {
Audio.muted = !Audio.muted;
Menu.triggerOption("Out of Bounds Vector Access");
}
function onMutePressed() {