forked from GitHub-Mirror/riotX-android
ExceptionHandler + Log in files for RageShake
This commit is contained in:
parent
bc467340c9
commit
be2dad9b17
@ -167,7 +167,11 @@ dependencies {
|
||||
implementation 'androidx.paging:paging-runtime:2.0.0'
|
||||
|
||||
implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1'
|
||||
|
||||
// Log
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
|
||||
// Debug
|
||||
implementation 'com.facebook.stetho:stetho:1.5.0'
|
||||
|
||||
// rx
|
||||
|
@ -26,6 +26,8 @@ import com.jakewharton.threetenabp.AndroidThreeTen
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.riotredesign.core.di.AppModule
|
||||
import im.vector.riotredesign.features.home.HomeModule
|
||||
import im.vector.riotredesign.features.rageshake.VectorFileLogger
|
||||
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
|
||||
import org.koin.log.EmptyLogger
|
||||
import org.koin.standalone.StandAloneContext.startKoin
|
||||
import timber.log.Timber
|
||||
@ -35,10 +37,17 @@ class Riot : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
VectorUncaughtExceptionHandler.activate(this)
|
||||
|
||||
// Log
|
||||
VectorFileLogger.init(this)
|
||||
Timber.plant(Timber.DebugTree(), VectorFileLogger)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
Stetho.initializeWithDefaults(this)
|
||||
}
|
||||
|
||||
AndroidThreeTen.init(this)
|
||||
BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
|
||||
val appModule = AppModule(applicationContext).definition
|
||||
|
@ -16,9 +16,11 @@
|
||||
|
||||
package im.vector.riotredesign.core.platform
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.annotation.*
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import butterknife.BindView
|
||||
@ -28,12 +30,15 @@ import com.airbnb.mvrx.BaseMvRxActivity
|
||||
import com.bumptech.glide.util.Util
|
||||
import im.vector.riotredesign.BuildConfig
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.features.rageshake.BugReportActivity
|
||||
import im.vector.riotredesign.features.rageshake.BugReporter
|
||||
import im.vector.riotredesign.features.rageshake.RageShake
|
||||
import im.vector.riotredesign.features.themes.ThemeUtils
|
||||
import im.vector.riotredesign.receivers.DebugReceiver
|
||||
import im.vector.ui.themes.ActivityOtherThemes
|
||||
import im.vector.ui.themes.ThemeUtils
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
abstract class RiotActivity : BaseMvRxActivity() {
|
||||
@ -113,10 +118,19 @@ abstract class RiotActivity : BaseMvRxActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
unBinder?.unbind()
|
||||
unBinder = null
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (this !is BugReportActivity) {
|
||||
rageShake?.start()
|
||||
}
|
||||
|
||||
DebugReceiver
|
||||
.getIntentFilter(this)
|
||||
@ -138,6 +152,38 @@ abstract class RiotActivity : BaseMvRxActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
|
||||
if (hasFocus && displayInFullscreen()) {
|
||||
setFullScreen()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration?) {
|
||||
super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig)
|
||||
|
||||
Timber.w("onMultiWindowModeChanged. isInMultiWindowMode: $isInMultiWindowMode")
|
||||
BugReporter.inMultiWindowMode = isInMultiWindowMode
|
||||
}
|
||||
|
||||
|
||||
/* ==========================================================================================
|
||||
* PRIVATE METHODS
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Force to render the activity in fullscreen
|
||||
*/
|
||||
private fun setFullScreen() {
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* MENU MANAGEMENT
|
||||
* ========================================================================================== */
|
||||
|
@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.utils
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.features.settings.VectorLocale
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Tells if the application ignores battery optimizations.
|
||||
*
|
||||
* Ignoring them allows the app to run in background to make background sync with the homeserver.
|
||||
* This user option appears on Android M but Android O enforces its usage and kills apps not
|
||||
* authorised by the user to run in background.
|
||||
*
|
||||
* @param context the context
|
||||
* @return true if battery optimisations are ignored
|
||||
*/
|
||||
fun isIgnoringBatteryOptimizations(context: Context): Boolean {
|
||||
// no issue before Android M, battery optimisations did not exist
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|
||||
|| (context.getSystemService(Context.POWER_SERVICE) as PowerManager?)?.isIgnoringBatteryOptimizations(context.packageName) == true
|
||||
}
|
||||
|
||||
/**
|
||||
* display the system dialog for granting this permission. If previously granted, the
|
||||
* system will not show it (so you should call this method).
|
||||
*
|
||||
* Note: If the user finally does not grant the permission, PushManager.isBackgroundSyncAllowed()
|
||||
* will return false and the notification privacy will fallback to "LOW_DETAIL".
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
fun requestDisablingBatteryOptimization(activity: Activity, fragment: Fragment?, requestCode: Int) {
|
||||
val intent = Intent()
|
||||
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
intent.data = Uri.parse("package:" + activity.packageName)
|
||||
if (fragment != null) {
|
||||
fragment.startActivityForResult(intent, requestCode)
|
||||
} else {
|
||||
activity.startActivityForResult(intent, requestCode)
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Clipboard helper
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Copy a text to the clipboard, and display a Toast when done
|
||||
*
|
||||
* @param context the context
|
||||
* @param text the text to copy
|
||||
*/
|
||||
fun copyToClipboard(context: Context, text: CharSequence) {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.primaryClip = ClipData.newPlainText("", text)
|
||||
context.toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the device locale
|
||||
*
|
||||
* @return the device locale
|
||||
*/
|
||||
fun getDeviceLocale(context: Context): Locale {
|
||||
var locale: Locale
|
||||
|
||||
locale = try {
|
||||
val packageManager = context.packageManager
|
||||
val resources = packageManager.getResourcesForApplication("android")
|
||||
resources.configuration.locale
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## getDeviceLocale() failed " + e.message)
|
||||
// Fallback to application locale
|
||||
VectorLocale.applicationLocale
|
||||
}
|
||||
|
||||
return locale
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows notification settings for the current app.
|
||||
* In android O will directly opens the notification settings, in lower version it will show the App settings
|
||||
*/
|
||||
fun startNotificationSettingsIntent(fragment: Fragment, requestCode: Int) {
|
||||
val intent = Intent()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, fragment.context?.packageName)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
intent.putExtra("app_package", fragment.context?.packageName)
|
||||
intent.putExtra("app_uid", fragment.context?.applicationInfo?.uid)
|
||||
} else {
|
||||
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||
intent.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
val uri = Uri.fromParts("package", fragment.activity?.packageName, null)
|
||||
intent.data = uri
|
||||
}
|
||||
fragment.startActivityForResult(intent, requestCode)
|
||||
}
|
||||
|
||||
// TODO This comes from NotificationUtils
|
||||
fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
|
||||
|
||||
/**
|
||||
* Shows notification system settings for the given channel id.
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
fun startNotificationChannelSettingsIntent(fragment: Fragment, channelID: String) {
|
||||
if (!supportNotificationChannels()) return
|
||||
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
|
||||
putExtra(Settings.EXTRA_APP_PACKAGE, fragment.context?.packageName)
|
||||
putExtra(Settings.EXTRA_CHANNEL_ID, channelID)
|
||||
}
|
||||
fragment.startActivity(intent)
|
||||
}
|
||||
|
||||
fun startAddGoogleAccountIntent(fragment: Fragment, requestCode: Int) {
|
||||
try {
|
||||
val intent = Intent(Settings.ACTION_ADD_ACCOUNT)
|
||||
intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf("com.google"))
|
||||
fragment.startActivityForResult(intent, requestCode)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
fragment.activity?.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
|
||||
fun startSharePlainTextIntent(fragment: Fragment, chooserTitle: String?, text: String, subject: String? = null) {
|
||||
val share = Intent(Intent.ACTION_SEND)
|
||||
share.type = "text/plain"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
share.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
|
||||
} else {
|
||||
share.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
// Add data to the intent, the receiving app will decide what to do with it.
|
||||
share.putExtra(Intent.EXTRA_SUBJECT, subject)
|
||||
share.putExtra(Intent.EXTRA_TEXT, text)
|
||||
try {
|
||||
fragment.startActivity(Intent.createChooser(share, chooserTitle))
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
fragment.activity?.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
|
||||
fun startImportTextFromFileIntent(fragment: Fragment, requestCode: Int) {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "text/plain"
|
||||
}
|
||||
if (intent.resolveActivity(fragment.activity!!.packageManager) != null) {
|
||||
fragment.startActivityForResult(intent, requestCode)
|
||||
} else {
|
||||
fragment.activity?.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
|
||||
// Not in KTX anymore
|
||||
fun Context.toast(resId: Int) {
|
||||
Toast.makeText(this, resId, Toast.LENGTH_SHORT).show()
|
||||
}
|
@ -21,6 +21,7 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
@ -34,6 +35,8 @@ import im.vector.riotredesign.core.platform.OnBackPressed
|
||||
import im.vector.riotredesign.core.platform.RiotActivity
|
||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||
import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
|
||||
import im.vector.riotredesign.features.rageshake.BugReporter
|
||||
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.scope.ext.android.bindScope
|
||||
@ -74,6 +77,21 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (VectorUncaughtExceptionHandler.didAppCrash(this)) {
|
||||
VectorUncaughtExceptionHandler.clearAppCrashStatus(this)
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.send_bug_report_app_crashed)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.yes) { _, _ -> BugReporter.openBugReportScreen(this) }
|
||||
.setNegativeButton(R.string.no) { _, _ -> BugReporter.deleteCrashFile(this) }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun configure(toolbar: Toolbar) {
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setHomeButtonEnabled(true)
|
||||
|
@ -70,8 +70,8 @@ class BugReportActivity : RiotActivity() {
|
||||
override fun initUiAndData() {
|
||||
configureToolbar()
|
||||
|
||||
if (BugReporter.getScreenshot() != null) {
|
||||
mScreenShotPreview.setImageBitmap(BugReporter.getScreenshot())
|
||||
if (BugReporter.screenshot != null) {
|
||||
mScreenShotPreview.setImageBitmap(BugReporter.screenshot)
|
||||
} else {
|
||||
mScreenShotPreview.isVisible = false
|
||||
mIncludeScreenShotButton.isChecked = false
|
||||
@ -189,7 +189,7 @@ class BugReportActivity : RiotActivity() {
|
||||
|
||||
@OnCheckedChanged(R.id.bug_report_button_include_screenshot)
|
||||
internal fun onSendScreenshotChanged() {
|
||||
mScreenShotPreview.isVisible = mIncludeScreenShotButton.isChecked && BugReporter.getScreenshot() != null
|
||||
mScreenShotPreview.isVisible = mIncludeScreenShotButton.isChecked && BugReporter.screenshot != null
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
|
@ -1,728 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.rageshake;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import im.vector.riotredesign.BuildConfig;
|
||||
import im.vector.riotredesign.R;
|
||||
import im.vector.riotredesign.core.extensions.BasicExtensionsKt;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* BugReporter creates and sends the bug reports.
|
||||
*/
|
||||
public class BugReporter {
|
||||
private static final String LOG_TAG = BugReporter.class.getSimpleName();
|
||||
|
||||
private static boolean sInMultiWindowMode;
|
||||
|
||||
public static void setMultiWindowMode(boolean inMultiWindowMode) {
|
||||
sInMultiWindowMode = inMultiWindowMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bug report upload listener
|
||||
*/
|
||||
public interface IMXBugReportListener {
|
||||
/**
|
||||
* The bug report has been cancelled
|
||||
*/
|
||||
void onUploadCancelled();
|
||||
|
||||
/**
|
||||
* The bug report upload failed.
|
||||
*
|
||||
* @param reason the failure reason
|
||||
*/
|
||||
void onUploadFailed(String reason);
|
||||
|
||||
/**
|
||||
* The upload progress (in percent)
|
||||
*
|
||||
* @param progress the upload progress
|
||||
*/
|
||||
void onProgress(int progress);
|
||||
|
||||
/**
|
||||
* The bug report upload succeeded.
|
||||
*/
|
||||
void onUploadSucceed();
|
||||
}
|
||||
|
||||
// filenames
|
||||
private static final String LOG_CAT_ERROR_FILENAME = "logcatError.log";
|
||||
private static final String LOG_CAT_FILENAME = "logcat.log";
|
||||
private static final String LOG_CAT_SCREENSHOT_FILENAME = "screenshot.png";
|
||||
private static final String CRASH_FILENAME = "crash.log";
|
||||
|
||||
|
||||
// the http client
|
||||
private static final OkHttpClient mOkHttpClient = new OkHttpClient();
|
||||
|
||||
// the pending bug report call
|
||||
private static Call mBugReportCall = null;
|
||||
|
||||
|
||||
// boolean to cancel the bug report
|
||||
private static boolean mIsCancelled = false;
|
||||
|
||||
/**
|
||||
* Send a bug report.
|
||||
*
|
||||
* @param context the application context
|
||||
* @param withDevicesLogs true to include the device log
|
||||
* @param withCrashLogs true to include the crash logs
|
||||
* @param withScreenshot true to include the screenshot
|
||||
* @param theBugDescription the bug description
|
||||
* @param listener the listener
|
||||
*/
|
||||
public static void sendBugReport(final Context context,
|
||||
final boolean withDevicesLogs,
|
||||
final boolean withCrashLogs,
|
||||
final boolean withScreenshot,
|
||||
final String theBugDescription,
|
||||
final IMXBugReportListener listener) {
|
||||
new AsyncTask<Void, Integer, String>() {
|
||||
|
||||
// enumerate files to delete
|
||||
final List<File> mBugReportFiles = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected String doInBackground(Void... voids) {
|
||||
String bugDescription = theBugDescription;
|
||||
String serverError = null;
|
||||
String crashCallStack = getCrashDescription(context);
|
||||
|
||||
if (null != crashCallStack) {
|
||||
bugDescription += "\n\n\n\n--------------------------------- crash call stack ---------------------------------\n";
|
||||
bugDescription += crashCallStack;
|
||||
}
|
||||
|
||||
List<File> gzippedFiles = new ArrayList<>();
|
||||
|
||||
if (withDevicesLogs) {
|
||||
// TODO Timber
|
||||
/*
|
||||
List<File> files = org.matrix.androidsdk.util.Timber.addLogFiles(new ArrayList<File>());
|
||||
|
||||
for (File f : files) {
|
||||
if (!mIsCancelled) {
|
||||
File gzippedFile = compressFile(f);
|
||||
|
||||
if (null != gzippedFile) {
|
||||
gzippedFiles.add(gzippedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
if (!mIsCancelled && (withCrashLogs || withDevicesLogs)) {
|
||||
File gzippedLogcat = saveLogCat(context, false);
|
||||
|
||||
if (null != gzippedLogcat) {
|
||||
if (gzippedFiles.size() == 0) {
|
||||
gzippedFiles.add(gzippedLogcat);
|
||||
} else {
|
||||
gzippedFiles.add(0, gzippedLogcat);
|
||||
}
|
||||
}
|
||||
|
||||
File crashDescription = getCrashFile(context);
|
||||
if (crashDescription.exists()) {
|
||||
File compressedCrashDescription = compressFile(crashDescription);
|
||||
|
||||
if (null != compressedCrashDescription) {
|
||||
if (gzippedFiles.size() == 0) {
|
||||
gzippedFiles.add(compressedCrashDescription);
|
||||
} else {
|
||||
gzippedFiles.add(0, compressedCrashDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO MXSession session = Matrix.getInstance(context).getDefaultSession();
|
||||
|
||||
String deviceId = "undefined";
|
||||
String userId = "undefined";
|
||||
String matrixSdkVersion = "undefined";
|
||||
String olmVersion = "undefined";
|
||||
|
||||
/*
|
||||
TODO
|
||||
if (null != session) {
|
||||
userId = session.getMyUserId();
|
||||
deviceId = session.getCredentials().deviceId;
|
||||
matrixSdkVersion = session.getVersion(true);
|
||||
olmVersion = session.getCryptoVersion(context, true);
|
||||
}
|
||||
*/
|
||||
|
||||
if (!mIsCancelled) {
|
||||
// build the multi part request
|
||||
BugReporterMultipartBody.Builder builder = new BugReporterMultipartBody.Builder()
|
||||
.addFormDataPart("text", "[RiotX] " + bugDescription)
|
||||
.addFormDataPart("app", "riot-android")
|
||||
// TODO .addFormDataPart("user_agent", RestClient.getUserAgent())
|
||||
.addFormDataPart("user_id", userId)
|
||||
.addFormDataPart("device_id", deviceId)
|
||||
// TODO .addFormDataPart("version", Matrix.getInstance(context).getVersion(true, false))
|
||||
.addFormDataPart("branch_name", context.getString(R.string.git_branch_name))
|
||||
.addFormDataPart("matrix_sdk_version", matrixSdkVersion)
|
||||
.addFormDataPart("olm_version", olmVersion)
|
||||
.addFormDataPart("device", Build.MODEL.trim())
|
||||
.addFormDataPart("lazy_loading", BasicExtensionsKt.toOnOff(true))
|
||||
.addFormDataPart("multi_window", BasicExtensionsKt.toOnOff(sInMultiWindowMode))
|
||||
.addFormDataPart("os", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ") "
|
||||
+ Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME)
|
||||
.addFormDataPart("locale", Locale.getDefault().toString())
|
||||
// TODO .addFormDataPart("app_language", VectorLocale.INSTANCE.getApplicationLocale().toString())
|
||||
// TODO .addFormDataPart("default_app_language", SystemUtilsKt.getDeviceLocale(context).toString())
|
||||
// TODO .addFormDataPart("theme", ThemeUtils.INSTANCE.getApplicationTheme(context))
|
||||
;
|
||||
|
||||
String buildNumber = context.getString(R.string.build_number);
|
||||
if (!TextUtils.isEmpty(buildNumber) && !buildNumber.equals("0")) {
|
||||
builder.addFormDataPart("build_number", buildNumber);
|
||||
}
|
||||
|
||||
// add the gzipped files
|
||||
for (File file : gzippedFiles) {
|
||||
builder.addFormDataPart("compressed-log", file.getName(), RequestBody.create(MediaType.parse("application/octet-stream"), file));
|
||||
}
|
||||
|
||||
mBugReportFiles.addAll(gzippedFiles);
|
||||
|
||||
if (withScreenshot) {
|
||||
Bitmap bitmap = mScreenshot;
|
||||
|
||||
if (null != bitmap) {
|
||||
File logCatScreenshotFile = new File(context.getCacheDir().getAbsolutePath(), LOG_CAT_SCREENSHOT_FILENAME);
|
||||
|
||||
if (logCatScreenshotFile.exists()) {
|
||||
logCatScreenshotFile.delete();
|
||||
}
|
||||
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(logCatScreenshotFile);
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
|
||||
fos.flush();
|
||||
fos.close();
|
||||
|
||||
builder.addFormDataPart("file",
|
||||
logCatScreenshotFile.getName(), RequestBody.create(MediaType.parse("application/octet-stream"), logCatScreenshotFile));
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "## sendBugReport() : fail to write screenshot" + e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mScreenshot = null;
|
||||
|
||||
// add some github labels
|
||||
builder.addFormDataPart("label", BuildConfig.VERSION_NAME);
|
||||
builder.addFormDataPart("label", BuildConfig.FLAVOR_DESCRIPTION);
|
||||
builder.addFormDataPart("label", context.getString(R.string.git_branch_name));
|
||||
|
||||
// Special for RiotX
|
||||
builder.addFormDataPart("label", "[RiotX]");
|
||||
|
||||
if (getCrashFile(context).exists()) {
|
||||
builder.addFormDataPart("label", "crash");
|
||||
deleteCrashFile(context);
|
||||
}
|
||||
|
||||
BugReporterMultipartBody requestBody = builder.build();
|
||||
|
||||
// add a progress listener
|
||||
requestBody.setWriteListener(new BugReporterMultipartBody.WriteListener() {
|
||||
@Override
|
||||
public void onWrite(long totalWritten, long contentLength) {
|
||||
int percentage;
|
||||
|
||||
if (-1 != contentLength) {
|
||||
if (totalWritten > contentLength) {
|
||||
percentage = 100;
|
||||
} else {
|
||||
percentage = (int) (totalWritten * 100 / contentLength);
|
||||
}
|
||||
} else {
|
||||
percentage = 0;
|
||||
}
|
||||
|
||||
if (mIsCancelled && (null != mBugReportCall)) {
|
||||
mBugReportCall.cancel();
|
||||
}
|
||||
|
||||
Timber.d("## onWrite() : " + percentage + "%");
|
||||
publishProgress(percentage);
|
||||
}
|
||||
});
|
||||
|
||||
// build the request
|
||||
Request request = new Request.Builder()
|
||||
.url(context.getString(R.string.bug_report_url))
|
||||
.post(requestBody)
|
||||
.build();
|
||||
|
||||
int responseCode = HttpURLConnection.HTTP_INTERNAL_ERROR;
|
||||
Response response = null;
|
||||
String errorMessage = null;
|
||||
|
||||
// trigger the request
|
||||
try {
|
||||
mBugReportCall = mOkHttpClient.newCall(request);
|
||||
response = mBugReportCall.execute();
|
||||
responseCode = response.code();
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "response " + e.getMessage());
|
||||
errorMessage = e.getLocalizedMessage();
|
||||
}
|
||||
|
||||
// if the upload failed, try to retrieve the reason
|
||||
if (responseCode != HttpURLConnection.HTTP_OK) {
|
||||
if (null != errorMessage) {
|
||||
serverError = "Failed with error " + errorMessage;
|
||||
} else if ((null == response) || (null == response.body())) {
|
||||
serverError = "Failed with error " + responseCode;
|
||||
} else {
|
||||
InputStream is = null;
|
||||
|
||||
try {
|
||||
is = response.body().byteStream();
|
||||
|
||||
if (null != is) {
|
||||
int ch;
|
||||
StringBuilder b = new StringBuilder();
|
||||
while ((ch = is.read()) != -1) {
|
||||
b.append((char) ch);
|
||||
}
|
||||
serverError = b.toString();
|
||||
is.close();
|
||||
|
||||
// check if the error message
|
||||
try {
|
||||
JSONObject responseJSON = new JSONObject(serverError);
|
||||
serverError = responseJSON.getString("error");
|
||||
} catch (JSONException e) {
|
||||
Timber.e(e, "doInBackground ; Json conversion failed " + e.getMessage());
|
||||
}
|
||||
|
||||
// should never happen
|
||||
if (null == serverError) {
|
||||
serverError = "Failed with error " + responseCode;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "## sendBugReport() : failed to parse error " + e.getMessage());
|
||||
} finally {
|
||||
try {
|
||||
if (null != is) {
|
||||
is.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "## sendBugReport() : failed to close the error stream " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return serverError;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(Integer... progress) {
|
||||
super.onProgressUpdate(progress);
|
||||
|
||||
if (null != listener) {
|
||||
try {
|
||||
listener.onProgress((null == progress) ? 0 : progress[0]);
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "## onProgress() : failed " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String reason) {
|
||||
mBugReportCall = null;
|
||||
|
||||
// delete when the bug report has been successfully sent
|
||||
for (File file : mBugReportFiles) {
|
||||
file.delete();
|
||||
}
|
||||
|
||||
if (null != listener) {
|
||||
try {
|
||||
if (mIsCancelled) {
|
||||
listener.onUploadCancelled();
|
||||
} else if (null == reason) {
|
||||
listener.onUploadSucceed();
|
||||
} else {
|
||||
listener.onUploadFailed(reason);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "## onPostExecute() : failed " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private static Bitmap mScreenshot = null;
|
||||
|
||||
/**
|
||||
* Get current Screenshot
|
||||
*
|
||||
* @return screenshot or null if not available
|
||||
*/
|
||||
@Nullable
|
||||
public static Bitmap getScreenshot() {
|
||||
return mScreenshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a bug report either with email or with Vector.
|
||||
*/
|
||||
public static void sendBugReport(Activity activity) {
|
||||
mScreenshot = takeScreenshot(activity);
|
||||
|
||||
Intent intent = new Intent(activity, BugReportActivity.class);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// crash report management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Provides the crash file
|
||||
*
|
||||
* @param context the context
|
||||
* @return the crash file
|
||||
*/
|
||||
private static File getCrashFile(Context context) {
|
||||
return new File(context.getCacheDir().getAbsolutePath(), CRASH_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the crash file
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
public static void deleteCrashFile(Context context) {
|
||||
File crashFile = getCrashFile(context);
|
||||
|
||||
if (crashFile.exists()) {
|
||||
crashFile.delete();
|
||||
}
|
||||
|
||||
// Also reset the screenshot
|
||||
mScreenshot = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the crash report
|
||||
*
|
||||
* @param context the context
|
||||
* @param crashDescription teh crash description
|
||||
*/
|
||||
public static void saveCrashReport(Context context, String crashDescription) {
|
||||
File crashFile = getCrashFile(context);
|
||||
|
||||
if (crashFile.exists()) {
|
||||
crashFile.delete();
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(crashDescription)) {
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(crashFile);
|
||||
OutputStreamWriter osw = new OutputStreamWriter(fos);
|
||||
osw.write(crashDescription);
|
||||
osw.close();
|
||||
|
||||
fos.flush();
|
||||
fos.close();
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "## saveCrashReport() : fail to write " + e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the crash description file and return its content.
|
||||
*
|
||||
* @param context teh context
|
||||
* @return the crash description
|
||||
*/
|
||||
private static String getCrashDescription(Context context) {
|
||||
String crashDescription = null;
|
||||
File crashFile = getCrashFile(context);
|
||||
|
||||
if (crashFile.exists()) {
|
||||
try {
|
||||
FileInputStream fis = new FileInputStream(crashFile);
|
||||
InputStreamReader isr = new InputStreamReader(fis);
|
||||
|
||||
char[] buffer = new char[fis.available()];
|
||||
int len = isr.read(buffer, 0, fis.available());
|
||||
crashDescription = String.valueOf(buffer, 0, len);
|
||||
isr.close();
|
||||
fis.close();
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "## getCrashDescription() : fail to read " + e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return crashDescription;
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Screenshot management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Take a screenshot of the display.
|
||||
*
|
||||
* @return the screenshot
|
||||
*/
|
||||
private static Bitmap takeScreenshot(Activity activity) {
|
||||
// get content view
|
||||
View contentView = activity.findViewById(android.R.id.content);
|
||||
if (contentView == null) {
|
||||
Timber.e("Cannot find content view on " + activity + ". Cannot take screenshot.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// get the root view to snapshot
|
||||
View rootView = contentView.getRootView();
|
||||
if (rootView == null) {
|
||||
Timber.e("Cannot find root view on " + activity + ". Cannot take screenshot.");
|
||||
return null;
|
||||
}
|
||||
// refresh it
|
||||
rootView.setDrawingCacheEnabled(false);
|
||||
rootView.setDrawingCacheEnabled(true);
|
||||
|
||||
try {
|
||||
Bitmap bitmap = rootView.getDrawingCache();
|
||||
|
||||
// Make a copy, because if Activity is destroyed, the bitmap will be recycled
|
||||
bitmap = Bitmap.createBitmap(bitmap);
|
||||
|
||||
return bitmap;
|
||||
} catch (OutOfMemoryError oom) {
|
||||
Timber.e(oom, "Cannot get drawing cache for " + activity + " OOM.");
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "Cannot get snapshot of screen: " + e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Logcat management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Save the logcat
|
||||
*
|
||||
* @param context the context
|
||||
* @param isErrorLogcat true to save the error logcat
|
||||
* @return the file if the operation succeeds
|
||||
*/
|
||||
private static File saveLogCat(Context context, boolean isErrorLogcat) {
|
||||
File logCatErrFile = new File(context.getCacheDir().getAbsolutePath(), isErrorLogcat ? LOG_CAT_ERROR_FILENAME : LOG_CAT_FILENAME);
|
||||
|
||||
if (logCatErrFile.exists()) {
|
||||
logCatErrFile.delete();
|
||||
}
|
||||
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(logCatErrFile);
|
||||
OutputStreamWriter osw = new OutputStreamWriter(fos);
|
||||
getLogCatError(osw, isErrorLogcat);
|
||||
osw.close();
|
||||
|
||||
fos.flush();
|
||||
fos.close();
|
||||
|
||||
return compressFile(logCatErrFile);
|
||||
} catch (OutOfMemoryError error) {
|
||||
Timber.e(error, "## saveLogCat() : fail to write logcat" + error.toString());
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "## saveLogCat() : fail to write logcat" + e.toString());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final int BUFFER_SIZE = 1024 * 1024 * 50;
|
||||
|
||||
private static final String[] LOGCAT_CMD_ERROR = new String[]{
|
||||
"logcat", ///< Run 'logcat' command
|
||||
"-d", ///< Dump the log rather than continue outputting it
|
||||
"-v", // formatting
|
||||
"threadtime", // include timestamps
|
||||
"AndroidRuntime:E " + ///< Pick all AndroidRuntime errors (such as uncaught exceptions)"communicatorjni:V " + ///< All communicatorjni logging
|
||||
"libcommunicator:V " + ///< All libcommunicator logging
|
||||
"DEBUG:V " + ///< All DEBUG logging - which includes native land crashes (seg faults, etc)
|
||||
"*:S" ///< Everything else silent, so don't pick it..
|
||||
};
|
||||
|
||||
private static final String[] LOGCAT_CMD_DEBUG = new String[]{
|
||||
"logcat",
|
||||
"-d",
|
||||
"-v",
|
||||
"threadtime",
|
||||
"*:*"
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the logs
|
||||
*
|
||||
* @param streamWriter the stream writer
|
||||
* @param isErrorLogCat true to save the error logs
|
||||
*/
|
||||
private static void getLogCatError(OutputStreamWriter streamWriter, boolean isErrorLogCat) {
|
||||
Process logcatProc;
|
||||
|
||||
try {
|
||||
logcatProc = Runtime.getRuntime().exec(isErrorLogCat ? LOGCAT_CMD_ERROR : LOGCAT_CMD_DEBUG);
|
||||
} catch (IOException e1) {
|
||||
return;
|
||||
}
|
||||
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
String separator = System.getProperty("line.separator");
|
||||
reader = new BufferedReader(new InputStreamReader(logcatProc.getInputStream()), BUFFER_SIZE);
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
streamWriter.append(line);
|
||||
streamWriter.append(separator);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "getLog fails with " + e.getLocalizedMessage());
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "getLog fails with " + e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// File compression management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* GZip a file
|
||||
*
|
||||
* @param fin the input file
|
||||
* @return the gzipped file
|
||||
*/
|
||||
private static File compressFile(File fin) {
|
||||
Timber.d("## compressFile() : compress " + fin.getName());
|
||||
|
||||
File dstFile = new File(fin.getParent(), fin.getName() + ".gz");
|
||||
|
||||
if (dstFile.exists()) {
|
||||
dstFile.delete();
|
||||
}
|
||||
|
||||
FileOutputStream fos = null;
|
||||
GZIPOutputStream gos = null;
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
fos = new FileOutputStream(dstFile);
|
||||
gos = new GZIPOutputStream(fos);
|
||||
|
||||
inputStream = new FileInputStream(fin);
|
||||
int n;
|
||||
|
||||
byte[] buffer = new byte[2048];
|
||||
while ((n = inputStream.read(buffer)) != -1) {
|
||||
gos.write(buffer, 0, n);
|
||||
}
|
||||
|
||||
gos.close();
|
||||
inputStream.close();
|
||||
|
||||
Timber.d("## compressFile() : " + fin.length() + " compressed to " + dstFile.length() + " bytes");
|
||||
return dstFile;
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "## compressFile() failed " + e.getMessage());
|
||||
} catch (OutOfMemoryError oom) {
|
||||
Timber.e(oom, "## compressFile() failed " + oom.getMessage());
|
||||
} finally {
|
||||
try {
|
||||
if (null != fos) {
|
||||
fos.close();
|
||||
}
|
||||
if (null != gos) {
|
||||
gos.close();
|
||||
}
|
||||
if (null != inputStream) {
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "## compressFile() failed to close inputStream " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
697
app/src/main/java/im/vector/riotredesign/features/rageshake/BugReporter.kt
Executable file
697
app/src/main/java/im/vector/riotredesign/features/rageshake/BugReporter.kt
Executable file
@ -0,0 +1,697 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.rageshake
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import im.vector.riotredesign.BuildConfig
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.extensions.toOnOff
|
||||
import im.vector.riotredesign.core.utils.getDeviceLocale
|
||||
import im.vector.riotredesign.features.settings.VectorLocale
|
||||
import im.vector.riotredesign.features.themes.ThemeUtils
|
||||
import okhttp3.*
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
import java.io.*
|
||||
import java.net.HttpURLConnection
|
||||
import java.util.*
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
/**
|
||||
* BugReporter creates and sends the bug reports.
|
||||
*/
|
||||
object BugReporter {
|
||||
var inMultiWindowMode = false
|
||||
|
||||
// filenames
|
||||
private const val LOG_CAT_ERROR_FILENAME = "logcatError.log"
|
||||
private const val LOG_CAT_FILENAME = "logcat.log"
|
||||
private const val LOG_CAT_SCREENSHOT_FILENAME = "screenshot.png"
|
||||
private const val CRASH_FILENAME = "crash.log"
|
||||
|
||||
|
||||
// the http client
|
||||
private val mOkHttpClient = OkHttpClient()
|
||||
|
||||
// the pending bug report call
|
||||
private var mBugReportCall: Call? = null
|
||||
|
||||
|
||||
// boolean to cancel the bug report
|
||||
private val mIsCancelled = false
|
||||
|
||||
/**
|
||||
* Get current Screenshot
|
||||
*
|
||||
* @return screenshot or null if not available
|
||||
*/
|
||||
var screenshot: Bitmap? = null
|
||||
private set
|
||||
|
||||
private const val BUFFER_SIZE = 1024 * 1024 * 50
|
||||
|
||||
private val LOGCAT_CMD_ERROR = arrayOf("logcat", ///< Run 'logcat' command
|
||||
"-d", ///< Dump the log rather than continue outputting it
|
||||
"-v", // formatting
|
||||
"threadtime", // include timestamps
|
||||
"AndroidRuntime:E " + ///< Pick all AndroidRuntime errors (such as uncaught exceptions)"communicatorjni:V " + ///< All communicatorjni logging
|
||||
|
||||
"libcommunicator:V " + ///< All libcommunicator logging
|
||||
|
||||
"DEBUG:V " + ///< All DEBUG logging - which includes native land crashes (seg faults, etc)
|
||||
|
||||
"*:S" ///< Everything else silent, so don't pick it..
|
||||
)
|
||||
|
||||
private val LOGCAT_CMD_DEBUG = arrayOf("logcat", "-d", "-v", "threadtime", "*:*")
|
||||
|
||||
/**
|
||||
* Bug report upload listener
|
||||
*/
|
||||
interface IMXBugReportListener {
|
||||
/**
|
||||
* The bug report has been cancelled
|
||||
*/
|
||||
fun onUploadCancelled()
|
||||
|
||||
/**
|
||||
* The bug report upload failed.
|
||||
*
|
||||
* @param reason the failure reason
|
||||
*/
|
||||
fun onUploadFailed(reason: String?)
|
||||
|
||||
/**
|
||||
* The upload progress (in percent)
|
||||
*
|
||||
* @param progress the upload progress
|
||||
*/
|
||||
fun onProgress(progress: Int)
|
||||
|
||||
/**
|
||||
* The bug report upload succeeded.
|
||||
*/
|
||||
fun onUploadSucceed()
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a bug report.
|
||||
*
|
||||
* @param context the application context
|
||||
* @param withDevicesLogs true to include the device log
|
||||
* @param withCrashLogs true to include the crash logs
|
||||
* @param withScreenshot true to include the screenshot
|
||||
* @param theBugDescription the bug description
|
||||
* @param listener the listener
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
fun sendBugReport(context: Context,
|
||||
withDevicesLogs: Boolean,
|
||||
withCrashLogs: Boolean,
|
||||
withScreenshot: Boolean,
|
||||
theBugDescription: String,
|
||||
listener: IMXBugReportListener?) {
|
||||
object : AsyncTask<Void, Int, String>() {
|
||||
|
||||
// enumerate files to delete
|
||||
val mBugReportFiles: MutableList<File> = ArrayList()
|
||||
|
||||
override fun doInBackground(vararg voids: Void): String? {
|
||||
var bugDescription = theBugDescription
|
||||
var serverError: String? = null
|
||||
val crashCallStack = getCrashDescription(context)
|
||||
|
||||
if (null != crashCallStack) {
|
||||
bugDescription += "\n\n\n\n--------------------------------- crash call stack ---------------------------------\n"
|
||||
bugDescription += crashCallStack
|
||||
}
|
||||
|
||||
val gzippedFiles = ArrayList<File>()
|
||||
|
||||
if (withDevicesLogs) {
|
||||
val files = VectorFileLogger.addLogFiles(ArrayList<File>())
|
||||
|
||||
for (f in files) {
|
||||
if (!mIsCancelled) {
|
||||
val gzippedFile = compressFile(f)
|
||||
|
||||
if (null != gzippedFile) {
|
||||
gzippedFiles.add(gzippedFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Delete the sent files?
|
||||
}
|
||||
|
||||
if (!mIsCancelled && (withCrashLogs || withDevicesLogs)) {
|
||||
val gzippedLogcat = saveLogCat(context, false)
|
||||
|
||||
if (null != gzippedLogcat) {
|
||||
if (gzippedFiles.size == 0) {
|
||||
gzippedFiles.add(gzippedLogcat)
|
||||
} else {
|
||||
gzippedFiles.add(0, gzippedLogcat)
|
||||
}
|
||||
}
|
||||
|
||||
val crashDescription = getCrashFile(context)
|
||||
if (crashDescription.exists()) {
|
||||
val compressedCrashDescription = compressFile(crashDescription)
|
||||
|
||||
if (null != compressedCrashDescription) {
|
||||
if (gzippedFiles.size == 0) {
|
||||
gzippedFiles.add(compressedCrashDescription)
|
||||
} else {
|
||||
gzippedFiles.add(0, compressedCrashDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO MXSession session = Matrix.getInstance(context).getDefaultSession();
|
||||
|
||||
val deviceId = "undefined"
|
||||
val userId = "undefined"
|
||||
val matrixSdkVersion = "undefined"
|
||||
val olmVersion = "undefined"
|
||||
|
||||
/*
|
||||
TODO
|
||||
if (null != session) {
|
||||
userId = session.getMyUserId();
|
||||
deviceId = session.getCredentials().deviceId;
|
||||
matrixSdkVersion = session.getVersion(true);
|
||||
olmVersion = session.getCryptoVersion(context, true);
|
||||
}
|
||||
*/
|
||||
|
||||
if (!mIsCancelled) {
|
||||
// build the multi part request
|
||||
val builder = BugReporterMultipartBody.Builder()
|
||||
.addFormDataPart("text", "[RiotX] $bugDescription")
|
||||
.addFormDataPart("app", "riot-android")
|
||||
// TODO .addFormDataPart("user_agent", RestClient.getUserAgent())
|
||||
.addFormDataPart("user_id", userId)
|
||||
.addFormDataPart("device_id", deviceId)
|
||||
// TODO .addFormDataPart("version", Matrix.getInstance(context).getVersion(true, false))
|
||||
.addFormDataPart("branch_name", context.getString(R.string.git_branch_name))
|
||||
.addFormDataPart("matrix_sdk_version", matrixSdkVersion)
|
||||
.addFormDataPart("olm_version", olmVersion)
|
||||
.addFormDataPart("device", Build.MODEL.trim { it <= ' ' })
|
||||
.addFormDataPart("lazy_loading", true.toOnOff())
|
||||
.addFormDataPart("multi_window", inMultiWindowMode.toOnOff())
|
||||
.addFormDataPart("os", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ") "
|
||||
+ Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME)
|
||||
.addFormDataPart("locale", Locale.getDefault().toString())
|
||||
.addFormDataPart("app_language", VectorLocale.applicationLocale.toString())
|
||||
.addFormDataPart("default_app_language", getDeviceLocale(context).toString())
|
||||
.addFormDataPart("theme", ThemeUtils.getApplicationTheme(context))
|
||||
|
||||
val buildNumber = context.getString(R.string.build_number)
|
||||
if (!TextUtils.isEmpty(buildNumber) && buildNumber != "0") {
|
||||
builder.addFormDataPart("build_number", buildNumber)
|
||||
}
|
||||
|
||||
// add the gzipped files
|
||||
for (file in gzippedFiles) {
|
||||
builder.addFormDataPart("compressed-log", file.name, RequestBody.create(MediaType.parse("application/octet-stream"), file))
|
||||
}
|
||||
|
||||
mBugReportFiles.addAll(gzippedFiles)
|
||||
|
||||
if (withScreenshot) {
|
||||
val bitmap = screenshot
|
||||
|
||||
if (null != bitmap) {
|
||||
val logCatScreenshotFile = File(context.cacheDir.absolutePath, LOG_CAT_SCREENSHOT_FILENAME)
|
||||
|
||||
if (logCatScreenshotFile.exists()) {
|
||||
logCatScreenshotFile.delete()
|
||||
}
|
||||
|
||||
try {
|
||||
val fos = FileOutputStream(logCatScreenshotFile)
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
|
||||
fos.flush()
|
||||
fos.close()
|
||||
|
||||
builder.addFormDataPart("file",
|
||||
logCatScreenshotFile.name, RequestBody.create(MediaType.parse("application/octet-stream"), logCatScreenshotFile))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## sendBugReport() : fail to write screenshot$e")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
screenshot = null
|
||||
|
||||
// add some github labels
|
||||
builder.addFormDataPart("label", BuildConfig.VERSION_NAME)
|
||||
builder.addFormDataPart("label", BuildConfig.FLAVOR_DESCRIPTION)
|
||||
builder.addFormDataPart("label", context.getString(R.string.git_branch_name))
|
||||
|
||||
// Special for RiotX
|
||||
builder.addFormDataPart("label", "[RiotX]")
|
||||
|
||||
if (getCrashFile(context).exists()) {
|
||||
builder.addFormDataPart("label", "crash")
|
||||
deleteCrashFile(context)
|
||||
}
|
||||
|
||||
val requestBody = builder.build()
|
||||
|
||||
// add a progress listener
|
||||
requestBody.setWriteListener { totalWritten, contentLength ->
|
||||
val percentage: Int
|
||||
|
||||
if (-1L != contentLength) {
|
||||
if (totalWritten > contentLength) {
|
||||
percentage = 100
|
||||
} else {
|
||||
percentage = (totalWritten * 100 / contentLength).toInt()
|
||||
}
|
||||
} else {
|
||||
percentage = 0
|
||||
}
|
||||
|
||||
if (mIsCancelled && null != mBugReportCall) {
|
||||
mBugReportCall!!.cancel()
|
||||
}
|
||||
|
||||
Timber.d("## onWrite() : $percentage%")
|
||||
publishProgress(percentage)
|
||||
}
|
||||
|
||||
// build the request
|
||||
val request = Request.Builder()
|
||||
.url(context.getString(R.string.bug_report_url))
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
var responseCode = HttpURLConnection.HTTP_INTERNAL_ERROR
|
||||
var response: Response? = null
|
||||
var errorMessage: String? = null
|
||||
|
||||
// trigger the request
|
||||
try {
|
||||
mBugReportCall = mOkHttpClient.newCall(request)
|
||||
response = mBugReportCall!!.execute()
|
||||
responseCode = response!!.code()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "response " + e.message)
|
||||
errorMessage = e.localizedMessage
|
||||
}
|
||||
|
||||
// if the upload failed, try to retrieve the reason
|
||||
if (responseCode != HttpURLConnection.HTTP_OK) {
|
||||
if (null != errorMessage) {
|
||||
serverError = "Failed with error $errorMessage"
|
||||
} else if (null == response || null == response.body()) {
|
||||
serverError = "Failed with error $responseCode"
|
||||
} else {
|
||||
var `is`: InputStream? = null
|
||||
|
||||
try {
|
||||
`is` = response.body()!!.byteStream()
|
||||
|
||||
if (null != `is`) {
|
||||
var ch = `is`.read()
|
||||
val b = StringBuilder()
|
||||
while (ch != -1) {
|
||||
b.append(ch.toChar())
|
||||
ch = `is`.read()
|
||||
}
|
||||
serverError = b.toString()
|
||||
`is`.close()
|
||||
|
||||
// check if the error message
|
||||
try {
|
||||
val responseJSON = JSONObject(serverError)
|
||||
serverError = responseJSON.getString("error")
|
||||
} catch (e: JSONException) {
|
||||
Timber.e(e, "doInBackground ; Json conversion failed " + e.message)
|
||||
}
|
||||
|
||||
// should never happen
|
||||
if (null == serverError) {
|
||||
serverError = "Failed with error $responseCode"
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## sendBugReport() : failed to parse error " + e.message)
|
||||
} finally {
|
||||
try {
|
||||
`is`?.close()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## sendBugReport() : failed to close the error stream " + e.message)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return serverError
|
||||
}
|
||||
|
||||
|
||||
override fun onProgressUpdate(vararg progress: Int?) {
|
||||
if (null != listener) {
|
||||
try {
|
||||
listener.onProgress(progress?.get(0) ?: 0)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onProgress() : failed " + e.message)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostExecute(reason: String?) {
|
||||
mBugReportCall = null
|
||||
|
||||
// delete when the bug report has been successfully sent
|
||||
for (file in mBugReportFiles) {
|
||||
file.delete()
|
||||
}
|
||||
|
||||
if (null != listener) {
|
||||
try {
|
||||
if (mIsCancelled) {
|
||||
listener.onUploadCancelled()
|
||||
} else if (null == reason) {
|
||||
listener.onUploadSucceed()
|
||||
} else {
|
||||
listener.onUploadFailed(reason)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onPostExecute() : failed " + e.message)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a bug report either with email or with Vector.
|
||||
*/
|
||||
fun openBugReportScreen(activity: Activity) {
|
||||
screenshot = takeScreenshot(activity)
|
||||
|
||||
val intent = Intent(activity, BugReportActivity::class.java)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// crash report management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Provides the crash file
|
||||
*
|
||||
* @param context the context
|
||||
* @return the crash file
|
||||
*/
|
||||
private fun getCrashFile(context: Context): File {
|
||||
return File(context.cacheDir.absolutePath, CRASH_FILENAME)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the crash file
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
fun deleteCrashFile(context: Context) {
|
||||
val crashFile = getCrashFile(context)
|
||||
|
||||
if (crashFile.exists()) {
|
||||
crashFile.delete()
|
||||
}
|
||||
|
||||
// Also reset the screenshot
|
||||
screenshot = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the crash report
|
||||
*
|
||||
* @param context the context
|
||||
* @param crashDescription teh crash description
|
||||
*/
|
||||
fun saveCrashReport(context: Context, crashDescription: String) {
|
||||
val crashFile = getCrashFile(context)
|
||||
|
||||
if (crashFile.exists()) {
|
||||
crashFile.delete()
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(crashDescription)) {
|
||||
try {
|
||||
val fos = FileOutputStream(crashFile)
|
||||
val osw = OutputStreamWriter(fos)
|
||||
osw.write(crashDescription)
|
||||
osw.close()
|
||||
|
||||
fos.flush()
|
||||
fos.close()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## saveCrashReport() : fail to write $e")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the crash description file and return its content.
|
||||
*
|
||||
* @param context teh context
|
||||
* @return the crash description
|
||||
*/
|
||||
private fun getCrashDescription(context: Context): String? {
|
||||
var crashDescription: String? = null
|
||||
val crashFile = getCrashFile(context)
|
||||
|
||||
if (crashFile.exists()) {
|
||||
try {
|
||||
val fis = FileInputStream(crashFile)
|
||||
val isr = InputStreamReader(fis)
|
||||
|
||||
val buffer = CharArray(fis.available())
|
||||
val len = isr.read(buffer, 0, fis.available())
|
||||
crashDescription = String(buffer, 0, len)
|
||||
isr.close()
|
||||
fis.close()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## getCrashDescription() : fail to read $e")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return crashDescription
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Screenshot management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Take a screenshot of the display.
|
||||
*
|
||||
* @return the screenshot
|
||||
*/
|
||||
private fun takeScreenshot(activity: Activity): Bitmap? {
|
||||
// get content view
|
||||
val contentView = activity.findViewById<View>(android.R.id.content)
|
||||
if (contentView == null) {
|
||||
Timber.e("Cannot find content view on $activity. Cannot take screenshot.")
|
||||
return null
|
||||
}
|
||||
|
||||
// get the root view to snapshot
|
||||
val rootView = contentView.rootView
|
||||
if (rootView == null) {
|
||||
Timber.e("Cannot find root view on $activity. Cannot take screenshot.")
|
||||
return null
|
||||
}
|
||||
// refresh it
|
||||
rootView.isDrawingCacheEnabled = false
|
||||
rootView.isDrawingCacheEnabled = true
|
||||
|
||||
try {
|
||||
var bitmap = rootView.drawingCache
|
||||
|
||||
// Make a copy, because if Activity is destroyed, the bitmap will be recycled
|
||||
bitmap = Bitmap.createBitmap(bitmap)
|
||||
|
||||
return bitmap
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
Timber.e(oom, "Cannot get drawing cache for $activity OOM.")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Cannot get snapshot of screen: $e")
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Logcat management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Save the logcat
|
||||
*
|
||||
* @param context the context
|
||||
* @param isErrorLogcat true to save the error logcat
|
||||
* @return the file if the operation succeeds
|
||||
*/
|
||||
private fun saveLogCat(context: Context, isErrorLogcat: Boolean): File? {
|
||||
val logCatErrFile = File(context.cacheDir.absolutePath, if (isErrorLogcat) LOG_CAT_ERROR_FILENAME else LOG_CAT_FILENAME)
|
||||
|
||||
if (logCatErrFile.exists()) {
|
||||
logCatErrFile.delete()
|
||||
}
|
||||
|
||||
try {
|
||||
val fos = FileOutputStream(logCatErrFile)
|
||||
val osw = OutputStreamWriter(fos)
|
||||
getLogCatError(osw, isErrorLogcat)
|
||||
osw.close()
|
||||
|
||||
fos.flush()
|
||||
fos.close()
|
||||
|
||||
return compressFile(logCatErrFile)
|
||||
} catch (error: OutOfMemoryError) {
|
||||
Timber.e(error, "## saveLogCat() : fail to write logcat$error")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## saveLogCat() : fail to write logcat$e")
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the logs
|
||||
*
|
||||
* @param streamWriter the stream writer
|
||||
* @param isErrorLogCat true to save the error logs
|
||||
*/
|
||||
private fun getLogCatError(streamWriter: OutputStreamWriter, isErrorLogCat: Boolean) {
|
||||
val logcatProc: Process
|
||||
|
||||
try {
|
||||
logcatProc = Runtime.getRuntime().exec(if (isErrorLogCat) LOGCAT_CMD_ERROR else LOGCAT_CMD_DEBUG)
|
||||
} catch (e1: IOException) {
|
||||
return
|
||||
}
|
||||
|
||||
var reader: BufferedReader? = null
|
||||
try {
|
||||
val separator = System.getProperty("line.separator")
|
||||
reader = BufferedReader(InputStreamReader(logcatProc.inputStream), BUFFER_SIZE)
|
||||
var line = reader.readLine()
|
||||
while (line != null) {
|
||||
streamWriter.append(line)
|
||||
streamWriter.append(separator)
|
||||
line = reader.readLine()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "getLog fails with " + e.localizedMessage)
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "getLog fails with " + e.localizedMessage)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// File compression management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* GZip a file
|
||||
*
|
||||
* @param fin the input file
|
||||
* @return the gzipped file
|
||||
*/
|
||||
private fun compressFile(fin: File): File? {
|
||||
Timber.d("## compressFile() : compress " + fin.name)
|
||||
|
||||
val dstFile = File(fin.parent, fin.name + ".gz")
|
||||
|
||||
if (dstFile.exists()) {
|
||||
dstFile.delete()
|
||||
}
|
||||
|
||||
var fos: FileOutputStream? = null
|
||||
var gos: GZIPOutputStream? = null
|
||||
var inputStream: InputStream? = null
|
||||
try {
|
||||
fos = FileOutputStream(dstFile)
|
||||
gos = GZIPOutputStream(fos)
|
||||
|
||||
inputStream = FileInputStream(fin)
|
||||
|
||||
val buffer = ByteArray(2048)
|
||||
var n = inputStream.read(buffer)
|
||||
while (n != -1) {
|
||||
gos.write(buffer, 0, n)
|
||||
n = inputStream.read(buffer)
|
||||
}
|
||||
|
||||
gos.close()
|
||||
inputStream.close()
|
||||
|
||||
Timber.d("## compressFile() : " + fin.length() + " compressed to " + dstFile.length() + " bytes")
|
||||
return dstFile
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## compressFile() failed " + e.message)
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
Timber.e(oom, "## compressFile() failed " + oom.message)
|
||||
} finally {
|
||||
try {
|
||||
fos?.close()
|
||||
gos?.close()
|
||||
inputStream?.close()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## compressFile() failed to close inputStream " + e.message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
@ -94,7 +94,7 @@ class RageShake(val activity: Activity) : ShakeDetector.Listener {
|
||||
}
|
||||
|
||||
private fun openBugReportScreen() {
|
||||
BugReporter.sendBugReport(activity)
|
||||
BugReporter.openBugReportScreen(activity)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.rageshake
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.logging.*
|
||||
import java.util.logging.Formatter
|
||||
|
||||
object VectorFileLogger : Timber.DebugTree() {
|
||||
|
||||
private val LOG_SIZE_BYTES = 50 * 1024 * 1024 // 50MB
|
||||
|
||||
// relatively large rotation count because closing > opening the app rotates the log (!)
|
||||
private val LOG_ROTATION_COUNT = 15
|
||||
|
||||
private val sLogger = Logger.getLogger("im.vector.riotredesign")
|
||||
private lateinit var sFileHandler: FileHandler
|
||||
private lateinit var sCacheDirectory: File
|
||||
private var sFileName = "riotx"
|
||||
|
||||
fun init(context: Context) {
|
||||
val logsDirectoryFile = context.cacheDir.absolutePath + "/logs"
|
||||
|
||||
setLogDirectory(File(logsDirectoryFile))
|
||||
init("RiotXLog")
|
||||
}
|
||||
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
if (t != null) {
|
||||
logToFile(t)
|
||||
}
|
||||
|
||||
logToFile("$priority ", tag ?: "Tag", message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the directory to put log files.
|
||||
*
|
||||
* @param cacheDir The directory, usually [android.content.ContextWrapper.getCacheDir]
|
||||
*/
|
||||
private fun setLogDirectory(cacheDir: File) {
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkdirs()
|
||||
}
|
||||
sCacheDirectory = cacheDir
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises the logger. Should be called AFTER [Log.setLogDirectory].
|
||||
*
|
||||
* @param fileName the base file name
|
||||
*/
|
||||
private fun init(fileName: String) {
|
||||
try {
|
||||
if (!TextUtils.isEmpty(fileName)) {
|
||||
sFileName = fileName
|
||||
}
|
||||
sFileHandler = FileHandler(sCacheDirectory.absolutePath + "/" + sFileName + ".%g.txt", LOG_SIZE_BYTES, LOG_ROTATION_COUNT)
|
||||
sFileHandler.formatter = LogFormatter()
|
||||
sLogger.useParentHandlers = false
|
||||
sLogger.level = Level.ALL
|
||||
sLogger.addHandler(sFileHandler)
|
||||
} catch (e: IOException) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds our own log files to the provided list of files.
|
||||
*
|
||||
* @param files The list of files to add to.
|
||||
* @return The same list with more files added.
|
||||
*/
|
||||
fun addLogFiles(files: MutableList<File>): List<File> {
|
||||
try {
|
||||
// reported by GA
|
||||
if (null != sFileHandler) {
|
||||
sFileHandler.flush()
|
||||
val absPath = sCacheDirectory.absolutePath
|
||||
|
||||
for (i in 0..LOG_ROTATION_COUNT) {
|
||||
val filepath = "$absPath/$sFileName.$i.txt"
|
||||
val file = File(filepath)
|
||||
if (file.exists()) {
|
||||
files.add(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## addLogFiles() failed : " + e.message)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
class LogFormatter : Formatter() {
|
||||
private val LINE_SEPARATOR = System.getProperty("line.separator") ?: "\n"
|
||||
|
||||
override fun format(r: LogRecord): String {
|
||||
if (!mIsTimeZoneSet) {
|
||||
DATE_FORMAT.timeZone = TimeZone.getTimeZone("UTC")
|
||||
mIsTimeZoneSet = true
|
||||
}
|
||||
|
||||
val thrown = r.thrown
|
||||
if (thrown != null) {
|
||||
val sw = StringWriter()
|
||||
val pw = PrintWriter(sw)
|
||||
sw.write(r.message)
|
||||
sw.write(LINE_SEPARATOR)
|
||||
thrown.printStackTrace(pw)
|
||||
pw.flush()
|
||||
return sw.toString()
|
||||
} else {
|
||||
val b = StringBuilder()
|
||||
val date = DATE_FORMAT.format(Date(r.millis))
|
||||
b.append(date)
|
||||
b.append("Z ")
|
||||
b.append(r.message)
|
||||
b.append(LINE_SEPARATOR)
|
||||
return b.toString()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
|
||||
private var mIsTimeZoneSet = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an Throwable
|
||||
*
|
||||
* @param throwable the throwable to log
|
||||
*/
|
||||
private fun logToFile(throwable: Throwable?) {
|
||||
if (null == sCacheDirectory || throwable == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val errors = StringWriter()
|
||||
throwable.printStackTrace(PrintWriter(errors))
|
||||
|
||||
sLogger.info(errors.toString())
|
||||
}
|
||||
|
||||
private fun logToFile(level: String, tag: String, content: String) {
|
||||
if (null == sCacheDirectory) {
|
||||
return
|
||||
}
|
||||
|
||||
val b = StringBuilder()
|
||||
b.append(Thread.currentThread().id)
|
||||
b.append(" ")
|
||||
b.append(level)
|
||||
b.append("/")
|
||||
b.append(tag)
|
||||
b.append(": ")
|
||||
b.append(content)
|
||||
sLogger.info(b.toString())
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.rageshake
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import im.vector.riotredesign.BuildConfig
|
||||
import timber.log.Timber
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object VectorUncaughtExceptionHandler : Thread.UncaughtExceptionHandler {
|
||||
|
||||
// key to save the crash status
|
||||
private const val PREFS_CRASH_KEY = "PREFS_CRASH_KEY"
|
||||
|
||||
private var vectorVersion: String = ""
|
||||
private var matrixSdkVersion: String = ""
|
||||
|
||||
private var previousHandler: Thread.UncaughtExceptionHandler? = null
|
||||
|
||||
private lateinit var context: Context
|
||||
|
||||
/**
|
||||
* Activate this handler
|
||||
*/
|
||||
fun activate(context: Context) {
|
||||
this.context = context
|
||||
|
||||
previousHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* An uncaught exception has been triggered
|
||||
*
|
||||
* @param thread the thread
|
||||
* @param throwable the throwable
|
||||
* @return the exception description
|
||||
*/
|
||||
override fun uncaughtException(thread: Thread, throwable: Throwable) {
|
||||
if (context == null) {
|
||||
previousHandler?.uncaughtException(thread, throwable)
|
||||
return
|
||||
}
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
putBoolean(PREFS_CRASH_KEY, true)
|
||||
}
|
||||
|
||||
val b = StringBuilder()
|
||||
val appName = "RiotX" // TODO Matrix.getApplicationName()
|
||||
|
||||
b.append(appName + " Build : " + BuildConfig.VERSION_CODE + "\n")
|
||||
b.append("$appName Version : $vectorVersion\n")
|
||||
b.append("SDK Version : $matrixSdkVersion\n")
|
||||
b.append("Phone : " + Build.MODEL.trim() + " (" + Build.VERSION.INCREMENTAL + " " + Build.VERSION.RELEASE + " " + Build.VERSION.CODENAME + ")\n")
|
||||
|
||||
b.append("Memory statuses \n")
|
||||
|
||||
var freeSize = 0L
|
||||
var totalSize = 0L
|
||||
var usedSize = -1L
|
||||
try {
|
||||
val info = Runtime.getRuntime()
|
||||
freeSize = info.freeMemory()
|
||||
totalSize = info.totalMemory()
|
||||
usedSize = totalSize - freeSize
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
b.append("usedSize " + usedSize / 1048576L + " MB\n")
|
||||
b.append("freeSize " + freeSize / 1048576L + " MB\n")
|
||||
b.append("totalSize " + totalSize / 1048576L + " MB\n")
|
||||
|
||||
b.append("Thread: ")
|
||||
b.append(thread.name)
|
||||
|
||||
/*
|
||||
val a = VectorApp.getCurrentActivity()
|
||||
if (a != null) {
|
||||
b.append(", Activity:")
|
||||
b.append(a.localClassName)
|
||||
}
|
||||
*/
|
||||
|
||||
b.append(", Exception: ")
|
||||
|
||||
val sw = StringWriter()
|
||||
val pw = PrintWriter(sw, true)
|
||||
throwable.printStackTrace(pw)
|
||||
b.append(sw.buffer.toString())
|
||||
Timber.e("FATAL EXCEPTION " + b.toString())
|
||||
|
||||
val bugDescription = b.toString()
|
||||
|
||||
BugReporter.saveCrashReport(context, bugDescription)
|
||||
|
||||
// Show the classical system popup
|
||||
previousHandler?.uncaughtException(thread, throwable)
|
||||
}
|
||||
|
||||
// TODO Call me
|
||||
fun setVersions(vectorVersion: String, matrixSdkVersion: String) {
|
||||
this.vectorVersion = vectorVersion
|
||||
this.matrixSdkVersion = matrixSdkVersion
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the application crashed
|
||||
*
|
||||
* @return true if the application crashed
|
||||
*/
|
||||
fun didAppCrash(context: Context): Boolean {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(PREFS_CRASH_KEY, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the crash status
|
||||
*/
|
||||
fun clearAppCrashStatus(context: Context) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
remove(PREFS_CRASH_KEY)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.text.TextUtils
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
/**
|
||||
* Object to manage the Font Scale choice of the user
|
||||
*/
|
||||
object FontScale {
|
||||
// Key for the SharedPrefs
|
||||
private const val APPLICATION_FONT_SCALE_KEY = "APPLICATION_FONT_SCALE_KEY"
|
||||
|
||||
// Possible values for the SharedPrefs
|
||||
private const val FONT_SCALE_TINY = "FONT_SCALE_TINY"
|
||||
private const val FONT_SCALE_SMALL = "FONT_SCALE_SMALL"
|
||||
private const val FONT_SCALE_NORMAL = "FONT_SCALE_NORMAL"
|
||||
private const val FONT_SCALE_LARGE = "FONT_SCALE_LARGE"
|
||||
private const val FONT_SCALE_LARGER = "FONT_SCALE_LARGER"
|
||||
private const val FONT_SCALE_LARGEST = "FONT_SCALE_LARGEST"
|
||||
private const val FONT_SCALE_HUGE = "FONT_SCALE_HUGE"
|
||||
|
||||
private val fontScaleToPrefValue = mapOf(
|
||||
0.70f to FONT_SCALE_TINY,
|
||||
0.85f to FONT_SCALE_SMALL,
|
||||
1.00f to FONT_SCALE_NORMAL,
|
||||
1.15f to FONT_SCALE_LARGE,
|
||||
1.30f to FONT_SCALE_LARGER,
|
||||
1.45f to FONT_SCALE_LARGEST,
|
||||
1.60f to FONT_SCALE_HUGE
|
||||
)
|
||||
|
||||
private val prefValueToNameResId = mapOf(
|
||||
FONT_SCALE_TINY to R.string.tiny,
|
||||
FONT_SCALE_SMALL to R.string.small,
|
||||
FONT_SCALE_NORMAL to R.string.normal,
|
||||
FONT_SCALE_LARGE to R.string.large,
|
||||
FONT_SCALE_LARGER to R.string.larger,
|
||||
FONT_SCALE_LARGEST to R.string.largest,
|
||||
FONT_SCALE_HUGE to R.string.huge
|
||||
)
|
||||
|
||||
/**
|
||||
* Get the font scale value from SharedPrefs. Init the SharedPrefs if necessary
|
||||
*
|
||||
* @return the font scale
|
||||
*/
|
||||
fun getFontScalePrefValue(context: Context): String {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
var scalePreferenceValue: String
|
||||
|
||||
if (!preferences.contains(APPLICATION_FONT_SCALE_KEY)) {
|
||||
val fontScale = context.resources.configuration.fontScale
|
||||
|
||||
scalePreferenceValue = FONT_SCALE_NORMAL
|
||||
|
||||
if (fontScaleToPrefValue.containsKey(fontScale)) {
|
||||
scalePreferenceValue = fontScaleToPrefValue[fontScale] as String
|
||||
}
|
||||
|
||||
preferences.edit {
|
||||
putString(APPLICATION_FONT_SCALE_KEY, scalePreferenceValue)
|
||||
}
|
||||
} else {
|
||||
scalePreferenceValue = preferences.getString(APPLICATION_FONT_SCALE_KEY, FONT_SCALE_NORMAL)
|
||||
}
|
||||
|
||||
return scalePreferenceValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the font scale value
|
||||
*
|
||||
* @return the font scale
|
||||
*/
|
||||
fun getFontScale(context: Context): Float {
|
||||
val fontScale = getFontScalePrefValue(context)
|
||||
|
||||
if (fontScaleToPrefValue.containsValue(fontScale)) {
|
||||
for (entry in fontScaleToPrefValue) {
|
||||
if (TextUtils.equals(entry.value, fontScale)) {
|
||||
return entry.key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 1.0f
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the font scale description
|
||||
*
|
||||
* @return the font description
|
||||
*/
|
||||
fun getFontScaleDescription(context: Context): String {
|
||||
val fontScale = getFontScalePrefValue(context)
|
||||
|
||||
return if (prefValueToNameResId.containsKey(fontScale)) {
|
||||
context.getString(prefValueToNameResId[fontScale] as Int)
|
||||
} else context.getString(R.string.normal)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the font size from the locale description.
|
||||
*
|
||||
* @param fontScaleDescription the font scale description
|
||||
*/
|
||||
fun updateFontScale(context: Context, fontScaleDescription: String) {
|
||||
for (entry in prefValueToNameResId) {
|
||||
if (TextUtils.equals(context.getString(entry.value), fontScaleDescription)) {
|
||||
saveFontScale(context, entry.key)
|
||||
}
|
||||
}
|
||||
|
||||
val config = Configuration(context.resources.configuration)
|
||||
config.fontScale = getFontScale(context)
|
||||
context.resources.updateConfiguration(config, context.resources.displayMetrics)
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the new font scale
|
||||
*
|
||||
* @param scaleValue the text scale
|
||||
*/
|
||||
fun saveFontScale(context: Context, scaleValue: String) {
|
||||
if (!TextUtils.isEmpty(scaleValue)) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit {
|
||||
putString(APPLICATION_FONT_SCALE_KEY, scaleValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.preference.PreferenceManager
|
||||
import android.text.TextUtils
|
||||
import android.util.Pair
|
||||
import androidx.core.content.edit
|
||||
import im.vector.riotredesign.R
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Object to manage the Locale choice of the user
|
||||
*/
|
||||
object VectorLocale {
|
||||
private const val APPLICATION_LOCALE_COUNTRY_KEY = "APPLICATION_LOCALE_COUNTRY_KEY"
|
||||
private const val APPLICATION_LOCALE_VARIANT_KEY = "APPLICATION_LOCALE_VARIANT_KEY"
|
||||
private const val APPLICATION_LOCALE_LANGUAGE_KEY = "APPLICATION_LOCALE_LANGUAGE_KEY"
|
||||
|
||||
private val defaultLocale = Locale("en", "US")
|
||||
|
||||
/**
|
||||
* The supported application languages
|
||||
*/
|
||||
var supportedLocales = ArrayList<Locale>()
|
||||
private set
|
||||
|
||||
/**
|
||||
* Provides the current application locale
|
||||
*/
|
||||
var applicationLocale = defaultLocale
|
||||
private set
|
||||
|
||||
/**
|
||||
* Init this object
|
||||
*/
|
||||
fun init(context: Context) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
if (preferences.contains(APPLICATION_LOCALE_LANGUAGE_KEY)) {
|
||||
applicationLocale = Locale(preferences.getString(APPLICATION_LOCALE_LANGUAGE_KEY, ""),
|
||||
preferences.getString(APPLICATION_LOCALE_COUNTRY_KEY, ""),
|
||||
preferences.getString(APPLICATION_LOCALE_VARIANT_KEY, "")
|
||||
)
|
||||
} else {
|
||||
applicationLocale = Locale.getDefault()
|
||||
|
||||
// detect if the default language is used
|
||||
val defaultStringValue = getString(context, defaultLocale, R.string.resources_country_code)
|
||||
if (TextUtils.equals(defaultStringValue, getString(context, applicationLocale, R.string.resources_country_code))) {
|
||||
applicationLocale = defaultLocale
|
||||
}
|
||||
|
||||
saveApplicationLocale(context, applicationLocale)
|
||||
}
|
||||
|
||||
// init the known locales in background, using kotlin coroutines
|
||||
GlobalScope.launch {
|
||||
initApplicationLocales(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the new application locale.
|
||||
*/
|
||||
fun saveApplicationLocale(context: Context, locale: Locale) {
|
||||
applicationLocale = locale
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
val language = locale.language
|
||||
if (TextUtils.isEmpty(language)) {
|
||||
remove(APPLICATION_LOCALE_LANGUAGE_KEY)
|
||||
} else {
|
||||
putString(APPLICATION_LOCALE_LANGUAGE_KEY, language)
|
||||
}
|
||||
|
||||
val country = locale.country
|
||||
if (TextUtils.isEmpty(country)) {
|
||||
remove(APPLICATION_LOCALE_COUNTRY_KEY)
|
||||
} else {
|
||||
putString(APPLICATION_LOCALE_COUNTRY_KEY, country)
|
||||
}
|
||||
|
||||
val variant = locale.variant
|
||||
if (TextUtils.isEmpty(variant)) {
|
||||
remove(APPLICATION_LOCALE_VARIANT_KEY)
|
||||
} else {
|
||||
putString(APPLICATION_LOCALE_VARIANT_KEY, variant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get String from a locale
|
||||
*
|
||||
* @param context the context
|
||||
* @param locale the locale
|
||||
* @param resourceId the string resource id
|
||||
* @return the localized string
|
||||
*/
|
||||
private fun getString(context: Context, locale: Locale, resourceId: Int): String {
|
||||
var result: String
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
val config = Configuration(context.resources.configuration)
|
||||
config.setLocale(locale)
|
||||
try {
|
||||
result = context.createConfigurationContext(config).getText(resourceId).toString()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## getString() failed : " + e.message)
|
||||
// use the default one
|
||||
result = context.getString(resourceId)
|
||||
}
|
||||
} else {
|
||||
val resources = context.resources
|
||||
val conf = resources.configuration
|
||||
val savedLocale = conf.locale
|
||||
conf.locale = locale
|
||||
resources.updateConfiguration(conf, null)
|
||||
|
||||
// retrieve resources from desired locale
|
||||
result = resources.getString(resourceId)
|
||||
|
||||
// restore original locale
|
||||
conf.locale = savedLocale
|
||||
resources.updateConfiguration(conf, null)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the supported application locales list
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
private fun initApplicationLocales(context: Context) {
|
||||
val knownLocalesSet = HashSet<Pair<String, String>>()
|
||||
|
||||
try {
|
||||
val availableLocales = Locale.getAvailableLocales()
|
||||
|
||||
for (locale in availableLocales) {
|
||||
knownLocalesSet.add(Pair(getString(context, locale, R.string.resources_language),
|
||||
getString(context, locale, R.string.resources_country_code)))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## getApplicationLocales() : failed " + e.message)
|
||||
knownLocalesSet.add(Pair(context.getString(R.string.resources_language), context.getString(R.string.resources_country_code)))
|
||||
}
|
||||
|
||||
supportedLocales.clear()
|
||||
|
||||
for (knownLocale in knownLocalesSet) {
|
||||
supportedLocales.add(Locale(knownLocale.first, knownLocale.second))
|
||||
}
|
||||
|
||||
// sort by human display names
|
||||
supportedLocales.sortWith(Comparator { lhs, rhs -> localeToLocalisedString(lhs).compareTo(localeToLocalisedString(rhs)) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a locale to a string
|
||||
*
|
||||
* @param locale the locale to convert
|
||||
* @return the string
|
||||
*/
|
||||
fun localeToLocalisedString(locale: Locale): String {
|
||||
var res = locale.getDisplayLanguage(locale)
|
||||
|
||||
if (!TextUtils.isEmpty(locale.getDisplayCountry(locale))) {
|
||||
res += " (" + locale.getDisplayCountry(locale) + ")"
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.ui.themes
|
||||
package im.vector.riotredesign.features.themes
|
||||
|
||||
|
||||
import android.app.Activity
|
||||
@ -29,6 +29,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.ui.themes.ActivityOtherThemes
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
@ -36,8 +37,6 @@ import java.util.*
|
||||
* Util class for managing themes.
|
||||
*/
|
||||
object ThemeUtils {
|
||||
const val LOG_TAG = "ThemeUtils"
|
||||
|
||||
// preference key
|
||||
const val APPLICATION_THEME_KEY = "APPLICATION_THEME_KEY"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user