diff --git a/app/build.gradle b/app/build.gradle index 9db7a601..2f4238e1 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 59584326..319575c1 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 00000000..50c9b4d4 --- /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 2938252c..a4d202d8 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 2b3ae6f6..403f242b 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 00000000..8ee90669 --- /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 00000000..5709cba0 --- /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 00000000..48796efa --- /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 00000000..44bcd5cb --- /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 00000000..da17de49 --- /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 00000000..cacf4574 --- /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 00000000..ff2ab3db --- /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 00000000..ad4b5ff4 --- /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 00000000..7c4c853b --- /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 00000000..8d4c0d06 --- /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 00000000..9bbc84c3 --- /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 00000000..761929f4 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 00000000..65c214d2 --- /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 00000000..17408c8b --- /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 00000000..3c45c023 --- /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 00000000..1e9a4500 --- /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 00000000..88bb178a --- /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 00000000..2d527d0b --- /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 00000000..ee2a3a09 --- /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 00000000..cfaebbda --- /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 00000000..70a2bda8 --- /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 00000000..b86cbd3b --- /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 00000000..9a67df11 --- /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 00000000..9a67df11 --- /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 00000000..b86cbd3b --- /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 00000000..9a67df11 --- /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 00000000..9a67df11 --- /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 00000000..ceedc6ae --- /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 00000000..c9d1d884 --- /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 00000000..f56addee --- /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 00000000..c86eaf3b --- /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 00000000..7b2d95e2 --- /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 00000000..03412fc4 --- /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 00000000..4d60be2f --- /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 00000000..faa6f5b6 --- /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 00000000..199c2e73 --- /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 00000000..2c13ba76 --- /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 00000000..b65b9b00 --- /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 00000000..8adc855a --- /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 00000000..74b4a8c4 --- /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 00000000..3166002c --- /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 00000000..432be45b --- /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 00000000..f0f38a64 --- /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 00000000..ce8ba4eb --- /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 00000000..a46d4697 --- /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 00000000..9625ac48 --- /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 00000000..ee338a5a --- /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 00000000..cb302f51 --- /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 00000000..b2d360f2 --- /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 00000000..a7a286e7 --- /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 00000000..e2c7613c --- /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 00000000..82916ee6 --- /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 00000000..c37895bb --- /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 00000000..17cb3d75 --- /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 90e3e22a..2a1fc868 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 00000000..f60d4991 --- /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 25fe7ea9..00000000 --- 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 00000000..9bfb9af8 --- /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 00000000..f816b9be --- /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 00000000..9359d054 --- /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 00000000..190c838a --- /dev/null +++ b/app/src/main/res/values/theme_light.xml @@ -0,0 +1,266 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +