diff --git a/app/build.gradle b/app/build.gradle
index 9db7a601e9..2f4238e1c5 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -183,6 +183,9 @@ dependencies {
// FP
implementation "io.arrow-kt:arrow-core:$arrow_version"
+ // Pref
+ implementation 'androidx.preference:preference:1.0.0'
+
// UI
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.android.material:material:1.1.0-alpha02'
@@ -190,6 +193,13 @@ dependencies {
implementation "ru.noties.markwon:core:$markwon_version"
implementation "ru.noties.markwon:html:$markwon_version"
+ // Butterknife
+ implementation 'com.jakewharton:butterknife:10.1.0'
+ kapt 'com.jakewharton:butterknife-compiler:10.1.0'
+
+ // Shake detection
+ implementation 'com.squareup:seismic:1.0.2'
+
// Image Loading
implementation "com.github.piasy:BigImageViewer:$big_image_viewer_version"
implementation "com.github.piasy:GlideImageLoader:$big_image_viewer_version"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5958432633..319575c102 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,7 +12,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
- android:theme="@style/Theme.Riot">
+ android:theme="@style/AppTheme.Light">
+
\ No newline at end of file
diff --git a/app/src/main/java/im/vector/riotredesign/core/extensions/BasicExtensions.kt b/app/src/main/java/im/vector/riotredesign/core/extensions/BasicExtensions.kt
new file mode 100644
index 0000000000..50c9b4d474
--- /dev/null
+++ b/app/src/main/java/im/vector/riotredesign/core/extensions/BasicExtensions.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.extensions
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+
+fun Boolean.toOnOff() = if (this) "ON" else "OFF"
+
+/**
+ * Apply argument to a Fragment
+ */
+fun T.withArgs(block: Bundle.() -> Unit) = apply { arguments = Bundle().apply(block) }
\ No newline at end of file
diff --git a/app/src/main/java/im/vector/riotredesign/core/platform/RiotActivity.kt b/app/src/main/java/im/vector/riotredesign/core/platform/RiotActivity.kt
index 2938252c0b..a4d202d808 100644
--- a/app/src/main/java/im/vector/riotredesign/core/platform/RiotActivity.kt
+++ b/app/src/main/java/im/vector/riotredesign/core/platform/RiotActivity.kt
@@ -17,15 +17,41 @@
package im.vector.riotredesign.core.platform
import android.os.Bundle
-import androidx.annotation.MainThread
+import android.view.Menu
+import android.view.MenuItem
+import androidx.annotation.*
+import androidx.appcompat.widget.Toolbar
+import butterknife.BindView
+import butterknife.ButterKnife
+import butterknife.Unbinder
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.RageShake
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
+
abstract class RiotActivity : BaseMvRxActivity() {
+ /* ==========================================================================================
+ * UI
+ * ========================================================================================== */
+
+ @Nullable
+ @BindView(R.id.toolbar)
+ protected lateinit var toolbar: Toolbar
+
+ /* ==========================================================================================
+ * DATA
+ * ========================================================================================== */
+
+ private var unBinder: Unbinder? = null
+
+ private var savedInstanceState: Bundle? = null
// For debug only
private var debugReceiver: DebugReceiver? = null
@@ -33,6 +59,8 @@ abstract class RiotActivity : BaseMvRxActivity() {
private val uiDisposables = CompositeDisposable()
private val restorables = ArrayList()
+ private var rageShake: RageShake? = null
+
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
restorables.forEach { it.onSaveInstanceState(outState) }
@@ -55,9 +83,41 @@ abstract class RiotActivity : BaseMvRxActivity() {
return this
}
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // Shake detector
+ rageShake = RageShake(this)
+
+ ThemeUtils.setActivityTheme(this, getOtherThemes())
+
+ doBeforeSetContentView()
+
+ if (getLayoutRes() != -1) {
+ setContentView(getLayoutRes())
+ }
+
+ unBinder = ButterKnife.bind(this)
+
+ this.savedInstanceState = savedInstanceState
+
+ initUiAndData()
+
+ val titleRes = getTitleRes()
+ if (titleRes != -1) {
+ supportActionBar?.let {
+ it.setTitle(titleRes)
+ } ?: run {
+ setTitle(titleRes)
+ }
+ }
+ }
+
override fun onResume() {
super.onResume()
+ rageShake?.start()
+
DebugReceiver
.getIntentFilter(this)
.takeIf { BuildConfig.DEBUG }
@@ -70,10 +130,97 @@ abstract class RiotActivity : BaseMvRxActivity() {
override fun onPause() {
super.onPause()
+ rageShake?.stop()
+
debugReceiver?.let {
unregisterReceiver(debugReceiver)
debugReceiver = null
}
}
+ /* ==========================================================================================
+ * MENU MANAGEMENT
+ * ========================================================================================== */
+
+ final override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ val menuRes = getMenuRes()
+
+ if (menuRes != -1) {
+ menuInflater.inflate(menuRes, menu)
+ ThemeUtils.tintMenuIcons(menu, ThemeUtils.getColor(this, getMenuTint()))
+ return true
+ }
+
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == android.R.id.home) {
+ setResult(RESULT_CANCELED)
+ finish()
+ return true
+ }
+
+ return super.onOptionsItemSelected(item)
+ }
+
+ /* ==========================================================================================
+ * PROTECTED METHODS
+ * ========================================================================================== */
+
+ /**
+ * Get the saved instance state.
+ * Ensure {@link isFirstCreation()} returns false before calling this
+ *
+ * @return
+ */
+ protected fun getSavedInstanceState(): Bundle {
+ return savedInstanceState!!
+ }
+
+ /**
+ * Is first creation
+ *
+ * @return true if Activity is created for the first time (and not restored by the system)
+ */
+ protected fun isFirstCreation() = savedInstanceState == null
+
+ /**
+ * Configure the Toolbar. It MUST be present in your layout with id "toolbar"
+ */
+ protected fun configureToolbar() {
+ setSupportActionBar(toolbar)
+
+ supportActionBar?.let {
+ it.setDisplayShowHomeEnabled(true)
+ it.setDisplayHomeAsUpEnabled(true)
+ }
+ }
+
+ /* ==========================================================================================
+ * OPEN METHODS
+ * ========================================================================================== */
+
+ @LayoutRes
+ open fun getLayoutRes() = -1
+
+ open fun displayInFullscreen() = false
+
+ open fun doBeforeSetContentView() = Unit
+
+ open fun initUiAndData() = Unit
+
+ @StringRes
+ open fun getTitleRes() = -1
+
+ @MenuRes
+ open fun getMenuRes() = -1
+
+ @AttrRes
+ open fun getMenuTint() = 0 // TODO R.attr.vctr_icon_tint_on_dark_action_bar_color
+
+ /**
+ * Return a object containing other themes for this activity
+ */
+ open fun getOtherThemes(): ActivityOtherThemes = ActivityOtherThemes.Default
}
\ No newline at end of file
diff --git a/app/src/main/java/im/vector/riotredesign/features/media/MediaViewerActivity.kt b/app/src/main/java/im/vector/riotredesign/features/media/MediaViewerActivity.kt
index 2b3ae6f697..403f242b96 100644
--- a/app/src/main/java/im/vector/riotredesign/features/media/MediaViewerActivity.kt
+++ b/app/src/main/java/im/vector/riotredesign/features/media/MediaViewerActivity.kt
@@ -21,7 +21,6 @@ package im.vector.riotredesign.features.media
import android.content.Context
import android.content.Intent
import android.os.Bundle
-import android.view.MenuItem
import androidx.appcompat.widget.Toolbar
import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator
import com.github.piasy.biv.view.GlideImageViewFactory
@@ -54,17 +53,6 @@ class MediaViewerActivity : RiotActivity() {
}
}
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- android.R.id.home -> {
- finish()
- return true
- }
- }
- return true
- }
-
-
companion object {
private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA"
diff --git a/app/src/main/java/im/vector/riotredesign/features/rageshake/BugReportActivity.kt b/app/src/main/java/im/vector/riotredesign/features/rageshake/BugReportActivity.kt
new file mode 100755
index 0000000000..8ee90669e6
--- /dev/null
+++ b/app/src/main/java/im/vector/riotredesign/features/rageshake/BugReportActivity.kt
@@ -0,0 +1,209 @@
+/*
+ * 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.text.TextUtils
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.widget.*
+import androidx.core.view.isVisible
+import butterknife.BindView
+import butterknife.OnCheckedChanged
+import butterknife.OnTextChanged
+import im.vector.riotredesign.R
+import im.vector.riotredesign.core.platform.RiotActivity
+import timber.log.Timber
+
+/**
+ * Form to send a bug report
+ */
+class BugReportActivity : RiotActivity() {
+
+ /* ==========================================================================================
+ * UI
+ * ========================================================================================== */
+
+ @BindView(R.id.bug_report_edit_text)
+ lateinit var mBugReportText: EditText
+
+ @BindView(R.id.bug_report_button_include_logs)
+ lateinit var mIncludeLogsButton: CheckBox
+
+ @BindView(R.id.bug_report_button_include_crash_logs)
+ lateinit var mIncludeCrashLogsButton: CheckBox
+
+ @BindView(R.id.bug_report_button_include_screenshot)
+ lateinit var mIncludeScreenShotButton: CheckBox
+
+ @BindView(R.id.bug_report_screenshot_preview)
+ lateinit var mScreenShotPreview: ImageView
+
+ @BindView(R.id.bug_report_progress_view)
+ lateinit var mProgressBar: ProgressBar
+
+ @BindView(R.id.bug_report_progress_text_view)
+ lateinit var mProgressTextView: TextView
+
+ @BindView(R.id.bug_report_scrollview)
+ lateinit var mScrollView: View
+
+ @BindView(R.id.bug_report_mask_view)
+ lateinit var mMaskView: View
+
+ override fun getLayoutRes() = R.layout.activity_bug_report
+
+ override fun initUiAndData() {
+ configureToolbar()
+
+ if (BugReporter.getScreenshot() != null) {
+ mScreenShotPreview.setImageBitmap(BugReporter.getScreenshot())
+ } else {
+ mScreenShotPreview.isVisible = false
+ mIncludeScreenShotButton.isChecked = false
+ mIncludeScreenShotButton.isEnabled = false
+ }
+ }
+
+ override fun getMenuRes() = R.menu.bug_report
+
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ menu.findItem(R.id.ic_action_send_bug_report)?.let {
+ val isValid = mBugReportText.text.toString().trim().length > 10
+ && !mMaskView.isVisible
+
+ it.isEnabled = isValid
+ it.icon.alpha = if (isValid) 255 else 100
+ }
+
+ return super.onPrepareOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.ic_action_send_bug_report -> {
+ sendBugReport()
+ return true
+ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+
+ /**
+ * Send the bug report
+ */
+ private fun sendBugReport() {
+ mScrollView.alpha = 0.3f
+ mMaskView.isVisible = true
+
+ invalidateOptionsMenu()
+
+ mProgressTextView.isVisible = true
+ mProgressTextView.text = getString(R.string.send_bug_report_progress, 0.toString() + "")
+
+ mProgressBar.isVisible = true
+ mProgressBar.progress = 0
+
+ BugReporter.sendBugReport(this,
+ mIncludeLogsButton.isChecked,
+ mIncludeCrashLogsButton.isChecked,
+ mIncludeScreenShotButton.isChecked,
+ mBugReportText.text.toString(),
+ object : BugReporter.IMXBugReportListener {
+ override fun onUploadFailed(reason: String?) {
+ try {
+ if (!TextUtils.isEmpty(reason)) {
+ Toast.makeText(this@BugReportActivity,
+ getString(R.string.send_bug_report_failed, reason), Toast.LENGTH_LONG).show()
+ }
+ } catch (e: Exception) {
+ Timber.e(e, "## onUploadFailed() : failed to display the toast " + e.message)
+ }
+
+ mMaskView.isVisible = false
+ mProgressBar.isVisible = false
+ mProgressTextView.isVisible = false
+ mScrollView.alpha = 1.0f
+
+ invalidateOptionsMenu()
+ }
+
+ override fun onUploadCancelled() {
+ onUploadFailed(null)
+ }
+
+ override fun onProgress(progress: Int) {
+ var progress = progress
+ if (progress > 100) {
+ Timber.e("## onProgress() : progress > 100")
+ progress = 100
+ } else if (progress < 0) {
+ Timber.e("## onProgress() : progress < 0")
+ progress = 0
+ }
+
+ mProgressBar.progress = progress
+ mProgressTextView.text = getString(R.string.send_bug_report_progress, progress.toString() + "")
+ }
+
+ override fun onUploadSucceed() {
+ try {
+ Toast.makeText(this@BugReportActivity, R.string.send_bug_report_sent, Toast.LENGTH_LONG).show()
+ } catch (e: Exception) {
+ Timber.e(e, "## onUploadSucceed() : failed to dismiss the toast " + e.message)
+ }
+
+ try {
+ finish()
+ } catch (e: Exception) {
+ Timber.e(e, "## onUploadSucceed() : failed to dismiss the dialog " + e.message)
+ }
+
+ }
+ })
+ }
+
+ /* ==========================================================================================
+ * UI Event
+ * ========================================================================================== */
+
+ @OnTextChanged(R.id.bug_report_edit_text)
+ internal fun textChanged() {
+ invalidateOptionsMenu()
+ }
+
+ @OnCheckedChanged(R.id.bug_report_button_include_screenshot)
+ internal fun onSendScreenshotChanged() {
+ mScreenShotPreview.isVisible = mIncludeScreenShotButton.isChecked && BugReporter.getScreenshot() != null
+ }
+
+ override fun onBackPressed() {
+ // Ensure there is no crash status remaining, which will be sent later on by mistake
+ BugReporter.deleteCrashFile(this)
+
+ super.onBackPressed()
+ }
+
+ /* ==========================================================================================
+ * Companion
+ * ========================================================================================== */
+
+ companion object {
+ private val LOG_TAG = BugReportActivity::class.java.simpleName
+ }
+}
diff --git a/app/src/main/java/im/vector/riotredesign/features/rageshake/BugReporter.java b/app/src/main/java/im/vector/riotredesign/features/rageshake/BugReporter.java
new file mode 100755
index 0000000000..5709cba0b2
--- /dev/null
+++ b/app/src/main/java/im/vector/riotredesign/features/rageshake/BugReporter.java
@@ -0,0 +1,728 @@
+/*
+ * 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() {
+
+ // enumerate files to delete
+ final List 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 gzippedFiles = new ArrayList<>();
+
+ if (withDevicesLogs) {
+ // TODO Timber
+ /*
+ List files = org.matrix.androidsdk.util.Timber.addLogFiles(new ArrayList());
+
+ 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;
+ }
+}
diff --git a/app/src/main/java/im/vector/riotredesign/features/rageshake/BugReporterMultipartBody.java b/app/src/main/java/im/vector/riotredesign/features/rageshake/BugReporterMultipartBody.java
new file mode 100755
index 0000000000..48796efa11
--- /dev/null
+++ b/app/src/main/java/im/vector/riotredesign/features/rageshake/BugReporterMultipartBody.java
@@ -0,0 +1,300 @@
+/*
+ * 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 java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import okhttp3.Headers;
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import okhttp3.internal.Util;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.ByteString;
+
+// simplified version of MultipartBody (OkHttp 3.6.0)
+public class BugReporterMultipartBody extends RequestBody {
+
+ /**
+ * Listener
+ */
+ public interface WriteListener {
+ /**
+ * Upload listener
+ *
+ * @param totalWritten total written bytes
+ * @param contentLength content length
+ */
+ void onWrite(long totalWritten, long contentLength);
+ }
+
+ private static final MediaType FORM = MediaType.parse("multipart/form-data");
+
+ private static final byte[] COLONSPACE = {':', ' '};
+ private static final byte[] CRLF = {'\r', '\n'};
+ private static final byte[] DASHDASH = {'-', '-'};
+
+ private final ByteString mBoundary;
+ private final MediaType mContentType;
+ private final List mParts;
+ private long mContentLength = -1L;
+
+ // listener
+ private WriteListener mWriteListener;
+
+ //
+ private List mContentLengthSize = null;
+
+ private BugReporterMultipartBody(ByteString boundary, List parts) {
+ mBoundary = boundary;
+ mContentType = MediaType.parse(FORM + "; boundary=" + boundary.utf8());
+ mParts = Util.immutableList(parts);
+ }
+
+ @Override
+ public MediaType contentType() {
+ return mContentType;
+ }
+
+ @Override
+ public long contentLength() throws IOException {
+ long result = mContentLength;
+ if (result != -1L) return result;
+ return mContentLength = writeOrCountBytes(null, true);
+ }
+
+ @Override
+ public void writeTo(BufferedSink sink) throws IOException {
+ writeOrCountBytes(sink, false);
+ }
+
+ /**
+ * Set the listener
+ *
+ * @param listener the
+ */
+ public void setWriteListener(WriteListener listener) {
+ mWriteListener = listener;
+ }
+
+ /**
+ * Warn the listener that some bytes have been written
+ *
+ * @param totalWrittenBytes the total written bytes
+ */
+ private void onWrite(long totalWrittenBytes) {
+ if ((null != mWriteListener) && (mContentLength > 0)) {
+ mWriteListener.onWrite(totalWrittenBytes, mContentLength);
+ }
+ }
+
+ /**
+ * Either writes this request to {@code sink} or measures its content length. We have one method
+ * do double-duty to make sure the counting and content are consistent, particularly when it comes
+ * to awkward operations like measuring the encoded length of header strings, or the
+ * length-in-digits of an encoded integer.
+ */
+ private long writeOrCountBytes(BufferedSink sink, boolean countBytes) throws IOException {
+ long byteCount = 0L;
+
+ Buffer byteCountBuffer = null;
+ if (countBytes) {
+ sink = byteCountBuffer = new Buffer();
+ mContentLengthSize = new ArrayList<>();
+ }
+
+ for (int p = 0, partCount = mParts.size(); p < partCount; p++) {
+ Part part = mParts.get(p);
+ Headers headers = part.headers;
+ RequestBody body = part.body;
+
+ sink.write(DASHDASH);
+ sink.write(mBoundary);
+ sink.write(CRLF);
+
+ if (headers != null) {
+ for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
+ sink.writeUtf8(headers.name(h))
+ .write(COLONSPACE)
+ .writeUtf8(headers.value(h))
+ .write(CRLF);
+ }
+ }
+
+ MediaType contentType = body.contentType();
+ if (contentType != null) {
+ sink.writeUtf8("Content-Type: ")
+ .writeUtf8(contentType.toString())
+ .write(CRLF);
+ }
+
+ int contentLength = (int) body.contentLength();
+ if (contentLength != -1) {
+ sink.writeUtf8("Content-Length: ")
+ .writeUtf8(contentLength + "")
+ .write(CRLF);
+ } else if (countBytes) {
+ // We can't measure the body's size without the sizes of its components.
+ byteCountBuffer.clear();
+ return -1L;
+ }
+
+ sink.write(CRLF);
+
+ if (countBytes) {
+ byteCount += contentLength;
+ mContentLengthSize.add(byteCount);
+ } else {
+ body.writeTo(sink);
+
+ // warn the listener of upload progress
+ // sink.buffer().size() does not give the right value
+ // assume that some data are popped
+ if ((null != mContentLengthSize) && (p < mContentLengthSize.size())) {
+ onWrite(mContentLengthSize.get(p));
+ }
+ }
+ sink.write(CRLF);
+ }
+
+ sink.write(DASHDASH);
+ sink.write(mBoundary);
+ sink.write(DASHDASH);
+ sink.write(CRLF);
+
+ if (countBytes) {
+ byteCount += byteCountBuffer.size();
+ byteCountBuffer.clear();
+ }
+
+ return byteCount;
+ }
+
+ private static void appendQuotedString(StringBuilder target, String key) {
+ target.append('"');
+ for (int i = 0, len = key.length(); i < len; i++) {
+ char ch = key.charAt(i);
+ switch (ch) {
+ case '\n':
+ target.append("%0A");
+ break;
+ case '\r':
+ target.append("%0D");
+ break;
+ case '"':
+ target.append("%22");
+ break;
+ default:
+ target.append(ch);
+ break;
+ }
+ }
+ target.append('"');
+ }
+
+ public static final class Part {
+ public static Part create(Headers headers, RequestBody body) {
+ if (body == null) {
+ throw new NullPointerException("body == null");
+ }
+ if (headers != null && headers.get("Content-Type") != null) {
+ throw new IllegalArgumentException("Unexpected header: Content-Type");
+ }
+ if (headers != null && headers.get("Content-Length") != null) {
+ throw new IllegalArgumentException("Unexpected header: Content-Length");
+ }
+ return new Part(headers, body);
+ }
+
+ public static Part createFormData(String name, String value) {
+ return createFormData(name, null, RequestBody.create(null, value));
+ }
+
+ public static Part createFormData(String name, String filename, RequestBody body) {
+ if (name == null) {
+ throw new NullPointerException("name == null");
+ }
+ StringBuilder disposition = new StringBuilder("form-data; name=");
+ appendQuotedString(disposition, name);
+
+ if (filename != null) {
+ disposition.append("; filename=");
+ appendQuotedString(disposition, filename);
+ }
+
+ return create(Headers.of("Content-Disposition", disposition.toString()), body);
+ }
+
+ final Headers headers;
+ final RequestBody body;
+
+ private Part(Headers headers, RequestBody body) {
+ this.headers = headers;
+ this.body = body;
+ }
+ }
+
+ public static final class Builder {
+ private final ByteString boundary;
+ private final List parts = new ArrayList<>();
+
+ public Builder() {
+ this(UUID.randomUUID().toString());
+ }
+
+ public Builder(String boundary) {
+ this.boundary = ByteString.encodeUtf8(boundary);
+ }
+
+ /**
+ * Add a form data part to the body.
+ */
+ public Builder addFormDataPart(String name, String value) {
+ return addPart(Part.createFormData(name, value));
+ }
+
+ /**
+ * Add a form data part to the body.
+ */
+ public Builder addFormDataPart(String name, String filename, RequestBody body) {
+ return addPart(Part.createFormData(name, filename, body));
+ }
+
+ /**
+ * Add a part to the body.
+ */
+ public Builder addPart(Part part) {
+ if (part == null) throw new NullPointerException("part == null");
+ parts.add(part);
+ return this;
+ }
+
+ /**
+ * Assemble the specified parts into a request body.
+ */
+ public BugReporterMultipartBody build() {
+ if (parts.isEmpty()) {
+ throw new IllegalStateException("Multipart body must have at least one part.");
+ }
+ return new BugReporterMultipartBody(boundary, parts);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/im/vector/riotredesign/features/rageshake/RageShake.kt b/app/src/main/java/im/vector/riotredesign/features/rageshake/RageShake.kt
new file mode 100644
index 0000000000..44bcd5cbf0
--- /dev/null
+++ b/app/src/main/java/im/vector/riotredesign/features/rageshake/RageShake.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.app.Activity
+import android.content.Context
+import android.hardware.Sensor
+import android.hardware.SensorManager
+import android.preference.PreferenceManager
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.edit
+import com.squareup.seismic.ShakeDetector
+import im.vector.riotredesign.R
+
+class RageShake(val activity: Activity) : ShakeDetector.Listener {
+
+ private var shakeDetector: ShakeDetector? = null
+
+ private var dialogDisplayed = false
+
+ fun start() {
+ if (!isEnable(activity)) {
+ return
+ }
+
+
+ val sensorManager = activity.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager
+
+ if (sensorManager == null) {
+ return
+ }
+
+ shakeDetector = ShakeDetector(this).apply {
+ start(sensorManager)
+ }
+ }
+
+ fun stop() {
+ shakeDetector?.stop()
+ }
+
+ /**
+ * Enable the feature, and start it
+ */
+ fun enable() {
+ PreferenceManager.getDefaultSharedPreferences(activity).edit {
+ putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true)
+ }
+
+ start()
+ }
+
+ /**
+ * Disable the feature, and stop it
+ */
+ fun disable() {
+ PreferenceManager.getDefaultSharedPreferences(activity).edit {
+ putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, false)
+ }
+
+ stop()
+ }
+
+ override fun hearShake() {
+ if (dialogDisplayed) {
+ // Filtered!
+ return
+ }
+
+ dialogDisplayed = true
+
+ AlertDialog.Builder(activity)
+ .setMessage(R.string.send_bug_report_alert_message)
+ .setPositiveButton(R.string.yes) { _, _ -> openBugReportScreen() }
+ .setNeutralButton(R.string.disable) { _, _ -> disable() }
+ .setOnDismissListener { dialogDisplayed = false }
+ .setNegativeButton(R.string.no, null)
+ .show()
+ }
+
+ private fun openBugReportScreen() {
+ BugReporter.sendBugReport(activity)
+ }
+
+ companion object {
+ private const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"
+
+ /**
+ * Check if the feature is available
+ */
+ fun isAvailable(context: Context): Boolean {
+ return (context.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager)
+ ?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null
+ }
+
+ /**
+ * Check if the feature is enable (enabled by default)
+ */
+ private fun isEnable(context: Context): Boolean {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/im/vector/riotredesign/features/themes/ActivityOtherThemes.kt b/app/src/main/java/im/vector/riotredesign/features/themes/ActivityOtherThemes.kt
new file mode 100644
index 0000000000..da17de4962
--- /dev/null
+++ b/app/src/main/java/im/vector/riotredesign/features/themes/ActivityOtherThemes.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.ui.themes
+
+import androidx.annotation.StyleRes
+import im.vector.riotredesign.R
+
+/**
+ * Class to manage Activity other possible themes.
+ * Note that style for light theme is default and is declared in the Android Manifest
+ */
+sealed class ActivityOtherThemes(@StyleRes val dark: Int,
+ @StyleRes val black: Int,
+ @StyleRes val status: Int) {
+
+ object Default : ActivityOtherThemes(
+ R.style.AppTheme_Dark,
+ R.style.AppTheme_Black,
+ R.style.AppTheme_Status
+ )
+
+ object NoActionBarFullscreen : ActivityOtherThemes(
+ R.style.AppTheme_NoActionBar_FullScreen_Dark,
+ R.style.AppTheme_NoActionBar_FullScreen_Black,
+ R.style.AppTheme_NoActionBar_FullScreen_Status
+ )
+
+ object Home : ActivityOtherThemes(
+ R.style.HomeActivityTheme_Dark,
+ R.style.HomeActivityTheme_Black,
+ R.style.HomeActivityTheme_Status
+ )
+
+ object Group : ActivityOtherThemes(
+ R.style.GroupAppTheme_Dark,
+ R.style.GroupAppTheme_Black,
+ R.style.GroupAppTheme_Status
+ )
+
+ object Picker : ActivityOtherThemes(
+ R.style.CountryPickerTheme_Dark,
+ R.style.CountryPickerTheme_Black,
+ R.style.CountryPickerTheme_Status
+ )
+
+ object Lock : ActivityOtherThemes(
+ R.style.Theme_Vector_Lock_Dark,
+ R.style.Theme_Vector_Lock_Light,
+ R.style.Theme_Vector_Lock_Status
+ )
+
+ object Search : ActivityOtherThemes(
+ R.style.SearchesAppTheme_Dark,
+ R.style.SearchesAppTheme_Black,
+ R.style.SearchesAppTheme_Status
+ )
+
+ object Call : ActivityOtherThemes(
+ R.style.CallActivityTheme_Dark,
+ R.style.CallActivityTheme_Black,
+ R.style.CallActivityTheme_Status
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/im/vector/riotredesign/features/themes/ThemeUtils.kt b/app/src/main/java/im/vector/riotredesign/features/themes/ThemeUtils.kt
new file mode 100644
index 0000000000..cacf457401
--- /dev/null
+++ b/app/src/main/java/im/vector/riotredesign/features/themes/ThemeUtils.kt
@@ -0,0 +1,228 @@
+/*
+ * 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.ui.themes
+
+
+import android.app.Activity
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.text.TextUtils
+import android.util.TypedValue
+import android.view.Menu
+import androidx.annotation.AttrRes
+import androidx.annotation.ColorInt
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.preference.PreferenceManager
+import im.vector.riotredesign.R
+import timber.log.Timber
+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"
+
+ // the theme possible values
+ private const val THEME_DARK_VALUE = "dark"
+ private const val THEME_LIGHT_VALUE = "light"
+ private const val THEME_BLACK_VALUE = "black"
+ private const val THEME_STATUS_VALUE = "status"
+
+ private val mColorByAttr = HashMap()
+
+ /**
+ * Provides the selected application theme
+ *
+ * @param context the context
+ * @return the selected application theme
+ */
+ fun getApplicationTheme(context: Context): String {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE)
+ }
+
+ /**
+ * Update the application theme
+ *
+ * @param aTheme the new theme
+ */
+ fun setApplicationTheme(context: Context, aTheme: String) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putString(APPLICATION_THEME_KEY, aTheme)
+ .apply()
+
+ /* TODO
+ when (aTheme) {
+ THEME_DARK_VALUE -> VectorApp.getInstance().setTheme(R.style.AppTheme_Dark)
+ THEME_BLACK_VALUE -> VectorApp.getInstance().setTheme(R.style.AppTheme_Black)
+ THEME_STATUS_VALUE -> VectorApp.getInstance().setTheme(R.style.AppTheme_Status)
+ else -> VectorApp.getInstance().setTheme(R.style.AppTheme_Light)
+ }
+ */
+
+ mColorByAttr.clear()
+ }
+
+ /**
+ * Set the activity theme according to the selected one.
+ *
+ * @param activity the activity
+ */
+ fun setActivityTheme(activity: Activity, otherThemes: ActivityOtherThemes) {
+ when (getApplicationTheme(activity)) {
+ THEME_DARK_VALUE -> activity.setTheme(otherThemes.dark)
+ THEME_BLACK_VALUE -> activity.setTheme(otherThemes.black)
+ THEME_STATUS_VALUE -> activity.setTheme(otherThemes.status)
+ }
+
+ mColorByAttr.clear()
+ }
+
+ /**
+ * Set the TabLayout colors.
+ * It seems that there is no proper way to manage it with the manifest file.
+ *
+ * @param activity the activity
+ * @param layout the layout
+ */
+ /*
+ fun setTabLayoutTheme(activity: Activity, layout: TabLayout) {
+ if (activity is VectorGroupDetailsActivity) {
+ val textColor: Int
+ val underlineColor: Int
+ val backgroundColor: Int
+
+ if (TextUtils.equals(getApplicationTheme(activity), THEME_LIGHT_VALUE)) {
+ textColor = ContextCompat.getColor(activity, android.R.color.white)
+ underlineColor = textColor
+ backgroundColor = ContextCompat.getColor(activity, R.color.tab_groups)
+ } else if (TextUtils.equals(getApplicationTheme(activity), THEME_STATUS_VALUE)) {
+ textColor = ContextCompat.getColor(activity, android.R.color.white)
+ underlineColor = textColor
+ backgroundColor = getColor(activity, R.attr.colorPrimary)
+ } else {
+ textColor = ContextCompat.getColor(activity, R.color.tab_groups)
+ underlineColor = textColor
+ backgroundColor = getColor(activity, R.attr.colorPrimary)
+ }
+
+ layout.setTabTextColors(textColor, textColor)
+ layout.setSelectedTabIndicatorColor(underlineColor)
+ layout.setBackgroundColor(backgroundColor)
+ }
+ }
+ */
+
+ /**
+ * Translates color attributes to colors
+ *
+ * @param c Context
+ * @param colorAttribute Color Attribute
+ * @return Requested Color
+ */
+ @ColorInt
+ fun getColor(c: Context, @AttrRes colorAttribute: Int): Int {
+ if (mColorByAttr.containsKey(colorAttribute)) {
+ return mColorByAttr[colorAttribute] as Int
+ }
+
+ var matchedColor: Int
+
+ try {
+ val color = TypedValue()
+ c.theme.resolveAttribute(colorAttribute, color, true)
+ matchedColor = color.data
+ } catch (e: Exception) {
+ Timber.e(e, "Unable to get color")
+ matchedColor = ContextCompat.getColor(c, android.R.color.holo_red_dark)
+ }
+
+ mColorByAttr[colorAttribute] = matchedColor
+
+ return matchedColor
+ }
+
+ /**
+ * Get the resource Id applied to the current theme
+ *
+ * @param c the context
+ * @param resourceId the resource id
+ * @return the resource Id for the current theme
+ */
+ fun getResourceId(c: Context, resourceId: Int): Int {
+ if (TextUtils.equals(getApplicationTheme(c), THEME_LIGHT_VALUE)
+ || TextUtils.equals(getApplicationTheme(c), THEME_STATUS_VALUE)) {
+ return when (resourceId) {
+ R.drawable.line_divider_dark -> R.drawable.line_divider_light
+ R.style.Floating_Actions_Menu -> R.style.Floating_Actions_Menu_Light
+ else -> resourceId
+ }
+ }
+ return resourceId
+ }
+
+ /**
+ * Update the menu icons colors
+ *
+ * @param menu the menu
+ * @param color the color
+ */
+ fun tintMenuIcons(menu: Menu, color: Int) {
+ for (i in 0 until menu.size()) {
+ val item = menu.getItem(i)
+ val drawable = item.icon
+ if (drawable != null) {
+ val wrapped = DrawableCompat.wrap(drawable)
+ drawable.mutate()
+ DrawableCompat.setTint(wrapped, color)
+ item.icon = drawable
+ }
+ }
+ }
+
+ /**
+ * Tint the drawable with a theme attribute
+ *
+ * @param context the context
+ * @param drawable the drawable to tint
+ * @param attribute the theme color
+ * @return the tinted drawable
+ */
+ fun tintDrawable(context: Context, drawable: Drawable, @AttrRes attribute: Int): Drawable {
+ return tintDrawableWithColor(drawable, getColor(context, attribute))
+ }
+
+ /**
+ * Tint the drawable with a color integer
+ *
+ * @param drawable the drawable to tint
+ * @param color the color
+ * @return the tinted drawable
+ */
+ fun tintDrawableWithColor(drawable: Drawable, @ColorInt color: Int): Drawable {
+ val tinted = DrawableCompat.wrap(drawable)
+ drawable.mutate()
+ DrawableCompat.setTint(tinted, color)
+ return tinted
+ }
+}
diff --git a/app/src/main/res/color/button_text_color_selector.xml b/app/src/main/res/color/button_text_color_selector.xml
new file mode 100644
index 0000000000..ff2ab3dba4
--- /dev/null
+++ b/app/src/main/res/color/button_text_color_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/color/home_bottom_nav_view_tint.xml b/app/src/main/res/color/home_bottom_nav_view_tint.xml
new file mode 100644
index 0000000000..ad4b5ff429
--- /dev/null
+++ b/app/src/main/res/color/home_bottom_nav_view_tint.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/color/primary_text_color_selector_dark.xml b/app/src/main/res/color/primary_text_color_selector_dark.xml
new file mode 100644
index 0000000000..7c4c853b58
--- /dev/null
+++ b/app/src/main/res/color/primary_text_color_selector_dark.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/color/primary_text_color_selector_light.xml b/app/src/main/res/color/primary_text_color_selector_light.xml
new file mode 100644
index 0000000000..8d4c0d06c4
--- /dev/null
+++ b/app/src/main/res/color/primary_text_color_selector_light.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/color/primary_text_color_selector_status.xml b/app/src/main/res/color/primary_text_color_selector_status.xml
new file mode 100644
index 0000000000..9bbc84c321
--- /dev/null
+++ b/app/src/main/res/color/primary_text_color_selector_status.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-xxhdpi/ic_material_send_black.png b/app/src/main/res/drawable-xxhdpi/ic_material_send_black.png
new file mode 100755
index 0000000000..761929f431
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_material_send_black.png differ
diff --git a/app/src/main/res/drawable/bg_tombstone_predecessor.xml b/app/src/main/res/drawable/bg_tombstone_predecessor.xml
new file mode 100644
index 0000000000..65c214d2cd
--- /dev/null
+++ b/app/src/main/res/drawable/bg_tombstone_predecessor.xml
@@ -0,0 +1,22 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/call_header_transparent_bg.xml b/app/src/main/res/drawable/call_header_transparent_bg.xml
new file mode 100644
index 0000000000..17408c8b8f
--- /dev/null
+++ b/app/src/main/res/drawable/call_header_transparent_bg.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/direct_chat_circle_black.xml b/app/src/main/res/drawable/direct_chat_circle_black.xml
new file mode 100644
index 0000000000..3c45c0231f
--- /dev/null
+++ b/app/src/main/res/drawable/direct_chat_circle_black.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/direct_chat_circle_dark.xml b/app/src/main/res/drawable/direct_chat_circle_dark.xml
new file mode 100644
index 0000000000..1e9a4500f4
--- /dev/null
+++ b/app/src/main/res/drawable/direct_chat_circle_dark.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/direct_chat_circle_light.xml b/app/src/main/res/drawable/direct_chat_circle_light.xml
new file mode 100644
index 0000000000..88bb178a9b
--- /dev/null
+++ b/app/src/main/res/drawable/direct_chat_circle_light.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/direct_chat_circle_status.xml b/app/src/main/res/drawable/direct_chat_circle_status.xml
new file mode 100644
index 0000000000..2d527d0b4d
--- /dev/null
+++ b/app/src/main/res/drawable/direct_chat_circle_status.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/line_divider_dark.xml b/app/src/main/res/drawable/line_divider_dark.xml
new file mode 100644
index 0000000000..ee2a3a0972
--- /dev/null
+++ b/app/src/main/res/drawable/line_divider_dark.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/line_divider_light.xml b/app/src/main/res/drawable/line_divider_light.xml
new file mode 100644
index 0000000000..cfaebbda7d
--- /dev/null
+++ b/app/src/main/res/drawable/line_divider_light.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/pill_background_bing.xml b/app/src/main/res/drawable/pill_background_bing.xml
new file mode 100644
index 0000000000..70a2bda8a4
--- /dev/null
+++ b/app/src/main/res/drawable/pill_background_bing.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/pill_background_room_alias_dark.xml b/app/src/main/res/drawable/pill_background_room_alias_dark.xml
new file mode 100644
index 0000000000..b86cbd3b78
--- /dev/null
+++ b/app/src/main/res/drawable/pill_background_room_alias_dark.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/pill_background_room_alias_light.xml b/app/src/main/res/drawable/pill_background_room_alias_light.xml
new file mode 100644
index 0000000000..9a67df1182
--- /dev/null
+++ b/app/src/main/res/drawable/pill_background_room_alias_light.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/pill_background_room_alias_status.xml b/app/src/main/res/drawable/pill_background_room_alias_status.xml
new file mode 100644
index 0000000000..9a67df1182
--- /dev/null
+++ b/app/src/main/res/drawable/pill_background_room_alias_status.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/pill_background_user_id_dark.xml b/app/src/main/res/drawable/pill_background_user_id_dark.xml
new file mode 100644
index 0000000000..b86cbd3b78
--- /dev/null
+++ b/app/src/main/res/drawable/pill_background_user_id_dark.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/pill_background_user_id_light.xml b/app/src/main/res/drawable/pill_background_user_id_light.xml
new file mode 100644
index 0000000000..9a67df1182
--- /dev/null
+++ b/app/src/main/res/drawable/pill_background_user_id_light.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/pill_background_user_id_status.xml b/app/src/main/res/drawable/pill_background_user_id_status.xml
new file mode 100644
index 0000000000..9a67df1182
--- /dev/null
+++ b/app/src/main/res/drawable/pill_background_user_id_status.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/riot_animated_logo.xml b/app/src/main/res/drawable/riot_animated_logo.xml
new file mode 100644
index 0000000000..ceedc6aea1
--- /dev/null
+++ b/app/src/main/res/drawable/riot_animated_logo.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/searches_cursor_background.xml b/app/src/main/res/drawable/searches_cursor_background.xml
new file mode 100644
index 0000000000..c9d1d88498
--- /dev/null
+++ b/app/src/main/res/drawable/searches_cursor_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shadow_bottom_dark.xml b/app/src/main/res/drawable/shadow_bottom_dark.xml
new file mode 100644
index 0000000000..f56addee06
--- /dev/null
+++ b/app/src/main/res/drawable/shadow_bottom_dark.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shadow_bottom_light.xml b/app/src/main/res/drawable/shadow_bottom_light.xml
new file mode 100755
index 0000000000..c86eaf3b3b
--- /dev/null
+++ b/app/src/main/res/drawable/shadow_bottom_light.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shadow_top_dark.xml b/app/src/main/res/drawable/shadow_top_dark.xml
new file mode 100644
index 0000000000..7b2d95e2b6
--- /dev/null
+++ b/app/src/main/res/drawable/shadow_top_dark.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shadow_top_light.xml b/app/src/main/res/drawable/shadow_top_light.xml
new file mode 100755
index 0000000000..03412fc414
--- /dev/null
+++ b/app/src/main/res/drawable/shadow_top_light.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/splash.xml b/app/src/main/res/drawable/splash.xml
new file mode 100644
index 0000000000..4d60be2f30
--- /dev/null
+++ b/app/src/main/res/drawable/splash.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/sticker_description_background.xml b/app/src/main/res/drawable/sticker_description_background.xml
new file mode 100644
index 0000000000..faa6f5b6f4
--- /dev/null
+++ b/app/src/main/res/drawable/sticker_description_background.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/sticker_description_triangle.xml b/app/src/main/res/drawable/sticker_description_triangle.xml
new file mode 100644
index 0000000000..199c2e7313
--- /dev/null
+++ b/app/src/main/res/drawable/sticker_description_triangle.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_background_fab_label.xml b/app/src/main/res/drawable/vector_background_fab_label.xml
new file mode 100644
index 0000000000..2c13ba76ee
--- /dev/null
+++ b/app/src/main/res/drawable/vector_background_fab_label.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_background_fab_label_light.xml b/app/src/main/res/drawable/vector_background_fab_label_light.xml
new file mode 100644
index 0000000000..b65b9b00cf
--- /dev/null
+++ b/app/src/main/res/drawable/vector_background_fab_label_light.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_medias_picker_button_background.xml b/app/src/main/res/drawable/vector_medias_picker_button_background.xml
new file mode 100644
index 0000000000..8adc855a90
--- /dev/null
+++ b/app/src/main/res/drawable/vector_medias_picker_button_background.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_tabbar_background_dark.xml b/app/src/main/res/drawable/vector_tabbar_background_dark.xml
new file mode 100644
index 0000000000..74b4a8c4c3
--- /dev/null
+++ b/app/src/main/res/drawable/vector_tabbar_background_dark.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_tabbar_background_group_light.xml b/app/src/main/res/drawable/vector_tabbar_background_group_light.xml
new file mode 100644
index 0000000000..3166002c08
--- /dev/null
+++ b/app/src/main/res/drawable/vector_tabbar_background_group_light.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_tabbar_background_light.xml b/app/src/main/res/drawable/vector_tabbar_background_light.xml
new file mode 100644
index 0000000000..432be45bbe
--- /dev/null
+++ b/app/src/main/res/drawable/vector_tabbar_background_light.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_tabbar_background_status.xml b/app/src/main/res/drawable/vector_tabbar_background_status.xml
new file mode 100644
index 0000000000..f0f38a6439
--- /dev/null
+++ b/app/src/main/res/drawable/vector_tabbar_background_status.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_tabbar_selected_background_dark.xml b/app/src/main/res/drawable/vector_tabbar_selected_background_dark.xml
new file mode 100644
index 0000000000..ce8ba4eb8d
--- /dev/null
+++ b/app/src/main/res/drawable/vector_tabbar_selected_background_dark.xml
@@ -0,0 +1,22 @@
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_tabbar_selected_background_group_light.xml b/app/src/main/res/drawable/vector_tabbar_selected_background_group_light.xml
new file mode 100644
index 0000000000..a46d469734
--- /dev/null
+++ b/app/src/main/res/drawable/vector_tabbar_selected_background_group_light.xml
@@ -0,0 +1,22 @@
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_tabbar_selected_background_light.xml b/app/src/main/res/drawable/vector_tabbar_selected_background_light.xml
new file mode 100644
index 0000000000..9625ac483f
--- /dev/null
+++ b/app/src/main/res/drawable/vector_tabbar_selected_background_light.xml
@@ -0,0 +1,22 @@
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_tabbar_selected_background_status.xml b/app/src/main/res/drawable/vector_tabbar_selected_background_status.xml
new file mode 100644
index 0000000000..ee338a5a39
--- /dev/null
+++ b/app/src/main/res/drawable/vector_tabbar_selected_background_status.xml
@@ -0,0 +1,22 @@
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_tabbar_unselected_background_dark.xml b/app/src/main/res/drawable/vector_tabbar_unselected_background_dark.xml
new file mode 100644
index 0000000000..cb302f5111
--- /dev/null
+++ b/app/src/main/res/drawable/vector_tabbar_unselected_background_dark.xml
@@ -0,0 +1,9 @@
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_tabbar_unselected_background_group_light.xml b/app/src/main/res/drawable/vector_tabbar_unselected_background_group_light.xml
new file mode 100644
index 0000000000..b2d360f2e0
--- /dev/null
+++ b/app/src/main/res/drawable/vector_tabbar_unselected_background_group_light.xml
@@ -0,0 +1,9 @@
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_tabbar_unselected_background_light.xml b/app/src/main/res/drawable/vector_tabbar_unselected_background_light.xml
new file mode 100644
index 0000000000..a7a286e724
--- /dev/null
+++ b/app/src/main/res/drawable/vector_tabbar_unselected_background_light.xml
@@ -0,0 +1,9 @@
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/vector_tabbar_unselected_background_status.xml b/app/src/main/res/drawable/vector_tabbar_unselected_background_status.xml
new file mode 100644
index 0000000000..e2c7613c50
--- /dev/null
+++ b/app/src/main/res/drawable/vector_tabbar_unselected_background_status.xml
@@ -0,0 +1,9 @@
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_bug_report.xml b/app/src/main/res/layout/activity_bug_report.xml
new file mode 100644
index 0000000000..82916ee69a
--- /dev/null
+++ b/app/src/main/res/layout/activity_bug_report.xml
@@ -0,0 +1,192 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/bug_report.xml b/app/src/main/res/menu/bug_report.xml
new file mode 100755
index 0000000000..c37895bb8d
--- /dev/null
+++ b/app/src/main/res/menu/bug_report.xml
@@ -0,0 +1,12 @@
+
+
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 0000000000..17cb3d7564
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 90e3e22a0b..2a1fc86806 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -18,8 +18,4 @@
#a5a5a5
#61708B
- #FFC7C7C7
- #FF999999
- #FFF56679
-
diff --git a/app/src/main/res/values/colors_riot.xml b/app/src/main/res/values/colors_riot.xml
new file mode 100644
index 0000000000..f60d4991f3
--- /dev/null
+++ b/app/src/main/res/values/colors_riot.xml
@@ -0,0 +1,150 @@
+
+
+
+
+ #70BF56
+ #ff4b55
+ #ff4b55
+
+
+ #ff4b55
+ #FFC7C7C7
+ #FF999999
+
+
+ #BD79CC
+ #744C7F
+ #F8A15F
+ #D97051
+ @color/accent_color_light
+ #5EA584
+ #a6d0e5
+ #81bddb
+
+
+ #7F03b381
+ #7F03b381
+ #7F03b381
+ #7F586C7B
+
+
+
+
+ #FFFFFFFF
+
+ #FF181B21
+
+ #F000
+ #FFEEF2F5
+
+
+ #FF1A2027
+
+ #FF27303A
+
+ #03b381
+
+
+ #FF0D0E10
+
+ #FF15171B
+
+ #03b381
+
+
+ #000
+
+ #FF060708
+
+ #FF465561
+ #FF586C7B
+ #FF586C7B
+
+
+ #EEEFEF
+
+ #FF61708B
+
+ #FF22262E
+
+ @color/primary_color_light
+ @color/primary_color_dark
+ @color/primary_color_status
+
+ @color/primary_color_light
+ @color/primary_color_dark
+ @color/primary_color_status
+
+
+ #FFFFFF
+ #FFFFFF
+
+ #903C3C3C
+ #CCDDDDDD
+
+
+
+ #FF2E2F32
+ #FF9E9E9E
+
+ #FF9E9E9E
+ @color/riot_primary_text_color_light
+
+
+ #FFEDF3FF
+ #FFA1B2D1
+
+ #FFA1B2D1
+ @color/riot_primary_text_color_dark
+
+
+ #FF70808D
+ #7F70808D
+
+ #7F70808D
+ @color/riot_primary_text_color_status
+
+
+ #FFDDDDDD
+ @android:color/transparent
+
+
+ #2f9edb
+ @color/vector_fuchsia_color
+
+
+ #03b381
+ #368bd6
+ #ac3ba8
+
+
+ #FFF56679
+ #FFFFC666
+ #FFF8E71C
+ #FF7AC9A1
+ #FF9E9E9E
+
+
+ #FFFFFFFF
+ #FF7F7F7F
+
+
+ #368bd6
+ #ac3ba8
+ #03b381
+ #e64f7a
+ #ff812d
+ #2dc2c5
+ #5c56f5
+ #74d12c
+
+
+ #368BD6
+ #368BD6
+ #368BD6
+
+
+ #368BD6
+
+
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
deleted file mode 100644
index 25fe7ea98a..0000000000
--- a/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/values/styles_riot.xml b/app/src/main/res/values/styles_riot.xml
new file mode 100644
index 0000000000..9bfb9af87a
--- /dev/null
+++ b/app/src/main/res/values/styles_riot.xml
@@ -0,0 +1,249 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/theme_black.xml b/app/src/main/res/values/theme_black.xml
new file mode 100644
index 0000000000..f816b9befa
--- /dev/null
+++ b/app/src/main/res/values/theme_black.xml
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/theme_dark.xml b/app/src/main/res/values/theme_dark.xml
new file mode 100644
index 0000000000..9359d0549b
--- /dev/null
+++ b/app/src/main/res/values/theme_dark.xml
@@ -0,0 +1,263 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/theme_light.xml b/app/src/main/res/values/theme_light.xml
new file mode 100644
index 0000000000..190c838a43
--- /dev/null
+++ b/app/src/main/res/values/theme_light.xml
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/theme_status.xml b/app/src/main/res/values/theme_status.xml
new file mode 100644
index 0000000000..6f21726c58
--- /dev/null
+++ b/app/src/main/res/values/theme_status.xml
@@ -0,0 +1,260 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+