diff --git a/build.gradle b/build.gradle
index 7e567983fc..479eb154c8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -11,6 +11,7 @@ buildscript {
} }
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
+ classpath 'com.google.gms:google-services:4.2.0'
classpath "com.airbnb.okreplay:gradle-plugin:1.4.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.6.2'
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
index ad45328206..e5ee1c0e63 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
@@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session
import androidx.annotation.MainThread
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.content.ContentUrlResolver
+import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.user.UserService
@@ -27,7 +28,7 @@ import im.vector.matrix.android.api.session.user.UserService
* This interface defines interactions with a session.
* An instance of a session will be provided by the SDK.
*/
-interface Session : RoomService, GroupService, UserService {
+interface Session : RoomService, GroupService, UserService, CryptoService {
/**
* The params associated to the session
@@ -69,5 +70,4 @@ interface Session : RoomService, GroupService, UserService {
// Not used at the moment
interface Listener
-
}
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt
new file mode 100644
index 0000000000..86c8a86f9a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.matrix.android.api.session.crypto
+
+interface CryptoService {
+
+ // Not supported for the moment
+ fun isCryptoEnabled() = false
+}
\ No newline at end of file
diff --git a/vector/build.gradle b/vector/build.gradle
index 6856d5914d..3957a8a0ff 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -74,10 +74,12 @@ android {
buildTypes {
debug {
resValue "bool", "debug_mode", "true"
+ buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
}
release {
resValue "bool", "debug_mode", "false"
+ buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
@@ -181,6 +183,9 @@ dependencies {
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
implementation 'com.airbnb.android:mvrx:0.7.0'
+ // Work
+ implementation "android.arch.work:work-runtime-ktx:1.0.0"
+
// FP
implementation "io.arrow-kt:arrow-core:$arrow_version"
@@ -209,14 +214,23 @@ dependencies {
implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
+ // Badge for compatibility
+ implementation 'me.leolin:ShortcutBadger:1.1.2@aar'
+
// DI
implementation "org.koin:koin-android:$koin_version"
implementation "org.koin:koin-android-scope:$koin_version"
+ // gplay flavor only
+ gplayImplementation 'com.google.firebase:firebase-core:16.0.8'
+ gplayImplementation 'com.google.firebase:firebase-messaging:17.5.0'
+
// TESTS
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}
-
+if (!getGradle().getStartParameter().getTaskRequests().toString().contains("fdroid")) {
+ apply plugin: 'com.google.gms.google-services'
+}
diff --git a/vector/src/fdroid/AndroidManifest.xml b/vector/src/fdroid/AndroidManifest.xml
new file mode 100644
index 0000000000..01babce90a
--- /dev/null
+++ b/vector/src/fdroid/AndroidManifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/fdroid/java/im/vector/push/fcm/FcmHelper.java b/vector/src/fdroid/java/im/vector/push/fcm/FcmHelper.java
new file mode 100755
index 0000000000..ba8badef83
--- /dev/null
+++ b/vector/src/fdroid/java/im/vector/push/fcm/FcmHelper.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2014 OpenMarket 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.push.fcm;
+
+import android.app.Activity;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class FcmHelper {
+
+ /**
+ * Retrieves the FCM registration token.
+ *
+ * @return the FCM token or null if not received from FCM
+ */
+ @Nullable
+ public static String getFcmToken(Context context) {
+ return null;
+ }
+
+ /**
+ * Store FCM token to the SharedPrefs
+ *
+ * @param context android context
+ * @param token the token to store
+ */
+ public static void storeFcmToken(@NonNull Context context,
+ @Nullable String token) {
+ // No op
+ }
+
+ /**
+ * onNewToken may not be called on application upgrade, so ensure my shared pref is set
+ *
+ * @param activity the first launch Activity
+ */
+ public static void ensureFcmTokenIsRetrieved(final Activity activity) {
+ // No op
+ }
+}
diff --git a/vector/src/fdroid/java/im/vector/push/fcm/NotificationTroubleshootTestManagerFactory.kt b/vector/src/fdroid/java/im/vector/push/fcm/NotificationTroubleshootTestManagerFactory.kt
new file mode 100644
index 0000000000..5a81c79d3b
--- /dev/null
+++ b/vector/src/fdroid/java/im/vector/push/fcm/NotificationTroubleshootTestManagerFactory.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.push.fcm
+
+import androidx.fragment.app.Fragment
+import im.vector.fragments.troubleshoot.TestAccountSettings
+import im.vector.fragments.troubleshoot.TestDeviceSettings
+import im.vector.matrix.android.api.session.Session
+import im.vector.push.fcm.troubleshoot.TestAutoStartBoot
+import im.vector.push.fcm.troubleshoot.TestBackgroundRestrictions
+import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager
+import im.vector.riotredesign.features.settings.troubleshoot.TestBingRulesSettings
+import im.vector.riotredesign.features.settings.troubleshoot.TestSystemSettings
+
+class NotificationTroubleshootTestManagerFactory {
+
+ companion object {
+ fun createTestManager(fragment: Fragment, session: Session?): NotificationTroubleshootTestManager {
+ val mgr = NotificationTroubleshootTestManager(fragment)
+ mgr.addTest(TestSystemSettings(fragment))
+ if (session != null) {
+ mgr.addTest(TestAccountSettings(fragment, session))
+ }
+ mgr.addTest(TestDeviceSettings(fragment))
+ if (session != null) {
+ mgr.addTest(TestBingRulesSettings(fragment, session))
+ }
+ // mgr.addTest(TestNotificationServiceRunning(fragment))
+ // mgr.addTest(TestServiceRestart(fragment))
+ mgr.addTest(TestAutoStartBoot(fragment))
+ mgr.addTest(TestBackgroundRestrictions(fragment))
+ // mgr.addTest(TestBatteryOptimization(fragment))
+ return mgr
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestAutoStartBoot.kt b/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestAutoStartBoot.kt
new file mode 100644
index 0000000000..4e33423629
--- /dev/null
+++ b/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestAutoStartBoot.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.push.fcm.troubleshoot
+
+import androidx.fragment.app.Fragment
+import im.vector.riotredesign.R
+import im.vector.riotredesign.features.settings.PreferencesManager
+import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest
+
+/**
+ * Test that the application is started on boot
+ */
+class TestAutoStartBoot(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_service_boot_title) {
+
+ override fun perform() {
+ if (PreferencesManager.autoStartOnBoot(fragment.context)) {
+ description = fragment.getString(R.string.settings_troubleshoot_test_service_boot_success)
+ status = TestStatus.SUCCESS
+ quickFix = null
+ } else {
+ description = fragment.getString(R.string.settings_troubleshoot_test_service_boot_failed)
+ quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_service_boot_quickfix) {
+ override fun doFix() {
+ PreferencesManager.setAutoStartOnBoot(fragment.context, true)
+ manager?.retry()
+ }
+ }
+ status = TestStatus.FAILED
+ }
+ }
+}
\ No newline at end of file
diff --git a/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestBackgroundRestrictions.kt b/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestBackgroundRestrictions.kt
new file mode 100644
index 0000000000..1ad298c60d
--- /dev/null
+++ b/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestBackgroundRestrictions.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.push.fcm.troubleshoot
+
+import android.content.Context
+import android.net.ConnectivityManager
+import androidx.core.net.ConnectivityManagerCompat
+import androidx.fragment.app.Fragment
+import im.vector.riotredesign.R
+import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest
+
+class TestBackgroundRestrictions(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_bg_restricted_title) {
+
+ override fun perform() {
+ (fragment.context!!.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).apply {
+ // Checks if the device is on a metered network
+ if (isActiveNetworkMetered) {
+ // Checks user’s Data Saver settings.
+ val restrictBackgroundStatus = ConnectivityManagerCompat.getRestrictBackgroundStatus(this)
+ when (restrictBackgroundStatus) {
+ ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED -> {
+ // Background data usage is blocked for this app. Wherever possible,
+ // the app should also use less data in the foreground.
+ description = fragment.getString(R.string.settings_troubleshoot_test_bg_restricted_failed,
+ "RESTRICT_BACKGROUND_STATUS_ENABLED")
+ status = TestStatus.FAILED
+ quickFix = null
+ }
+ ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED -> {
+ // The app is whitelisted. Wherever possible,
+ // the app should use less data in the foreground and background.
+ description = fragment.getString(R.string.settings_troubleshoot_test_bg_restricted_success,
+ "RESTRICT_BACKGROUND_STATUS_WHITELISTED")
+ status = TestStatus.SUCCESS
+ quickFix = null
+ }
+ ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED -> {
+ // Data Saver is disabled. Since the device is connected to a
+ // metered network, the app should use less data wherever possible.
+ description = fragment.getString(R.string.settings_troubleshoot_test_bg_restricted_success,
+ "RESTRICT_BACKGROUND_STATUS_DISABLED")
+ status = TestStatus.SUCCESS
+ quickFix = null
+ }
+
+ }
+
+ } else {
+ // The device is not on a metered network.
+ // Use data as required to perform syncs, downloads, and updates.
+ description = fragment.getString(R.string.settings_troubleshoot_test_bg_restricted_success, "")
+ status = TestStatus.SUCCESS
+ quickFix = null
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestBatteryOptimization.kt b/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestBatteryOptimization.kt
new file mode 100644
index 0000000000..c7d6001853
--- /dev/null
+++ b/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestBatteryOptimization.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.push.fcm.troubleshoot
+
+import androidx.fragment.app.Fragment
+import im.vector.riotredesign.R
+import im.vector.riotredesign.core.utils.isIgnoringBatteryOptimizations
+import im.vector.riotredesign.core.utils.requestDisablingBatteryOptimization
+import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager
+import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest
+
+// Not used anymore
+class TestBatteryOptimization(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_battery_title) {
+
+ override fun perform() {
+
+ if (fragment.context != null && isIgnoringBatteryOptimizations(fragment.context!!)) {
+ description = fragment.getString(R.string.settings_troubleshoot_test_battery_success)
+ status = TestStatus.SUCCESS
+ quickFix = null
+ } else {
+ description = fragment.getString(R.string.settings_troubleshoot_test_battery_failed)
+ quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_battery_quickfix) {
+ override fun doFix() {
+ fragment.activity?.let {
+ requestDisablingBatteryOptimization(it, fragment, NotificationTroubleshootTestManager.REQ_CODE_FIX)
+ }
+ }
+ }
+ status = TestStatus.FAILED
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/vector/src/fdroid/java/im/vector/receiver/OnApplicationUpgradeReceiver.java b/vector/src/fdroid/java/im/vector/receiver/OnApplicationUpgradeReceiver.java
new file mode 100644
index 0000000000..a90ca6b1ad
--- /dev/null
+++ b/vector/src/fdroid/java/im/vector/receiver/OnApplicationUpgradeReceiver.java
@@ -0,0 +1,34 @@
+/*
+ * 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.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import timber.log.Timber;
+
+public class OnApplicationUpgradeReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Timber.d("## onReceive() : Application has been upgraded, restart event stream service.");
+
+ // Start Event stream
+ // TODO EventStreamServiceX.Companion.onApplicationUpgrade(context);
+ }
+}
diff --git a/vector/src/gplay/AndroidManifest.xml b/vector/src/gplay/AndroidManifest.xml
new file mode 100755
index 0000000000..e48db66767
--- /dev/null
+++ b/vector/src/gplay/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/gplay/google-services.json b/vector/src/gplay/google-services.json
new file mode 100644
index 0000000000..8ffc2cef44
--- /dev/null
+++ b/vector/src/gplay/google-services.json
@@ -0,0 +1,46 @@
+{
+ "project_info": {
+ "project_number": "912726360885",
+ "firebase_url": "https://vector-alpha.firebaseio.com",
+ "project_id": "vector-alpha",
+ "storage_bucket": "vector-alpha.appspot.com"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:912726360885:android:448c9b63161abc9c",
+ "android_client_info": {
+ "package_name": "im.vector.riotredesign"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
+ }
+ ],
+ "services": {
+ "analytics_service": {
+ "status": 1
+ },
+ "appinvite_service": {
+ "status": 1,
+ "other_platform_oauth_client": []
+ },
+ "ads_service": {
+ "status": 2
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/vector/src/gplay/java/im/vector/push/fcm/FcmHelper.java b/vector/src/gplay/java/im/vector/push/fcm/FcmHelper.java
new file mode 100755
index 0000000000..ff5ed9449a
--- /dev/null
+++ b/vector/src/gplay/java/im/vector/push/fcm/FcmHelper.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2014 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.push.fcm;
+
+import android.app.Activity;
+import android.content.Context;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+import com.google.android.gms.tasks.OnFailureListener;
+import com.google.android.gms.tasks.OnSuccessListener;
+import com.google.firebase.iid.FirebaseInstanceId;
+import com.google.firebase.iid.InstanceIdResult;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import im.vector.riotredesign.R;
+import timber.log.Timber;
+
+/**
+ * This class store the FCM token in SharedPrefs and ensure this token is retrieved.
+ * It has an alter ego in the fdroid variant.
+ */
+public class FcmHelper {
+ private static final String LOG_TAG = FcmHelper.class.getSimpleName();
+
+ private static final String PREFS_KEY_FCM_TOKEN = "FCM_TOKEN";
+
+ /**
+ * Retrieves the FCM registration token.
+ *
+ * @return the FCM token or null if not received from FCM
+ */
+ @Nullable
+ public static String getFcmToken(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getString(PREFS_KEY_FCM_TOKEN, null);
+ }
+
+ /**
+ * Store FCM token to the SharedPrefs
+ *
+ * @param context android context
+ * @param token the token to store
+ */
+ public static void storeFcmToken(@NonNull Context context,
+ @Nullable String token) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putString(PREFS_KEY_FCM_TOKEN, token)
+ .apply();
+ }
+
+ /**
+ * onNewToken may not be called on application upgrade, so ensure my shared pref is set
+ *
+ * @param activity the first launch Activity
+ */
+ public static void ensureFcmTokenIsRetrieved(final Activity activity) {
+ if (TextUtils.isEmpty(getFcmToken(activity))) {
+
+
+ //vfe: according to firebase doc
+ //'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
+ if (checkPlayServices(activity)) {
+ try {
+ FirebaseInstanceId.getInstance().getInstanceId()
+ .addOnSuccessListener(activity, new OnSuccessListener() {
+ @Override
+ public void onSuccess(InstanceIdResult instanceIdResult) {
+ storeFcmToken(activity, instanceIdResult.getToken());
+ }
+ })
+ .addOnFailureListener(activity, new OnFailureListener() {
+ @Override
+ public void onFailure(@NonNull Exception e) {
+ Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.getMessage());
+ }
+ });
+ } catch (Throwable e) {
+ Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.getMessage());
+ }
+ } else {
+ Toast.makeText(activity, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show();
+ Timber.e("No valid Google Play Services found. Cannot use FCM.");
+ }
+ }
+ }
+
+ /**
+ * Check the device to make sure it has the Google Play Services APK. If
+ * it doesn't, display a dialog that allows users to download the APK from
+ * the Google Play Store or enable it in the device's system settings.
+ */
+ private static boolean checkPlayServices(Activity activity) {
+ GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
+ int resultCode = apiAvailability.isGooglePlayServicesAvailable(activity);
+ if (resultCode != ConnectionResult.SUCCESS) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/vector/src/gplay/java/im/vector/push/fcm/NotificationTroubleshootTestManagerFactory.kt b/vector/src/gplay/java/im/vector/push/fcm/NotificationTroubleshootTestManagerFactory.kt
new file mode 100644
index 0000000000..be03c08ea1
--- /dev/null
+++ b/vector/src/gplay/java/im/vector/push/fcm/NotificationTroubleshootTestManagerFactory.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.push.fcm
+
+import androidx.fragment.app.Fragment
+import im.vector.fragments.troubleshoot.TestAccountSettings
+import im.vector.matrix.android.api.session.Session
+import im.vector.push.fcm.troubleshoot.TestFirebaseToken
+import im.vector.push.fcm.troubleshoot.TestPlayServices
+import im.vector.push.fcm.troubleshoot.TestTokenRegistration
+import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager
+import im.vector.riotredesign.features.settings.troubleshoot.TestBingRulesSettings
+import im.vector.riotredesign.features.settings.troubleshoot.TestDeviceSettings
+import im.vector.riotredesign.features.settings.troubleshoot.TestSystemSettings
+
+class NotificationTroubleshootTestManagerFactory {
+
+ companion object {
+ fun createTestManager(fragment: Fragment, session: Session?): NotificationTroubleshootTestManager {
+ val mgr = NotificationTroubleshootTestManager(fragment)
+ mgr.addTest(TestSystemSettings(fragment))
+ if (session != null) {
+ mgr.addTest(TestAccountSettings(fragment, session))
+ }
+ mgr.addTest(TestDeviceSettings(fragment))
+ if (session != null) {
+ mgr.addTest(TestBingRulesSettings(fragment, session))
+ }
+ mgr.addTest(TestPlayServices(fragment))
+ mgr.addTest(TestFirebaseToken(fragment))
+ mgr.addTest(TestTokenRegistration(fragment))
+ return mgr
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/vector/src/gplay/java/im/vector/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/push/fcm/VectorFirebaseMessagingService.kt
new file mode 100755
index 0000000000..1c74d09798
--- /dev/null
+++ b/vector/src/gplay/java/im/vector/push/fcm/VectorFirebaseMessagingService.kt
@@ -0,0 +1,271 @@
+/*
+ * 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.push.fcm
+
+import android.os.Handler
+import android.os.Looper
+import android.text.TextUtils
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.events.model.Event
+import im.vector.riotredesign.BuildConfig
+import im.vector.riotredesign.R
+import im.vector.riotredesign.core.preference.BingRule
+import im.vector.riotredesign.features.badge.BadgeProxy
+import im.vector.riotredesign.features.notifications.NotifiableEventResolver
+import im.vector.riotredesign.features.notifications.NotifiableMessageEvent
+import im.vector.riotredesign.features.notifications.NotificationDrawerManager
+import im.vector.riotredesign.features.notifications.SimpleNotifiableEvent
+import org.koin.android.ext.android.inject
+import timber.log.Timber
+
+/**
+ * Class extending FirebaseMessagingService.
+ */
+class VectorFirebaseMessagingService : FirebaseMessagingService() {
+
+ val notificationDrawerManager by inject()
+
+ private val notifiableEventResolver by lazy {
+ NotifiableEventResolver(this)
+ }
+
+ // UI handler
+ private val mUIHandler by lazy {
+ Handler(Looper.getMainLooper())
+ }
+
+ /**
+ * Called when message is received.
+ *
+ * @param message the message
+ */
+ override fun onMessageReceived(message: RemoteMessage?) {
+ if (message == null || message.data == null) {
+ Timber.e("## onMessageReceived() : received a null message or message with no data")
+ return
+ }
+ if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
+ Timber.i("## onMessageReceived()" + message.data.toString())
+ Timber.i("## onMessageReceived() from FCM with priority " + message.priority)
+ }
+
+ //safe guard
+ /* TODO
+ val pushManager = Matrix.getInstance(applicationContext).pushManager
+ if (!pushManager.areDeviceNotificationsAllowed()) {
+ Timber.i("## onMessageReceived() : the notifications are disabled")
+ return
+ }
+ */
+
+ //TODO if the app is in foreground, we could just ignore this. The sync loop is already going?
+ // TODO mUIHandler.post { onMessageReceivedInternal(message.data, pushManager) }
+ }
+
+ /**
+ * Called if InstanceID token is updated. This may occur if the security of
+ * the previous token had been compromised. Note that this is also called
+ * when the InstanceID token is initially generated, so this is where
+ * you retrieve the token.
+ */
+ override fun onNewToken(refreshedToken: String?) {
+ Timber.i("onNewToken: FCM Token has been updated")
+ FcmHelper.storeFcmToken(this, refreshedToken)
+ // TODO Matrix.getInstance(this)?.pushManager?.resetFCMRegistration(refreshedToken)
+ }
+
+ override fun onDeletedMessages() {
+ Timber.d("## onDeletedMessages()")
+ }
+
+ /**
+ * Internal receive method
+ *
+ * @param data Data map containing message data as key/value pairs.
+ * For Set of keys use data.keySet().
+ */
+ private fun onMessageReceivedInternal(data: Map /*, pushManager: PushManager*/) {
+ try {
+ if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
+ Timber.i("## onMessageReceivedInternal() : $data")
+ }
+ // update the badge counter
+ val unreadCount = data.get("unread")?.let { Integer.parseInt(it) } ?: 0
+ BadgeProxy.updateBadgeCount(applicationContext, unreadCount)
+
+ /* TODO
+ val session = Matrix.getInstance(applicationContext)?.defaultSession
+
+ if (VectorApp.isAppInBackground() && !pushManager.isBackgroundSyncAllowed) {
+ //Notification contains metadata and maybe data information
+ handleNotificationWithoutSyncingMode(data, session)
+ } else {
+ // Safe guard... (race?)
+ if (isEventAlreadyKnown(data["event_id"], data["room_id"])) return
+ //Catch up!!
+ EventStreamServiceX.onPushReceived(this)
+ }
+ */
+ } catch (e: Exception) {
+ Timber.e(e, "## onMessageReceivedInternal() failed : " + e.message)
+ }
+ }
+
+ // check if the event was not yet received
+ // a previous catchup might have already retrieved the notified event
+ private fun isEventAlreadyKnown(eventId: String?, roomId: String?): Boolean {
+ if (null != eventId && null != roomId) {
+ try {
+ /* TODO
+ val sessions = Matrix.getInstance(applicationContext).sessions
+
+ if (null != sessions && !sessions.isEmpty()) {
+ for (session in sessions) {
+ if (session.dataHandler?.store?.isReady == true) {
+ session.dataHandler.store?.getEvent(eventId, roomId)?.let {
+ Timber.e("## isEventAlreadyKnown() : ignore the event " + eventId
+ + " in room " + roomId + " because it is already known")
+ return true
+ }
+ }
+ }
+ }
+ */
+ } catch (e: Exception) {
+ Timber.e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined " + e.message)
+ }
+
+ }
+ return false
+ }
+
+ private fun handleNotificationWithoutSyncingMode(data: Map, session: Session?) {
+
+ if (session == null) {
+ Timber.e("## handleNotificationWithoutSyncingMode cannot find session")
+ return
+ }
+
+ // The Matrix event ID of the event being notified about.
+ // This is required if the notification is about a particular Matrix event.
+ // It may be omitted for notifications that only contain updated badge counts.
+ // This ID can and should be used to detect duplicate notification requests.
+ val eventId = data["event_id"] ?: return //Just ignore
+
+
+ val eventType = data["type"]
+ if (eventType == null) {
+ //Just add a generic unknown event
+ val simpleNotifiableEvent = SimpleNotifiableEvent(
+ session.sessionParams.credentials.userId,
+ eventId,
+ true, //It's an issue in this case, all event will bing even if expected to be silent.
+ title = getString(R.string.notification_unknown_new_event),
+ description = "",
+ type = null,
+ timestamp = System.currentTimeMillis(),
+ soundName = BingRule.ACTION_VALUE_DEFAULT,
+ isPushGatewayEvent = true
+ )
+ notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent)
+ notificationDrawerManager.refreshNotificationDrawer(null)
+
+ return
+ } else {
+
+ val event = parseEvent(data)
+ if (event?.roomId == null) {
+ //unsupported event
+ Timber.e("Received an event with no room id")
+ return
+ } else {
+
+ var notifiableEvent = notifiableEventResolver.resolveEvent(event, null, null /* TODO session.fulfillRule(event) */, session)
+
+ if (notifiableEvent == null) {
+ Timber.e("Unsupported notifiable event ${eventId}")
+ if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
+ Timber.e("--> ${event}")
+ }
+ } else {
+
+
+ if (notifiableEvent is NotifiableMessageEvent) {
+ if (TextUtils.isEmpty(notifiableEvent.senderName)) {
+ notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: ""
+ }
+ if (TextUtils.isEmpty(notifiableEvent.roomName)) {
+ notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""
+ }
+ }
+
+ notifiableEvent.isPushGatewayEvent = true
+ notifiableEvent.matrixID = session.sessionParams.credentials.userId
+ notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
+ notificationDrawerManager.refreshNotificationDrawer(null)
+ }
+ }
+ }
+ }
+
+ private fun findRoomNameBestEffort(data: Map, session: Session?): String? {
+ var roomName: String? = data["room_name"]
+ val roomId = data["room_id"]
+ if (null == roomName && null != roomId) {
+ // Try to get the room name from our store
+ /*
+ TODO
+ if (session?.dataHandler?.store?.isReady == true) {
+ val room = session.getRoom(roomId)
+ roomName = room?.getRoomDisplayName(this)
+ }
+ */
+ }
+ return roomName
+ }
+
+ /**
+ * Try to create an event from the FCM data
+ *
+ * @param data the FCM data
+ * @return the event
+ */
+ private fun parseEvent(data: Map?): Event? {
+ // accept only event with room id.
+ if (null == data || !data.containsKey("room_id") || !data.containsKey("event_id")) {
+ return null
+ }
+
+ try {
+ return Event(eventId = data["event_id"],
+ sender = data["sender"],
+ roomId = data["room_id"],
+ type = data.getValue("type"),
+ // TODO content = data.getValue("content"),
+ originServerTs = System.currentTimeMillis())
+ } catch (e: Exception) {
+ Timber.e(e, "buildEvent fails " + e.localizedMessage)
+ }
+
+ return null
+ }
+}
diff --git a/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestFirebaseToken.kt b/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestFirebaseToken.kt
new file mode 100644
index 0000000000..7b3d9be404
--- /dev/null
+++ b/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestFirebaseToken.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.push.fcm.troubleshoot
+
+import androidx.fragment.app.Fragment
+import com.google.firebase.iid.FirebaseInstanceId
+import im.vector.riotredesign.R
+import im.vector.riotredesign.core.utils.startAddGoogleAccountIntent
+import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager
+import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest
+import timber.log.Timber
+
+/*
+* Test that app can successfully retrieve a token via firebase
+ */
+class TestFirebaseToken(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_fcm_title) {
+
+ override fun perform() {
+ status = TestStatus.RUNNING
+ val activity = fragment.activity
+ if (activity != null) {
+ try {
+ FirebaseInstanceId.getInstance().instanceId
+ .addOnCompleteListener(activity) { task ->
+ if (!task.isSuccessful) {
+ val errorMsg = if (task.exception == null) "Unknown" else task.exception!!.localizedMessage
+ //Can't find where this constant is (not documented -or deprecated in docs- and all obfuscated)
+ if ("SERVICE_NOT_AVAILABLE".equals(errorMsg)) {
+ description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg)
+ } else if ("TOO_MANY_REGISTRATIONS".equals(errorMsg)) {
+ description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg)
+ } else if ("ACCOUNT_MISSING".equals(errorMsg)) {
+ description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed_account_missing, errorMsg)
+ quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_fcm_failed_account_missing_quick_fix) {
+ override fun doFix() {
+ startAddGoogleAccountIntent(fragment, NotificationTroubleshootTestManager.REQ_CODE_FIX)
+ }
+ }
+ } else {
+ description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed, errorMsg)
+ }
+ status = TestStatus.FAILED
+ } else {
+ task.result?.token?.let {
+ val tok = it.substring(0, Math.min(8, it.length)) + "********************"
+ description = fragment.getString(R.string.settings_troubleshoot_test_fcm_success, tok)
+ Timber.e("Retrieved FCM token success [$it].")
+ }
+ status = TestStatus.SUCCESS
+ }
+ }
+ } catch (e: Throwable) {
+ description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed, e.localizedMessage)
+ status = TestStatus.FAILED
+ }
+ } else {
+ status = TestStatus.FAILED
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestPlayServices.kt b/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestPlayServices.kt
new file mode 100644
index 0000000000..a5f93f4f8c
--- /dev/null
+++ b/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestPlayServices.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.push.fcm.troubleshoot
+
+import androidx.fragment.app.Fragment
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
+import im.vector.riotredesign.R
+import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest
+import timber.log.Timber
+
+/*
+* Check that the play services APK is available an up-to-date. If needed provide quick fix to install it.
+ */
+class TestPlayServices(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_play_services_title) {
+
+ override fun perform() {
+ val apiAvailability = GoogleApiAvailability.getInstance()
+ val resultCode = apiAvailability.isGooglePlayServicesAvailable(fragment.context)
+ if (resultCode == ConnectionResult.SUCCESS) {
+ quickFix = null
+ description = fragment.getString(R.string.settings_troubleshoot_test_play_services_success)
+ status = TestStatus.SUCCESS
+ } else {
+ if (apiAvailability.isUserResolvableError(resultCode)) {
+ quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_play_services_quickfix) {
+ override fun doFix() {
+ fragment.activity?.let {
+ apiAvailability.getErrorDialog(it, resultCode, 9000 /*hey does the magic number*/).show()
+ }
+ }
+ }
+ Timber.e("Play Services apk error $resultCode -> ${apiAvailability.getErrorString(resultCode)}.")
+ }
+
+ description = fragment.getString(R.string.settings_troubleshoot_test_play_services_failed, apiAvailability.getErrorString(resultCode))
+ status = TestStatus.FAILED
+ }
+ }
+
+}
+
diff --git a/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestTokenRegistration.kt b/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestTokenRegistration.kt
new file mode 100644
index 0000000000..38b7375a71
--- /dev/null
+++ b/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestTokenRegistration.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.push.fcm.troubleshoot
+
+import androidx.fragment.app.Fragment
+import im.vector.riotredesign.R
+import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest
+
+/**
+ * Force registration of the token to HomeServer
+ */
+class TestTokenRegistration(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_token_registration_title) {
+
+ override fun perform() {
+ /*
+ TODO
+ Matrix.getInstance(VectorApp.getInstance().baseContext).pushManager.forceSessionsRegistration(object : ApiCallback {
+ override fun onSuccess(info: Void?) {
+ description = fragment.getString(R.string.settings_troubleshoot_test_token_registration_success)
+ status = TestStatus.SUCCESS
+ }
+
+ override fun onNetworkError(e: Exception?) {
+ description = fragment.getString(R.string.settings_troubleshoot_test_token_registration_failed, e?.localizedMessage)
+ status = TestStatus.FAILED
+ }
+
+ override fun onMatrixError(e: MatrixError?) {
+ description = fragment.getString(R.string.settings_troubleshoot_test_token_registration_failed, e?.localizedMessage)
+ status = TestStatus.FAILED
+ }
+
+ override fun onUnexpectedError(e: Exception?) {
+ description = fragment.getString(R.string.settings_troubleshoot_test_token_registration_failed, e?.localizedMessage)
+ status = TestStatus.FAILED
+ }
+ })
+ */
+
+ status = TestStatus.FAILED
+
+ }
+
+}
\ No newline at end of file
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 9155aa00d2..9e73f7a867 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -32,6 +32,15 @@
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt
index 9c14a508c0..3fd6987997 100644
--- a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt
+++ b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt
@@ -26,6 +26,7 @@ import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
+import im.vector.riotredesign.features.notifications.NotificationDrawerManager
import org.koin.dsl.module.module
class AppModule(private val context: Context) {
@@ -64,6 +65,10 @@ class AppModule(private val context: Context) {
RoomSummaryComparator()
}
+ single {
+ NotificationDrawerManager(context)
+ }
+
factory {
Matrix.getInstance().currentSession!!
}
diff --git a/vector/src/main/java/im/vector/riotredesign/core/extensions/UrlExtensions.kt b/vector/src/main/java/im/vector/riotredesign/core/extensions/UrlExtensions.kt
new file mode 100644
index 0000000000..e087c3fdb3
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/extensions/UrlExtensions.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.core.extensions
+
+import java.net.URLEncoder
+
+/**
+ * Append param and value to a Url, using "?" or "&". Value parameter will be encoded
+ * Return this for chaining purpose
+ */
+fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder {
+ if (contains("?")) {
+ append("&")
+ } else {
+ append("?")
+ }
+
+ append(param)
+ append("=")
+ append(URLEncoder.encode(value, "utf-8"))
+
+ return this
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/extensions/ViewExtensions.kt b/vector/src/main/java/im/vector/riotredesign/core/extensions/ViewExtensions.kt
new file mode 100644
index 0000000000..99af0a3854
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/extensions/ViewExtensions.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.text.InputType
+import android.view.View
+import android.view.ViewGroup
+import android.widget.EditText
+import androidx.appcompat.widget.SearchView
+import im.vector.riotredesign.R
+
+/**
+ * Remove left margin of a SearchView
+ */
+fun SearchView.withoutLeftMargin() {
+ (findViewById(R.id.search_edit_frame))?.let {
+ val searchEditFrameParams = it.layoutParams as ViewGroup.MarginLayoutParams
+ searchEditFrameParams.leftMargin = 0
+ it.layoutParams = searchEditFrameParams
+ }
+
+ (findViewById(R.id.search_mag_icon))?.let {
+ val searchIconParams = it.layoutParams as ViewGroup.MarginLayoutParams
+ searchIconParams.leftMargin = 0
+ it.layoutParams = searchIconParams
+ }
+}
+
+fun EditText.showPassword(visible: Boolean, updateCursor: Boolean = true) {
+ if (visible) {
+ inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
+ } else {
+ inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
+ }
+ if (updateCursor) setSelection(text?.length ?: 0)
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/AddressPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/AddressPreference.kt
new file mode 100755
index 0000000000..e3fb1a079c
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/preference/AddressPreference.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ImageView
+import androidx.preference.PreferenceViewHolder
+import im.vector.riotredesign.R
+
+/**
+ * Preference used in Room setting for Room aliases
+ */
+class AddressPreference : VectorPreference {
+
+ // members
+ private var mMainAddressIconView: ImageView? = null
+ private var mIsMainIconVisible = false
+
+ /**
+ * @return the main icon view.
+ */
+ val mainIconView: View?
+ get() = mMainAddressIconView
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+
+ init {
+ widgetLayoutResource = R.layout.vector_settings_address_preference
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+
+ val view = holder.itemView
+ mMainAddressIconView = view.findViewById(R.id.main_address_icon_view)
+ mMainAddressIconView!!.visibility = if (mIsMainIconVisible) View.VISIBLE else View.GONE
+ }
+
+ /**
+ * Set the main address icon visibility.
+ *
+ * @param isVisible true to display the main icon
+ */
+ fun setMainIconVisible(isVisible: Boolean) {
+ mIsMainIconVisible = isVisible
+
+ mMainAddressIconView?.visibility = if (mIsMainIconVisible) View.VISIBLE else View.GONE
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/BingRulePreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/BingRulePreference.kt
new file mode 100755
index 0000000000..bd10f70941
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/preference/BingRulePreference.kt
@@ -0,0 +1,243 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.view.View
+import android.widget.RadioGroup
+import android.widget.TextView
+import androidx.preference.PreferenceViewHolder
+import im.vector.riotredesign.R
+
+// TODO Replace by real Bingrule class
+class BingRule(rule: BingRule) {
+ fun shouldNotNotify() = false
+ fun shouldNotify() = false
+ fun setNotify(b: Boolean) {
+
+ }
+
+ fun setHighlight(b: Boolean) {
+
+ }
+
+ fun removeNotificationSound() {
+
+ }
+
+ val ruleId: CharSequence? = null
+ var isEnabled = false
+ var notificationSound: String? = null
+ val kind: CharSequence? = null
+
+ companion object {
+ const val RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS = "TODO"
+ const val ACTION_VALUE_DEFAULT = "TODO"
+ const val KIND_UNDERRIDE = "TODO"
+ const val RULE_ID_INVITE_ME = "TODO"
+ const val RULE_ID_CALL = "TODO"
+ const val ACTION_VALUE_RING = "TODO"
+ const val RULE_ID_DISABLE_ALL = "TODO"
+ const val ACTION_DONT_NOTIFY = "TODO"
+ const val RULE_ID_CONTAIN_DISPLAY_NAME = "TODO"
+ const val RULE_ID_CONTAIN_USER_NAME = "TODO"
+ const val RULE_ID_ONE_TO_ONE_ROOM = "TODO"
+ const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = "TODO"
+ }
+
+}
+
+class BingRulePreference : VectorPreference {
+
+ /**
+ * @return the selected bing rule
+ */
+ var rule: BingRule? = null
+ private set
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+
+ init {
+ layoutResource = R.layout.vector_preference_bing_rule
+ }
+
+ /**
+ * @return the bing rule status index
+ */
+ val ruleStatusIndex: Int
+ get() {
+ if (null != rule) {
+ if (TextUtils.equals(rule!!.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
+ if (rule!!.shouldNotNotify()) {
+ return if (rule!!.isEnabled) {
+ NOTIFICATION_OFF_INDEX
+ } else {
+ NOTIFICATION_SILENT_INDEX
+ }
+ } else if (rule!!.shouldNotify()) {
+ return NOTIFICATION_NOISY_INDEX
+ }
+ }
+
+ if (rule!!.isEnabled) {
+ return if (rule!!.shouldNotNotify()) {
+ NOTIFICATION_OFF_INDEX
+ } else if (null != rule!!.notificationSound) {
+ NOTIFICATION_NOISY_INDEX
+ } else {
+ NOTIFICATION_SILENT_INDEX
+ }
+ }
+ }
+
+ return NOTIFICATION_OFF_INDEX
+ }
+
+ /**
+ * Update the bing rule.
+ *
+ * @param aBingRule
+ */
+ fun setBingRule(aBingRule: BingRule) {
+ rule = aBingRule
+ refreshSummary()
+ }
+
+ /**
+ * Refresh the summary
+ */
+ private fun refreshSummary() {
+ summary = context.getString(when (ruleStatusIndex) {
+ NOTIFICATION_OFF_INDEX -> R.string.notification_off
+ NOTIFICATION_SILENT_INDEX -> R.string.notification_silent
+ else -> R.string.notification_noisy
+ })
+ }
+
+ /**
+ * Create a bing rule with the updated required at index.
+ *
+ * @param index index
+ * @return a bing rule with the updated flags / null if there is no update
+ */
+ fun createRule(index: Int): BingRule? {
+ var rule: BingRule? = null
+
+ if (null != this.rule && index != ruleStatusIndex) {
+ rule = BingRule(this.rule!!)
+
+ if (TextUtils.equals(rule.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
+ when (index) {
+ NOTIFICATION_OFF_INDEX -> {
+ rule.isEnabled = true
+ rule.setNotify(false)
+ }
+ NOTIFICATION_SILENT_INDEX -> {
+ rule.isEnabled = false
+ rule.setNotify(false)
+ }
+ NOTIFICATION_NOISY_INDEX -> {
+ rule.isEnabled = true
+ rule.setNotify(true)
+ rule.notificationSound = BingRule.ACTION_VALUE_DEFAULT
+ }
+ }
+
+ return rule
+ }
+
+
+ if (NOTIFICATION_OFF_INDEX == index) {
+ if (TextUtils.equals(this.rule!!.kind, BingRule.KIND_UNDERRIDE)
+ || TextUtils.equals(rule.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
+ rule.setNotify(false)
+ } else {
+ rule.isEnabled = false
+ }
+ } else {
+ rule.isEnabled = true
+ rule.setNotify(true)
+ rule.setHighlight(!TextUtils.equals(this.rule!!.kind, BingRule.KIND_UNDERRIDE)
+ && !TextUtils.equals(rule.ruleId, BingRule.RULE_ID_INVITE_ME)
+ && NOTIFICATION_NOISY_INDEX == index)
+ if (NOTIFICATION_NOISY_INDEX == index) {
+ rule.notificationSound = if (TextUtils.equals(rule.ruleId, BingRule.RULE_ID_CALL))
+ BingRule.ACTION_VALUE_RING
+ else
+ BingRule.ACTION_VALUE_DEFAULT
+ } else {
+ rule.removeNotificationSound()
+ }
+ }
+ }
+
+ return rule
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+
+ holder.itemView.findViewById(android.R.id.summary)?.visibility = View.GONE
+ holder.itemView.setOnClickListener(null)
+ holder.itemView.setOnLongClickListener(null)
+
+ val radioGroup = holder.findViewById(R.id.bingPreferenceRadioGroup) as? RadioGroup
+ radioGroup?.setOnCheckedChangeListener(null)
+
+ when (ruleStatusIndex) {
+ NOTIFICATION_OFF_INDEX -> {
+ radioGroup?.check(R.id.bingPreferenceRadioBingRuleOff)
+ }
+ NOTIFICATION_SILENT_INDEX -> {
+ radioGroup?.check(R.id.bingPreferenceRadioBingRuleSilent)
+ }
+ else -> {
+ radioGroup?.check(R.id.bingPreferenceRadioBingRuleNoisy)
+ }
+ }
+
+ radioGroup?.setOnCheckedChangeListener { group, checkedId ->
+ when (checkedId) {
+ R.id.bingPreferenceRadioBingRuleOff -> {
+ onPreferenceChangeListener?.onPreferenceChange(this, NOTIFICATION_OFF_INDEX)
+ }
+ R.id.bingPreferenceRadioBingRuleSilent -> {
+ onPreferenceChangeListener?.onPreferenceChange(this, NOTIFICATION_SILENT_INDEX)
+ }
+ R.id.bingPreferenceRadioBingRuleNoisy -> {
+ onPreferenceChangeListener?.onPreferenceChange(this, NOTIFICATION_NOISY_INDEX)
+ }
+ }
+ }
+
+ }
+
+
+ companion object {
+
+ // index in mRuleStatuses
+ private const val NOTIFICATION_OFF_INDEX = 0
+ private const val NOTIFICATION_SILENT_INDEX = 1
+ private const val NOTIFICATION_NOISY_INDEX = 2
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/ProgressBarPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/ProgressBarPreference.kt
new file mode 100755
index 0000000000..de36106c28
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/preference/ProgressBarPreference.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.preference.Preference
+import im.vector.riotredesign.R
+
+class ProgressBarPreference : Preference {
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+
+ init {
+ layoutResource = R.layout.vector_settings_spinner_preference
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/RoomAvatarPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/RoomAvatarPreference.kt
new file mode 100644
index 0000000000..9c77360750
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/preference/RoomAvatarPreference.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.util.AttributeSet
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.room.Room
+
+/**
+ * Specialized class to target a Room avatar preference.
+ * Based don the avatar preference class it redefines refreshAvatar() and
+ * add the new method setConfiguration().
+ */
+class RoomAvatarPreference : UserAvatarPreference {
+
+ private var mRoom: Room? = null
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+
+ override fun refreshAvatar() {
+ if (null != mAvatarView && null != mRoom) {
+ // TODO
+ // VectorUtils.loadRoomAvatar(context, mSession, mAvatarView, mRoom)
+ }
+ }
+
+ fun setConfiguration(aSession: Session, aRoom: Room) {
+ mSession = aSession
+ mRoom = aRoom
+ refreshAvatar()
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/UserAvatarPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/UserAvatarPreference.kt
new file mode 100755
index 0000000000..0514e51610
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/preference/UserAvatarPreference.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.ImageView
+import android.widget.ProgressBar
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import im.vector.matrix.android.api.session.Session
+import im.vector.riotredesign.R
+
+open class UserAvatarPreference : Preference {
+
+ internal var mAvatarView: ImageView? = null
+ internal var mSession: Session? = null
+ private var mLoadingProgressBar: ProgressBar? = null
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+
+ init {
+ widgetLayoutResource = R.layout.vector_settings_round_avatar
+ isIconSpaceReserved = false
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+
+ mAvatarView = holder.itemView.findViewById(R.id.settings_avatar)
+ mLoadingProgressBar = holder.itemView.findViewById(R.id.avatar_update_progress_bar)
+ refreshAvatar()
+ }
+
+ open fun refreshAvatar() {
+ if (null != mAvatarView && null != mSession) {
+ // TODO
+ // val myUser = mSession!!.myUser
+ // VectorUtils.loadUserAvatar(context, mSession, mAvatarView, myUser.avatarUrl, myUser.user_id, myUser.displayname)
+ }
+ }
+
+ fun setSession(session: Session) {
+ mSession = session
+ refreshAvatar()
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorEditTextPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorEditTextPreference.kt
new file mode 100644
index 0000000000..973857b1d9
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorEditTextPreference.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.TextView
+import androidx.preference.EditTextPreference
+import androidx.preference.PreferenceViewHolder
+import im.vector.riotredesign.R
+import timber.log.Timber
+
+/**
+ * Use this class to create an EditTextPreference form code and avoid a crash (see https://code.google.com/p/android/issues/detail?id=231576)
+ */
+class VectorEditTextPreference : EditTextPreference {
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+
+ init {
+ dialogLayoutResource = R.layout.dialog_preference_edit_text
+ isIconSpaceReserved = false
+ }
+
+ // No single line for title
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ // display the title in multi-line to avoid ellipsis.
+ try {
+ holder.itemView.findViewById(android.R.id.title)?.setSingleLine(false)
+ } catch (e: Exception) {
+ Timber.e(e, "onBindView " + e.message)
+ }
+
+ super.onBindViewHolder(holder)
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorGroupPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorGroupPreference.kt
new file mode 100644
index 0000000000..553fd07ead
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorGroupPreference.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.preference.PreferenceViewHolder
+import androidx.preference.SwitchPreference
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.group.Group
+import im.vector.riotredesign.R
+
+class VectorGroupPreference : SwitchPreference {
+
+ private var mAvatarView: ImageView? = null
+
+ private var mGroup: Group? = null
+ private var mSession: Session? = null
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+
+ val createdView = holder.itemView
+
+ if (mAvatarView == null) {
+ try {
+ // insert the group avatar to the left
+ val iconView = createdView.findViewById(android.R.id.icon)
+
+ var iconViewParent = iconView.parent
+
+ while (null != iconViewParent.parent) {
+ iconViewParent = iconViewParent.parent
+ }
+
+ val inflater = LayoutInflater.from(context)
+ val layout = inflater.inflate(R.layout.vector_settings_round_group_avatar, (iconViewParent as LinearLayout), false) as FrameLayout
+ mAvatarView = layout.findViewById(R.id.settings_round_group_avatar)
+
+ val params = LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ params.gravity = Gravity.CENTER
+ layout.layoutParams = params
+ iconViewParent.addView(layout, 0)
+
+ } catch (e: Exception) {
+ mAvatarView = null
+ }
+
+ }
+
+ refreshAvatar()
+ }
+
+ /**
+ * Init the group information
+ *
+ * @param group the group
+ * @param session the session
+ */
+ fun setGroup(group: Group, session: Session) {
+ mGroup = group
+ mSession = session
+
+ refreshAvatar()
+ }
+
+ /**
+ * Refresh the avatar
+ */
+ private fun refreshAvatar() {
+ if (null != mAvatarView && null != mSession && null != mGroup) {
+ // TODO
+ // VectorUtils.loadGroupAvatar(context, mSession, mAvatarView, mGroup)
+ }
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorListPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorListPreference.kt
new file mode 100644
index 0000000000..5ea5db3664
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorListPreference.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import androidx.preference.ListPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import im.vector.riotredesign.R
+
+/**
+ * Customize ListPreference class to add a warning icon to the right side of the list.
+ */
+class VectorListPreference : ListPreference {
+
+ //
+ private var mWarningIconView: View? = null
+ private var mIsWarningIconVisible = false
+ private var mWarningIconClickListener: OnPreferenceWarningIconClickListener? = null
+
+ /**
+ * Interface definition for a callback to be invoked when the warning icon is clicked.
+ */
+ interface OnPreferenceWarningIconClickListener {
+ /**
+ * Called when a warning icon has been clicked.
+ *
+ * @param preference The Preference that was clicked.
+ */
+ fun onWarningIconClick(preference: Preference)
+ }
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
+
+ init {
+ widgetLayoutResource = R.layout.vector_settings_list_preference_with_warning
+ isIconSpaceReserved = false
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+
+ val view = holder.itemView
+
+ mWarningIconView = view.findViewById(R.id.list_preference_warning_icon)
+ mWarningIconView!!.visibility = if (mIsWarningIconVisible) View.VISIBLE else View.GONE
+
+ mWarningIconView!!.setOnClickListener {
+ if (null != mWarningIconClickListener) {
+ mWarningIconClickListener!!.onWarningIconClick(this@VectorListPreference)
+ }
+ }
+ }
+
+ /**
+ * Sets the callback to be invoked when this warning icon is clicked.
+ *
+ * @param onPreferenceWarningIconClickListener The callback to be invoked.
+ */
+ fun setOnPreferenceWarningIconClickListener(onPreferenceWarningIconClickListener: OnPreferenceWarningIconClickListener) {
+ mWarningIconClickListener = onPreferenceWarningIconClickListener
+ }
+
+ /**
+ * Set the warning icon visibility.
+ *
+ * @param isVisible to display the icon
+ */
+ fun setWarningIconVisible(isVisible: Boolean) {
+ mIsWarningIconVisible = isVisible
+
+ if (null != mWarningIconView) {
+ mWarningIconView!!.visibility = if (mIsWarningIconVisible) View.VISIBLE else View.GONE
+ }
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreference.kt
new file mode 100755
index 0000000000..6b46a6332d
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreference.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.preference
+
+import android.animation.Animator
+import android.animation.ArgbEvaluator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Color
+import android.graphics.Typeface
+import android.util.AttributeSet
+import android.view.View
+import android.widget.TextView
+import androidx.core.animation.doOnEnd
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import im.vector.riotredesign.R
+import im.vector.riotredesign.features.themes.ThemeUtils
+import timber.log.Timber
+
+
+/**
+ * create a Preference with a dedicated click/long click methods.
+ * It also allow the title to be displayed on several lines
+ */
+open class VectorPreference : Preference {
+
+ var mTypeface = Typeface.NORMAL
+
+ // long press listener
+ /**
+ * Returns the callback to be invoked when this Preference is long clicked.
+ *
+ * @return The callback to be invoked.
+ */
+ /**
+ * Sets the callback to be invoked when this Preference is long clicked.
+ *
+ * @param onPreferenceLongClickListener The callback to be invoked.
+ */
+ var onPreferenceLongClickListener: OnPreferenceLongClickListener? = null
+
+ /**
+ * Interface definition for a callback to be invoked when a preference is
+ * long clicked.
+ */
+ interface OnPreferenceLongClickListener {
+ /**
+ * Called when a Preference has been clicked.
+ *
+ * @param preference The Preference that was clicked.
+ * @return True if the click was handled.
+ */
+ fun onPreferenceLongClick(preference: Preference): Boolean
+ }
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+
+ init {
+ isIconSpaceReserved = false
+ }
+
+ var isHighlighted = false
+ set(value) {
+ field = value
+ notifyChanged()
+ }
+
+ var currentHighlightAnimator: Animator? = null
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ val itemView = holder.itemView
+ addClickListeners(itemView)
+
+ // display the title in multi-line to avoid ellipsis.
+ try {
+ val title = itemView.findViewById(android.R.id.title)
+ val summary = itemView.findViewById(android.R.id.summary)
+ if (title != null) {
+ title.setSingleLine(false)
+ title.setTypeface(null, mTypeface)
+ }
+
+ if (title !== summary) {
+ summary.setTypeface(null, mTypeface)
+ }
+
+ //cancel existing animation (find a way to resume if happens during anim?)
+ currentHighlightAnimator?.cancel()
+ if (isHighlighted) {
+ val colorFrom = Color.TRANSPARENT
+ val colorTo = ThemeUtils.getColor(itemView.context, R.attr.colorAccent)
+ currentHighlightAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorFrom, colorTo).apply {
+ duration = 250 // milliseconds
+ addUpdateListener { animator ->
+ itemView?.setBackgroundColor(animator.animatedValue as Int)
+ }
+ doOnEnd {
+ currentHighlightAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorTo, colorFrom).apply {
+ duration = 250 // milliseconds
+ addUpdateListener { animator ->
+ itemView?.setBackgroundColor(animator.animatedValue as Int)
+ }
+ doOnEnd {
+ isHighlighted = false
+ }
+ start()
+ }
+ }
+ startDelay = 200
+ start()
+ }
+ } else {
+ itemView.setBackgroundColor(Color.TRANSPARENT)
+ }
+
+ } catch (e: Exception) {
+ Timber.e(LOG_TAG, "onBindView " + e.message, e)
+ }
+
+ super.onBindViewHolder(holder)
+ }
+
+ /**
+ * @param view
+ */
+ private fun addClickListeners(view: View) {
+ view.setOnLongClickListener {
+ if (null != onPreferenceLongClickListener) {
+ onPreferenceLongClickListener!!.onPreferenceLongClick(this@VectorPreference)
+ } else false
+ }
+
+ view.setOnClickListener {
+ // call only the click listener
+ if (onPreferenceClickListener != null) {
+ onPreferenceClickListener.onPreferenceClick(this@VectorPreference)
+ }
+ }
+ }
+
+ companion object {
+ private val LOG_TAG = VectorPreference::class.java.simpleName
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreferenceCategory.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreferenceCategory.kt
new file mode 100644
index 0000000000..ebe49127d8
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreferenceCategory.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.graphics.Typeface
+import android.util.AttributeSet
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.preference.PreferenceCategory
+import androidx.preference.PreferenceViewHolder
+
+/**
+ * Customize PreferenceCategory class to redefine some attributes.
+ */
+class VectorPreferenceCategory : PreferenceCategory {
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
+
+ init {
+ isIconSpaceReserved = false
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+
+ val titleTextView = holder.itemView.findViewById(android.R.id.title)
+
+ titleTextView?.setTypeface(null, Typeface.BOLD)
+
+ // "isIconSpaceReserved = false" does not work for preference category, so remove the padding
+ (titleTextView?.parent as? ViewGroup)?.setPadding(0, 0, 0, 0)
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreferenceDivider.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreferenceDivider.kt
new file mode 100644
index 0000000000..7cb1ec4cdf
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreferenceDivider.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.preference.Preference
+import im.vector.riotredesign.R
+
+/**
+ * Divider for Preference screen
+ */
+class VectorPreferenceDivider @JvmOverloads constructor(context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0
+) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+
+ init {
+ layoutResource = R.layout.vector_preference_divider
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorSwitchPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorSwitchPreference.kt
new file mode 100644
index 0000000000..27af34551b
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorSwitchPreference.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.TextView
+import androidx.preference.PreferenceViewHolder
+import androidx.preference.SwitchPreference
+
+/**
+ * Switch preference with title on multiline (only used in XML)
+ */
+class VectorSwitchPreference : SwitchPreference {
+
+ // Note: @JvmOverload does not work here...
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
+
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(context: Context) : super(context)
+
+ init {
+ isIconSpaceReserved = false
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ // display the title in multi-line to avoid ellipsis.
+ holder.itemView.findViewById(android.R.id.title)?.setSingleLine(false)
+
+ super.onBindViewHolder(holder)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/CallService.kt b/vector/src/main/java/im/vector/riotredesign/core/services/CallService.kt
new file mode 100644
index 0000000000..cd9f1329e7
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/services/CallService.kt
@@ -0,0 +1,207 @@
+/*
+ * 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.core.services
+
+import android.content.Context
+import android.content.Intent
+import androidx.core.content.ContextCompat
+import im.vector.riotredesign.features.notifications.NotificationUtils
+import timber.log.Timber
+
+/**
+ * Foreground service to manage calls
+ */
+class CallService : VectorService() {
+
+ /**
+ * call in progress (foreground notification)
+ */
+ private var mCallIdInProgress: String? = null
+
+ /**
+ * incoming (foreground notification)
+ */
+ private var mIncomingCallId: String? = null
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent == null) {
+ // Service started again by the system.
+ // TODO What do we do here?
+ return START_STICKY
+ }
+
+ when (intent.action) {
+ ACTION_INCOMING_CALL -> displayIncomingCallNotification(intent)
+ ACTION_PENDING_CALL -> displayCallInProgressNotification(intent)
+ ACTION_NO_ACTIVE_CALL -> hideCallNotifications()
+ else ->
+ // Should not happen
+ myStopSelf()
+ }
+
+ // We want the system to restore the service if killed
+ return START_STICKY
+ }
+
+ //================================================================================
+ // Call notification management
+ //================================================================================
+
+ /**
+ * Display a permanent notification when there is an incoming call.
+ *
+ * @param session the session
+ * @param isVideo true if this is a video call, false for voice call
+ * @param room the room
+ * @param callId the callId
+ */
+ private fun displayIncomingCallNotification(intent: Intent) {
+ Timber.d("displayIncomingCallNotification")
+
+ // TODO
+ /*
+
+ // the incoming call in progress is already displayed
+ if (!TextUtils.isEmpty(mIncomingCallId)) {
+ Timber.d("displayIncomingCallNotification : the incoming call in progress is already displayed")
+ } else if (!TextUtils.isEmpty(mCallIdInProgress)) {
+ Timber.d("displayIncomingCallNotification : a 'call in progress' notification is displayed")
+ } else if (null == CallsManager.getSharedInstance().activeCall) {
+ val callId = intent.getStringExtra(EXTRA_CALL_ID)
+
+ Timber.d("displayIncomingCallNotification : display the dedicated notification")
+ val notification = NotificationUtils.buildIncomingCallNotification(
+ this,
+ intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
+ intent.getStringExtra(EXTRA_ROOM_NAME),
+ intent.getStringExtra(EXTRA_MATRIX_ID),
+ callId)
+ startForeground(NOTIFICATION_ID, notification)
+
+ mIncomingCallId = callId
+
+ // turn the screen on for 3 seconds
+ if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) {
+ try {
+ val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
+ val wl = pm.newWakeLock(
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP,
+ CallService::class.java.simpleName)
+ wl.acquire(3000)
+ wl.release()
+ } catch (re: RuntimeException) {
+ Timber.e(re, "displayIncomingCallNotification : failed to turn screen on ")
+ }
+
+ }
+ } else {
+ Timber.i("displayIncomingCallNotification : do not display the incoming call notification because there is a pending call")
+ }// test if there is no active call
+ */
+ }
+
+ /**
+ * Display a call in progress notification.
+ */
+ private fun displayCallInProgressNotification(intent: Intent) {
+ val callId = intent.getStringExtra(EXTRA_CALL_ID)
+
+ val notification = NotificationUtils.buildPendingCallNotification(applicationContext,
+ intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
+ intent.getStringExtra(EXTRA_ROOM_NAME),
+ intent.getStringExtra(EXTRA_ROOM_ID),
+ intent.getStringExtra(EXTRA_MATRIX_ID),
+ callId)
+
+ startForeground(NOTIFICATION_ID, notification)
+
+ mCallIdInProgress = callId
+ }
+
+ /**
+ * Hide the permanent call notifications
+ */
+ private fun hideCallNotifications() {
+ val notification = NotificationUtils.buildCallEndedNotification(applicationContext)
+
+ // It's mandatory to startForeground to avoid crash
+ startForeground(NOTIFICATION_ID, notification)
+
+ myStopSelf()
+ }
+
+ companion object {
+ private const val NOTIFICATION_ID = 6480
+
+ private const val ACTION_INCOMING_CALL = "im.vector.riotredesign.core.services.CallService.INCOMING_CALL"
+ private const val ACTION_PENDING_CALL = "im.vector.riotredesign.core.services.CallService.PENDING_CALL"
+ private const val ACTION_NO_ACTIVE_CALL = "im.vector.riotredesign.core.services.CallService.NO_ACTIVE_CALL"
+
+ private const val EXTRA_IS_VIDEO = "EXTRA_IS_VIDEO"
+ private const val EXTRA_ROOM_NAME = "EXTRA_ROOM_NAME"
+ private const val EXTRA_ROOM_ID = "EXTRA_ROOM_ID"
+ private const val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID"
+ private const val EXTRA_CALL_ID = "EXTRA_CALL_ID"
+
+ fun onIncomingCall(context: Context,
+ isVideo: Boolean,
+ roomName: String,
+ roomId: String,
+ matrixId: String,
+ callId: String) {
+ val intent = Intent(context, CallService::class.java)
+ .apply {
+ action = ACTION_INCOMING_CALL
+ putExtra(EXTRA_IS_VIDEO, isVideo)
+ putExtra(EXTRA_ROOM_NAME, roomName)
+ putExtra(EXTRA_ROOM_ID, roomId)
+ putExtra(EXTRA_MATRIX_ID, matrixId)
+ putExtra(EXTRA_CALL_ID, callId)
+ }
+
+ ContextCompat.startForegroundService(context, intent)
+ }
+
+ fun onPendingCall(context: Context,
+ isVideo: Boolean,
+ roomName: String,
+ roomId: String,
+ matrixId: String,
+ callId: String) {
+ val intent = Intent(context, CallService::class.java)
+ .apply {
+ action = ACTION_PENDING_CALL
+ putExtra(EXTRA_IS_VIDEO, isVideo)
+ putExtra(EXTRA_ROOM_NAME, roomName)
+ putExtra(EXTRA_ROOM_ID, roomId)
+ putExtra(EXTRA_MATRIX_ID, matrixId)
+ putExtra(EXTRA_CALL_ID, callId)
+ }
+
+ ContextCompat.startForegroundService(context, intent)
+ }
+
+ fun onNoActiveCall(context: Context) {
+ val intent = Intent(context, CallService::class.java)
+ .apply {
+ action = ACTION_NO_ACTIVE_CALL
+ }
+
+ ContextCompat.startForegroundService(context, intent)
+ }
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/EventStreamServiceX.kt b/vector/src/main/java/im/vector/riotredesign/core/services/EventStreamServiceX.kt
new file mode 100644
index 0000000000..306063297e
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/services/EventStreamServiceX.kt
@@ -0,0 +1,583 @@
+/*
+ * 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.core.services
+
+import android.content.Context
+import android.content.Intent
+import androidx.core.content.ContextCompat
+import androidx.work.Constraints
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.events.model.Event
+import im.vector.riotredesign.R
+import im.vector.riotredesign.features.notifications.NotifiableEventResolver
+import im.vector.riotredesign.features.notifications.NotificationUtils
+import org.koin.android.ext.android.inject
+import timber.log.Timber
+import java.util.concurrent.TimeUnit
+
+/**
+ * A service in charge of controlling whether the event stream is running or not.
+ *
+ * It manages messages notifications displayed to the end user.
+ */
+class EventStreamServiceX : VectorService() {
+
+ /**
+ * Managed session (no multi session for Riot)
+ */
+ private val mSession by inject()
+
+ /**
+ * Set to true to simulate a push immediately when service is destroyed
+ */
+ private var mSimulatePushImmediate = false
+
+ /**
+ * The current state.
+ */
+ private var serviceState = ServiceState.INIT
+ set(newServiceState) {
+ Timber.i("setServiceState from $field to $newServiceState")
+ field = newServiceState
+ }
+
+ /**
+ * Push manager
+ */
+ // TODO private var mPushManager: PushManager? = null
+
+ private var mNotifiableEventResolver: NotifiableEventResolver? = null
+
+ /**
+ * Live events listener
+ */
+ /* TODO
+ private val mEventsListener = object : MXEventListener() {
+ override fun onBingEvent(event: Event, roomState: RoomState, bingRule: BingRule) {
+ if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
+ Timber.i("%%%%%%%% MXEventListener: the event $event")
+ }
+
+ Timber.i("prepareNotification : " + event.eventId + " in " + roomState.roomId)
+ val session = Matrix.getMXSession(applicationContext, event.matrixId)
+
+ // invalid session ?
+ // should never happen.
+ // But it could be triggered because of multi accounts management.
+ // The dedicated account is removing but some pushes are still received.
+ if (null == session || !session.isAlive) {
+ Timber.i("prepareNotification : don't bing - no session")
+ return
+ }
+
+ if (EventType.CALL_INVITE == event.type) {
+ handleCallInviteEvent(event)
+ return
+ }
+
+
+ val notifiableEvent = mNotifiableEventResolver!!.resolveEvent(event, roomState, bingRule, session)
+ if (notifiableEvent != null) {
+ VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
+ }
+ }
+
+ override fun onLiveEventsChunkProcessed(fromToken: String, toToken: String) {
+ Timber.i("%%%%%%%% MXEventListener: onLiveEventsChunkProcessed[$fromToken->$toToken]")
+
+ VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(OutdatedEventDetector(this@EventStreamServiceX))
+
+ // do not suspend the application if there is some active calls
+ if (ServiceState.CATCHUP == serviceState) {
+ val hasActiveCalls = mSession?.mCallsManager?.hasActiveCalls() == true
+
+ // if there are some active calls, the catchup should not be stopped.
+ // because an user could answer to a call from another device.
+ // there will no push because it is his own message.
+ // so, the client has no choice to catchup until the ring is shutdown
+ if (hasActiveCalls) {
+ Timber.i("onLiveEventsChunkProcessed : Catchup again because there are active calls")
+ catchup(false)
+ } else if (ServiceState.CATCHUP == serviceState) {
+ Timber.i("onLiveEventsChunkProcessed : no Active call")
+ CallsManager.getSharedInstance().checkDeadCalls()
+ stop()
+ }
+ }
+ }
+ }
+ */
+
+ /**
+ * Service internal state
+ */
+ private enum class ServiceState {
+ // Initial state
+ INIT,
+ // Service is started for a Catchup. Once the catchup is finished the service will be stopped
+ CATCHUP,
+ // Service is started, and session is monitored
+ STARTED
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ // Cancel any previous worker
+ cancelAnySimulatedPushSchedule()
+
+ // no intent : restarted by Android
+ if (null == intent) {
+ // Cannot happen anymore
+ Timber.e("onStartCommand : null intent")
+ myStopSelf()
+ return START_NOT_STICKY
+ }
+
+ val action = intent.action
+
+ Timber.i("onStartCommand with action : $action (current state $serviceState)")
+
+ // Manage foreground notification
+ when (action) {
+ ACTION_BOOT_COMPLETE,
+ ACTION_APPLICATION_UPGRADE,
+ ACTION_SIMULATED_PUSH_RECEIVED -> {
+ // Display foreground notification
+ Timber.i("startForeground")
+ val notification = NotificationUtils.buildForegroundServiceNotification(this, R.string.notification_sync_in_progress)
+ startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
+ }
+ ACTION_GO_TO_FOREGROUND -> {
+ // Stop foreground notification display
+ Timber.i("stopForeground")
+ stopForeground(true)
+ }
+ }
+
+ if (null == mSession) {
+ Timber.e("onStartCommand : no sessions")
+ myStopSelf()
+ return START_NOT_STICKY
+ }
+
+ when (action) {
+ ACTION_START,
+ ACTION_GO_TO_FOREGROUND ->
+ when (serviceState) {
+ ServiceState.INIT ->
+ start(false)
+ ServiceState.CATCHUP ->
+ // A push has been received before, just change state, to avoid stopping the service when catchup is over
+ serviceState = ServiceState.STARTED
+ ServiceState.STARTED -> {
+ // Nothing to do
+ }
+ }
+ ACTION_STOP,
+ ACTION_GO_TO_BACKGROUND,
+ ACTION_LOGOUT ->
+ stop()
+ ACTION_PUSH_RECEIVED,
+ ACTION_SIMULATED_PUSH_RECEIVED ->
+ when (serviceState) {
+ ServiceState.INIT ->
+ start(true)
+ ServiceState.CATCHUP ->
+ catchup(true)
+ ServiceState.STARTED ->
+ // Nothing to do
+ Unit
+ }
+ ACTION_PUSH_UPDATE -> pushStatusUpdate()
+ ACTION_BOOT_COMPLETE -> {
+ // No FCM only
+ mSimulatePushImmediate = true
+ stop()
+ }
+ ACTION_APPLICATION_UPGRADE -> {
+ // FDroid only
+ catchup(true)
+ }
+ else -> {
+ // Should not happen
+ }
+ }
+
+ // We don't want the service to be restarted automatically by the System
+ return START_NOT_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+
+ // Schedule worker?
+ scheduleSimulatedPushIfNeeded()
+ }
+
+ /**
+ * Tell the WorkManager to cancel any schedule of push simulation
+ */
+ private fun cancelAnySimulatedPushSchedule() {
+ WorkManager.getInstance().cancelAllWorkByTag(PUSH_SIMULATOR_REQUEST_TAG)
+ }
+
+ /**
+ * Configure the WorkManager to schedule a simulated push, if necessary
+ */
+ private fun scheduleSimulatedPushIfNeeded() {
+ if (shouldISimulatePush()) {
+ val delay = if (mSimulatePushImmediate) 0 else 60_000 // TODO mPushManager?.backgroundSyncDelay ?: let { 60_000 }
+ Timber.i("## service is schedule to restart in $delay millis, if network is connected")
+
+ val pushSimulatorRequest = OneTimeWorkRequestBuilder()
+ .setInitialDelay(delay.toLong(), TimeUnit.MILLISECONDS)
+ .setConstraints(Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build())
+ .addTag(PUSH_SIMULATOR_REQUEST_TAG)
+ .build()
+
+ WorkManager.getInstance().let {
+ // Cancel any previous worker
+ it.cancelAllWorkByTag(PUSH_SIMULATOR_REQUEST_TAG)
+ it.enqueue(pushSimulatorRequest)
+ }
+ }
+ }
+
+ /**
+ * Start the even stream.
+ *
+ * @param session the session
+ */
+ private fun startEventStream(session: Session) {
+ /* TODO
+ // resume if it was only suspended
+ if (null != session.currentSyncToken) {
+ session.resumeEventStream()
+ } else {
+ session.startEventStream(store?.eventStreamToken)
+ }
+ */
+ }
+
+ /**
+ * Monitor the provided session.
+ *
+ * @param session the session
+ */
+ private fun monitorSession(session: Session) {
+ /* TODO
+ session.dataHandler.addListener(mEventsListener)
+ CallsManager.getSharedInstance().addSession(session)
+
+ val store = session.dataHandler.store
+
+ // the store is ready (no data loading in progress...)
+ if (store!!.isReady) {
+ startEventStream(session, store)
+ } else {
+ // wait that the store is ready before starting the events stream
+ store.addMXStoreListener(object : MXStoreListener() {
+ override fun onStoreReady(accountId: String) {
+ startEventStream(session, store)
+
+ store.removeMXStoreListener(this)
+ }
+
+ override fun onStoreCorrupted(accountId: String, description: String) {
+ // start a new initial sync
+ if (null == store.eventStreamToken) {
+ startEventStream(session, store)
+ } else {
+ // the data are out of sync
+ Matrix.getInstance(applicationContext)!!.reloadSessions(applicationContext)
+ }
+
+ store.removeMXStoreListener(this)
+ }
+
+ override fun onStoreOOM(accountId: String, description: String) {
+ val uiHandler = Handler(mainLooper)
+
+ uiHandler.post {
+ Toast.makeText(applicationContext, "$accountId : $description", Toast.LENGTH_LONG).show()
+ Matrix.getInstance(applicationContext)!!.reloadSessions(applicationContext)
+ }
+ }
+ })
+
+ store.open()
+ }
+ */
+ }
+
+ /**
+ * internal start.
+ */
+ private fun start(forPush: Boolean) {
+ val applicationContext = applicationContext
+ // TODO mPushManager = Matrix.getInstance(applicationContext)!!.pushManager
+ mNotifiableEventResolver = NotifiableEventResolver(applicationContext)
+
+ monitorSession(mSession!!)
+
+ serviceState = if (forPush) {
+ ServiceState.CATCHUP
+ } else {
+ ServiceState.STARTED
+ }
+ }
+
+ /**
+ * internal stop.
+ */
+ private fun stop() {
+ Timber.i("## stop(): the service is stopped")
+
+ /* TODO
+ if (null != mSession && mSession!!.isAlive) {
+ mSession!!.stopEventStream()
+ mSession!!.dataHandler.removeListener(mEventsListener)
+ CallsManager.getSharedInstance().removeSession(mSession)
+ }
+ mSession = null
+ */
+
+ // Stop the service
+ myStopSelf()
+ }
+
+ /**
+ * internal catchup method.
+ *
+ * @param checkState true to check if the current state allow to perform a catchup
+ */
+ private fun catchup(checkState: Boolean) {
+ var canCatchup = true
+
+ if (!checkState) {
+ Timber.i("catchup without checking serviceState ")
+ } else {
+ Timber.i("catchup with serviceState " + serviceState + " CurrentActivity ") // TODO + VectorApp.getCurrentActivity())
+
+ /* TODO
+ // the catchup should only be done
+ // 1- the serviceState is in catchup : the event stream might have gone to sleep between two catchups
+ // 2- the thread is suspended
+ // 3- the application has been launched by a push so there is no displayed activity
+ canCatchup = (serviceState == ServiceState.CATCHUP
+ //|| (serviceState == ServiceState.PAUSE)
+ || ServiceState.STARTED == serviceState && null == VectorApp.getCurrentActivity())
+ */
+ }
+
+ if (canCatchup) {
+ if (mSession != null) {
+ // TODO mSession!!.catchupEventStream()
+ } else {
+ Timber.i("catchup no session")
+ }
+
+ serviceState = ServiceState.CATCHUP
+ } else {
+ Timber.i("No catchup is triggered because there is already a running event thread")
+ }
+ }
+
+ /**
+ * The push status has been updated (i.e disabled or enabled).
+ * TODO Useless now?
+ */
+ private fun pushStatusUpdate() {
+ Timber.i("## pushStatusUpdate")
+ }
+
+ /* ==========================================================================================
+ * Push simulator
+ * ========================================================================================== */
+
+ /**
+ * @return true if the FCM is disable or not setup, user allowed background sync, user wants notification
+ */
+ private fun shouldISimulatePush(): Boolean {
+ return false
+
+ /* TODO
+
+ if (Matrix.getInstance(applicationContext)?.defaultSession == null) {
+ Timber.i("## shouldISimulatePush: NO: no session")
+
+ return false
+ }
+
+ mPushManager?.let { pushManager ->
+ if (pushManager.useFcm()
+ && !TextUtils.isEmpty(pushManager.currentRegistrationToken)
+ && pushManager.isServerRegistered) {
+ // FCM is ok
+ Timber.i("## shouldISimulatePush: NO: FCM is up")
+ return false
+ }
+
+ if (!pushManager.isBackgroundSyncAllowed) {
+ // User has disabled background sync
+ Timber.i("## shouldISimulatePush: NO: background sync not allowed")
+ return false
+ }
+
+ if (!pushManager.areDeviceNotificationsAllowed()) {
+ // User does not want notifications
+ Timber.i("## shouldISimulatePush: NO: user does not want notification")
+ return false
+ }
+ }
+
+ // Lets simulate push
+ Timber.i("## shouldISimulatePush: YES")
+ return true
+ */
+ }
+
+
+ //================================================================================
+ // Call management
+ //================================================================================
+
+ private fun handleCallInviteEvent(event: Event) {
+ /*
+ TODO
+ val session = Matrix.getMXSession(applicationContext, event.matrixId)
+
+ // invalid session ?
+ // should never happen.
+ // But it could be triggered because of multi accounts management.
+ // The dedicated account is removing but some pushes are still received.
+ if (null == session || !session.isAlive) {
+ Timber.d("prepareCallNotification : don't bing - no session")
+ return
+ }
+
+ val room: Room? = session.dataHandler.getRoom(event.roomId)
+
+ // invalid room ?
+ if (null == room) {
+ Timber.i("prepareCallNotification : don't bing - the room does not exist")
+ return
+ }
+
+ var callId: String? = null
+ var isVideo = false
+
+ try {
+ callId = event.contentAsJsonObject?.get("call_id")?.asString
+
+ // Check if it is a video call
+ val offer = event.contentAsJsonObject?.get("offer")?.asJsonObject
+ val sdp = offer?.get("sdp")
+ val sdpValue = sdp?.asString
+
+ isVideo = sdpValue?.contains("m=video") == true
+ } catch (e: Exception) {
+ Timber.e("prepareNotification : getContentAsJsonObject " + e.message, e)
+ }
+
+ if (!TextUtils.isEmpty(callId)) {
+ CallService.onIncomingCall(this,
+ isVideo,
+ room.getRoomDisplayName(this),
+ room.roomId,
+ session.myUserId!!,
+ callId!!)
+ }
+ */
+ }
+
+ companion object {
+ private const val PUSH_SIMULATOR_REQUEST_TAG = "PUSH_SIMULATOR_REQUEST_TAG"
+
+ private const val ACTION_START = "im.vector.riotredesign.core.services.EventStreamServiceX.START"
+ private const val ACTION_LOGOUT = "im.vector.riotredesign.core.services.EventStreamServiceX.LOGOUT"
+ private const val ACTION_GO_TO_FOREGROUND = "im.vector.riotredesign.core.services.EventStreamServiceX.GO_TO_FOREGROUND"
+ private const val ACTION_GO_TO_BACKGROUND = "im.vector.riotredesign.core.services.EventStreamServiceX.GO_TO_BACKGROUND"
+ private const val ACTION_PUSH_UPDATE = "im.vector.riotredesign.core.services.EventStreamServiceX.PUSH_UPDATE"
+ private const val ACTION_PUSH_RECEIVED = "im.vector.riotredesign.core.services.EventStreamServiceX.PUSH_RECEIVED"
+ private const val ACTION_SIMULATED_PUSH_RECEIVED = "im.vector.riotredesign.core.services.EventStreamServiceX.SIMULATED_PUSH_RECEIVED"
+ private const val ACTION_STOP = "im.vector.riotredesign.core.services.EventStreamServiceX.STOP"
+ private const val ACTION_BOOT_COMPLETE = "im.vector.riotredesign.core.services.EventStreamServiceX.BOOT_COMPLETE"
+ private const val ACTION_APPLICATION_UPGRADE = "im.vector.riotredesign.core.services.EventStreamServiceX.APPLICATION_UPGRADE"
+
+ /* ==========================================================================================
+ * Events sent to the service
+ * ========================================================================================== */
+
+ fun onApplicationStarted(context: Context) {
+ sendAction(context, ACTION_START)
+ }
+
+ fun onLogout(context: Context) {
+ sendAction(context, ACTION_LOGOUT)
+ }
+
+ fun onAppGoingToForeground(context: Context) {
+ sendAction(context, ACTION_GO_TO_FOREGROUND)
+ }
+
+ fun onAppGoingToBackground(context: Context) {
+ sendAction(context, ACTION_GO_TO_BACKGROUND)
+ }
+
+ fun onPushUpdate(context: Context) {
+ sendAction(context, ACTION_PUSH_UPDATE)
+ }
+
+ fun onPushReceived(context: Context) {
+ sendAction(context, ACTION_PUSH_RECEIVED)
+ }
+
+ fun onSimulatedPushReceived(context: Context) {
+ sendAction(context, ACTION_SIMULATED_PUSH_RECEIVED, true)
+ }
+
+ fun onApplicationStopped(context: Context) {
+ sendAction(context, ACTION_STOP)
+ }
+
+ fun onBootComplete(context: Context) {
+ sendAction(context, ACTION_BOOT_COMPLETE, true)
+ }
+
+ fun onApplicationUpgrade(context: Context) {
+ sendAction(context, ACTION_APPLICATION_UPGRADE, true)
+ }
+
+ private fun sendAction(context: Context, action: String, foreground: Boolean = false) {
+ Timber.i("sendAction $action")
+
+ val intent = Intent(context, EventStreamServiceX::class.java)
+ intent.action = action
+
+ if (foreground) {
+ ContextCompat.startForegroundService(context, intent)
+ } else {
+ context.startService(intent)
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/PushSimulatorWorker.kt b/vector/src/main/java/im/vector/riotredesign/core/services/PushSimulatorWorker.kt
new file mode 100644
index 0000000000..d3f93f3280
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/services/PushSimulatorWorker.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.core.services
+
+import android.content.Context
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+
+/**
+ * This class simulate push event when FCM is not working/disabled
+ */
+class PushSimulatorWorker(val context: Context,
+ workerParams: WorkerParameters) : Worker(context, workerParams) {
+
+ override fun doWork(): Result {
+ // Simulate a Push
+ EventStreamServiceX.onSimulatedPushReceived(context)
+
+ // Indicate whether the task finished successfully with the Result
+ return Result.success()
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/VectorService.kt b/vector/src/main/java/im/vector/riotredesign/core/services/VectorService.kt
new file mode 100644
index 0000000000..4b4c500d01
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/services/VectorService.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.core.services
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import timber.log.Timber
+
+/**
+ * Parent class for all services
+ */
+abstract class VectorService : Service() {
+
+ /**
+ * Tells if the service self destroyed.
+ */
+ private var mIsSelfDestroyed = false
+
+ override fun onCreate() {
+ super.onCreate()
+
+ Timber.i("## onCreate() : $this")
+ }
+
+ override fun onDestroy() {
+ Timber.i("## onDestroy() : $this")
+
+ if (!mIsSelfDestroyed) {
+ Timber.w("## Destroy by the system : $this")
+ }
+
+ super.onDestroy()
+ }
+
+ protected fun myStopSelf() {
+ mIsSelfDestroyed = true
+ stopSelf()
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/RingtoneUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/RingtoneUtils.kt
new file mode 100644
index 0000000000..89c800d49e
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/utils/RingtoneUtils.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2018 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotredesign.core.utils
+
+import android.content.Context
+import android.media.Ringtone
+import android.media.RingtoneManager
+import android.net.Uri
+import android.preference.PreferenceManager
+import androidx.core.content.edit
+import im.vector.riotredesign.features.settings.PreferencesManager
+
+/**
+ * This file manages the sound ringtone for calls.
+ * It allows you to use the default Riot Ringtone, or the standard ringtone or set a different one from the available choices
+ * in Android.
+ */
+
+/**
+ * Returns a Uri object that points to a specific Ringtone.
+ *
+ * If no Ringtone was explicitly set using Riot, it will return the Uri for the current system
+ * ringtone for calls.
+ *
+ * @return the [Uri] of the currently set [Ringtone]
+ * @see Ringtone
+ */
+fun getCallRingtoneUri(context: Context): Uri? {
+ val callRingtone: String? = PreferenceManager.getDefaultSharedPreferences(context)
+ .getString(PreferencesManager.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, null)
+
+ callRingtone?.let {
+ return Uri.parse(it)
+ }
+
+ return try {
+ // Use current system notification sound for incoming calls per default (note that it can return null)
+ RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE)
+ } catch (e: SecurityException) {
+ // Ignore for now
+ null
+ }
+}
+
+/**
+ * Returns a Ringtone object that can then be played.
+ *
+ * If no Ringtone was explicitly set using Riot, it will return the current system ringtone
+ * for calls.
+ *
+ * @return the currently set [Ringtone]
+ * @see Ringtone
+ */
+fun getCallRingtone(context: Context): Ringtone? {
+ getCallRingtoneUri(context)?.let {
+ // Note that it can also return null
+ return RingtoneManager.getRingtone(context, it)
+ }
+
+ return null
+}
+
+/**
+ * Returns a String with the name of the current Ringtone.
+ *
+ * If no Ringtone was explicitly set using Riot, it will return the name of the current system
+ * ringtone for calls.
+ *
+ * @return the name of the currently set [Ringtone], or null
+ * @see Ringtone
+ */
+fun getCallRingtoneName(context: Context): String? {
+ return getCallRingtone(context)?.getTitle(context)
+}
+
+/**
+ * Sets the selected ringtone for riot calls.
+ *
+ * @param ringtoneUri
+ * @see Ringtone
+ */
+fun setCallRingtoneUri(context: Context, ringtoneUri: Uri) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit {
+ putString(PreferencesManager.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, ringtoneUri.toString())
+ }
+}
+
+/**
+ * Set using Riot default ringtone
+ */
+fun useRiotDefaultRingtone(context: Context): Boolean {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(PreferencesManager.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, true)
+}
+
+/**
+ * Ask if default Riot ringtone has to be used
+ */
+fun setUseRiotDefaultRingtone(context: Context, useRiotDefault: Boolean) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit {
+ putBoolean(PreferencesManager.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, useRiotDefault)
+ }
+}
+
diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/SecretStoringUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/SecretStoringUtils.kt
new file mode 100644
index 0000000000..e139ee61a4
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/core/utils/SecretStoringUtils.kt
@@ -0,0 +1,576 @@
+package im.vector.riotredesign.core.utils
+
+import android.content.Context
+import android.os.Build
+import android.security.KeyPairGeneratorSpec
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import androidx.annotation.RequiresApi
+import java.io.*
+import java.math.BigInteger
+import java.security.KeyPairGenerator
+import java.security.KeyStore
+import java.security.SecureRandom
+import java.util.*
+import javax.crypto.*
+import javax.crypto.spec.GCMParameterSpec
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.PBEKeySpec
+import javax.crypto.spec.SecretKeySpec
+import javax.security.auth.x500.X500Principal
+
+
+/**
+ * Offers simple methods to securely store secrets in an Android Application.
+ * The encryption keys are randomly generated and securely managed by the key store, thus your secrets
+ * are safe. You only need to remember a key alias to perform encrypt/decrypt operations.
+ *
+ * Android M++
+ * On android M+, the keystore can generates and store AES keys via API. But below API M this functionality
+ * is not available.
+ *
+ * Android [K-M[
+ * For android >=KITKAT and Older androids
+ * For older androids as a fallback we generate an AES key from the alias using PBKDF2 with random salt.
+ * The salt and iv are stored with encrypted data.
+ *
+ * Sample usage:
+ *
+ * val secret = "The answer is 42"
+ * val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias", context)
+ * //This can be stored anywhere e.g. encoded in b64 and stored in preference for example
+ *
+ * //to get back the secret, just call
+ * val kDecripted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context)
+ *
+ *
+ * You can also just use this utility to store a secret key, and use any encryption algorthim that you want.
+ *
+ * Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you
+ * add a pin or change the schema); So you might and with a useless pile of bytes.
+ */
+object SecretStoringUtils {
+
+ private const val ANDROID_KEY_STORE = "AndroidKeyStore"
+ private const val AES_MODE = "AES/GCM/NoPadding";
+ private const val RSA_MODE = "RSA/ECB/PKCS1Padding"
+
+ const val FORMAT_API_M: Byte = 0
+ const val FORMAT_1: Byte = 1
+ const val FORMAT_2: Byte = 2
+
+ val keyStore: KeyStore by lazy {
+ KeyStore.getInstance(ANDROID_KEY_STORE).apply {
+ load(null)
+ }
+ }
+
+ private val secureRandom = SecureRandom()
+
+ /**
+ * Encrypt the given secret using the android Keystore.
+ * On android >= M, will directly use the keystore to generate a symetric key
+ * On KitKat >= KitKat and = Build.VERSION_CODES.M) {
+ return encryptStringM(secret, keyAlias)
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ return encryptStringJ(secret, keyAlias, context)
+ } else {
+ return encryptForOldDevicesNotGood(secret, keyAlias)
+ }
+ }
+
+ /**
+ * Decrypt a secret that was encrypted by #securelyStoreString()
+ */
+ @Throws(Exception::class)
+ fun loadSecureSecret(encrypted: ByteArray, keyAlias: String, context: Context): String? {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return decryptStringM(encrypted, keyAlias)
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ return decryptStringJ(encrypted, keyAlias, context)
+ } else {
+ return decryptForOldDevicesNotGood(encrypted, keyAlias)
+ }
+ }
+
+ fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream, context: Context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ saveSecureObjectM(keyAlias, output, any)
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ return saveSecureObjectK(keyAlias, output, any, context)
+ } else {
+ return saveSecureObjectOldNotGood(keyAlias, output, any)
+ }
+ }
+
+ fun loadSecureSecret(inputStream: InputStream, keyAlias: String, context: Context): T? {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return loadSecureObjectM(keyAlias, inputStream)
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ return loadSecureObjectK(keyAlias, inputStream, context)
+ } else {
+ return loadSecureObjectOldNotGood(keyAlias, inputStream)
+ }
+ }
+
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ fun getOrGenerateSymmetricKeyForAlias(alias: String): SecretKey {
+ val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
+ ?.secretKey
+ if (secretKeyEntry == null) {
+ //we generate it
+ val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
+ val keyGenSpec = KeyGenParameterSpec.Builder(alias,
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ .setKeySize(128)
+ .build()
+ generator.init(keyGenSpec)
+ return generator.generateKey()
+ }
+ return secretKeyEntry
+ }
+
+
+ /*
+ Symetric Key Generation is only available in M, so before M the idea is to:
+ - Generate a pair of RSA keys;
+ - Generate a random AES key;
+ - Encrypt the AES key using the RSA public key;
+ - Store the encrypted AES
+ Generate a key pair for encryption
+ */
+ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+ fun getOrGenerateKeyPairForAlias(alias: String, context: Context): KeyStore.PrivateKeyEntry {
+ val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry)
+
+ if (privateKeyEntry != null) return privateKeyEntry
+
+ val start = Calendar.getInstance()
+ val end = Calendar.getInstance()
+ end.add(Calendar.YEAR, 30)
+
+ val spec = KeyPairGeneratorSpec.Builder(context)
+ .setAlias(alias)
+ .setSubject(X500Principal("CN=$alias"))
+ .setSerialNumber(BigInteger.TEN)
+ //.setEncryptionRequired() requires that the phone as a pin/schema
+ .setStartDate(start.time)
+ .setEndDate(end.time)
+ .build()
+ KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE).run {
+ initialize(spec)
+ generateKeyPair()
+ }
+ return (keyStore.getEntry(alias, null) as KeyStore.PrivateKeyEntry)
+
+ }
+
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ fun encryptStringM(text: String, keyAlias: String): ByteArray? {
+ val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
+
+ val cipher = Cipher.getInstance(AES_MODE)
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey)
+ val iv = cipher.iv
+ //we happen the iv to the final result
+ val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
+ return formatMMake(iv, encryptedBytes)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String {
+ val (iv, encryptedText) = formatMExtract(ByteArrayInputStream(encryptedChunk))
+
+ val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
+
+ val cipher = Cipher.getInstance(AES_MODE)
+ val spec = GCMParameterSpec(128, iv)
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
+
+ return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+ fun encryptStringJ(text: String, keyAlias: String, context: Context): ByteArray? {
+ //we generate a random symetric key
+ val key = ByteArray(16)
+ secureRandom.nextBytes(key)
+ val sKey = SecretKeySpec(key, "AES")
+
+ //we encrypt this key thanks to the key store
+ val encryptedKey = rsaEncrypt(keyAlias, key, context)
+
+ val cipher = Cipher.getInstance(AES_MODE)
+ cipher.init(Cipher.ENCRYPT_MODE, sKey)
+ val iv = cipher.iv
+ val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
+
+ return format1Make(encryptedKey, iv, encryptedBytes)
+ }
+
+ fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray {
+ val salt = ByteArray(8)
+ secureRandom.nextBytes(salt)
+ val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
+ val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)
+ val tmp = factory.generateSecret(spec)
+ val sKey = SecretKeySpec(tmp.encoded, "AES")
+
+ val cipher = Cipher.getInstance(AES_MODE)
+ cipher.init(Cipher.ENCRYPT_MODE, sKey)
+ val iv = cipher.iv
+ val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
+
+ return format2Make(salt, iv, encryptedBytes)
+ }
+
+ fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? {
+
+ val (salt, iv, encrypted) = format2Extract(ByteArrayInputStream(data))
+ val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
+ val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)
+ val tmp = factory.generateSecret(spec)
+ val sKey = SecretKeySpec(tmp.encoded, "AES")
+
+ val cipher = Cipher.getInstance(AES_MODE)
+// cipher.init(Cipher.ENCRYPT_MODE, sKey)
+// val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
+
+ val specIV = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
+ cipher.init(Cipher.DECRYPT_MODE, sKey, specIV)
+
+ return String(cipher.doFinal(encrypted), Charsets.UTF_8)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.KITKAT)
+ fun decryptStringJ(data: ByteArray, keyAlias: String, context: Context): String? {
+
+ val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data))
+
+ //we need to decrypt the key
+ val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context)
+ val cipher = Cipher.getInstance(AES_MODE)
+ val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
+ cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
+
+ return String(cipher.doFinal(encrypted), Charsets.UTF_8)
+
+ }
+
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ @Throws(IOException::class)
+ fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) {
+ val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
+
+ val cipher = Cipher.getInstance(AES_MODE)
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/)
+ val iv = cipher.iv
+
+ val bos1 = ByteArrayOutputStream()
+ ObjectOutputStream(bos1).use {
+ it.writeObject(writeObject)
+ }
+ //Have to do it like that if i encapsulate the outputstream, the cipher could fail saying reuse IV
+ val doFinal = cipher.doFinal(bos1.toByteArray())
+ output.write(FORMAT_API_M.toInt())
+ output.write(iv.size)
+ output.write(iv)
+ output.write(doFinal)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.KITKAT)
+ fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any, context: Context) {
+ //we generate a random symetric key
+ val key = ByteArray(16)
+ secureRandom.nextBytes(key)
+ val sKey = SecretKeySpec(key, "AES")
+
+ //we encrypt this key thanks to the key store
+ val encryptedKey = rsaEncrypt(keyAlias, key, context)
+
+ val cipher = Cipher.getInstance(AES_MODE)
+ cipher.init(Cipher.ENCRYPT_MODE, sKey)
+ val iv = cipher.iv
+
+ val bos1 = ByteArrayOutputStream()
+ val cos = CipherOutputStream(bos1, cipher)
+ ObjectOutputStream(cos).use {
+ it.writeObject(writeObject)
+ }
+
+ output.write(FORMAT_1.toInt())
+ output.write((encryptedKey.size and 0xFF00).shr(8))
+ output.write(encryptedKey.size and 0x00FF)
+ output.write(encryptedKey)
+ output.write(iv.size)
+ output.write(iv)
+ output.write(bos1.toByteArray())
+ }
+
+ fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) {
+ val salt = ByteArray(8)
+ secureRandom.nextBytes(salt)
+ val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
+ val tmp = factory.generateSecret(PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128))
+ val secretKey = SecretKeySpec(tmp.encoded, "AES")
+
+
+ val cipher = Cipher.getInstance(AES_MODE)
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey)
+ val iv = cipher.iv
+
+ val bos1 = ByteArrayOutputStream()
+ ObjectOutputStream(bos1).use {
+ it.writeObject(writeObject)
+ }
+ //Have to do it like that if i encapsulate the outputstream, the cipher could fail saying reuse IV
+ val doFinal = cipher.doFinal(bos1.toByteArray())
+
+ output.write(FORMAT_2.toInt())
+ output.write(salt.size)
+ output.write(salt)
+ output.write(iv.size)
+ output.write(iv)
+ output.write(doFinal)
+ }
+
+// @RequiresApi(Build.VERSION_CODES.M)
+// @Throws(IOException::class)
+// fun saveSecureObjectM(keyAlias: String, file: File, writeObject: Any) {
+// FileOutputStream(file).use {
+// saveSecureObjectM(keyAlias, it, writeObject)
+// }
+// }
+//
+// @RequiresApi(Build.VERSION_CODES.M)
+// @Throws(IOException::class)
+// fun loadSecureObjectM(keyAlias: String, file: File): T? {
+// FileInputStream(file).use {
+// return loadSecureObjectM(keyAlias, it)
+// }
+// }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ @Throws(IOException::class)
+ fun loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? {
+ val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
+
+ val format = inputStream.read()
+ assert(format.toByte() == FORMAT_API_M)
+
+ val ivSize = inputStream.read()
+ val iv = ByteArray(ivSize)
+ inputStream.read(iv, 0, ivSize)
+ val cipher = Cipher.getInstance(AES_MODE)
+ val spec = GCMParameterSpec(128, iv)
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
+
+ CipherInputStream(inputStream, cipher).use { cipherInputStream ->
+ ObjectInputStream(cipherInputStream).use {
+ val readObject = it.readObject()
+ return readObject as? T
+ }
+ }
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.KITKAT)
+ @Throws(IOException::class)
+ fun loadSecureObjectK(keyAlias: String, inputStream: InputStream, context: Context): T? {
+
+ val (encryptedKey, iv, encrypted) = format1Extract(inputStream)
+
+ //we need to decrypt the key
+ val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context)
+ val cipher = Cipher.getInstance(AES_MODE)
+ val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
+ cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
+
+ val encIS = ByteArrayInputStream(encrypted)
+
+ CipherInputStream(encIS, cipher).use { cipherInputStream ->
+ ObjectInputStream(cipherInputStream).use {
+ val readObject = it.readObject()
+ return readObject as? T
+ }
+ }
+ }
+
+ @Throws(Exception::class)
+ fun loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? {
+
+ val (salt, iv, encrypted) = format2Extract(inputStream)
+
+ val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
+ val tmp = factory.generateSecret(PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128))
+ val sKey = SecretKeySpec(tmp.encoded, "AES")
+ //we need to decrypt the key
+
+ val cipher = Cipher.getInstance(AES_MODE)
+ val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
+ cipher.init(Cipher.DECRYPT_MODE, sKey, spec)
+
+ val encIS = ByteArrayInputStream(encrypted)
+
+ CipherInputStream(encIS, cipher).use {
+ ObjectInputStream(it).use {
+ val readObject = it.readObject()
+ return readObject as? T
+ }
+ }
+ }
+
+
+ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+ @Throws(Exception::class)
+ private fun rsaEncrypt(alias: String, secret: ByteArray, context: Context): ByteArray {
+ val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context)
+ // Encrypt the text
+ val inputCipher = Cipher.getInstance(RSA_MODE)
+ inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)
+
+ val outputStream = ByteArrayOutputStream()
+ val cipherOutputStream = CipherOutputStream(outputStream, inputCipher)
+ cipherOutputStream.write(secret)
+ cipherOutputStream.close()
+
+ return outputStream.toByteArray()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+ @Throws(Exception::class)
+ private fun rsaDecrypt(alias: String, encrypted: InputStream, context: Context): ByteArray {
+ val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context)
+ val output = Cipher.getInstance(RSA_MODE)
+ output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey)
+
+ val bos = ByteArrayOutputStream()
+ CipherInputStream(encrypted, output).use {
+ it.copyTo(bos)
+ }
+
+ return bos.toByteArray()
+ }
+
+ private fun formatMExtract(bis: InputStream): Pair {
+ val format = bis.read().toByte()
+ assert(format == FORMAT_API_M)
+
+ val ivSize = bis.read()
+ val iv = ByteArray(ivSize)
+ bis.read(iv, 0, ivSize)
+
+
+ val bos = ByteArrayOutputStream()
+ var next = bis.read()
+ while (next != -1) {
+ bos.write(next)
+ next = bis.read()
+ }
+ val encrypted = bos.toByteArray()
+ return Pair(iv, encrypted)
+ }
+
+ private fun formatMMake(iv: ByteArray, data: ByteArray): ByteArray {
+ val bos = ByteArrayOutputStream(2 + iv.size + data.size)
+ bos.write(FORMAT_API_M.toInt())
+ bos.write(iv.size)
+ bos.write(iv)
+ bos.write(data)
+ return bos.toByteArray()
+ }
+
+ private fun format1Extract(bis: InputStream): Triple {
+
+ val format = bis.read()
+ assert(format.toByte() == FORMAT_1)
+
+ val keySizeBig = bis.read()
+ val keySizeLow = bis.read()
+ val encryptedKeySize = keySizeBig.shl(8) + keySizeLow
+ val encryptedKey = ByteArray(encryptedKeySize)
+ bis.read(encryptedKey)
+
+ val ivSize = bis.read()
+ val iv = ByteArray(ivSize)
+ bis.read(iv)
+
+ val bos = ByteArrayOutputStream()
+
+ var next = bis.read()
+ while (next != -1) {
+ bos.write(next)
+ next = bis.read()
+ }
+ val encrypted = bos.toByteArray()
+ return Triple(encryptedKey, iv, encrypted)
+ }
+
+ private fun format1Make(encryptedKey: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray {
+ val bos = ByteArrayOutputStream(4 + encryptedKey.size + iv.size + encryptedBytes.size)
+ bos.write(FORMAT_1.toInt())
+ bos.write((encryptedKey.size and 0xFF00).shr(8))
+ bos.write(encryptedKey.size and 0x00FF)
+ bos.write(encryptedKey)
+ bos.write(iv.size)
+ bos.write(iv)
+ bos.write(encryptedBytes)
+
+ return bos.toByteArray()
+ }
+
+ private fun format2Make(salt: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray {
+ val bos = ByteArrayOutputStream(3 + salt.size + iv.size + encryptedBytes.size)
+ bos.write(FORMAT_2.toInt())
+ bos.write(salt.size)
+ bos.write(salt)
+ bos.write(iv.size)
+ bos.write(iv)
+ bos.write(encryptedBytes)
+
+ return bos.toByteArray()
+ }
+
+ private fun format2Extract(bis: InputStream): Triple {
+
+ val format = bis.read()
+ assert(format.toByte() == FORMAT_2)
+
+ val saltSize = bis.read()
+ val salt = ByteArray(saltSize)
+ bis.read(salt)
+
+ val ivSize = bis.read()
+ val iv = ByteArray(ivSize)
+ bis.read(iv)
+
+ val bos = ByteArrayOutputStream()
+
+ var next = bis.read()
+ while (next != -1) {
+ bos.write(next)
+ next = bis.read()
+ }
+ val encrypted = bos.toByteArray()
+ return Triple(salt, iv, encrypted)
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/SystemUtils.kt
index 5cf3ca492f..d21f0d7c19 100644
--- a/vector/src/main/java/im/vector/riotredesign/core/utils/SystemUtils.kt
+++ b/vector/src/main/java/im/vector/riotredesign/core/utils/SystemUtils.kt
@@ -26,6 +26,7 @@ import android.provider.Settings
import android.widget.Toast
import androidx.fragment.app.Fragment
import im.vector.riotredesign.R
+import im.vector.riotredesign.features.notifications.supportNotificationChannels
import im.vector.riotredesign.features.settings.VectorLocale
import timber.log.Timber
import java.util.*
@@ -124,10 +125,6 @@ fun startNotificationSettingsIntent(fragment: Fragment, requestCode: Int) {
fragment.startActivityForResult(intent, requestCode)
}
-// TODO This comes from NotificationUtils
-fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
-
-
/**
* Shows notification system settings for the given channel id.
*/
@@ -184,3 +181,8 @@ fun startImportTextFromFileIntent(fragment: Fragment, requestCode: Int) {
fun Context.toast(resId: Int) {
Toast.makeText(this, resId, Toast.LENGTH_SHORT).show()
}
+
+// Not in KTX anymore
+fun Context.toast(message: String) {
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
+}
diff --git a/vector/src/main/java/im/vector/riotredesign/features/badge/BadgeProxy.kt b/vector/src/main/java/im/vector/riotredesign/features/badge/BadgeProxy.kt
new file mode 100644
index 0000000000..dd529ff5be
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/badge/BadgeProxy.kt
@@ -0,0 +1,133 @@
+/*
+ * 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.badge
+
+import android.content.Context
+import android.os.Build
+import im.vector.matrix.android.api.session.Session
+import me.leolin.shortcutbadger.ShortcutBadger
+import timber.log.Timber
+
+/**
+ * Manage application badge (displayed in the launcher)
+ */
+object BadgeProxy {
+
+ /**
+ * Badge is now managed by notification channel, so no need to use compatibility library in recent versions
+ *
+ * @return true if library ShortcutBadger can be used
+ */
+ private fun useShortcutBadger() = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
+
+ /**
+ * Update the application badge value.
+ *
+ * @param context the context
+ * @param badgeValue the new badge value
+ */
+ fun updateBadgeCount(context: Context, badgeValue: Int) {
+ if (!useShortcutBadger()) {
+ return
+ }
+
+ try {
+ ShortcutBadger.setBadge(context, badgeValue)
+ } catch (e: Exception) {
+ Timber.e(e, "## updateBadgeCount(): Exception Msg=" + e.message)
+ }
+
+ }
+
+ /**
+ * Refresh the badge count for specific configurations.
+ * The refresh is only effective if the device is:
+ * * offline * does not support FCM
+ * * FCM registration failed
+ *
Notifications rooms are parsed to track the notification count value.
+ *
+ * @param aSession session value
+ * @param aContext App context
+ */
+ fun specificUpdateBadgeUnreadCount(aSession: Session?, aContext: Context?) {
+ if (!useShortcutBadger()) {
+ return
+ }
+
+ /* TODO
+ val dataHandler: MXDataHandler
+
+ // sanity check
+ if (null == aContext || null == aSession) {
+ Timber.w("## specificUpdateBadgeUnreadCount(): invalid input null values")
+ } else {
+ dataHandler = aSession.dataHandler
+
+ if (dataHandler == null) {
+ Timber.w("## specificUpdateBadgeUnreadCount(): invalid DataHandler instance")
+ } else {
+ if (aSession.isAlive) {
+ var isRefreshRequired: Boolean
+ val pushManager = Matrix.getInstance(aContext)!!.pushManager
+
+ // update the badge count if the device is offline, FCM is not supported or FCM registration failed
+ isRefreshRequired = !Matrix.getInstance(aContext)!!.isConnected
+ isRefreshRequired = isRefreshRequired or (null != pushManager && (!pushManager.useFcm() || !pushManager.hasRegistrationToken()))
+
+ if (isRefreshRequired) {
+ updateBadgeCount(aContext, dataHandler)
+ }
+ }
+ }
+ }
+ */
+ }
+
+ /**
+ * Update the badge count value according to the rooms content.
+ *
+ * @param aContext App context
+ * @param aDataHandler data handler instance
+ */
+ private fun updateBadgeCount(aSession: Session?, aContext: Context?) {
+ if (!useShortcutBadger()) {
+ return
+ }
+
+ /* TODO
+ //sanity check
+ if (null == aContext || null == aDataHandler) {
+ Timber.w("## updateBadgeCount(): invalid input null values")
+ } else if (null == aDataHandler.store) {
+ Timber.w("## updateBadgeCount(): invalid store instance")
+ } else {
+ val roomCompleteList = ArrayList(aDataHandler.store.rooms)
+ var unreadRoomsCount = 0
+
+ for (room in roomCompleteList) {
+ if (room.notificationCount > 0) {
+ unreadRoomsCount++
+ }
+ }
+
+ // update the badge counter
+ Timber.d("## updateBadgeCount(): badge update count=$unreadRoomsCount")
+ updateBadgeCount(aContext, unreadRoomsCount)
+ }
+ */
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt
index c44b25ca79..206a7e8bc5 100644
--- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt
+++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt
@@ -37,6 +37,7 @@ import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
import im.vector.riotredesign.features.rageshake.BugReporter
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
+import im.vector.riotredesign.features.settings.VectorSettingsActivity
import kotlinx.android.synthetic.main.activity_home.*
import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
@@ -101,12 +102,18 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable {
drawerToggle.syncState()
}
+ override fun getMenuRes() = R.menu.home
+
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
drawerLayout.openDrawer(GravityCompat.START)
return true
}
+ R.id.sliding_menu_settings -> {
+ startActivity(VectorSettingsActivity.getIntent(this, "TODO"))
+ return true
+ }
}
return true
diff --git a/vector/src/main/java/im/vector/riotredesign/features/homeserver/ServerUrlsRepository.kt b/vector/src/main/java/im/vector/riotredesign/features/homeserver/ServerUrlsRepository.kt
new file mode 100644
index 0000000000..2893dded74
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/homeserver/ServerUrlsRepository.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.homeserver
+
+import android.content.Context
+import android.text.TextUtils
+import androidx.core.content.edit
+import androidx.preference.PreferenceManager
+import im.vector.riotredesign.R
+
+/**
+ * Object to store and retrieve home and identity server urls
+ */
+object ServerUrlsRepository {
+
+ // Keys used to store default servers urls from the referrer
+ private const val DEFAULT_REFERRER_HOME_SERVER_URL_PREF = "default_referrer_home_server_url"
+ private const val DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF = "default_referrer_identity_server_url"
+
+ // Keys used to store current home server url and identity url
+ const val HOME_SERVER_URL_PREF = "home_server_url"
+ const val IDENTITY_SERVER_URL_PREF = "identity_server_url"
+
+ /**
+ * Save home and identity sever urls received by the Referrer receiver
+ */
+ fun setDefaultUrlsFromReferrer(context: Context, homeServerUrl: String, identityServerUrl: String) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit {
+ if (!TextUtils.isEmpty(homeServerUrl)) {
+ putString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, homeServerUrl)
+ }
+
+ if (!TextUtils.isEmpty(identityServerUrl)) {
+ putString(DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF, identityServerUrl)
+ }
+ }
+ }
+
+ /**
+ * Save home and identity sever urls entered by the user. May be custom or default value
+ */
+ fun saveServerUrls(context: Context, homeServerUrl: String, identityServerUrl: String) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit {
+ putString(HOME_SERVER_URL_PREF, homeServerUrl)
+ putString(IDENTITY_SERVER_URL_PREF, identityServerUrl)
+ }
+ }
+
+ /**
+ * Return last used home server url, or the default one from referrer or the default one from resources
+ */
+ fun getLastHomeServerUrl(context: Context): String {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+
+ return prefs.getString(HOME_SERVER_URL_PREF,
+ prefs.getString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF,
+ getDefaultHomeServerUrl(context)))
+ }
+
+
+ /**
+ * Return last used identity server url, or the default one from referrer or the default one from resources
+ */
+ fun getLastIdentityServerUrl(context: Context): String {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+
+ return prefs.getString(IDENTITY_SERVER_URL_PREF,
+ prefs.getString(DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF,
+ getDefaultIdentityServerUrl(context)))
+ }
+
+ /**
+ * Return true if url is the default home server url form resources
+ */
+ fun isDefaultHomeServerUrl(context: Context, url: String) = url == getDefaultHomeServerUrl(context)
+
+ /**
+ * Return true if url is the default identity server url form resources
+ */
+ fun isDefaultIdentityServerUrl(context: Context, url: String) = url == getDefaultIdentityServerUrl(context)
+
+ /**
+ * Return default home server url from resources
+ */
+ fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.default_hs_server_url)
+
+ /**
+ * Return default identity server url from resources
+ */
+ fun getDefaultIdentityServerUrl(context: Context): String = context.getString(R.string.default_identity_server_url)
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/IconLoader.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/IconLoader.kt
new file mode 100644
index 0000000000..61da3ea399
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/IconLoader.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.notifications
+
+import android.content.Context
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.annotation.WorkerThread
+import androidx.core.graphics.drawable.IconCompat
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.DecodeFormat
+import com.bumptech.glide.request.RequestOptions
+import timber.log.Timber
+
+/**
+ * FIXME It works, but it does not refresh the notification, when it's already displayed
+ */
+class IconLoader(val context: Context,
+ val listener: IconLoaderListener) {
+
+ /**
+ * Avatar Url -> Icon
+ */
+ private val cache = HashMap()
+
+ // URLs to load
+ private val toLoad = HashSet()
+
+ // Black list of URLs (broken URL, etc.)
+ private val blacklist = HashSet()
+
+ private var uiHandler = Handler()
+
+ private val handlerThread: HandlerThread = HandlerThread("IconLoader", Thread.MIN_PRIORITY)
+ private var backgroundHandler: Handler
+
+ init {
+ handlerThread.start()
+ backgroundHandler = Handler(handlerThread.looper)
+ }
+
+ /**
+ * Get icon of a user.
+ * If already in cache, use it, else load it and call IconLoaderListener.onIconsLoaded() when ready
+ */
+ fun getUserIcon(path: String?): IconCompat? {
+ if (path == null) {
+ return null
+ }
+
+ synchronized(cache) {
+ if (cache[path] != null) {
+ return cache[path]
+ }
+
+ // Add to the queue, if not blacklisted
+ if (!blacklist.contains(path)) {
+ if (toLoad.contains(path)) {
+ // Wait
+ } else {
+ toLoad.add(path)
+
+ backgroundHandler.post {
+ loadUserIcon(path)
+ }
+ }
+ }
+ }
+
+ return null
+ }
+
+ @WorkerThread
+ private fun loadUserIcon(path: String) {
+ val iconCompat = path.let {
+ try {
+ Glide.with(context)
+ .asBitmap()
+ .load(path)
+ .apply(RequestOptions.circleCropTransform()
+ .format(DecodeFormat.PREFER_ARGB_8888))
+ .submit()
+ .get()
+ } catch (e: Exception) {
+ Timber.e(e, "decodeFile failed")
+ null
+ }?.let { bitmap ->
+ IconCompat.createWithBitmap(bitmap)
+ }
+ }
+
+ synchronized(cache) {
+ if (iconCompat == null) {
+ // Add to the blacklist
+ blacklist.add(path)
+ } else {
+ cache[path] = iconCompat
+ }
+
+ toLoad.remove(path)
+
+ if (toLoad.isEmpty()) {
+ uiHandler.post {
+ listener.onIconsLoaded()
+ }
+ }
+ }
+ }
+
+
+ interface IconLoaderListener {
+ fun onIconsLoaded()
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/InviteNotifiableEvent.kt
new file mode 100644
index 0000000000..34838de2eb
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/InviteNotifiableEvent.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.notifications
+
+import androidx.core.app.NotificationCompat
+
+
+data class InviteNotifiableEvent(
+ override var matrixID: String?,
+ override val eventId: String,
+ var roomId: String,
+ override var noisy: Boolean,
+ override val title: String,
+ override val description: String,
+ override val type: String?,
+ override val timestamp: Long,
+ override var soundName: String?,
+ override var isPushGatewayEvent: Boolean = false) : NotifiableEvent {
+
+ override var hasBeenDisplayed: Boolean = false
+ override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
+
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEvent.kt
new file mode 100644
index 0000000000..5e2cb667ec
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEvent.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.notifications
+
+import java.io.Serializable
+
+interface NotifiableEvent : Serializable {
+ var matrixID: String?
+ val eventId: String
+ var noisy: Boolean
+ val title: String
+ val description: String?
+ val type: String?
+ val timestamp: Long
+ //NotificationCompat.VISIBILITY_PUBLIC , VISIBILITY_PRIVATE , VISIBILITY_SECRET
+ var lockScreenVisibility: Int
+ // Compat: Only for android <7, for newer version the sound is defined in the channel
+ var soundName: String?
+ var hasBeenDisplayed: Boolean
+ //Used to know if event should be replaced with the one coming from eventstream
+ var isPushGatewayEvent: Boolean
+}
+
diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEventResolver.kt
new file mode 100644
index 0000000000..83860d232d
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEventResolver.kt
@@ -0,0 +1,188 @@
+/*
+ * 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.notifications
+
+import android.content.Context
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.events.model.Event
+import im.vector.riotredesign.core.preference.BingRule
+
+// TODO Remove
+class RoomState {
+
+}
+
+
+/**
+ * The notifiable event resolver is able to create a NotifiableEvent (view model for notifications) from an sdk Event.
+ * It is used as a bridge between the Event Thread and the NotificationDrawerManager.
+ * The NotifiableEventResolver is the only aware of session/store, the NotificationDrawerManager has no knowledge of that,
+ * this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
+ */
+class NotifiableEventResolver(val context: Context) {
+
+ //private val eventDisplay = RiotEventDisplay(context)
+
+ fun resolveEvent(event: Event, roomState: RoomState?, bingRule: BingRule?, session: Session): NotifiableEvent? {
+ // TODO
+ return null
+ /*
+ val store = session.dataHandler.store
+ if (store == null) {
+ Log.e("## NotifiableEventResolver, unable to get store")
+ //TODO notify somehow that something did fail?
+ return null
+ }
+
+ when (event.type) {
+ EventType.MESSAGE -> {
+ return resolveMessageEvent(event, bingRule, session, store)
+ }
+ EventType.ENCRYPTED -> {
+ val messageEvent = resolveMessageEvent(event, bingRule, session, store)
+ messageEvent?.lockScreenVisibility = NotificationCompat.VISIBILITY_PRIVATE
+ return messageEvent
+ }
+ EventType.STATE_ROOM_MEMBER -> {
+ return resolveStateRoomEvent(event, bingRule, session, store)
+ }
+ else -> {
+
+ //If the event can be displayed, display it as is
+ eventDisplay.getTextualDisplay(event, roomState)?.toString()?.let { body ->
+ return SimpleNotifiableEvent(
+ session.myUserId,
+ eventId = event.eventId,
+ noisy = bingRule?.notificationSound != null,
+ timestamp = event.originServerTs,
+ description = body,
+ soundName = bingRule?.notificationSound,
+ title = context.getString(R.string.notification_unknown_new_event),
+ type = event.type)
+ }
+
+ //Unsupported event
+ Timber.w("NotifiableEventResolver Received an unsupported event matching a bing rule")
+ return null
+ }
+ }
+ */
+ }
+
+ /*
+ private fun resolveMessageEvent(event: Event, bingRule: BingRule?, session: MXSession, store: IMXStore): NotifiableEvent? {
+ //If we are here, that means that the event should be notified to the user, we check now how it should be presented (sound)
+ val soundName = bingRule?.notificationSound
+ val noisy = bingRule?.notificationSound != null
+
+ //The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...)
+ val room = store.getRoom(event.roomId /*roomID cannot be null (see Matrix SDK code)*/)
+
+ if (room == null) {
+ Timber.e("## Unable to resolve room for eventId [${event.eventId}] and roomID [${event.roomId}]")
+ // Ok room is not known in store, but we can still display something
+ val body = eventDisplay.getTextualDisplay(event, null)?.toString()
+ ?: context.getString(R.string.notification_unknown_new_event)
+ val roomName = context.getString(R.string.notification_unknown_room_name)
+ val senderDisplayName = event.sender ?: ""
+
+ val notifiableEvent = NotifiableMessageEvent(
+ eventId = event.eventId,
+ timestamp = event.originServerTs,
+ noisy = noisy,
+ senderName = senderDisplayName,
+ senderId = event.sender,
+ body = body,
+ roomId = event.roomId,
+ roomName = roomName)
+
+ notifiableEvent.matrixID = session.myUserId
+ notifiableEvent.soundName = soundName
+
+ return notifiableEvent
+ } else {
+
+ val body = eventDisplay.getTextualDisplay(event, room.state)?.toString()
+ ?: context.getString(R.string.notification_unknown_new_event)
+ val roomName = room.getRoomDisplayName(context)
+ val senderDisplayName = room.state.getMemberName(event.sender) ?: event.sender ?: ""
+
+ val notifiableEvent = NotifiableMessageEvent(
+ eventId = event.eventId,
+ timestamp = event.originServerTs,
+ noisy = noisy,
+ senderName = senderDisplayName,
+ senderId = event.sender,
+ body = body,
+ roomId = event.roomId,
+ roomName = roomName,
+ roomIsDirect = room.isDirect)
+
+ notifiableEvent.matrixID = session.myUserId
+ notifiableEvent.soundName = soundName
+
+
+ val roomAvatarPath = session.mediaCache?.thumbnailCacheFile(room.avatarUrl, 50)
+ if (roomAvatarPath != null) {
+ notifiableEvent.roomAvatarPath = roomAvatarPath.path
+ } else {
+ // prepare for the next time
+ session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), room.avatarUrl, 50)
+ }
+
+ room.state.getMember(event.sender)?.avatarUrl?.let {
+ val size = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
+ val userAvatarUrlPath = session.mediaCache?.thumbnailCacheFile(it, size)
+ if (userAvatarUrlPath != null) {
+ notifiableEvent.senderAvatarPath = userAvatarUrlPath.path
+ } else {
+ // prepare for the next time
+ session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), it, size)
+ }
+ }
+
+ return notifiableEvent
+ }
+ }
+
+ private fun resolveStateRoomEvent(event: Event, bingRule: BingRule?, session: MXSession, store: IMXStore): NotifiableEvent? {
+ if (RoomMember.MEMBERSHIP_INVITE == event.contentAsJsonObject?.getAsJsonPrimitive("membership")?.asString) {
+ val room = store.getRoom(event.roomId /*roomID cannot be null (see Matrix SDK code)*/)
+ val body = eventDisplay.getTextualDisplay(event, room.state)?.toString()
+ ?: context.getString(R.string.notification_new_invitation)
+ return InviteNotifiableEvent(
+ session.myUserId,
+ eventId = event.eventId,
+ roomId = event.roomId,
+ timestamp = event.originServerTs,
+ noisy = bingRule?.notificationSound != null,
+ title = context.getString(R.string.notification_new_invitation),
+ description = body,
+ soundName = bingRule?.notificationSound,
+ type = event.type,
+ isPushGatewayEvent = false)
+ } else {
+ Timber.e("## unsupported notifiable event for eventId [${event.eventId}]")
+ if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
+ Timber.e("## unsupported notifiable event for event [${event}]")
+ }
+ //TODO generic handling?
+ }
+ return null
+ }
+ */
+}
+
diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableMessageEvent.kt
new file mode 100644
index 0000000000..784aac2bd1
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableMessageEvent.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.notifications
+
+import androidx.core.app.NotificationCompat
+import im.vector.matrix.android.api.session.events.model.EventType
+
+data class NotifiableMessageEvent(
+ override val eventId: String,
+ override var noisy: Boolean,
+ override val timestamp: Long,
+ var senderName: String?,
+ var senderId: String?,
+ var body: String?,
+ var roomId: String,
+ var roomName: String?,
+ var roomIsDirect: Boolean = false
+) : NotifiableEvent {
+
+
+ override var matrixID: String? = null
+ override var soundName: String? = null
+ override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
+ override var hasBeenDisplayed: Boolean = false
+
+ var roomAvatarPath: String? = null
+ var senderAvatarPath: String? = null
+
+ override var isPushGatewayEvent: Boolean = false
+
+ override val type: String
+ get() = EventType.MESSAGE
+
+ override val description: String?
+ get() = body ?: ""
+
+ override val title: String
+ get() = senderName ?: ""
+
+ //This is used for >N notification, as the result of a smart reply
+ var outGoingMessage = false
+ var outGoingMessageFailed = false
+
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt
new file mode 100644
index 0000000000..d7f7aaddbc
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt
@@ -0,0 +1,182 @@
+/*
+ * 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.notifications
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import androidx.core.app.RemoteInput
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.room.Room
+import org.koin.standalone.KoinComponent
+import org.koin.standalone.inject
+import timber.log.Timber
+
+/**
+ * Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.)
+ */
+class NotificationBroadcastReceiver : BroadcastReceiver(), KoinComponent {
+
+ private val notificationDrawerManager by inject()
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (intent == null || context == null) return
+
+ Timber.d("ReplyNotificationBroadcastReceiver received : $intent")
+
+ when (intent.action) {
+ NotificationUtils.SMART_REPLY_ACTION ->
+ handleSmartReply(intent, context)
+ NotificationUtils.DISMISS_ROOM_NOTIF_ACTION ->
+ intent.getStringExtra(KEY_ROOM_ID)?.let {
+ notificationDrawerManager.clearMessageEventOfRoom(it)
+ }
+ NotificationUtils.DISMISS_SUMMARY_ACTION ->
+ notificationDrawerManager.clearAllEvents()
+ NotificationUtils.MARK_ROOM_READ_ACTION ->
+ intent.getStringExtra(KEY_ROOM_ID)?.let {
+ notificationDrawerManager.clearMessageEventOfRoom(it)
+ handleMarkAsRead(context, it)
+ }
+ }
+ }
+
+ private fun handleMarkAsRead(context: Context, roomId: String) {
+ /*
+ TODO
+ Matrix.getInstance(context)?.defaultSession?.let { session ->
+ session.dataHandler
+ ?.getRoom(roomId)
+ ?.markAllAsRead(object : SimpleApiCallback() {
+ override fun onSuccess(void: Void?) {
+ // Ignore
+ }
+ })
+ }
+ */
+ }
+
+ private fun handleSmartReply(intent: Intent, context: Context) {
+ /*
+ TODO
+ val message = getReplyMessage(intent)
+ val roomId = intent.getStringExtra(KEY_ROOM_ID)
+
+ if (TextUtils.isEmpty(message) || TextUtils.isEmpty(roomId)) {
+ //ignore this event
+ //Can this happen? should we update notification?
+ return
+ }
+ val matrixId = intent.getStringExtra(EXTRA_MATRIX_ID)
+ Matrix.getInstance(context)?.getSession(matrixId)?.let { session ->
+ session.dataHandler?.getRoom(roomId)?.let { room ->
+ sendMatrixEvent(message!!, session, roomId!!, room, context)
+ }
+ }
+ */
+ }
+
+ private fun sendMatrixEvent(message: String, session: Session, roomId: String, room: Room, context: Context?) {
+ /*
+ TODO
+
+ val mxMessage = Message()
+ mxMessage.msgtype = Message.MSGTYPE_TEXT
+ mxMessage.body = message
+
+ val event = Event(mxMessage, session.credentials.userId, roomId)
+ room.storeOutgoingEvent(event)
+ room.sendEvent(event, object : ApiCallback {
+ override fun onSuccess(info: Void?) {
+ Timber.d("Send message : onSuccess ")
+ val notifiableMessageEvent = NotifiableMessageEvent(
+ event.eventId,
+ false,
+ System.currentTimeMillis(),
+ session.myUser?.displayname
+ ?: context?.getString(R.string.notification_sender_me),
+ session.myUserId,
+ message,
+ roomId,
+ room.getRoomDisplayName(context),
+ room.isDirect)
+ notifiableMessageEvent.outGoingMessage = true
+ VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent)
+ VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(null)
+ }
+
+ override fun onNetworkError(e: Exception) {
+ Timber.d("Send message : onNetworkError " + e.message, e)
+ onSmartReplyFailed(e.localizedMessage)
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ Timber.d("Send message : onMatrixError " + e.message)
+ if (e is MXCryptoError) {
+ Toast.makeText(context, e.detailedErrorDescription, Toast.LENGTH_SHORT).show()
+ onSmartReplyFailed(e.detailedErrorDescription)
+ } else {
+ Toast.makeText(context, e.localizedMessage, Toast.LENGTH_SHORT).show()
+ onSmartReplyFailed(e.localizedMessage)
+ }
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ Timber.e(e, "Send message : onUnexpectedError " + e.message)
+ onSmartReplyFailed(e.message)
+ }
+
+
+ fun onSmartReplyFailed(reason: String?) {
+ val notifiableMessageEvent = NotifiableMessageEvent(
+ event.eventId,
+ false,
+ System.currentTimeMillis(),
+ session.myUser?.displayname
+ ?: context?.getString(R.string.notification_sender_me),
+ session.myUserId,
+ message,
+ roomId,
+ room.getRoomDisplayName(context),
+ room.isDirect)
+ notifiableMessageEvent.outGoingMessage = true
+ notifiableMessageEvent.outGoingMessageFailed = true
+
+ VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent)
+ VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(null)
+ }
+ })
+ */
+ }
+
+
+ private fun getReplyMessage(intent: Intent?): String? {
+ if (intent != null) {
+ val remoteInput = RemoteInput.getResultsFromIntent(intent);
+ if (remoteInput != null) {
+ return remoteInput.getCharSequence(KEY_TEXT_REPLY)?.toString()
+ }
+ }
+ return null
+ }
+
+ companion object {
+ const val KEY_ROOM_ID = "roomID"
+ const val KEY_TEXT_REPLY = "key_text_reply"
+ const val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID"
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt
new file mode 100644
index 0000000000..1e0e3c44f2
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt
@@ -0,0 +1,463 @@
+/*
+ * 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.notifications
+
+import android.app.Notification
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.text.TextUtils
+import androidx.core.app.NotificationCompat
+import androidx.core.app.Person
+import im.vector.matrix.android.api.session.Session
+import im.vector.riotredesign.BuildConfig
+import im.vector.riotredesign.R
+import im.vector.riotredesign.core.utils.SecretStoringUtils
+import timber.log.Timber
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+
+/**
+ * The NotificationDrawerManager receives notification events as they arrived (from event stream or fcm) and
+ * organise them in order to display them in the notification drawer.
+ * Events can be grouped into the same notification, old (already read) events can be removed to do some cleaning.
+ */
+class NotificationDrawerManager(val context: Context) {
+
+ //The first time the notification drawer is refreshed, we force re-render of all notifications
+ private var firstTime = true
+
+ private var eventList = loadEventInfo()
+ private var myUserDisplayName: String = ""
+ private var myUserAvatarUrl: String = ""
+
+ private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
+
+ private var currentRoomId: String? = null
+
+ private var iconLoader = IconLoader(context,
+ object : IconLoader.IconLoaderListener {
+ override fun onIconsLoaded() {
+ // Force refresh
+ refreshNotificationDrawer(null)
+ }
+ })
+
+ /**
+ * No multi session support for now
+ */
+ private fun initWithSession(session: Session?) {
+ session?.let {
+ /*
+ myUserDisplayName = it.myUser?.displayname ?: it.myUserId
+
+ // User Avatar
+ it.myUser?.avatarUrl?.let { avatarUrl ->
+ val userAvatarUrlPath = it.mediaCache?.thumbnailCacheFile(avatarUrl, avatarSize)
+ if (userAvatarUrlPath != null) {
+ myUserAvatarUrl = userAvatarUrlPath.path
+ } else {
+ // prepare for the next time
+ session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), avatarUrl, avatarSize)
+ }
+ }
+ */
+ }
+ }
+
+ /**
+ Should be called as soon as a new event is ready to be displayed.
+ The notification corresponding to this event will not be displayed until
+ #refreshNotificationDrawer() is called.
+ Events might be grouped and there might not be one notification per event!
+ */
+ fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
+ //If we support multi session, event list should be per userId
+ //Currently only manage single session
+ if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
+ Timber.d("%%%%%%%% onNotifiableEventReceived $notifiableEvent")
+ }
+ synchronized(eventList) {
+ val existing = eventList.firstOrNull { it.eventId == notifiableEvent.eventId }
+ if (existing != null) {
+ if (existing.isPushGatewayEvent) {
+ //Use the event coming from the event stream as it may contains more info than
+ //the fcm one (like type/content/clear text)
+ // In this case the message has already been notified, and might have done some noise
+ // So we want the notification to be updated even if it has already been displayed
+ // But it should make no noise (e.g when an encrypted message from FCM should be
+ // update with clear text after a sync)
+ notifiableEvent.hasBeenDisplayed = false
+ notifiableEvent.noisy = false
+ eventList.remove(existing)
+ eventList.add(notifiableEvent)
+
+ } else {
+ //keep the existing one, do not replace
+ }
+ } else {
+ eventList.add(notifiableEvent)
+ }
+
+ }
+ }
+
+ /**
+ Clear all known events and refresh the notification drawer
+ */
+ fun clearAllEvents() {
+ synchronized(eventList) {
+ eventList.clear()
+ }
+ refreshNotificationDrawer(null)
+ }
+
+ /** Clear all known message events for this room and refresh the notification drawer */
+ fun clearMessageEventOfRoom(roomId: String?) {
+ Timber.d("clearMessageEventOfRoom $roomId")
+
+ if (roomId != null) {
+ eventList.removeAll { e ->
+ if (e is NotifiableMessageEvent) {
+ return@removeAll e.roomId == roomId
+ }
+ return@removeAll false
+ }
+ NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID)
+ }
+ refreshNotificationDrawer(null)
+ }
+
+ /**
+ Should be called when the application is currently opened and showing timeline for the given roomId.
+ Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
+ */
+ fun setCurrentRoom(roomId: String?) {
+ var hasChanged: Boolean
+ synchronized(eventList) {
+ hasChanged = roomId != currentRoomId
+ currentRoomId = roomId
+ }
+ if (hasChanged) {
+ clearMessageEventOfRoom(roomId)
+ }
+ }
+
+ fun homeActivityDidResume(matrixID: String?) {
+ synchronized(eventList) {
+ eventList.removeAll { e ->
+ return@removeAll e !is NotifiableMessageEvent //messages are cleared when entering room
+ }
+ }
+ }
+
+ fun clearMemberShipNotificationForRoom(roomId: String) {
+ synchronized(eventList) {
+ eventList.removeAll { e ->
+ if (e is InviteNotifiableEvent) {
+ return@removeAll e.roomId == roomId
+ }
+ return@removeAll false
+ }
+ }
+ }
+
+
+ fun refreshNotificationDrawer(outdatedDetector: OutdatedEventDetector?) {
+ if (myUserDisplayName.isBlank()) {
+ // TODO
+ // initWithSession(Matrix.getInstance(context).defaultSession)
+ }
+
+ if (myUserDisplayName.isBlank()) {
+ // Should not happen, but myUserDisplayName cannot be blank if used to create a Person
+ return
+ }
+
+ synchronized(eventList) {
+
+ Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER ")
+ //TMP code
+ var hasNewEvent = false
+ var summaryIsNoisy = false
+ val summaryInboxStyle = NotificationCompat.InboxStyle()
+
+ //group events by room to create a single MessagingStyle notif
+ val roomIdToEventMap: MutableMap> = HashMap()
+ val simpleEvents: ArrayList = ArrayList()
+ val notifications: ArrayList = ArrayList()
+
+ val eventIterator = eventList.listIterator()
+ while (eventIterator.hasNext()) {
+ val event = eventIterator.next()
+ if (event is NotifiableMessageEvent) {
+ val roomId = event.roomId
+ var roomEvents = roomIdToEventMap[roomId]
+ if (roomEvents == null) {
+ roomEvents = ArrayList()
+ roomIdToEventMap[roomId] = roomEvents
+ }
+
+ if (shouldIgnoreMessageEventInRoom(roomId) || outdatedDetector?.isMessageOutdated(event) == true) {
+ //forget this event
+ eventIterator.remove()
+ } else {
+ roomEvents.add(event)
+ }
+ } else {
+ simpleEvents.add(event)
+ }
+ }
+
+
+ Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER ${roomIdToEventMap.size} room groups")
+
+ var globalLastMessageTimestamp = 0L
+
+ //events have been grouped
+ for ((roomId, events) in roomIdToEventMap) {
+
+ if (events.isEmpty()) {
+ //Just clear this notification
+ Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId has no more events")
+ NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID)
+ continue
+ }
+
+ val roomGroup = RoomEventGroupInfo(roomId)
+ roomGroup.hasNewEvent = false
+ roomGroup.shouldBing = false
+ roomGroup.isDirect = events[0].roomIsDirect
+ val roomName = events[0].roomName ?: events[0].senderName ?: ""
+ val style = NotificationCompat.MessagingStyle(Person.Builder()
+ .setName(myUserDisplayName)
+ .setIcon(iconLoader.getUserIcon(myUserAvatarUrl))
+ .setKey(events[0].matrixID)
+ .build())
+ roomGroup.roomDisplayName = roomName
+
+ style.isGroupConversation = !roomGroup.isDirect
+
+ if (!roomGroup.isDirect) {
+ style.conversationTitle = roomName
+ }
+
+ val largeBitmap = getRoomBitmap(events)
+
+
+ for (event in events) {
+ //if all events in this room have already been displayed there is no need to update it
+ if (!event.hasBeenDisplayed) {
+ roomGroup.shouldBing = roomGroup.shouldBing || event.noisy
+ roomGroup.customSound = event.soundName
+ }
+ roomGroup.hasNewEvent = roomGroup.hasNewEvent || !event.hasBeenDisplayed
+
+ val senderPerson = Person.Builder()
+ .setName(event.senderName)
+ .setIcon(iconLoader.getUserIcon(event.senderAvatarPath))
+ .setKey(event.senderId)
+ .build()
+
+ if (event.outGoingMessage && event.outGoingMessageFailed) {
+ style.addMessage(context.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson)
+ roomGroup.hasSmartReplyError = true
+ } else {
+ style.addMessage(event.body, event.timestamp, senderPerson)
+ }
+ event.hasBeenDisplayed = true //we can consider it as displayed
+
+ //It is possible that this event was previously shown as an 'anonymous' simple notif.
+ //And now it will be merged in a single MessageStyle notif, so we can clean to be sure
+ NotificationUtils.cancelNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID)
+ }
+
+ try {
+ val summaryLine = context.resources.getQuantityString(
+ R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size)
+ summaryInboxStyle.addLine(summaryLine)
+ } catch (e: Throwable) {
+ //String not found or bad format
+ Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string")
+ summaryInboxStyle.addLine(roomName)
+ }
+
+ if (firstTime || roomGroup.hasNewEvent) {
+ //Should update displayed notification
+ Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId need refresh")
+ val lastMessageTimestamp = events.last().timestamp
+
+ if (globalLastMessageTimestamp < lastMessageTimestamp) {
+ globalLastMessageTimestamp = lastMessageTimestamp
+ }
+
+ NotificationUtils.buildMessagesListNotification(context, style, roomGroup, largeBitmap, lastMessageTimestamp, myUserDisplayName)
+ ?.let {
+ //is there an id for this room?
+ notifications.add(it)
+ NotificationUtils.showNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID, it)
+ }
+ hasNewEvent = true
+ summaryIsNoisy = summaryIsNoisy || roomGroup.shouldBing
+ } else {
+ Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId is up to date")
+ }
+ }
+
+
+ //Handle simple events
+ for (event in simpleEvents) {
+ //We build a simple event
+ if (firstTime || !event.hasBeenDisplayed) {
+ NotificationUtils.buildSimpleEventNotification(context, event, null, myUserDisplayName)?.let {
+ notifications.add(it)
+ NotificationUtils.showNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID, it)
+ event.hasBeenDisplayed = true //we can consider it as displayed
+ hasNewEvent = true
+ summaryIsNoisy = summaryIsNoisy || event.noisy
+ summaryInboxStyle.addLine(event.description)
+ }
+ }
+ }
+
+
+ //======== Build summary notification =========
+ //On Android 7.0 (API level 24) and higher, the system automatically builds a summary for
+ // your group using snippets of text from each notification. The user can expand this
+ // notification to see each separate notification.
+ // To support older versions, which cannot show a nested group of notifications,
+ // you must create an extra notification that acts as the summary.
+ // This appears as the only notification and the system hides all the others.
+ // So this summary should include a snippet from all the other notifications,
+ // which the user can tap to open your app.
+ // The behavior of the group summary may vary on some device types such as wearables.
+ // To ensure the best experience on all devices and versions, always include a group summary when you create a group
+ // https://developer.android.com/training/notify-user/group
+
+ if (eventList.isEmpty()) {
+ NotificationUtils.cancelNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID)
+ } else {
+ val nbEvents = roomIdToEventMap.size + simpleEvents.size
+ val sumTitle = context.resources.getQuantityString(
+ R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
+ summaryInboxStyle.setBigContentTitle(sumTitle)
+ NotificationUtils.buildSummaryListNotification(
+ context,
+ summaryInboxStyle,
+ sumTitle,
+ noisy = hasNewEvent && summaryIsNoisy,
+ lastMessageTimestamp = globalLastMessageTimestamp
+ )?.let {
+ NotificationUtils.showNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID, it)
+ }
+
+ if (hasNewEvent && summaryIsNoisy) {
+ try {
+ // turn the screen on for 3 seconds
+ /*
+ TODO
+ if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) {
+ val pm = VectorApp.getInstance().getSystemService(Context.POWER_SERVICE) as PowerManager
+ val wl = pm.newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP,
+ NotificationDrawerManager::class.java.name)
+ wl.acquire(3000)
+ wl.release()
+ }
+ */
+ } catch (e: Throwable) {
+ Timber.e(e, "## Failed to turn screen on")
+ }
+
+ }
+ }
+ //notice that we can get bit out of sync with actual display but not a big issue
+ firstTime = false
+ }
+ }
+
+ private fun getRoomBitmap(events: ArrayList): Bitmap? {
+ if (events.isEmpty()) return null
+
+ //Use the last event (most recent?)
+ val roomAvatarPath = events[events.size - 1].roomAvatarPath
+ ?: events[events.size - 1].senderAvatarPath
+ if (!TextUtils.isEmpty(roomAvatarPath)) {
+ val options = BitmapFactory.Options()
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888
+ try {
+ return BitmapFactory.decodeFile(roomAvatarPath, options)
+ } catch (oom: OutOfMemoryError) {
+ Timber.e(oom, "decodeFile failed with an oom")
+ }
+
+ }
+ return null
+ }
+
+ private fun shouldIgnoreMessageEventInRoom(roomId: String?): Boolean {
+ return currentRoomId != null && roomId == currentRoomId
+ }
+
+
+ fun persistInfo() {
+ if (eventList.isEmpty()) {
+ deleteCachedRoomNotifications(context)
+ return
+ }
+ try {
+ val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
+ if (!file.exists()) file.createNewFile()
+ FileOutputStream(file).use {
+ SecretStoringUtils.securelyStoreObject(eventList, "notificationMgr", it, this.context)
+ }
+ } catch (e: Throwable) {
+ Timber.e(e, "## Failed to save cached notification info")
+ }
+ }
+
+ private fun loadEventInfo(): ArrayList {
+ try {
+ val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
+ if (file.exists()) {
+ FileInputStream(file).use {
+ val events: ArrayList? = SecretStoringUtils.loadSecureSecret(it, "notificationMgr", this.context)
+ if (events != null) {
+ return ArrayList(events.mapNotNull { it as? NotifiableEvent })
+ }
+ }
+ }
+ } catch (e: Throwable) {
+ Timber.e(e, "## Failed to load cached notification info")
+ }
+ return ArrayList()
+ }
+
+ private fun deleteCachedRoomNotifications(context: Context) {
+ val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
+ if (file.exists()) {
+ file.delete()
+ }
+ }
+
+ companion object {
+ private const val SUMMARY_NOTIFICATION_ID = 0
+ private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
+ private const val ROOM_EVENT_NOTIFICATION_ID = 2
+
+ private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache"
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationUtils.kt
new file mode 100755
index 0000000000..9596fdad42
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationUtils.kt
@@ -0,0 +1,721 @@
+/*
+ * 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.notifications
+
+import android.annotation.SuppressLint
+import android.annotation.TargetApi
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Build
+import android.text.TextUtils
+import androidx.annotation.StringRes
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.app.RemoteInput
+import androidx.core.app.TaskStackBuilder
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
+import im.vector.riotredesign.BuildConfig
+import im.vector.riotredesign.R
+import im.vector.riotredesign.core.utils.startNotificationChannelSettingsIntent
+import im.vector.riotredesign.features.home.HomeActivity
+import im.vector.riotredesign.features.settings.PreferencesManager
+import timber.log.Timber
+import java.util.*
+
+
+fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
+
+/**
+ * Util class for creating notifications.
+ */
+object NotificationUtils {
+
+ /* ==========================================================================================
+ * IDs for notifications
+ * ========================================================================================== */
+
+ /**
+ * Identifier of the foreground notification used to keep the application alive
+ * when it runs in background.
+ * This notification, which is not removable by the end user, displays what
+ * the application is doing while in background.
+ */
+ const val NOTIFICATION_ID_FOREGROUND_SERVICE = 61
+
+ /* ==========================================================================================
+ * IDs for actions
+ * ========================================================================================== */
+
+ private const val JOIN_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.JOIN_ACTION"
+ private const val REJECT_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.REJECT_ACTION"
+ private const val QUICK_LAUNCH_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.QUICK_LAUNCH_ACTION"
+ const val MARK_ROOM_READ_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.MARK_ROOM_READ_ACTION"
+ const val SMART_REPLY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.SMART_REPLY_ACTION"
+ const val DISMISS_SUMMARY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_SUMMARY_ACTION"
+ const val DISMISS_ROOM_NOTIF_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_ROOM_NOTIF_ACTION"
+ private const val TAP_TO_VIEW_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.TAP_TO_VIEW_ACTION"
+
+ /* ==========================================================================================
+ * IDs for channels
+ * ========================================================================================== */
+
+ // on devices >= android O, we need to define a channel for each notifications
+ private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID"
+
+ private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID"
+
+ private const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2"
+ private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2"
+
+ /* ==========================================================================================
+ * Channel names
+ * ========================================================================================== */
+
+ /**
+ * Create notification channels.
+ *
+ * @param context the context
+ */
+ @TargetApi(Build.VERSION_CODES.O)
+ fun createNotificationChannels(context: Context) {
+ if (!supportNotificationChannels()) {
+ return
+ }
+
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
+
+ //Migration - the noisy channel was deleted and recreated when sound preference was changed (id was DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE
+ // + currentTimeMillis).
+ //Now the sound can only be change directly in system settings, so for app upgrading we are deleting this former channel
+ //Starting from this version the channel will not be dynamic
+ for (channel in notificationManager.notificationChannels) {
+ val channelId = channel.id
+ val legacyBaseName = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE"
+ if (channelId.startsWith(legacyBaseName)) {
+ notificationManager.deleteNotificationChannel(channelId)
+ }
+ }
+ //Migration - Remove deprecated channels
+ for (channelId in listOf("DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID", "CALL_NOTIFICATION_CHANNEL_ID")) {
+ notificationManager.getNotificationChannel(channelId)?.let {
+ notificationManager.deleteNotificationChannel(channelId)
+ }
+ }
+
+ /**
+ * Default notification importance: shows everywhere, makes noise, but does not visually
+ * intrude.
+ */
+ notificationManager.createNotificationChannel(NotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.notification_noisy_notifications),
+ NotificationManager.IMPORTANCE_DEFAULT)
+ .apply {
+ description = context.getString(R.string.notification_noisy_notifications)
+ enableVibration(true)
+ enableLights(true)
+ lightColor = accentColor
+ })
+
+ /**
+ * Low notification importance: shows everywhere, but is not intrusive.
+ */
+ notificationManager.createNotificationChannel(NotificationChannel(SILENT_NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.notification_silent_notifications),
+ NotificationManager.IMPORTANCE_LOW)
+ .apply {
+ description = context.getString(R.string.notification_silent_notifications)
+ setSound(null, null)
+ enableLights(true)
+ lightColor = accentColor
+ })
+
+ notificationManager.createNotificationChannel(NotificationChannel(LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.notification_listening_for_events),
+ NotificationManager.IMPORTANCE_MIN)
+ .apply {
+ description = context.getString(R.string.notification_listening_for_events)
+ setSound(null, null)
+ setShowBadge(false)
+ })
+
+ notificationManager.createNotificationChannel(NotificationChannel(CALL_NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.call),
+ NotificationManager.IMPORTANCE_HIGH)
+ .apply {
+ description = context.getString(R.string.call)
+ setSound(null, null)
+ enableLights(true)
+ lightColor = accentColor
+ })
+ }
+
+ /**
+ * Build a polling thread listener notification
+ *
+ * @param context Android context
+ * @param subTitleResId subtitle string resource Id of the notification
+ * @return the polling thread listener notification
+ */
+ @SuppressLint("NewApi")
+ fun buildForegroundServiceNotification(context: Context, @StringRes subTitleResId: Int): Notification {
+ // build the pending intent go to the home screen if this is clicked.
+ val i = Intent(context, HomeActivity::class.java)
+ i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
+ val pi = PendingIntent.getActivity(context, 0, i, 0)
+
+ val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
+
+ val builder = NotificationCompat.Builder(context, LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(context.getString(subTitleResId))
+ .setCategory(NotificationCompat.CATEGORY_PROGRESS)
+ .setSmallIcon(R.drawable.logo_transparent)
+ .setProgress(0, 0, true)
+ .setColor(accentColor)
+ .setContentIntent(pi)
+
+ // hide the notification from the status bar
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ builder.priority = NotificationCompat.PRIORITY_MIN
+ }
+
+ val notification = builder.build()
+
+ notification.flags = notification.flags or Notification.FLAG_NO_CLEAR
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ // some devices crash if this field is not set
+ // even if it is deprecated
+
+ // setLatestEventInfo() is deprecated on Android M, so we try to use
+ // reflection at runtime, to avoid compiler error: "Cannot resolve method.."
+ try {
+ val deprecatedMethod = notification.javaClass
+ .getMethod("setLatestEventInfo",
+ Context::class.java,
+ CharSequence::class.java,
+ CharSequence::class.java,
+ PendingIntent::class.java)
+ deprecatedMethod.invoke(notification, context, context.getString(R.string.app_name), context.getString(subTitleResId), pi)
+ } catch (ex: Exception) {
+ Timber.e(ex, "## buildNotification(): Exception - setLatestEventInfo() Msg=" + ex.message)
+ }
+
+ }
+ return notification
+ }
+
+ /**
+ * Build an incoming call notification.
+ * This notification starts the VectorHomeActivity which is in charge of centralizing the incoming call flow.
+ *
+ * @param context the context.
+ * @param isVideo true if this is a video call, false for voice call
+ * @param roomName the room name in which the call is pending.
+ * @param matrixId the matrix id
+ * @param callId the call id.
+ * @return the call notification.
+ */
+ @SuppressLint("NewApi")
+ fun buildIncomingCallNotification(context: Context,
+ isVideo: Boolean,
+ roomName: String,
+ matrixId: String,
+ callId: String): Notification {
+ val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
+
+ val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(ensureTitleNotEmpty(context, roomName))
+ .apply {
+ if (isVideo) {
+ setContentText(context.getString(R.string.incoming_video_call))
+ } else {
+ setContentText(context.getString(R.string.incoming_voice_call))
+ }
+ }
+ .setSmallIcon(R.drawable.incoming_call_notification_transparent)
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+ .setLights(accentColor, 500, 500)
+
+ //Compat: Display the incoming call notification on the lock screen
+ builder.priority = NotificationCompat.PRIORITY_MAX
+
+ // clear the activity stack to home activity
+ val intent = Intent(context, HomeActivity::class.java)
+ .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
+ // TODO .putExtra(VectorHomeActivity.EXTRA_CALL_SESSION_ID, matrixId)
+ // TODO .putExtra(VectorHomeActivity.EXTRA_CALL_ID, callId)
+
+ // Recreate the back stack
+ val stackBuilder = TaskStackBuilder.create(context)
+ .addParentStack(HomeActivity::class.java)
+ .addNextIntent(intent)
+
+
+ // android 4.3 issue
+ // use a generator for the private requestCode.
+ // When using 0, the intent is not created/launched when the user taps on the notification.
+ //
+ val pendingIntent = stackBuilder.getPendingIntent(Random().nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT)
+
+ builder.setContentIntent(pendingIntent)
+
+ return builder.build()
+ }
+
+ /**
+ * Build a pending call notification
+ *
+ * @param context the context.
+ * @param isVideo true if this is a video call, false for voice call
+ * @param roomName the room name in which the call is pending.
+ * @param roomId the room Id
+ * @param matrixId the matrix id
+ * @param callId the call id.
+ * @return the call notification.
+ */
+ @SuppressLint("NewApi")
+ fun buildPendingCallNotification(context: Context,
+ isVideo: Boolean,
+ roomName: String,
+ roomId: String,
+ matrixId: String,
+ callId: String): Notification {
+
+ val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(ensureTitleNotEmpty(context, roomName))
+ .apply {
+ if (isVideo) {
+ setContentText(context.getString(R.string.video_call_in_progress))
+ } else {
+ setContentText(context.getString(R.string.call_in_progress))
+ }
+ }
+ .setSmallIcon(R.drawable.incoming_call_notification_transparent)
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+
+ // Display the pending call notification on the lock screen
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ builder.priority = NotificationCompat.PRIORITY_MAX
+ }
+
+ /* TODO
+ // Build the pending intent for when the notification is clicked
+ val roomIntent = Intent(context, VectorRoomActivity::class.java)
+ .putExtra(VectorRoomActivity.EXTRA_ROOM_ID, roomId)
+ .putExtra(VectorRoomActivity.EXTRA_MATRIX_ID, matrixId)
+ .putExtra(VectorRoomActivity.EXTRA_START_CALL_ID, callId)
+
+ // Recreate the back stack
+ val stackBuilder = TaskStackBuilder.create(context)
+ .addParentStack(VectorRoomActivity::class.java)
+ .addNextIntent(roomIntent)
+
+ // android 4.3 issue
+ // use a generator for the private requestCode.
+ // When using 0, the intent is not created/launched when the user taps on the notification.
+ //
+ val pendingIntent = stackBuilder.getPendingIntent(Random().nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT)
+
+ builder.setContentIntent(pendingIntent)
+ */
+
+ return builder.build()
+ }
+
+ /**
+ * Build a temporary (because service will be stopped just after) notification for the CallService, when a call is ended
+ */
+ fun buildCallEndedNotification(context: Context): Notification {
+ return NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(context.getString(R.string.call_ended))
+ .setSmallIcon(R.drawable.ic_material_call_end_grey)
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+ .build()
+ }
+
+ /**
+ * Build a notification for a Room
+ */
+ fun buildMessagesListNotification(context: Context,
+ messageStyle: NotificationCompat.MessagingStyle,
+ roomInfo: RoomEventGroupInfo,
+ largeIcon: Bitmap?,
+ lastMessageTimestamp: Long,
+ senderDisplayNameForReplyCompat: String?): Notification? {
+
+ val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
+ // Build the pending intent for when the notification is clicked
+ val openRoomIntent = buildOpenRoomIntent(context, roomInfo.roomId)
+ val smallIcon = if (roomInfo.shouldBing) R.drawable.icon_notif_important else R.drawable.logo_transparent
+
+ val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
+ return NotificationCompat.Builder(context, channelID)
+ .setWhen(lastMessageTimestamp)
+ // MESSAGING_STYLE sets title and content for API 16 and above devices.
+ .setStyle(messageStyle)
+
+ // A category allows groups of notifications to be ranked and filtered – per user or system settings.
+ // For example, alarm notifications should display before promo notifications, or message from known contact
+ // that can be displayed in not disturb mode if white listed (the later will need compat28.x)
+ .setCategory(NotificationCompat.CATEGORY_MESSAGE)
+
+ // Title for API < 16 devices.
+ .setContentTitle(roomInfo.roomDisplayName)
+ // Content for API < 16 devices.
+ .setContentText(context.getString(R.string.notification_new_messages))
+
+ // Number of new notifications for API <24 (M and below) devices.
+ .setSubText(context
+ .resources
+ .getQuantityString(R.plurals.room_new_messages_notification, messageStyle.messages.size, messageStyle.messages.size)
+ )
+
+ // Auto-bundling is enabled for 4 or more notifications on API 24+ (N+)
+ // devices and all Wear devices. But we want a custom grouping, so we specify the groupID
+ // TODO Group should be current user display name
+ .setGroup(context.getString(R.string.app_name))
+
+ //In order to avoid notification making sound twice (due to the summary notification)
+ .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
+
+ .setSmallIcon(smallIcon)
+
+ // Set primary color (important for Wear 2.0 Notifications).
+ .setColor(accentColor)
+
+ // Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for
+ // 'importance' which is set in the NotificationChannel. The integers representing
+ // 'priority' are different from 'importance', so make sure you don't mix them.
+ .apply {
+ priority = NotificationCompat.PRIORITY_DEFAULT
+ if (roomInfo.shouldBing) {
+ //Compat
+ PreferencesManager.getNotificationRingTone(context)?.let {
+ setSound(it)
+ }
+ setLights(accentColor, 500, 500)
+ } else {
+ priority = NotificationCompat.PRIORITY_LOW
+ }
+
+ //Add actions and notification intents
+ // Mark room as read
+ val markRoomReadIntent = Intent(context, NotificationBroadcastReceiver::class.java)
+ markRoomReadIntent.action = MARK_ROOM_READ_ACTION
+ markRoomReadIntent.data = Uri.parse("foobar://${roomInfo.roomId}")
+ markRoomReadIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId)
+ val markRoomReadPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), markRoomReadIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT)
+
+ addAction(NotificationCompat.Action(
+ R.drawable.ic_material_done_all_white,
+ context.getString(R.string.action_mark_room_read),
+ markRoomReadPendingIntent))
+
+ // Quick reply
+ if (!roomInfo.hasSmartReplyError) {
+ buildQuickReplyIntent(context, roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent ->
+ val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY)
+ .setLabel(context.getString(R.string.action_quick_reply))
+ .build()
+ NotificationCompat.Action.Builder(R.drawable.vector_notification_quick_reply,
+ context.getString(R.string.action_quick_reply), replyPendingIntent)
+ .addRemoteInput(remoteInput)
+ .build()?.let {
+ addAction(it)
+ }
+ }
+ }
+
+ if (openRoomIntent != null) {
+ setContentIntent(openRoomIntent)
+ }
+
+ if (largeIcon != null) {
+ setLargeIcon(largeIcon)
+ }
+
+ val intent = Intent(context, NotificationBroadcastReceiver::class.java)
+ intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId)
+ intent.action = DISMISS_ROOM_NOTIF_ACTION
+ val pendingIntent = PendingIntent.getBroadcast(context.applicationContext,
+ System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ setDeleteIntent(pendingIntent)
+ }
+ .build()
+ }
+
+
+ fun buildSimpleEventNotification(context: Context, simpleNotifiableEvent: NotifiableEvent, largeIcon: Bitmap?, matrixId: String): Notification? {
+ val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
+ // Build the pending intent for when the notification is clicked
+ val smallIcon = if (simpleNotifiableEvent.noisy) R.drawable.icon_notif_important else R.drawable.logo_transparent
+
+ val channelID = if (simpleNotifiableEvent.noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
+
+ return NotificationCompat.Builder(context, channelID)
+ .setContentTitle(context.getString(R.string.app_name))
+ .setContentText(simpleNotifiableEvent.description)
+ .setGroup(context.getString(R.string.app_name))
+ .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
+ .setSmallIcon(smallIcon)
+ .setColor(accentColor)
+ .apply {
+ if (simpleNotifiableEvent is InviteNotifiableEvent) {
+ /*
+ TODO
+ val roomId = simpleNotifiableEvent.roomId
+ // offer to type a quick reject button
+ val rejectIntent = JoinRoomActivity.getRejectRoomIntent(context, roomId, matrixId)
+
+ // the action must be unique else the parameters are ignored
+ rejectIntent.action = REJECT_ACTION
+ rejectIntent.data = Uri.parse("foobar://$roomId&$matrixId")
+ addAction(
+ R.drawable.vector_notification_reject_invitation,
+ context.getString(R.string.reject),
+ PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), rejectIntent, 0))
+
+ // offer to type a quick accept button
+ val joinIntent = JoinRoomActivity.getJoinRoomIntent(context, roomId, matrixId)
+
+ // the action must be unique else the parameters are ignored
+ joinIntent.action = JOIN_ACTION
+ joinIntent.data = Uri.parse("foobar://$roomId&$matrixId")
+ addAction(
+ R.drawable.vector_notification_accept_invitation,
+ context.getString(R.string.join),
+ PendingIntent.getActivity(context, 0, joinIntent, 0))
+ */
+ } else {
+ setAutoCancel(true)
+ }
+
+ val contentIntent = Intent(context, HomeActivity::class.java)
+ contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
+ //pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
+ contentIntent.data = Uri.parse("foobar://" + simpleNotifiableEvent.eventId)
+ setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, 0))
+
+ if (largeIcon != null) {
+ setLargeIcon(largeIcon)
+ }
+
+ if (simpleNotifiableEvent.noisy) {
+ //Compat
+ priority = NotificationCompat.PRIORITY_DEFAULT
+ PreferencesManager.getNotificationRingTone(context)?.let {
+ setSound(it)
+ }
+ setLights(accentColor, 500, 500)
+ } else {
+ priority = NotificationCompat.PRIORITY_LOW
+ }
+ setAutoCancel(true)
+ }
+ .build()
+ }
+
+ private fun buildOpenRoomIntent(context: Context, roomId: String): PendingIntent? {
+ // TODO
+ return null
+ /*
+ val roomIntentTap = Intent(context, VectorRoomActivity::class.java)
+ roomIntentTap.putExtra(VectorRoomActivity.EXTRA_ROOM_ID, roomId)
+ roomIntentTap.action = TAP_TO_VIEW_ACTION
+ //pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
+ roomIntentTap.data = Uri.parse("foobar://openRoom?$roomId")
+
+ // Recreate the back stack
+ return TaskStackBuilder.create(context)
+ .addNextIntentWithParentStack(Intent(context, VectorHomeActivity::class.java))
+ .addNextIntent(roomIntentTap)
+ .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
+ */
+ }
+
+ private fun buildOpenHomePendingIntentForSummary(context: Context): PendingIntent {
+ val intent = Intent(context, HomeActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
+ // TODO intent.putExtra(VectorHomeActivity.EXTRA_CLEAR_EXISTING_NOTIFICATION, true)
+ intent.data = Uri.parse("foobar://tapSummary")
+ return PendingIntent.getActivity(context, Random().nextInt(1000), intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ /*
+ Direct reply is new in Android N, and Android already handles the UI, so the right pending intent
+ here will ideally be a Service/IntentService (for a long running background task) or a BroadcastReceiver,
+ which runs on the UI thread. It also works without unlocking, making the process really fluid for the user.
+ However, for Android devices running Marshmallow and below (API level 23 and below),
+ it will be more appropriate to use an activity. Since you have to provide your own UI.
+ */
+ private fun buildQuickReplyIntent(context: Context, roomId: String, senderName: String?): PendingIntent? {
+ val intent: Intent
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ intent = Intent(context, NotificationBroadcastReceiver::class.java)
+ intent.action = SMART_REPLY_ACTION
+ intent.data = Uri.parse("foobar://$roomId")
+ intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
+ return PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), intent,
+ PendingIntent.FLAG_UPDATE_CURRENT)
+ } else {
+ /*
+ TODO
+ if (!LockScreenActivity.isDisplayingALockScreenActivity()) {
+ // start your activity for Android M and below
+ val quickReplyIntent = Intent(context, LockScreenActivity::class.java)
+ quickReplyIntent.putExtra(LockScreenActivity.EXTRA_ROOM_ID, roomId)
+ quickReplyIntent.putExtra(LockScreenActivity.EXTRA_SENDER_NAME, senderName ?: "")
+
+ // the action must be unique else the parameters are ignored
+ quickReplyIntent.action = QUICK_LAUNCH_ACTION
+ quickReplyIntent.data = Uri.parse("foobar://$roomId")
+ return PendingIntent.getActivity(context, 0, quickReplyIntent, 0)
+ }
+ */
+ }
+ return null
+ }
+
+ //// Number of new notifications for API <24 (M and below) devices.
+ /**
+ * Build the summary notification
+ */
+ fun buildSummaryListNotification(context: Context,
+ style: NotificationCompat.Style,
+ compatSummary: String,
+ noisy: Boolean,
+ lastMessageTimestamp: Long): Notification? {
+ val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
+ val smallIcon = if (noisy) R.drawable.icon_notif_important else R.drawable.logo_transparent
+
+ return NotificationCompat.Builder(context, if (noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID)
+ // used in compat < N, after summary is built based on child notifications
+ .setWhen(lastMessageTimestamp)
+ .setStyle(style)
+ .setContentTitle(context.getString(R.string.app_name))
+ .setCategory(NotificationCompat.CATEGORY_MESSAGE)
+ .setSmallIcon(smallIcon)
+ //set content text to support devices running API level < 24
+ .setContentText(compatSummary)
+ .setGroup(context.getString(R.string.app_name))
+ //set this notification as the summary for the group
+ .setGroupSummary(true)
+ .setColor(accentColor)
+ .apply {
+ if (noisy) {
+ //Compat
+ priority = NotificationCompat.PRIORITY_DEFAULT
+ PreferencesManager.getNotificationRingTone(context)?.let {
+ setSound(it)
+ }
+ setLights(accentColor, 500, 500)
+ } else {
+ //compat
+ priority = NotificationCompat.PRIORITY_LOW
+ }
+ }
+ .setContentIntent(buildOpenHomePendingIntentForSummary(context))
+ .setDeleteIntent(getDismissSummaryPendingIntent(context))
+ .build()
+
+ }
+
+ private fun getDismissSummaryPendingIntent(context: Context): PendingIntent {
+ val intent = Intent(context, NotificationBroadcastReceiver::class.java)
+ intent.action = DISMISS_SUMMARY_ACTION
+ intent.data = Uri.parse("foobar://deleteSummary")
+ return PendingIntent.getBroadcast(context.applicationContext,
+ 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ fun showNotificationMessage(context: Context, tag: String?, id: Int, notification: Notification) {
+ with(NotificationManagerCompat.from(context)) {
+ notify(tag, id, notification)
+ }
+ }
+
+ fun cancelNotificationMessage(context: Context, tag: String?, id: Int) {
+ NotificationManagerCompat.from(context)
+ .cancel(tag, id)
+ }
+
+ /**
+ * Cancel the foreground notification service
+ */
+ fun cancelNotificationForegroundService(context: Context) {
+ NotificationManagerCompat.from(context)
+ .cancel(NOTIFICATION_ID_FOREGROUND_SERVICE)
+ }
+
+ /**
+ * Cancel all the notification
+ */
+ fun cancelAllNotifications(context: Context) {
+ // Keep this try catch (reported by GA)
+ try {
+ NotificationManagerCompat.from(context)
+ .cancelAll()
+ } catch (e: Exception) {
+ Timber.e(e, "## cancelAllNotifications() failed " + e.message)
+ }
+ }
+
+ /**
+ * Return true it the user has enabled the do not disturb mode
+ */
+ fun isDoNotDisturbModeOn(context: Context): Boolean {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return false
+ }
+
+ // We cannot use NotificationManagerCompat here.
+ val setting = (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).currentInterruptionFilter
+
+ return setting == NotificationManager.INTERRUPTION_FILTER_NONE
+ || setting == NotificationManager.INTERRUPTION_FILTER_ALARMS
+ }
+
+ private fun ensureTitleNotEmpty(context: Context, title: String?): CharSequence {
+ if (TextUtils.isEmpty(title)) {
+ return context.getString(R.string.app_name)
+ }
+
+ return title!!
+ }
+
+ fun openSystemSettingsForSilentCategory(fragment: Fragment) {
+ startNotificationChannelSettingsIntent(fragment, SILENT_NOTIFICATION_CHANNEL_ID)
+ }
+
+ fun openSystemSettingsForNoisyCategory(fragment: Fragment) {
+ startNotificationChannelSettingsIntent(fragment, NOISY_NOTIFICATION_CHANNEL_ID)
+ }
+
+ fun openSystemSettingsForCallCategory(fragment: Fragment) {
+ startNotificationChannelSettingsIntent(fragment, CALL_NOTIFICATION_CHANNEL_ID)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/OutdatedEventDetector.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/OutdatedEventDetector.kt
new file mode 100644
index 0000000000..b1fc55c82c
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/OutdatedEventDetector.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.notifications
+
+import android.content.Context
+
+class OutdatedEventDetector(val context: Context) {
+
+ /**
+ * Returns true if the given event is outdated.
+ * Used to clean up notifications if a displayed message has been read on an
+ * other device.
+ */
+ fun isMessageOutdated(notifiableEvent: NotifiableEvent): Boolean {
+ if (notifiableEvent is NotifiableMessageEvent) {
+ val eventID = notifiableEvent.eventId
+ val roomID = notifiableEvent.roomId
+ /*
+ TODO
+ Matrix.getMXSession(context.applicationContext, notifiableEvent.matrixID)?.let { session ->
+ //find the room
+ if (session.isAlive) {
+ session.dataHandler.getRoom(roomID)?.let { room ->
+ if (room.isEventRead(eventID)) {
+ Timber.d("Notifiable Event $eventID is read, and should be removed")
+ return true
+ }
+ }
+ }
+ }
+ */
+ }
+ return false
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/RoomEventGroupInfo.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/RoomEventGroupInfo.kt
new file mode 100644
index 0000000000..e1c4e58280
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/RoomEventGroupInfo.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.notifications
+
+/**
+ * Data class to hold information about a group of notifications for a room
+ */
+data class RoomEventGroupInfo(
+ val roomId: String
+) {
+ var roomDisplayName: String = ""
+ var roomAvatarPath: String? = null
+ //An event in the list has not yet been display
+ var hasNewEvent: Boolean = false
+ //true if at least one on the not yet displayed event is noisy
+ var shouldBing: Boolean = false
+ var customSound: String? = null
+ var hasSmartReplyError = false
+ var isDirect = false
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/SimpleNotifiableEvent.kt
new file mode 100644
index 0000000000..b0226ca3f3
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/SimpleNotifiableEvent.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.notifications
+
+import androidx.core.app.NotificationCompat
+
+data class SimpleNotifiableEvent(
+ override var matrixID: String?,
+ override val eventId: String,
+ override var noisy: Boolean,
+ override val title: String,
+ override val description: String,
+ override val type: String?,
+ override val timestamp: Long,
+ override var soundName: String?,
+ override var isPushGatewayEvent: Boolean = false) : NotifiableEvent {
+
+ override var hasBeenDisplayed: Boolean = false
+ override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
+
+}
+
diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/PreferencesManager.java b/vector/src/main/java/im/vector/riotredesign/features/settings/PreferencesManager.java
new file mode 100755
index 0000000000..845af353fb
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/settings/PreferencesManager.java
@@ -0,0 +1,861 @@
+/*
+ * 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.settings;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceManager;
+import im.vector.riotredesign.R;
+import im.vector.riotredesign.features.homeserver.ServerUrlsRepository;
+import im.vector.riotredesign.features.themes.ThemeUtils;
+import timber.log.Timber;
+
+public class PreferencesManager {
+
+ public static final String VERSION_BUILD = "VERSION_BUILD";
+
+ public static final String SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY = "SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY_2";
+ public static final String SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY = "SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY";
+ public static final String SETTINGS_VERSION_PREFERENCE_KEY = "SETTINGS_VERSION_PREFERENCE_KEY";
+ public static final String SETTINGS_OLM_VERSION_PREFERENCE_KEY = "SETTINGS_OLM_VERSION_PREFERENCE_KEY";
+ public static final String SETTINGS_LOGGED_IN_PREFERENCE_KEY = "SETTINGS_LOGGED_IN_PREFERENCE_KEY";
+ public static final String SETTINGS_HOME_SERVER_PREFERENCE_KEY = "SETTINGS_HOME_SERVER_PREFERENCE_KEY";
+ public static final String SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY";
+ public static final String SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY = "SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY";
+ public static final String SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY = "SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY";
+ public static final String SETTINGS_NOTIFICATION_PRIVACY_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_PRIVACY_PREFERENCE_KEY";
+ public static final String SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY";
+ public static final String SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY";
+ public static final String SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY = "SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY";
+ public static final String SETTINGS_COPYRIGHT_PREFERENCE_KEY = "SETTINGS_COPYRIGHT_PREFERENCE_KEY";
+ public static final String SETTINGS_CLEAR_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_CACHE_PREFERENCE_KEY";
+ public static final String SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY";
+ public static final String SETTINGS_USER_SETTINGS_PREFERENCE_KEY = "SETTINGS_USER_SETTINGS_PREFERENCE_KEY";
+ public static final String SETTINGS_CONTACT_PREFERENCE_KEYS = "SETTINGS_CONTACT_PREFERENCE_KEYS";
+ public static final String SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY";
+ public static final String SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY";
+ public static final String SETTINGS_IGNORED_USERS_PREFERENCE_KEY = "SETTINGS_IGNORED_USERS_PREFERENCE_KEY";
+ public static final String SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY = "SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY";
+ public static final String SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY";
+ public static final String SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY";
+ public static final String SETTINGS_LABS_PREFERENCE_KEY = "SETTINGS_LABS_PREFERENCE_KEY";
+ public static final String SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY";
+ public static final String SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY";
+ public static final String SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY";
+ public static final String SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY";
+ public static final String SETTINGS_DEVICES_LIST_PREFERENCE_KEY = "SETTINGS_DEVICES_LIST_PREFERENCE_KEY";
+ public static final String SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY = "SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY";
+ public static final String SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY";
+ public static final String SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY
+ = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY";
+ public static final String SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY";
+ public static final String SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY";
+ public static final String SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY";
+ public static final String SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY";
+ public static final String SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY";
+ public static final String SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY";
+
+ public static final String SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY";
+
+ // user
+ public static final String SETTINGS_DISPLAY_NAME_PREFERENCE_KEY = "SETTINGS_DISPLAY_NAME_PREFERENCE_KEY";
+ public static final String SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY = "SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY";
+
+ // contacts
+ public static final String SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY = "SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY";
+
+ // interface
+ public static final String SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY = "SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY";
+ public static final String SETTINGS_INTERFACE_TEXT_SIZE_KEY = "SETTINGS_INTERFACE_TEXT_SIZE_KEY";
+ public static final String SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY";
+ private static final String SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY";
+ private static final String SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY";
+ private static final String SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY = "SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY";
+ private static final String SETTINGS_12_24_TIMESTAMPS_KEY = "SETTINGS_12_24_TIMESTAMPS_KEY";
+ private static final String SETTINGS_SHOW_READ_RECEIPTS_KEY = "SETTINGS_SHOW_READ_RECEIPTS_KEY";
+ private static final String SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY = "SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY";
+ private static final String SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY = "SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY";
+ private static final String SETTINGS_VIBRATE_ON_MENTION_KEY = "SETTINGS_VIBRATE_ON_MENTION_KEY";
+ private static final String SETTINGS_SEND_MESSAGE_WITH_ENTER = "SETTINGS_SEND_MESSAGE_WITH_ENTER";
+
+ // home
+ private static final String SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY = "SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY";
+ private static final String SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY = "SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY";
+
+ // flair
+ public static final String SETTINGS_GROUPS_FLAIR_KEY = "SETTINGS_GROUPS_FLAIR_KEY";
+
+ // notifications
+ public static final String SETTINGS_NOTIFICATIONS_KEY = "SETTINGS_NOTIFICATIONS_KEY";
+ public static final String SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY = "SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY";
+ public static final String SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY = "SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY";
+ public static final String SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY = "SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY";
+ public static final String SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY";
+ public static final String SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY";
+ public static final String SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY";
+ public static final String SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY";
+ public static final String SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY";
+ public static final String SETTINGS_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY = "SETTINGS_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY_2";
+ public static final String SETTINGS_CONTAINING_MY_USER_NAME_PREFERENCE_KEY = "SETTINGS_CONTAINING_MY_USER_NAME_PREFERENCE_KEY_2";
+ public static final String SETTINGS_MESSAGES_IN_ONE_TO_ONE_PREFERENCE_KEY = "SETTINGS_MESSAGES_IN_ONE_TO_ONE_PREFERENCE_KEY_2";
+ public static final String SETTINGS_MESSAGES_IN_GROUP_CHAT_PREFERENCE_KEY = "SETTINGS_MESSAGES_IN_GROUP_CHAT_PREFERENCE_KEY_2";
+ public static final String SETTINGS_INVITED_TO_ROOM_PREFERENCE_KEY = "SETTINGS_INVITED_TO_ROOM_PREFERENCE_KEY_2";
+ public static final String SETTINGS_CALL_INVITATIONS_PREFERENCE_KEY = "SETTINGS_CALL_INVITATIONS_PREFERENCE_KEY_2";
+
+ // media
+ private static final String SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY = "SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY";
+ private static final String SETTINGS_DEFAULT_MEDIA_SOURCE_KEY = "SETTINGS_DEFAULT_MEDIA_SOURCE_KEY";
+ private static final String SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY = "SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY";
+ private static final String SETTINGS_PLAY_SHUTTER_SOUND_KEY = "SETTINGS_PLAY_SHUTTER_SOUND_KEY";
+
+ // background sync
+ public static final String SETTINGS_START_ON_BOOT_PREFERENCE_KEY = "SETTINGS_START_ON_BOOT_PREFERENCE_KEY";
+ public static final String SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY";
+ public static final String SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY = "SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY";
+ public static final String SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY = "SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY";
+
+ // Calls
+ public static final String SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY = "SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY";
+ public static final String SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY = "SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY";
+
+ // labs
+ public static final String SETTINGS_LAZY_LOADING_PREFERENCE_KEY = "SETTINGS_LAZY_LOADING_PREFERENCE_KEY";
+ public static final String SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY = "SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY";
+ public static final String SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY = "SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY";
+ private static final String SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY = "SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY";
+ private static final String SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY = "SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY";
+ private static final String SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY = "SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY";
+
+ // analytics
+ public static final String SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY";
+ public static final String SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY";
+
+ // other
+ public static final String SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY";
+ private static final String SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY";
+ private static final String DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY = "DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY";
+ private static final String DID_MIGRATE_TO_NOTIFICATION_REWORK = "DID_MIGRATE_TO_NOTIFICATION_REWORK";
+ private static final String DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY = "DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY";
+ public static final String SETTINGS_DEACTIVATE_ACCOUNT_KEY = "SETTINGS_DEACTIVATE_ACCOUNT_KEY";
+ private static final String SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY";
+
+ private static final int MEDIA_SAVING_3_DAYS = 0;
+ private static final int MEDIA_SAVING_1_WEEK = 1;
+ private static final int MEDIA_SAVING_1_MONTH = 2;
+ private static final int MEDIA_SAVING_FOREVER = 3;
+
+ // some preferences keys must be kept after a logout
+ private static final List mKeysToKeepAfterLogout = Arrays.asList(
+ SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY,
+ SETTINGS_DEFAULT_MEDIA_SOURCE_KEY,
+ SETTINGS_PLAY_SHUTTER_SOUND_KEY,
+
+ SETTINGS_SEND_TYPING_NOTIF_KEY,
+ SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY,
+ SETTINGS_12_24_TIMESTAMPS_KEY,
+ SETTINGS_SHOW_READ_RECEIPTS_KEY,
+ SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY,
+ SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY,
+ SETTINGS_MEDIA_SAVING_PERIOD_KEY,
+ SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY,
+ SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY,
+ SETTINGS_SEND_MESSAGE_WITH_ENTER,
+
+ SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY,
+ SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY,
+ // Do not keep SETTINGS_LAZY_LOADING_PREFERENCE_KEY because the user may log in on a server which does not support lazy loading
+ SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY,
+ SETTINGS_START_ON_BOOT_PREFERENCE_KEY,
+ SETTINGS_INTERFACE_TEXT_SIZE_KEY,
+ SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY,
+ SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY,
+ SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY,
+
+ SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY,
+ SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY,
+ SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY,
+ SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY,
+ SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY,
+ SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY,
+ SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY,
+
+ SETTINGS_USE_RAGE_SHAKE_KEY
+ );
+
+ /**
+ * Clear the preferences.
+ *
+ * @param context the context
+ */
+ public static void clearPreferences(Context context) {
+ Set keysToKeep = new HashSet<>(mKeysToKeepAfterLogout);
+
+ // home server urls
+ keysToKeep.add(ServerUrlsRepository.HOME_SERVER_URL_PREF);
+ keysToKeep.add(ServerUrlsRepository.IDENTITY_SERVER_URL_PREF);
+
+ // theme
+ keysToKeep.add(ThemeUtils.APPLICATION_THEME_KEY);
+
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ SharedPreferences.Editor editor = preferences.edit();
+
+ // get all the existing keys
+ Set keys = preferences.getAll().keySet();
+ // remove the one to keep
+
+ keys.removeAll(keysToKeep);
+
+ for (String key : keys) {
+ editor.remove(key);
+ }
+
+ editor.apply();
+ }
+
+ /**
+ * Tells if we have already asked the user to disable battery optimisations on android >= M devices.
+ *
+ * @param context the context
+ * @return true if it was already requested
+ */
+ public static boolean didAskUserToIgnoreBatteryOptimizations(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY, false);
+ }
+
+ /**
+ * Mark as requested the question to disable battery optimisations.
+ *
+ * @param context the context
+ */
+ public static void setDidAskUserToIgnoreBatteryOptimizations(Context context) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY, true)
+ .apply();
+ }
+
+ public static boolean didMigrateToNotificationRework(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(DID_MIGRATE_TO_NOTIFICATION_REWORK, false);
+ }
+
+ public static void setDidMigrateToNotificationRework(Context context) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(DID_MIGRATE_TO_NOTIFICATION_REWORK, true)
+ .apply();
+ }
+
+ /**
+ * Tells if the timestamp must be displayed in 12h format
+ *
+ * @param context the context
+ * @return true if the time must be displayed in 12h format
+ */
+ public static boolean displayTimeIn12hFormat(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_12_24_TIMESTAMPS_KEY, false);
+ }
+
+ /**
+ * Tells if the join and leave membership events should be shown in the messages list.
+ *
+ * @param context the context
+ * @return true if the join and leave membership events should be shown in the messages list
+ */
+ public static boolean showJoinLeaveMessages(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY, true);
+ }
+
+ /**
+ * Tells if the avatar and display name events should be shown in the messages list.
+ *
+ * @param context the context
+ * @return true true if the avatar and display name events should be shown in the messages list.
+ */
+ public static boolean showAvatarDisplayNameChangeMessages(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY, true);
+ }
+
+ /**
+ * Tells the native camera to take a photo or record a video.
+ *
+ * @param context the context
+ * @return true to use the native camera app to record video or take photo.
+ */
+ public static boolean useNativeCamera(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY, false);
+ }
+
+ /**
+ * Tells if the send voice feature is enabled.
+ *
+ * @param context the context
+ * @return true if the send voice feature is enabled.
+ */
+ public static boolean isSendVoiceFeatureEnabled(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY, false);
+ }
+
+ /**
+ * Tells which compression level to use by default
+ *
+ * @param context the context
+ * @return the selected compression level
+ */
+ public static int getSelectedDefaultMediaCompressionLevel(Context context) {
+ return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY, "0"));
+ }
+
+ /**
+ * Tells which media source to use by default
+ *
+ * @param context the context
+ * @return the selected media source
+ */
+ public static int getSelectedDefaultMediaSource(Context context) {
+ return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_DEFAULT_MEDIA_SOURCE_KEY, "0"));
+ }
+
+ /**
+ * Tells whether to use shutter sound.
+ *
+ * @param context the context
+ * @return true if shutter sound should play
+ */
+ public static boolean useShutterSound(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true);
+ }
+
+ /**
+ * Update the notification ringtone
+ *
+ * @param context the context
+ * @param uri the new notification ringtone, or null for no RingTone
+ */
+ public static void setNotificationRingTone(Context context, @Nullable Uri uri) {
+ SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
+
+ String value = "";
+
+ if (null != uri) {
+ value = uri.toString();
+
+ if (value.startsWith("file://")) {
+ // it should never happen
+ // else android.os.FileUriExposedException will be triggered.
+ // see https://github.com/vector-im/riot-android/issues/1725
+ return;
+ }
+ }
+
+ editor.putString(SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY, value);
+ editor.apply();
+ }
+
+ /**
+ * Provides the selected notification ring tone
+ *
+ * @param context the context
+ * @return the selected ring tone or null for no RingTone
+ */
+ @Nullable
+ public static Uri getNotificationRingTone(Context context) {
+ String url = PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY, null);
+
+ // the user selects "None"
+ if (TextUtils.equals(url, "")) {
+ return null;
+ }
+
+ Uri uri = null;
+
+ // https://github.com/vector-im/riot-android/issues/1725
+ if ((null != url) && !url.startsWith("file://")) {
+ try {
+ uri = Uri.parse(url);
+ } catch (Exception e) {
+ Timber.e(e, "## getNotificationRingTone() : Uri.parse failed");
+ }
+ }
+
+ if (null == uri) {
+ uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
+ }
+
+ Timber.d("## getNotificationRingTone() returns " + uri);
+ return uri;
+ }
+
+ /**
+ * Provide the notification ringtone filename
+ *
+ * @param context the context
+ * @return the filename or null if "None" is selected
+ */
+ @Nullable
+ public static String getNotificationRingToneName(Context context) {
+ Uri toneUri = getNotificationRingTone(context);
+
+ if (null == toneUri) {
+ return null;
+ }
+
+ String name = null;
+
+ Cursor cursor = null;
+
+ try {
+ String[] proj = {MediaStore.Audio.Media.DATA};
+ cursor = context.getContentResolver().query(toneUri, proj, null, null, null);
+ int column_index = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA);
+ cursor.moveToFirst();
+
+ File file = new File(cursor.getString(column_index));
+ name = file.getName();
+
+ if (name.contains(".")) {
+ name = name.substring(0, name.lastIndexOf("."));
+ }
+ } catch (Exception e) {
+ Timber.e(e, "## getNotificationRingToneName() failed() : " + e.getMessage());
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return name;
+ }
+
+ /**
+ * Enable or disable the lazy loading
+ *
+ * @param context the context
+ * @param newValue true to enable lazy loading, false to disable it
+ */
+ public static void setUseLazyLoading(Context context, boolean newValue) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(SETTINGS_LAZY_LOADING_PREFERENCE_KEY, newValue)
+ .apply();
+ }
+
+ /**
+ * Tells if the lazy loading is enabled
+ *
+ * @param context the context
+ * @return true if the lazy loading of room members is enabled
+ */
+ public static boolean useLazyLoading(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_LAZY_LOADING_PREFERENCE_KEY, false);
+ }
+
+ /**
+ * User explicitly refuses the lazy loading.
+ *
+ * @param context the context
+ */
+ public static void setUserRefuseLazyLoading(Context context) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY, true)
+ .apply();
+ }
+
+ /**
+ * Tells if the user has explicitly refused the lazy loading
+ *
+ * @param context the context
+ * @return true if the user has explicitly refuse the lazy loading of room members
+ */
+ public static boolean hasUserRefusedLazyLoading(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY, false);
+ }
+
+ /**
+ * Tells if the data save mode is enabled
+ *
+ * @param context the context
+ * @return true if the data save mode is enabled
+ */
+ public static boolean useDataSaveMode(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY, false);
+ }
+
+ /**
+ * Tells if the conf calls must be done with Jitsi.
+ *
+ * @param context the context
+ * @return true if the conference call must be done with jitsi.
+ */
+ public static boolean useJitsiConfCall(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY, true);
+ }
+
+ /**
+ * Tells if the application is started on boot
+ *
+ * @param context the context
+ * @return true if the application must be started on boot
+ */
+ public static boolean autoStartOnBoot(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_START_ON_BOOT_PREFERENCE_KEY, true);
+ }
+
+ /**
+ * Tells if the application is started on boot
+ *
+ * @param context the context
+ * @param value true to start the application on boot
+ */
+ public static void setAutoStartOnBoot(Context context, boolean value) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(SETTINGS_START_ON_BOOT_PREFERENCE_KEY, value)
+ .apply();
+ }
+
+ /**
+ * Provides the selected saving period.
+ *
+ * @param context the context
+ * @return the selected period
+ */
+ public static int getSelectedMediasSavingPeriod(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getInt(SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY, MEDIA_SAVING_1_WEEK);
+ }
+
+ /**
+ * Updates the selected saving period.
+ *
+ * @param context the context
+ * @param index the selected period index
+ */
+ public static void setSelectedMediasSavingPeriod(Context context, int index) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putInt(SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY, index)
+ .apply();
+ }
+
+ /**
+ * Provides the minimum last access time to keep a media file.
+ *
+ * @param context the context
+ * @return the min last access time (in seconds)
+ */
+ public static long getMinMediasLastAccessTime(Context context) {
+ int selection = getSelectedMediasSavingPeriod(context);
+
+ switch (selection) {
+ case MEDIA_SAVING_3_DAYS:
+ return (System.currentTimeMillis() / 1000) - (3 * 24 * 60 * 60);
+ case MEDIA_SAVING_1_WEEK:
+ return (System.currentTimeMillis() / 1000) - (7 * 24 * 60 * 60);
+ case MEDIA_SAVING_1_MONTH:
+ return (System.currentTimeMillis() / 1000) - (30 * 24 * 60 * 60);
+ case MEDIA_SAVING_FOREVER:
+ return 0;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Provides the selected saving period.
+ *
+ * @param context the context
+ * @return the selected period
+ */
+ public static String getSelectedMediasSavingPeriodString(Context context) {
+ int selection = getSelectedMediasSavingPeriod(context);
+
+ switch (selection) {
+ case MEDIA_SAVING_3_DAYS:
+ return context.getString(R.string.media_saving_period_3_days);
+ case MEDIA_SAVING_1_WEEK:
+ return context.getString(R.string.media_saving_period_1_week);
+ case MEDIA_SAVING_1_MONTH:
+ return context.getString(R.string.media_saving_period_1_month);
+ case MEDIA_SAVING_FOREVER:
+ return context.getString(R.string.media_saving_period_forever);
+ }
+ return "?";
+ }
+
+ /**
+ * Fix some migration issues
+ */
+ public static void fixMigrationIssues(Context context) {
+ // some key names have been updated to supported language switch
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+
+ if (preferences.contains(context.getString(R.string.settings_pin_missed_notifications))) {
+ preferences.edit()
+ .putBoolean(SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY,
+ preferences.getBoolean(context.getString(R.string.settings_pin_missed_notifications), false))
+ .remove(context.getString(R.string.settings_pin_missed_notifications))
+ .apply();
+ }
+
+ if (preferences.contains(context.getString(R.string.settings_pin_unread_messages))) {
+ preferences.edit()
+ .putBoolean(SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY,
+ preferences.getBoolean(context.getString(R.string.settings_pin_unread_messages), false))
+ .remove(context.getString(R.string.settings_pin_unread_messages))
+ .apply();
+ }
+
+ if (preferences.contains("MARKDOWN_PREFERENCE_KEY")) {
+ preferences.edit()
+ .putBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, preferences.getBoolean("MARKDOWN_PREFERENCE_KEY", true))
+ .remove("MARKDOWN_PREFERENCE_KEY")
+ .apply();
+ }
+
+ if (preferences.contains("SETTINGS_DONT_SEND_TYPING_NOTIF_KEY")) {
+ preferences.edit()
+ .putBoolean(SETTINGS_SEND_TYPING_NOTIF_KEY, !preferences.getBoolean("SETTINGS_DONT_SEND_TYPING_NOTIF_KEY", true))
+ .remove("SETTINGS_DONT_SEND_TYPING_NOTIF_KEY")
+ .apply();
+ }
+
+ if (preferences.contains("SETTINGS_DISABLE_MARKDOWN_KEY")) {
+ preferences.edit()
+ .putBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, !preferences.getBoolean("SETTINGS_DISABLE_MARKDOWN_KEY", true))
+ .remove("SETTINGS_DISABLE_MARKDOWN_KEY")
+ .apply();
+ }
+
+ if (preferences.contains("SETTINGS_HIDE_READ_RECEIPTS")) {
+ preferences.edit()
+ .putBoolean(SETTINGS_SHOW_READ_RECEIPTS_KEY, !preferences.getBoolean("SETTINGS_HIDE_READ_RECEIPTS", true))
+ .remove("SETTINGS_HIDE_READ_RECEIPTS")
+ .apply();
+ }
+
+ if (preferences.contains("SETTINGS_HIDE_JOIN_LEAVE_MESSAGES_KEY")) {
+ preferences.edit()
+ .putBoolean(SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY, !preferences.getBoolean("SETTINGS_HIDE_JOIN_LEAVE_MESSAGES_KEY", true))
+ .remove("SETTINGS_HIDE_JOIN_LEAVE_MESSAGES_KEY")
+ .apply();
+ }
+
+ if (preferences.contains("SETTINGS_HIDE_AVATAR_DISPLAY_NAME_CHANGES")) {
+ preferences.edit()
+ .putBoolean(SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY,
+ !preferences.getBoolean("SETTINGS_HIDE_AVATAR_DISPLAY_NAME_CHANGES", true))
+ .remove("SETTINGS_HIDE_AVATAR_DISPLAY_NAME_CHANGES")
+ .apply();
+ }
+ }
+
+ /**
+ * Tells if the markdown is enabled
+ *
+ * @param context the context
+ * @return true if the markdown is enabled
+ */
+ public static boolean isMarkdownEnabled(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, true);
+ }
+
+ /**
+ * Update the markdown enable status.
+ *
+ * @param context the context
+ * @param isEnabled true to enable the markdown
+ */
+ public static void setMarkdownEnabled(Context context, boolean isEnabled) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, isEnabled)
+ .apply();
+ }
+
+ /**
+ * Tells if the read receipts should be shown
+ *
+ * @param context the context
+ * @return true if the read receipts should be shown
+ */
+ public static boolean showReadReceipts(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SHOW_READ_RECEIPTS_KEY, true);
+ }
+
+ /**
+ * Tells if the message timestamps must be always shown
+ *
+ * @param context the context
+ * @return true if the message timestamps must be always shown
+ */
+ public static boolean alwaysShowTimeStamps(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY, false);
+ }
+
+ /**
+ * Tells if the typing notifications should be sent
+ *
+ * @param context the context
+ * @return true to send the typing notifs
+ */
+ public static boolean sendTypingNotifs(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SEND_TYPING_NOTIF_KEY, true);
+ }
+
+ /**
+ * Tells of the missing notifications rooms must be displayed at left (home screen)
+ *
+ * @param context the context
+ * @return true to move the missed notifications to the left side
+ */
+ public static boolean pinMissedNotifications(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY, true);
+ }
+
+ /**
+ * Tells of the unread rooms must be displayed at left (home screen)
+ *
+ * @param context the context
+ * @return true to move the unread room to the left side
+ */
+ public static boolean pinUnreadMessages(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY, true);
+ }
+
+ /**
+ * Tells if the phone must vibrate when mentioning
+ *
+ * @param context the context
+ * @return true
+ */
+ public static boolean vibrateWhenMentioning(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_VIBRATE_ON_MENTION_KEY, false);
+ }
+
+ /**
+ * Tells if a dialog has been displayed to ask to use the analytics tracking (piwik, matomo, etc.).
+ *
+ * @param context the context
+ * @return true if a dialog has been displayed to ask to use the analytics tracking
+ */
+ public static boolean didAskToUseAnalytics(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY, false);
+ }
+
+ /**
+ * To call if the user has been asked for analytics tracking.
+ *
+ * @param context the context
+ */
+ public static void setDidAskToUseAnalytics(Context context) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY, true)
+ .apply();
+ }
+
+ /**
+ * Tells if the analytics tracking is authorized (piwik, matomo, etc.).
+ *
+ * @param context the context
+ * @return true if the analytics tracking is authorized
+ */
+ public static boolean useAnalytics(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_ANALYTICS_KEY, false);
+ }
+
+ /**
+ * Enable or disable the analytics tracking.
+ *
+ * @param context the context
+ * @param useAnalytics true to enable the analytics tracking
+ */
+ public static void setUseAnalytics(Context context, boolean useAnalytics) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(SETTINGS_USE_ANALYTICS_KEY, useAnalytics)
+ .apply();
+ }
+
+ /**
+ * Tells if media should be previewed before sending
+ *
+ * @param context the context
+ * @return true to preview media
+ */
+ public static boolean previewMediaWhenSending(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY, false);
+ }
+
+ /**
+ * Tells if message should be send by pressing enter on the soft keyboard
+ *
+ * @param context the context
+ * @return true to send message with enter
+ */
+ public static boolean sendMessageWithEnter(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SEND_MESSAGE_WITH_ENTER, false);
+ }
+
+ /**
+ * Tells if the rage shake is used.
+ *
+ * @param context the context
+ * @return true if the rage shake is used
+ */
+ public static boolean useRageshake(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true);
+ }
+
+ /**
+ * Update the rage shake status.
+ *
+ * @param context the context
+ * @param isEnabled true to enable the rage shake
+ */
+ public static void setUseRageshake(Context context, boolean isEnabled) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, isEnabled)
+ .apply();
+ }
+
+ /**
+ * Tells if all the events must be displayed ie even the redacted events.
+ *
+ * @param context the context
+ * @return true to display all the events even the redacted ones.
+ */
+ public static boolean displayAllEvents(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_DISPLAY_ALL_EVENTS_KEY, false);
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt
new file mode 100755
index 0000000000..f6279d1800
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2018 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package im.vector.riotredesign.features.settings
+
+import android.content.Context
+import android.content.Intent
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import im.vector.matrix.android.api.session.Session
+import im.vector.riotredesign.R
+import im.vector.riotredesign.core.platform.RiotActivity
+import org.koin.android.ext.android.inject
+
+/**
+ * Displays the client settings.
+ */
+class VectorSettingsActivity : RiotActivity(),
+ PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
+ FragmentManager.OnBackStackChangedListener,
+ VectorSettingsFragmentInteractionListener {
+
+ private lateinit var vectorSettingsPreferencesFragment: VectorSettingsPreferencesFragment
+
+ override fun getLayoutRes() = R.layout.activity_vector_settings
+
+ override fun getTitleRes() = R.string.title_activity_settings
+
+ private var keyToHighlight: String? = null
+
+ private val session by inject()
+
+ override fun initUiAndData() {
+ configureToolbar()
+
+ if (isFirstCreation()) {
+ vectorSettingsPreferencesFragment = VectorSettingsPreferencesFragment.newInstance(session.sessionParams.credentials.userId)
+ // display the fragment
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.vector_settings_page, vectorSettingsPreferencesFragment, FRAGMENT_TAG)
+ .commit()
+ } else {
+ vectorSettingsPreferencesFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as VectorSettingsPreferencesFragment
+ }
+
+
+ supportFragmentManager.addOnBackStackChangedListener(this)
+
+ }
+
+ override fun onDestroy() {
+ supportFragmentManager.removeOnBackStackChangedListener(this)
+ super.onDestroy()
+ }
+
+ override fun onBackStackChanged() {
+ if (0 == supportFragmentManager.backStackEntryCount) {
+ supportActionBar?.title = getString(getTitleRes())
+ }
+ }
+
+ override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat?, pref: Preference?): Boolean {
+ var oFragment: Fragment? = null
+
+ if (PreferencesManager.SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY == pref?.key) {
+ oFragment = VectorSettingsNotificationsTroubleshootFragment.newInstance(session.sessionParams.credentials.userId)
+ } else if (PreferencesManager.SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY == pref?.key) {
+ oFragment = VectorSettingsAdvancedNotificationPreferenceFragment.newInstance(session.sessionParams.credentials.userId)
+ }
+
+ if (oFragment != null) {
+ oFragment.setTargetFragment(caller, 0)
+ // Replace the existing Fragment with the new Fragment
+ supportFragmentManager.beginTransaction()
+ .setCustomAnimations(R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom,
+ R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom)
+ .replace(R.id.vector_settings_page, oFragment, pref?.title.toString())
+ .addToBackStack(null)
+ .commit()
+ return true
+ }
+ return false
+ }
+
+
+ override fun requestHighlightPreferenceKeyOnResume(key: String?) {
+ keyToHighlight = key
+ }
+
+ override fun requestedKeyToHighlight(): String? {
+ return keyToHighlight
+ }
+
+ companion object {
+ fun getIntent(context: Context, userId: String) = Intent(context, VectorSettingsActivity::class.java)
+ .apply {
+ //putExtra(MXCActionBarActivity.EXTRA_MATRIX_ID, userId)
+ }
+
+ private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt
new file mode 100644
index 0000000000..980859dd2b
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2018 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package im.vector.riotredesign.features.settings
+
+import android.app.Activity
+import android.content.Intent
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.View
+import androidx.core.content.edit
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceManager
+import im.vector.matrix.android.api.session.Session
+import im.vector.riotredesign.R
+import im.vector.riotredesign.core.extensions.withArgs
+import im.vector.riotredesign.core.platform.RiotActivity
+import im.vector.riotredesign.core.preference.BingRule
+import im.vector.riotredesign.core.preference.BingRulePreference
+import im.vector.riotredesign.features.notifications.NotificationUtils
+import im.vector.riotredesign.features.notifications.supportNotificationChannels
+import org.koin.android.ext.android.inject
+
+class VectorSettingsAdvancedNotificationPreferenceFragment : PreferenceFragmentCompat() {
+
+ // members
+ private val mSession by inject()
+ private var mLoadingView: View? = null
+
+ // events listener
+ /* TODO
+ private val mEventsListener = object : MXEventListener() {
+ override fun onBingRulesUpdate() {
+ refreshPreferences()
+ refreshDisplay()
+ }
+ }
+ */
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ // define the layout
+ addPreferencesFromResource(R.xml.vector_settings_notification_advanced_preferences)
+
+ val callNotificationsSystemOptions = findPreference(PreferencesManager.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY)
+ if (supportNotificationChannels()) {
+ callNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ NotificationUtils.openSystemSettingsForCallCategory(this)
+ false
+ }
+ } else {
+ callNotificationsSystemOptions.isVisible = false
+ }
+
+ val noisyNotificationsSystemOptions = findPreference(PreferencesManager.SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY)
+ if (supportNotificationChannels()) {
+ noisyNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ NotificationUtils.openSystemSettingsForNoisyCategory(this)
+ false
+ }
+ } else {
+ noisyNotificationsSystemOptions.isVisible = false
+ }
+
+ val silentNotificationsSystemOptions = findPreference(PreferencesManager.SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY)
+ if (supportNotificationChannels()) {
+ silentNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ NotificationUtils.openSystemSettingsForSilentCategory(this)
+ false
+ }
+ } else {
+ silentNotificationsSystemOptions.isVisible = false
+ }
+
+
+ // Ringtone
+ val ringtonePreference = findPreference(PreferencesManager.SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY)
+
+ if (supportNotificationChannels()) {
+ ringtonePreference.isVisible = false
+ } else {
+ ringtonePreference.summary = PreferencesManager.getNotificationRingToneName(activity)
+ ringtonePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER)
+ intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION)
+
+ if (null != PreferencesManager.getNotificationRingTone(activity)) {
+ intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, PreferencesManager.getNotificationRingTone(activity))
+ }
+
+ startActivityForResult(intent, REQUEST_NOTIFICATION_RINGTONE)
+ false
+ }
+ }
+
+ for (preferenceKey in mPrefKeyToBingRuleId.keys) {
+ val preference = findPreference(preferenceKey)
+ if (null != preference) {
+ if (preference is BingRulePreference) {
+ //preference.isEnabled = null != rules && isConnected && pushManager.areDeviceNotificationsAllowed()
+ val rule: BingRule? = null // TODO mSession.dataHandler.pushRules()?.findDefaultRule(mPrefKeyToBingRuleId[preferenceKey])
+
+ if (rule == null) {
+ // The rule is not defined, hide the preference
+ preference.isVisible = false
+ } else {
+ preference.isVisible = true
+ preference.setBingRule(rule)
+ preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+ val rule = preference.createRule(newValue as Int)
+ if (null != rule) {
+ /*
+ TODO
+ displayLoadingView()
+ mSession.dataHandler.bingRulesManager.updateRule(preference.rule,
+ rule,
+ object : BingRulesManager.onBingRuleUpdateListener {
+ private fun onDone() {
+ refreshDisplay()
+ hideLoadingView()
+ }
+
+ override fun onBingRuleUpdateSuccess() {
+ onDone()
+ }
+
+ override fun onBingRuleUpdateFailure(errorMessage: String) {
+ activity?.toast(errorMessage)
+ onDone()
+ }
+ })
+ */
+ }
+ false
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun refreshDisplay() {
+ listView?.adapter?.notifyDataSetChanged()
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (resultCode == Activity.RESULT_OK) {
+ when (requestCode) {
+ REQUEST_NOTIFICATION_RINGTONE -> {
+ PreferencesManager.setNotificationRingTone(activity,
+ data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) as Uri?)
+
+ // test if the selected ring tone can be played
+ val notificationRingToneName = PreferencesManager.getNotificationRingToneName(activity)
+ if (null != notificationRingToneName) {
+ PreferencesManager.setNotificationRingTone(activity, PreferencesManager.getNotificationRingTone(activity))
+ findPreference(PreferencesManager.SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY).summary = notificationRingToneName
+ }
+ }
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ (activity as? RiotActivity)?.supportActionBar?.setTitle(R.string.settings_notification_advanced)
+ // find the view from parent activity
+ mLoadingView = activity!!.findViewById(R.id.vector_settings_spinner_views)
+
+ /* TODO
+ if (mSession.isAlive) {
+
+ mSession.dataHandler.addListener(mEventsListener)
+
+ // refresh anything else
+ refreshPreferences()
+ refreshDisplay()
+ }
+ */
+ }
+
+ override fun onPause() {
+ super.onPause()
+
+ /* TODO
+ if (mSession.isAlive) {
+ mSession.dataHandler.removeListener(mEventsListener)
+ }
+ */
+ }
+
+ /**
+ * Refresh the known information about the account
+ */
+ private fun refreshPreferences() {
+ PreferenceManager.getDefaultSharedPreferences(activity).edit {
+ /* TODO
+ mSession.dataHandler.pushRules()?.let {
+ for (prefKey in mPrefKeyToBingRuleId.keys) {
+ val preference = findPreference(prefKey)
+
+ if (null != preference && preference is SwitchPreference) {
+ val ruleId = mPrefKeyToBingRuleId[prefKey]
+
+ val rule = it.findDefaultRule(ruleId)
+ var isEnabled = null != rule && rule.isEnabled
+
+ if (TextUtils.equals(ruleId, BingRule.RULE_ID_DISABLE_ALL) || TextUtils.equals(ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
+ isEnabled = !isEnabled
+ } else if (isEnabled) {
+ val actions = rule!!.actions
+
+ // no action -> noting will be done
+ if (null == actions || actions.isEmpty()) {
+ isEnabled = false
+ } else if (1 == actions.size) {
+ try {
+ isEnabled = !TextUtils.equals(actions[0] as String, BingRule.ACTION_DONT_NOTIFY)
+ } catch (e: Exception) {
+ Timber.e(LOG_TAG, "## refreshPreferences failed " + e.message, e)
+ }
+
+ }
+ }// check if the rule is only defined by don't notify
+
+ putBoolean(prefKey, isEnabled)
+ }
+ }
+ }
+ */
+ }
+ }
+
+
+ //==============================================================================================================
+ // Display methods
+ //==============================================================================================================
+
+ /**
+ * Display the loading view.
+ */
+ private fun displayLoadingView() {
+ if (null != mLoadingView) {
+ mLoadingView!!.visibility = View.VISIBLE
+ }
+ }
+
+ /**
+ * Hide the loading view.
+ */
+ private fun hideLoadingView() {
+ if (null != mLoadingView) {
+ mLoadingView!!.visibility = View.GONE
+ }
+ }
+
+
+ /* ==========================================================================================
+ * Companion
+ * ========================================================================================== */
+
+ companion object {
+ private const val REQUEST_NOTIFICATION_RINGTONE = 888
+
+ // preference name <-> rule Id
+ private var mPrefKeyToBingRuleId = mapOf(
+ PreferencesManager.SETTINGS_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY to BingRule.RULE_ID_CONTAIN_DISPLAY_NAME,
+ PreferencesManager.SETTINGS_CONTAINING_MY_USER_NAME_PREFERENCE_KEY to BingRule.RULE_ID_CONTAIN_USER_NAME,
+ PreferencesManager.SETTINGS_MESSAGES_IN_ONE_TO_ONE_PREFERENCE_KEY to BingRule.RULE_ID_ONE_TO_ONE_ROOM,
+ PreferencesManager.SETTINGS_MESSAGES_IN_GROUP_CHAT_PREFERENCE_KEY to BingRule.RULE_ID_ALL_OTHER_MESSAGES_ROOMS,
+ PreferencesManager.SETTINGS_INVITED_TO_ROOM_PREFERENCE_KEY to BingRule.RULE_ID_INVITE_ME,
+ PreferencesManager.SETTINGS_CALL_INVITATIONS_PREFERENCE_KEY to BingRule.RULE_ID_CALL,
+ PreferencesManager.SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY to BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS
+ )
+
+ fun newInstance(matrixId: String) = VectorSettingsAdvancedNotificationPreferenceFragment()
+ .withArgs {
+ // putString(MXCActionBarActivity.EXTRA_MATRIX_ID, matrixId)
+ }
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsFragmentInteractionListener.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsFragmentInteractionListener.kt
new file mode 100644
index 0000000000..f141f94908
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsFragmentInteractionListener.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2018 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package im.vector.riotredesign.features.settings
+
+interface VectorSettingsFragmentInteractionListener {
+
+ fun requestHighlightPreferenceKeyOnResume(key: String?)
+
+ fun requestedKeyToHighlight(): String?
+
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt
new file mode 100644
index 0000000000..c7ca4a656d
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2018 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package im.vector.riotredesign.features.settings
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.TextView
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.transition.TransitionManager
+import butterknife.BindView
+import im.vector.matrix.android.api.session.Session
+import im.vector.push.fcm.NotificationTroubleshootTestManagerFactory
+import im.vector.riotredesign.R
+import im.vector.riotredesign.core.extensions.withArgs
+import im.vector.riotredesign.core.platform.RiotActivity
+import im.vector.riotredesign.core.platform.RiotFragment
+import im.vector.riotredesign.features.rageshake.BugReporter
+import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager
+import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest
+import org.koin.android.ext.android.inject
+
+class VectorSettingsNotificationsTroubleshootFragment : RiotFragment() {
+
+ @BindView(R.id.troubleshoot_test_recycler_view)
+ lateinit var mRecyclerView: RecyclerView
+ @BindView(R.id.troubleshoot_bottom_view)
+ lateinit var mBottomView: ViewGroup
+ @BindView(R.id.toubleshoot_summ_description)
+ lateinit var mSummaryDescription: TextView
+ @BindView(R.id.troubleshoot_summ_button)
+ lateinit var mSummaryButton: Button
+ @BindView(R.id.troubleshoot_run_button)
+ lateinit var mRunButton: Button
+
+ private var testManager: NotificationTroubleshootTestManager? = null
+ // members
+ private val mSession by inject()
+
+ override fun getLayoutResId() = R.layout.fragment_settings_notifications_troubleshoot
+
+ private var interactionListener: VectorSettingsFragmentInteractionListener? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val appContext = activity!!.applicationContext
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val layoutManager = LinearLayoutManager(context)
+ mRecyclerView.layoutManager = layoutManager
+
+ val dividerItemDecoration = DividerItemDecoration(mRecyclerView.context,
+ layoutManager.orientation)
+ mRecyclerView.addItemDecoration(dividerItemDecoration)
+
+
+ mSummaryButton.setOnClickListener {
+ BugReporter.openBugReportScreen(activity!!)
+ }
+
+ mRunButton.setOnClickListener {
+ testManager?.retry()
+ }
+ startUI()
+ }
+
+ private fun startUI() {
+
+ mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_running_status,
+ 0, 0)
+
+ testManager = NotificationTroubleshootTestManagerFactory.createTestManager(this, mSession)
+
+ testManager?.statusListener = { troubleshootTestManager ->
+ if (isAdded) {
+ TransitionManager.beginDelayedTransition(mBottomView)
+ when (troubleshootTestManager.diagStatus) {
+ TroubleshootTest.TestStatus.NOT_STARTED -> {
+ mSummaryDescription.text = ""
+ mSummaryButton.visibility = View.GONE
+ mRunButton.visibility = View.VISIBLE
+ }
+ TroubleshootTest.TestStatus.RUNNING -> {
+ //Forces int type because it's breaking lint
+ val size: Int = troubleshootTestManager.testList.size
+ val currentTestIndex: Int = troubleshootTestManager.currentTestIndex
+ mSummaryDescription.text = getString(
+ R.string.settings_troubleshoot_diagnostic_running_status,
+ currentTestIndex,
+ size
+ )
+ mSummaryButton.visibility = View.GONE
+ mRunButton.visibility = View.GONE
+ }
+ TroubleshootTest.TestStatus.FAILED -> {
+ //check if there are quick fixes
+ var hasQuickFix = false
+ testManager?.testList?.let {
+ for (test in it) {
+ if (test.status == TroubleshootTest.TestStatus.FAILED && test.quickFix != null) {
+ hasQuickFix = true
+ break
+ }
+ }
+ }
+ if (hasQuickFix) {
+ mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_failure_status_with_quickfix)
+ } else {
+ mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_failure_status_no_quickfix)
+ }
+ mSummaryButton.visibility = View.VISIBLE
+ mRunButton.visibility = View.VISIBLE
+ }
+ TroubleshootTest.TestStatus.SUCCESS -> {
+ mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_success_status)
+ mSummaryButton.visibility = View.VISIBLE
+ mRunButton.visibility = View.VISIBLE
+ }
+ }
+ }
+
+ }
+ mRecyclerView.adapter = testManager?.adapter
+ testManager?.runDiagnostic()
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (resultCode == Activity.RESULT_OK && requestCode == NotificationTroubleshootTestManager.REQ_CODE_FIX) {
+ testManager?.retry()
+ return
+ }
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+
+ override fun onDetach() {
+ testManager?.cancel()
+ interactionListener = null
+ super.onDetach()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ (activity as? RiotActivity)?.supportActionBar?.setTitle(R.string.settings_notification_troubleshoot)
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ if (context is VectorSettingsFragmentInteractionListener) {
+ interactionListener = context
+ }
+ }
+
+ companion object {
+ // static constructor
+ fun newInstance(matrixId: String) = VectorSettingsNotificationsTroubleshootFragment()
+ .withArgs {
+ // TODO putString(MXCActionBarActivity.EXTRA_MATRIX_ID, matrixId)
+ }
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt
new file mode 100755
index 0000000000..24df2bf57b
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt
@@ -0,0 +1,2927 @@
+/*
+ * Copyright 2018 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotredesign.features.settings
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.content.SharedPreferences
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.AsyncTask
+import android.os.Bundle
+import android.provider.Settings
+import android.text.Editable
+import android.text.TextUtils
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import android.widget.*
+import androidx.appcompat.app.AlertDialog
+import androidx.core.content.ContextCompat
+import androidx.core.content.edit
+import androidx.core.view.isVisible
+import androidx.preference.*
+import com.google.android.material.textfield.TextInputEditText
+import com.google.android.material.textfield.TextInputLayout
+import im.vector.matrix.android.api.session.Session
+import im.vector.riotredesign.R
+import im.vector.riotredesign.core.extensions.showPassword
+import im.vector.riotredesign.core.extensions.withArgs
+import im.vector.riotredesign.core.platform.SimpleTextWatcher
+import im.vector.riotredesign.core.preference.BingRule
+import im.vector.riotredesign.core.preference.ProgressBarPreference
+import im.vector.riotredesign.core.preference.UserAvatarPreference
+import im.vector.riotredesign.core.preference.VectorPreference
+import im.vector.riotredesign.core.utils.*
+import im.vector.riotredesign.features.themes.ThemeUtils
+import org.koin.android.ext.android.inject
+import java.lang.ref.WeakReference
+import java.util.*
+
+class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
+
+ // members
+ private val mSession by inject()
+
+ // disable some updates if there is
+ // TODO private val mNetworkListener = IMXNetworkEventListener { refreshDisplay() }
+ // events listener
+ // TODO private val mEventsListener = object : MXEventListener() {
+ // TODO override fun onBingRulesUpdate() {
+ // TODO refreshPreferences()
+ // TODO refreshDisplay()
+ // TODO }
+
+ // TODO override fun onAccountInfoUpdate(myUser: MyUser) {
+ // TODO // refresh the settings value
+ // TODO PreferenceManager.getDefaultSharedPreferences(VectorApp.getInstance().applicationContext).edit {
+ // TODO putString(PreferencesManager.SETTINGS_DISPLAY_NAME_PREFERENCE_KEY, myUser.displayname)
+ // TODO }
+
+ // TODO refreshDisplay()
+ // TODO }
+ // TODO }
+
+ private var mLoadingView: View? = null
+
+ private var mDisplayedEmails = ArrayList()
+ private var mDisplayedPhoneNumber = ArrayList()
+
+ // TODO private var mMyDeviceInfo: DeviceInfo? = null
+
+ // TODO private var mDisplayedPushers = ArrayList()
+
+ private var interactionListener: VectorSettingsFragmentInteractionListener? = null
+
+ // devices: device IDs and device names
+ // TODO private var mDevicesNameList: List = ArrayList()
+ // used to avoid requesting to enter the password for each deletion
+ private var mAccountPassword: String? = null
+
+ // current publicised group list
+ private var mPublicisedGroups: MutableSet? = null
+
+ /* ==========================================================================================
+ * Preferences
+ * ========================================================================================== */
+
+ private val mUserSettingsCategory by lazy {
+ findPreference(PreferencesManager.SETTINGS_USER_SETTINGS_PREFERENCE_KEY) as PreferenceCategory
+ }
+ private val mUserAvatarPreference by lazy {
+ findPreference(PreferencesManager.SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY) as UserAvatarPreference
+ }
+ private val mDisplayNamePreference by lazy {
+ findPreference(PreferencesManager.SETTINGS_DISPLAY_NAME_PREFERENCE_KEY) as EditTextPreference
+ }
+ private val mPasswordPreference by lazy {
+ findPreference(PreferencesManager.SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY)
+ }
+
+ // Local contacts
+ private val mContactSettingsCategory by lazy {
+ findPreference(PreferencesManager.SETTINGS_CONTACT_PREFERENCE_KEYS) as PreferenceCategory
+ }
+
+ private val mContactPhonebookCountryPreference by lazy {
+ findPreference(PreferencesManager.SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY)
+ }
+
+ // Group Flairs
+ private val mGroupsFlairCategory by lazy {
+ findPreference(PreferencesManager.SETTINGS_GROUPS_FLAIR_KEY) as PreferenceCategory
+ }
+
+ // cryptography
+ private val mCryptographyCategory by lazy {
+ findPreference(PreferencesManager.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY) as PreferenceCategory
+ }
+ private val mCryptographyCategoryDivider by lazy {
+ findPreference(PreferencesManager.SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY)
+ }
+ // cryptography manage
+ private val mCryptographyManageCategory by lazy {
+ findPreference(PreferencesManager.SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY) as PreferenceCategory
+ }
+ private val mCryptographyManageCategoryDivider by lazy {
+ findPreference(PreferencesManager.SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY)
+ }
+ // displayed pushers
+ private val mPushersSettingsDivider by lazy {
+ findPreference(PreferencesManager.SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY)
+ }
+ private val mPushersSettingsCategory by lazy {
+ findPreference(PreferencesManager.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY) as PreferenceCategory
+ }
+ private val mDevicesListSettingsCategory by lazy {
+ findPreference(PreferencesManager.SETTINGS_DEVICES_LIST_PREFERENCE_KEY) as PreferenceCategory
+ }
+ private val mDevicesListSettingsCategoryDivider by lazy {
+ findPreference(PreferencesManager.SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY)
+ }
+ // displayed the ignored users list
+ private val mIgnoredUserSettingsCategoryDivider by lazy {
+ findPreference(PreferencesManager.SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY)
+ }
+ private val mIgnoredUserSettingsCategory by lazy {
+ findPreference(PreferencesManager.SETTINGS_IGNORED_USERS_PREFERENCE_KEY) as PreferenceCategory
+ }
+ // background sync category
+ private val mSyncRequestTimeoutPreference by lazy {
+ // ? Cause it can be removed
+ findPreference(PreferencesManager.SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY) as EditTextPreference?
+ }
+ private val mSyncRequestDelayPreference by lazy {
+ // ? Cause it can be removed
+ findPreference(PreferencesManager.SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY) as EditTextPreference?
+ }
+ private val mLabsCategory by lazy {
+ findPreference(PreferencesManager.SETTINGS_LABS_PREFERENCE_KEY) as PreferenceCategory
+ }
+ private val backgroundSyncCategory by lazy {
+ findPreference(PreferencesManager.SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY)
+ }
+ private val backgroundSyncDivider by lazy {
+ findPreference(PreferencesManager.SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY)
+ }
+ private val backgroundSyncPreference by lazy {
+ findPreference(PreferencesManager.SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY) as SwitchPreference
+ }
+ private val mUseRiotCallRingtonePreference by lazy {
+ findPreference(PreferencesManager.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY) as SwitchPreference
+ }
+ private val mCallRingtonePreference by lazy {
+ findPreference(PreferencesManager.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY)
+ }
+ private val notificationsSettingsCategory by lazy {
+ findPreference(PreferencesManager.SETTINGS_NOTIFICATIONS_KEY) as PreferenceCategory
+ }
+ private val mNotificationPrivacyPreference by lazy {
+ findPreference(PreferencesManager.SETTINGS_NOTIFICATION_PRIVACY_PREFERENCE_KEY)
+ }
+ private val selectedLanguagePreference by lazy {
+ findPreference(PreferencesManager.SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY)
+ }
+ private val textSizePreference by lazy {
+ findPreference(PreferencesManager.SETTINGS_INTERFACE_TEXT_SIZE_KEY)
+ }
+ private val cryptoInfoDeviceNamePreference by lazy {
+ findPreference(PreferencesManager.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY) as VectorPreference
+ }
+ private val cryptoInfoDeviceIdPreference by lazy {
+ findPreference(PreferencesManager.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY)
+ }
+
+ private val manageBackupPref by lazy {
+ findPreference(PreferencesManager.SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY)
+ }
+
+ private val exportPref by lazy {
+ findPreference(PreferencesManager.SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY)
+ }
+
+ private val importPref by lazy {
+ findPreference(PreferencesManager.SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY)
+ }
+
+ private val cryptoInfoTextPreference by lazy {
+ findPreference(PreferencesManager.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY)
+ }
+ // encrypt to unverified devices
+ private val sendToUnverifiedDevicesPref by lazy {
+ findPreference(PreferencesManager.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY) as SwitchPreference
+ }
+
+ /* ==========================================================================================
+ * Life cycle
+ * ========================================================================================== */
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ val appContext = activity?.applicationContext
+
+ // retrieve the arguments
+ /*
+ val sessionArg = Matrix.getInstance(appContext).getSession(arguments?.getString(ARG_MATRIX_ID))
+
+ // sanity checks
+ if (null == sessionArg || !sessionArg.isAlive) {
+ activity?.finish()
+ return
+ }
+
+ mSession = sessionArg
+ */
+
+ // define the layout
+ addPreferencesFromResource(R.xml.vector_settings_preferences)
+
+ // Avatar
+ mUserAvatarPreference.let {
+ it.setSession(mSession)
+ it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ onUpdateAvatarClick()
+ false
+ }
+ }
+
+ // Display name
+ mDisplayNamePreference.let {
+ it.summary = "TODO" // mSession.myUser.displayname
+ it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+ onDisplayNameClick(newValue?.let { (it as String).trim() })
+ false
+ }
+ }
+
+ // Password
+ mPasswordPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ onPasswordUpdateClick()
+ false
+ }
+
+ // Add Email
+ (findPreference(ADD_EMAIL_PREFERENCE_KEY) as EditTextPreference).let {
+ // It does not work on XML, do it here
+ it.icon = activity?.let {
+ ThemeUtils.tintDrawable(it,
+ ContextCompat.getDrawable(it, R.drawable.ic_add_black)!!, R.attr.vctr_settings_icon_tint_color)
+ }
+
+ // Unfortunately, this is not supported in lib v7
+ // it.editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+
+ it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+ addEmail((newValue as String).trim())
+ false
+ }
+ }
+
+ // Add phone number
+ findPreference(ADD_PHONE_NUMBER_PREFERENCE_KEY).let {
+ // It does not work on XML, do it here
+ it.icon = activity?.let {
+ ThemeUtils.tintDrawable(it,
+ ContextCompat.getDrawable(it, R.drawable.ic_add_black)!!, R.attr.vctr_settings_icon_tint_color)
+ }
+
+ it.setOnPreferenceClickListener {
+ // TODO val intent = PhoneNumberAdditionActivity.getIntent(activity, mSession.credentials.userId)
+ // startActivityForResult(intent, REQUEST_NEW_PHONE_NUMBER)
+ true
+ }
+ }
+
+ refreshEmailsList()
+ refreshPhoneNumbersList()
+
+ // Contacts
+ setContactsPreferences()
+
+ // user interface preferences
+ setUserInterfacePreferences()
+
+ // Url preview
+ (findPreference(PreferencesManager.SETTINGS_SHOW_URL_PREVIEW_KEY) as SwitchPreference).let {
+ /*
+ TODO
+ it.isChecked = mSession.isURLPreviewEnabled
+
+ it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+ if (null != newValue && newValue as Boolean != mSession.isURLPreviewEnabled) {
+ displayLoadingView()
+ mSession.setURLPreviewStatus(newValue, object : ApiCallback {
+ override fun onSuccess(info: Void?) {
+ it.isChecked = mSession.isURLPreviewEnabled
+ hideLoadingView()
+ }
+
+ private fun onError(errorMessage: String) {
+ activity?.toast(errorMessage)
+
+ onSuccess(null)
+ }
+
+ override fun onNetworkError(e: Exception) {
+ onError(e.localizedMessage)
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ onError(e.localizedMessage)
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ onError(e.localizedMessage)
+ }
+ })
+ }
+
+ false
+ }
+ */
+ }
+
+ // Themes
+ findPreference(ThemeUtils.APPLICATION_THEME_KEY)
+ .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+ if (newValue is String) {
+ // TODO VectorApp.updateApplicationTheme(newValue)
+ activity?.let {
+ it.startActivity(it.intent)
+ it.finish()
+ }
+ true
+ } else {
+ false
+ }
+ }
+
+ // Flair
+ refreshGroupFlairsList()
+
+ // push rules
+
+ // Notification privacy
+ mNotificationPrivacyPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ // TODO startActivity(NotificationPrivacyActivity.getIntent(activity))
+ true
+ }
+ refreshNotificationPrivacy()
+
+ for (preferenceKey in mPrefKeyToBingRuleId.keys) {
+ val preference = findPreference(preferenceKey)
+
+ if (null != preference) {
+ if (preference is SwitchPreference) {
+ preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValueAsVoid ->
+ // on some old android APIs,
+ // the callback is called even if there is no user interaction
+ // so the value will be checked to ensure there is really no update.
+ onPushRuleClick(preference.key, newValueAsVoid as Boolean)
+ true
+ }
+ }
+ }
+ }
+
+ // background sync tuning settings
+ // these settings are useless and hidden if the app is registered to the FCM push service
+ /*
+ TODO
+ val pushManager = Matrix.getInstance(appContext).pushManager
+ if (pushManager.useFcm() && pushManager.hasRegistrationToken()) {
+ // Hide the section
+ preferenceScreen.removePreference(backgroundSyncDivider)
+ preferenceScreen.removePreference(backgroundSyncCategory)
+ } else {
+ backgroundSyncPreference.let {
+ it.isChecked = pushManager.isBackgroundSyncAllowed
+
+ it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, aNewValue ->
+ val newValue = aNewValue as Boolean
+
+ if (newValue != pushManager.isBackgroundSyncAllowed) {
+ pushManager.isBackgroundSyncAllowed = newValue
+ }
+
+ displayLoadingView()
+
+ Matrix.getInstance(activity)?.pushManager?.forceSessionsRegistration(object : ApiCallback {
+ override fun onSuccess(info: Void?) {
+ hideLoadingView()
+ }
+
+ override fun onMatrixError(e: MatrixError?) {
+ hideLoadingView()
+ }
+
+ override fun onNetworkError(e: java.lang.Exception?) {
+ hideLoadingView()
+ }
+
+ override fun onUnexpectedError(e: java.lang.Exception?) {
+ hideLoadingView()
+ }
+ })
+
+ true
+ }
+ }
+ }
+ */
+
+ // Push target
+ refreshPushersList()
+
+ // Ignore users
+ refreshIgnoredUsersList()
+
+ // Lab
+ val useCryptoPref = findPreference(PreferencesManager.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY) as SwitchPreference
+ val cryptoIsEnabledPref = findPreference(PreferencesManager.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY)
+
+
+ if (mSession.isCryptoEnabled()) {
+ mLabsCategory.removePreference(useCryptoPref)
+
+ cryptoIsEnabledPref.isEnabled = false
+ } else {
+ mLabsCategory.removePreference(cryptoIsEnabledPref)
+
+ useCryptoPref.isChecked = false
+
+ useCryptoPref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValueAsVoid ->
+ if (TextUtils.isEmpty(mSession.sessionParams.credentials.deviceId)) {
+ activity?.let { activity ->
+ AlertDialog.Builder(activity)
+ .setMessage(R.string.room_settings_labs_end_to_end_warnings)
+ .setPositiveButton(R.string.logout) { _, _ ->
+ // TODO CommonActivityUtils.logout(activity)
+ }
+ .setNegativeButton(R.string.cancel) { _, _ ->
+ useCryptoPref.isChecked = false
+ }
+ .setOnCancelListener {
+ useCryptoPref.isChecked = false
+ }
+ .show()
+ }
+ } else {
+ val newValue = newValueAsVoid as Boolean
+
+ if (mSession.isCryptoEnabled() != newValue) {
+ /* TODO
+ displayLoadingView()
+
+ mSession.enableCrypto(newValue, object : ApiCallback {
+ private fun refresh() {
+ activity?.runOnUiThread {
+ hideLoadingView()
+ useCryptoPref.isChecked = mSession.isCryptoEnabled
+
+ if (mSession.isCryptoEnabled) {
+ mLabsCategory.removePreference(useCryptoPref)
+ mLabsCategory.addPreference(cryptoIsEnabledPref)
+ }
+ }
+ }
+
+ override fun onSuccess(info: Void?) {
+ useCryptoPref.isEnabled = false
+ refresh()
+ }
+
+ override fun onNetworkError(e: Exception) {
+ useCryptoPref.isChecked = false
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ useCryptoPref.isChecked = false
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ useCryptoPref.isChecked = false
+ }
+ })
+ */
+ }
+ }
+
+ true
+ }
+ }
+
+ // SaveMode Management
+ findPreference(PreferencesManager.SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY)
+ .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+ /* TODO
+ val sessions = Matrix.getMXSessions(activity)
+ for (session in sessions) {
+ session.setUseDataSaveMode(newValue as Boolean)
+ }
+ */
+
+ true
+ }
+
+ // Device list
+ refreshDevicesList()
+
+ // Advanced settings
+
+ // user account
+ findPreference(PreferencesManager.SETTINGS_LOGGED_IN_PREFERENCE_KEY)
+ .summary = mSession.sessionParams.credentials.userId
+
+ // home server
+ findPreference(PreferencesManager.SETTINGS_HOME_SERVER_PREFERENCE_KEY)
+ .summary = mSession.sessionParams.homeServerConnectionConfig.homeServerUri.toString()
+
+ // identity server
+ findPreference(PreferencesManager.SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY)
+ .summary = mSession.sessionParams.homeServerConnectionConfig.identityServerUri.toString()
+
+ // Analytics
+
+ // Analytics tracking management
+ (findPreference(PreferencesManager.SETTINGS_USE_ANALYTICS_KEY) as SwitchPreference).let {
+ // On if the analytics tracking is activated
+ it.isChecked = PreferencesManager.useAnalytics(appContext)
+
+ it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+ PreferencesManager.setUseAnalytics(appContext, newValue as Boolean)
+ true
+ }
+ }
+
+ // Rageshake Management
+ (findPreference(PreferencesManager.SETTINGS_USE_RAGE_SHAKE_KEY) as SwitchPreference).let {
+ it.isChecked = PreferencesManager.useRageshake(appContext)
+
+ it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+ PreferencesManager.setUseRageshake(appContext, newValue as Boolean)
+ true
+ }
+ }
+
+ // preference to start the App info screen, to facilitate App permissions access
+ findPreference(APP_INFO_LINK_PREFERENCE_KEY)
+ .onPreferenceClickListener = Preference.OnPreferenceClickListener {
+
+ activity?.let {
+ val intent = Intent().apply {
+ action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ val uri = appContext?.let { Uri.fromParts("package", it.packageName, null) }
+
+ data = uri
+ }
+ it.applicationContext.startActivity(intent)
+ }
+
+ true
+ }
+
+ // application version
+ (findPreference(PreferencesManager.SETTINGS_VERSION_PREFERENCE_KEY)).let {
+ it.summary = "TODO" // VectorUtils.getApplicationVersion(appContext)
+
+ it.setOnPreferenceClickListener {
+ appContext?.let {
+ copyToClipboard(it, "TODO") //VectorUtils.getApplicationVersion(it))
+ }
+ true
+ }
+ }
+
+ // olm version
+ findPreference(PreferencesManager.SETTINGS_OLM_VERSION_PREFERENCE_KEY)
+ // TODO .summary = mSession.getCryptoVersion(appContext, false)
+
+ // copyright
+ findPreference(PreferencesManager.SETTINGS_COPYRIGHT_PREFERENCE_KEY)
+ .onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ // TODO VectorUtils.displayAppCopyright()
+ false
+ }
+
+ // terms & conditions
+ findPreference(PreferencesManager.SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY)
+ .onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ // TODO VectorUtils.displayAppTac()
+ false
+ }
+
+ // privacy policy
+ findPreference(PreferencesManager.SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY)
+ .onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ // TODO VectorUtils.displayAppPrivacyPolicy()
+ false
+ }
+
+ // third party notice
+ findPreference(PreferencesManager.SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY)
+ .onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ // TODO VectorUtils.displayThirdPartyLicenses()
+ false
+ }
+
+ // update keep medias period
+ findPreference(PreferencesManager.SETTINGS_MEDIA_SAVING_PERIOD_KEY).let {
+ it.summary = PreferencesManager.getSelectedMediasSavingPeriodString(activity)
+
+ it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ context?.let { context: Context ->
+ AlertDialog.Builder(context)
+ .setSingleChoiceItems(R.array.media_saving_choice,
+ PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n ->
+ PreferencesManager.setSelectedMediasSavingPeriod(activity, n)
+ d.cancel()
+
+ it.summary = PreferencesManager.getSelectedMediasSavingPeriodString(activity)
+ }
+ .show()
+ }
+
+ false
+ }
+ }
+
+ // clear medias cache
+ findPreference(PreferencesManager.SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY).let {
+ /*
+ TODO
+ MXMediaCache.getCachesSize(activity, object : SimpleApiCallback() {
+ override fun onSuccess(size: Long) {
+ if (null != activity) {
+ it.summary = android.text.format.Formatter.formatFileSize(activity, size)
+ }
+ }
+ })
+
+ it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ displayLoadingView()
+
+ val task = ClearMediaCacheAsyncTask(
+ backgroundTask = {
+ mSession.mediaCache.clear()
+ activity?.let { it -> Glide.get(it).clearDiskCache() }
+ },
+ onCompleteTask = {
+ hideLoadingView()
+
+ MXMediaCache.getCachesSize(activity, object : SimpleApiCallback() {
+ override fun onSuccess(size: Long) {
+ it.summary = Formatter.formatFileSize(activity, size)
+ }
+ })
+ }
+ )
+
+ try {
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
+ } catch (e: Exception) {
+ Timber.e(e, "## mSession.getMediaCache().clear() failed " + e.message)
+ task.cancel(true)
+ hideLoadingView()
+ }
+
+ false
+ }
+ */
+ }
+
+ // Incoming call sounds
+ mUseRiotCallRingtonePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ activity?.let { setUseRiotDefaultRingtone(it, mUseRiotCallRingtonePreference.isChecked) }
+ false
+ }
+
+ mCallRingtonePreference.let {
+ activity?.let { activity -> it.summary = getCallRingtoneName(activity) }
+ it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ displayRingtonePicker()
+ false
+ }
+ }
+
+ // clear cache
+ findPreference(PreferencesManager.SETTINGS_CLEAR_CACHE_PREFERENCE_KEY).let {
+ /*
+ TODO
+ MXSession.getApplicationSizeCaches(activity, object : SimpleApiCallback() {
+ override fun onSuccess(size: Long) {
+ if (null != activity) {
+ it.summary = android.text.format.Formatter.formatFileSize(activity, size)
+ }
+ }
+ })
+ */
+
+ it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ displayLoadingView()
+ // TODO Matrix.getInstance(appContext).reloadSessions(appContext)
+ false
+ }
+ }
+
+ // Deactivate account section
+
+ // deactivate account
+ findPreference(PreferencesManager.SETTINGS_DEACTIVATE_ACCOUNT_KEY)
+ .onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ activity?.let {
+ // TODO startActivity(DeactivateAccountActivity.getIntent(it))
+ }
+
+ false
+ }
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ val view = super.onCreateView(inflater, container, savedInstanceState)
+
+ view?.apply {
+ val listView = findViewById(android.R.id.list)
+ listView?.setPadding(0, 0, 0, 0)
+ }
+
+ return view
+ }
+
+ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
+ // if the user toggles the contacts book permission
+ /* TODO
+ if (TextUtils.equals(key, ContactsManager.CONTACTS_BOOK_ACCESS_KEY)) {
+ // reset the current snapshot
+ ContactsManager.getInstance().clearSnapshot()
+ }
+ */
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ if (context is VectorSettingsFragmentInteractionListener) {
+ interactionListener = context
+ }
+ }
+
+ override fun onDetach() {
+ interactionListener = null
+ super.onDetach()
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ // find the view from parent activity
+ // TODO mLoadingView = activity?.findViewById(R.id.vector_settings_spinner_views)
+
+ /* TODO
+ if (mSession.isAlive) {
+ val context = activity?.applicationContext
+
+ mSession.dataHandler.addListener(mEventsListener)
+
+ Matrix.getInstance(context)?.addNetworkEventListener(mNetworkListener)
+
+ mSession.myUser.refreshThirdPartyIdentifiers(object : SimpleApiCallback() {
+ override fun onSuccess(info: Void?) {
+ // ensure that the activity still exists
+ // and the result is called in the right thread
+ activity?.runOnUiThread {
+ refreshEmailsList()
+ refreshPhoneNumbersList()
+ }
+ }
+ })
+
+ Matrix.getInstance(context)?.pushManager?.refreshPushersList(Matrix.getInstance(context)?.sessions, object : SimpleApiCallback(activity) {
+ override fun onSuccess(info: Void?) {
+ refreshPushersList()
+ }
+ })
+
+ PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this)
+
+ // refresh anything else
+ refreshPreferences()
+ refreshNotificationPrivacy()
+ refreshDisplay()
+ refreshBackgroundSyncPrefs()
+ }
+ */
+
+ interactionListener?.requestedKeyToHighlight()?.let { key ->
+ interactionListener?.requestHighlightPreferenceKeyOnResume(null)
+ val preference = findPreference(key)
+ (preference as? VectorPreference)?.isHighlighted = true
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+
+ val context = activity?.applicationContext
+
+ /* TODO
+ if (mSession.isAlive) {
+ mSession.dataHandler.removeListener(mEventsListener)
+ Matrix.getInstance(context)?.removeNetworkEventListener(mNetworkListener)
+ }
+ */
+
+ PreferenceManager.getDefaultSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(this)
+ }
+
+ // TODO Test
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ /* TODO
+ if (allGranted(grantResults)) {
+ if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) {
+ changeAvatar()
+ } else if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
+ exportKeys()
+ }
+ }
+ */
+ }
+
+ //==============================================================================================================
+ // Display methods
+ //==============================================================================================================
+
+ /**
+ * Display the loading view.
+ */
+ private fun displayLoadingView() {
+ // search the loading view from the upper view
+ if (null == mLoadingView) {
+ var parent = view
+
+ while (parent != null && mLoadingView == null) {
+ // TODO mLoadingView = parent.findViewById(R.id.vector_settings_spinner_views)
+ parent = parent.parent as View
+ }
+ } else {
+ mLoadingView?.visibility = View.VISIBLE
+ }
+ }
+
+ /**
+ * Hide the loading view.
+ */
+ private fun hideLoadingView() {
+ mLoadingView?.visibility = View.GONE
+ }
+
+ /**
+ * Hide the loading view and refresh the preferences.
+ *
+ * @param refresh true to refresh the display
+ */
+ private fun hideLoadingView(refresh: Boolean) {
+ mLoadingView?.visibility = View.GONE
+
+ if (refresh) {
+ refreshDisplay()
+ }
+ }
+
+ /**
+ * Refresh the preferences.
+ */
+ private fun refreshDisplay() {
+ /* TODO
+ // If Matrix instance is null, then connection can't be there
+ val isConnected = Matrix.getInstance(activity)?.isConnected ?: false
+ val appContext = activity?.applicationContext
+
+ val preferenceManager = preferenceManager
+
+ // refresh the avatar
+ mUserAvatarPreference.refreshAvatar()
+ mUserAvatarPreference.isEnabled = isConnected
+
+ // refresh the display name
+ mDisplayNamePreference.summary = mSession.myUser.displayname
+ mDisplayNamePreference.text = mSession.myUser.displayname
+ mDisplayNamePreference.isEnabled = isConnected
+
+ // change password
+ mPasswordPreference.isEnabled = isConnected
+
+ // update the push rules
+ val preferences = PreferenceManager.getDefaultSharedPreferences(appContext)
+
+ val rules = mSession.dataHandler.pushRules()
+
+ val pushManager = Matrix.getInstance(appContext)?.pushManager
+
+ for (preferenceKey in mPrefKeyToBingRuleId.keys) {
+ val preference = preferenceManager.findPreference(preferenceKey)
+
+ if (null != preference) {
+
+ if (preference is SwitchPreference) {
+ when (preferenceKey) {
+ PreferencesManager.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY ->
+ preference.isChecked = pushManager?.areDeviceNotificationsAllowed() ?: true
+
+ PreferencesManager.SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY -> {
+ preference.isChecked = pushManager?.isScreenTurnedOn ?: false
+ preference.isEnabled = pushManager?.areDeviceNotificationsAllowed() ?: true
+ }
+ else -> {
+ preference.isEnabled = null != rules && isConnected
+ preference.isChecked = preferences.getBoolean(preferenceKey, false)
+ }
+ }
+ }
+ }
+ }
+
+ // If notifications are disabled for the current user account or for the current user device
+ // The others notifications settings have to be disable too
+ val areNotificationAllowed = rules?.findDefaultRule(BingRule.RULE_ID_DISABLE_ALL)?.isEnabled == true
+
+ mNotificationPrivacyPreference.isEnabled = !areNotificationAllowed
+ && (pushManager?.areDeviceNotificationsAllowed() ?: true) && pushManager?.useFcm() ?: true
+ */
+ }
+
+ //==============================================================================================================
+ // Update items methods
+ //==============================================================================================================
+
+ /**
+ * Update the password.
+ */
+ private fun onPasswordUpdateClick() {
+ activity?.let { activity ->
+ val view: ViewGroup = activity.layoutInflater.inflate(R.layout.dialog_change_password, null) as ViewGroup
+
+ val showPassword: ImageView = view.findViewById(R.id.change_password_show_passwords)
+ val oldPasswordTil: TextInputLayout = view.findViewById(R.id.change_password_old_pwd_til)
+ val oldPasswordText: TextInputEditText = view.findViewById(R.id.change_password_old_pwd_text)
+ val newPasswordText: TextInputEditText = view.findViewById(R.id.change_password_new_pwd_text)
+ val confirmNewPasswordTil: TextInputLayout = view.findViewById(R.id.change_password_confirm_new_pwd_til)
+ val confirmNewPasswordText: TextInputEditText = view.findViewById(R.id.change_password_confirm_new_pwd_text)
+ val changePasswordLoader: View = view.findViewById(R.id.change_password_loader)
+
+ var passwordShown = false
+
+ showPassword.setOnClickListener(object : View.OnClickListener {
+ override fun onClick(v: View?) {
+ passwordShown = !passwordShown
+
+ oldPasswordText.showPassword(passwordShown)
+ newPasswordText.showPassword(passwordShown)
+ confirmNewPasswordText.showPassword(passwordShown)
+
+ showPassword.setImageResource(if (passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
+ }
+ })
+
+ val dialog = AlertDialog.Builder(activity)
+ .setView(view)
+ .setPositiveButton(R.string.settings_change_password_submit, null)
+ .setNegativeButton(R.string.cancel, null)
+ .setOnDismissListener {
+ val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.hideSoftInputFromWindow(view.applicationWindowToken, 0)
+ }
+ .create()
+
+ dialog.setOnShowListener {
+ val updateButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
+ updateButton.isEnabled = false
+
+ fun updateUi() {
+ val oldPwd = oldPasswordText.text.toString().trim()
+ val newPwd = newPasswordText.text.toString().trim()
+ val newConfirmPwd = confirmNewPasswordText.text.toString().trim()
+
+ updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty() && TextUtils.equals(newPwd, newConfirmPwd)
+
+ if (newPwd.isNotEmpty() && newConfirmPwd.isNotEmpty() && !TextUtils.equals(newPwd, newConfirmPwd)) {
+ confirmNewPasswordTil.error = getString(R.string.passwords_do_not_match)
+ }
+ }
+
+ oldPasswordText.addTextChangedListener(object : SimpleTextWatcher() {
+ override fun afterTextChanged(s: Editable) {
+ oldPasswordTil.error = null
+ updateUi()
+ }
+ })
+
+ newPasswordText.addTextChangedListener(object : SimpleTextWatcher() {
+ override fun afterTextChanged(s: Editable) {
+ confirmNewPasswordTil.error = null
+ updateUi()
+ }
+ })
+
+ confirmNewPasswordText.addTextChangedListener(object : SimpleTextWatcher() {
+ override fun afterTextChanged(s: Editable) {
+ confirmNewPasswordTil.error = null
+ updateUi()
+ }
+ })
+
+ fun showPasswordLoadingView(toShow: Boolean) {
+ if (toShow) {
+ showPassword.isEnabled = false
+ oldPasswordText.isEnabled = false
+ newPasswordText.isEnabled = false
+ confirmNewPasswordText.isEnabled = false
+ changePasswordLoader.isVisible = true
+ updateButton.isEnabled = false
+ } else {
+ showPassword.isEnabled = true
+ oldPasswordText.isEnabled = true
+ newPasswordText.isEnabled = true
+ confirmNewPasswordText.isEnabled = true
+ changePasswordLoader.isVisible = false
+ updateButton.isEnabled = true
+ }
+ }
+
+ updateButton.setOnClickListener {
+ if (passwordShown) {
+ // Hide passwords during processing
+ showPassword.performClick()
+ }
+
+ val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.hideSoftInputFromWindow(view.applicationWindowToken, 0)
+
+ val oldPwd = oldPasswordText.text.toString().trim()
+ val newPwd = newPasswordText.text.toString().trim()
+
+ /* TODO
+ showPasswordLoadingView(true)
+
+ mSession.updatePassword(oldPwd, newPwd, object : ApiCallback {
+ private fun onDone(@StringRes textResId: Int) {
+ showPasswordLoadingView(false)
+
+ if (textResId == R.string.settings_fail_to_update_password_invalid_current_password) {
+ oldPasswordTil.error = getString(textResId)
+ } else {
+ dialog.dismiss()
+ activity.toast(textResId, Toast.LENGTH_LONG)
+ }
+ }
+
+ override fun onSuccess(info: Void?) {
+ onDone(R.string.settings_password_updated)
+ }
+
+ override fun onNetworkError(e: Exception) {
+ onDone(R.string.settings_fail_to_update_password)
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ if (e.error == "Invalid password") {
+ onDone(R.string.settings_fail_to_update_password_invalid_current_password)
+ } else {
+ dialog.dismiss()
+ onDone(R.string.settings_fail_to_update_password)
+ }
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ onDone(R.string.settings_fail_to_update_password)
+ }
+ })
+ */
+ }
+ }
+ dialog.show()
+ }
+ }
+
+ /**
+ * Update a push rule.
+ */
+
+ private fun onPushRuleClick(preferenceKey: String, newValue: Boolean) {
+ /* TODO
+ val matrixInstance = Matrix.getInstance(context)
+ val pushManager = matrixInstance.pushManager
+
+ Timber.d("onPushRuleClick $preferenceKey : set to $newValue")
+
+ when (preferenceKey) {
+
+ PreferencesManager.SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY -> {
+ if (pushManager.isScreenTurnedOn != newValue) {
+ pushManager.isScreenTurnedOn = newValue
+ }
+ }
+
+ PreferencesManager.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY -> {
+ val isConnected = matrixInstance.isConnected
+ val isAllowed = pushManager.areDeviceNotificationsAllowed()
+
+ // avoid useless update
+ if (isAllowed == newValue) {
+ return
+ }
+
+ pushManager.setDeviceNotificationsAllowed(!isAllowed)
+
+ // when using FCM
+ // need to register on servers
+ if (isConnected && pushManager.useFcm() && (pushManager.isServerRegistered || pushManager.isServerUnRegistered)) {
+ val listener = object : ApiCallback {
+
+ private fun onDone() {
+ activity?.runOnUiThread {
+ hideLoadingView(true)
+ refreshPushersList()
+ }
+ }
+
+ override fun onSuccess(info: Void?) {
+ onDone()
+ }
+
+ override fun onMatrixError(e: MatrixError?) {
+ // Set again the previous state
+ pushManager.setDeviceNotificationsAllowed(isAllowed)
+ onDone()
+ }
+
+ override fun onNetworkError(e: java.lang.Exception?) {
+ // Set again the previous state
+ pushManager.setDeviceNotificationsAllowed(isAllowed)
+ onDone()
+ }
+
+ override fun onUnexpectedError(e: java.lang.Exception?) {
+ // Set again the previous state
+ pushManager.setDeviceNotificationsAllowed(isAllowed)
+ onDone()
+ }
+ }
+
+ displayLoadingView()
+ if (pushManager.isServerRegistered) {
+ pushManager.unregister(listener)
+ } else {
+ pushManager.register(listener)
+ }
+ }
+ }
+
+ // check if there is an update
+
+ // on some old android APIs,
+ // the callback is called even if there is no user interaction
+ // so the value will be checked to ensure there is really no update.
+ else -> {
+
+ val ruleId = mPrefKeyToBingRuleId[preferenceKey]
+ val rule = mSession.dataHandler.pushRules()?.findDefaultRule(ruleId)
+
+ // check if there is an update
+ var curValue = null != rule && rule.isEnabled
+
+ if (TextUtils.equals(ruleId, BingRule.RULE_ID_DISABLE_ALL) || TextUtils.equals(ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
+ curValue = !curValue
+ }
+
+ // on some old android APIs,
+ // the callback is called even if there is no user interaction
+ // so the value will be checked to ensure there is really no update.
+ if (newValue == curValue) {
+ return
+ }
+
+ if (null != rule) {
+ displayLoadingView()
+ mSession.dataHandler.bingRulesManager.updateEnableRuleStatus(rule, !rule.isEnabled, object : BingRulesManager.onBingRuleUpdateListener {
+ private fun onDone() {
+ refreshDisplay()
+ hideLoadingView()
+ }
+
+ override fun onBingRuleUpdateSuccess() {
+ onDone()
+ }
+
+ override fun onBingRuleUpdateFailure(errorMessage: String) {
+ activity?.toast(errorMessage)
+ onDone()
+ }
+ })
+ }
+ }
+ }
+ */
+ }
+
+ /**
+ * Update the displayname.
+ */
+ private fun onDisplayNameClick(value: String?) {
+ /* TODO
+ if (!TextUtils.equals(mSession.myUser.displayname, value)) {
+ displayLoadingView()
+
+ mSession.myUser.updateDisplayName(value, object : ApiCallback {
+ override fun onSuccess(info: Void?) {
+ // refresh the settings value
+ PreferenceManager.getDefaultSharedPreferences(activity).edit {
+ putString(PreferencesManager.SETTINGS_DISPLAY_NAME_PREFERENCE_KEY, value)
+ }
+
+ onCommonDone(null)
+
+ refreshDisplay()
+ }
+
+ override fun onNetworkError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ if (MatrixError.M_CONSENT_NOT_GIVEN == e.errcode) {
+ activity?.runOnUiThread {
+ hideLoadingView()
+ (activity as VectorAppCompatActivity).consentNotGivenHelper.displayDialog(e)
+ }
+ } else {
+ onCommonDone(e.localizedMessage)
+ }
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+ })
+ }
+ */
+ }
+
+ private fun displayRingtonePicker() {
+ val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
+ putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, getString(R.string.settings_call_ringtone_dialog_title))
+ putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false)
+ putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
+ putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE)
+ activity?.let { putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, getCallRingtoneUri(it)) }
+ }
+ startActivityForResult(intent, REQUEST_CALL_RINGTONE)
+ }
+
+ /**
+ * Update the avatar.
+ */
+ private fun onUpdateAvatarClick() {
+ /* TODO
+ if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
+ changeAvatar()
+ }
+ */
+ }
+
+ private fun changeAvatar() {
+ /* TODO
+ val intent = Intent(activity, VectorMediaPickerActivity::class.java)
+ intent.putExtra(VectorMediaPickerActivity.EXTRA_AVATAR_MODE, true)
+ startActivityForResult(intent, VectorUtils.TAKE_IMAGE)
+ */
+ }
+
+ /**
+ * Refresh the notification privacy setting
+ */
+ private fun refreshNotificationPrivacy() {
+ /* TODO
+ val pushManager = Matrix.getInstance(activity).pushManager
+
+ // this setting apply only with FCM for the moment
+ if (pushManager.useFcm()) {
+ val notificationPrivacyString = NotificationPrivacyActivity.getNotificationPrivacyString(activity,
+ pushManager.notificationPrivacy)
+ mNotificationPrivacyPreference.summary = notificationPrivacyString
+ } else {
+ notificationsSettingsCategory.removePreference(mNotificationPrivacyPreference)
+ }
+ */
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+
+ if (resultCode == Activity.RESULT_OK) {
+ when (requestCode) {
+ REQUEST_CALL_RINGTONE -> {
+ val callRingtoneUri: Uri? = data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
+ val thisActivity = activity
+ if (callRingtoneUri != null && thisActivity != null) {
+ setCallRingtoneUri(thisActivity, callRingtoneUri)
+ mCallRingtonePreference.summary = getCallRingtoneName(thisActivity)
+ }
+ }
+ REQUEST_E2E_FILE_REQUEST_CODE -> importKeys(data)
+ REQUEST_NEW_PHONE_NUMBER -> refreshPhoneNumbersList()
+ REQUEST_PHONEBOOK_COUNTRY -> onPhonebookCountryUpdate(data)
+ REQUEST_LOCALE -> {
+ activity?.let {
+ startActivity(it.intent)
+ it.finish()
+ }
+ }
+ /* TODO
+ VectorUtils.TAKE_IMAGE -> {
+ val thumbnailUri = VectorUtils.getThumbnailUriFromIntent(activity, data, mSession.mediaCache)
+
+ if (null != thumbnailUri) {
+ displayLoadingView()
+
+ val resource = ResourceUtils.openResource(activity, thumbnailUri, null)
+
+ if (null != resource) {
+ mSession.mediaCache.uploadContent(resource.mContentStream, null, resource.mMimeType, null, object : MXMediaUploadListener() {
+
+ override fun onUploadError(uploadId: String?, serverResponseCode: Int, serverErrorMessage: String?) {
+ activity?.runOnUiThread { onCommonDone(serverResponseCode.toString() + " : " + serverErrorMessage) }
+ }
+
+ override fun onUploadComplete(uploadId: String?, contentUri: String?) {
+ activity?.runOnUiThread {
+ mSession.myUser.updateAvatarUrl(contentUri, object : ApiCallback {
+ override fun onSuccess(info: Void?) {
+ onCommonDone(null)
+ refreshDisplay()
+ }
+
+ override fun onNetworkError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ if (MatrixError.M_CONSENT_NOT_GIVEN == e.errcode) {
+ activity?.runOnUiThread {
+ hideLoadingView()
+ (activity as VectorAppCompatActivity).consentNotGivenHelper.displayDialog(e)
+ }
+ } else {
+ onCommonDone(e.localizedMessage)
+ }
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+ })
+ }
+ }
+ })
+ }
+ }
+ }
+ */
+ }
+ }
+ }
+
+ /**
+ * Refresh the known information about the account
+ */
+ private fun refreshPreferences() {
+ PreferenceManager.getDefaultSharedPreferences(activity).edit {
+ putString(PreferencesManager.SETTINGS_DISPLAY_NAME_PREFERENCE_KEY, "TODO") //mSession.myUser.displayname)
+ putString(PreferencesManager.SETTINGS_VERSION_PREFERENCE_KEY, "TODO") // VectorUtils.getApplicationVersion(activity))
+
+ /* TODO
+ mSession.dataHandler.pushRules()?.let {
+ for (preferenceKey in mPrefKeyToBingRuleId.keys) {
+ val preference = findPreference(preferenceKey)
+
+ if (null != preference && preference is SwitchPreference) {
+ val ruleId = mPrefKeyToBingRuleId[preferenceKey]
+
+ val rule = it.findDefaultRule(ruleId)
+ var isEnabled = null != rule && rule.isEnabled
+
+ if (TextUtils.equals(ruleId, BingRule.RULE_ID_DISABLE_ALL) || TextUtils.equals(ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
+ isEnabled = !isEnabled
+ } else if (isEnabled) {
+ val actions = rule?.actions
+
+ // no action -> noting will be done
+ if (null == actions || actions.isEmpty()) {
+ isEnabled = false
+ } else if (1 == actions.size) {
+ try {
+ isEnabled = !TextUtils.equals(actions[0] as String, BingRule.ACTION_DONT_NOTIFY)
+ } catch (e: Exception) {
+ Timber.e(e, "## refreshPreferences failed " + e.message)
+ }
+
+ }
+ }// check if the rule is only defined by don't notify
+
+ putBoolean(preferenceKey, isEnabled)
+ }
+ }
+ }
+ */
+ }
+ }
+
+ /**
+ * Display a dialog which asks confirmation for the deletion of a 3pid
+ *
+ * @param pid the 3pid to delete
+ * @param preferenceSummary the displayed 3pid
+ */
+ private fun displayDelete3PIDConfirmationDialog(/* TODO pid: ThirdPartyIdentifier,*/ preferenceSummary: CharSequence) {
+ val mediumFriendlyName = "TODO" // ThreePid.getMediumFriendlyName(pid.medium, activity).toLowerCase(VectorLocale.applicationLocale)
+ val dialogMessage = getString(R.string.settings_delete_threepid_confirmation, mediumFriendlyName, preferenceSummary)
+
+ activity?.let {
+ AlertDialog.Builder(it)
+ .setTitle(R.string.dialog_title_confirmation)
+ .setMessage(dialogMessage)
+ .setPositiveButton(R.string.remove) { _, _ ->
+ /* TODO
+ displayLoadingView()
+
+ mSession.myUser.delete3Pid(pid, object : ApiCallback {
+ override fun onSuccess(info: Void?) {
+ when (pid.medium) {
+ ThreePid.MEDIUM_EMAIL -> refreshEmailsList()
+ ThreePid.MEDIUM_MSISDN -> refreshPhoneNumbersList()
+ }
+ onCommonDone(null)
+ }
+
+ override fun onNetworkError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ onCommonDone(e.localizedMessage)
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+ })
+ */
+ }
+ .setNegativeButton(R.string.cancel, null)
+ .show()
+ }
+ }
+
+ //==============================================================================================================
+ // ignored users list management
+ //==============================================================================================================
+
+ /**
+ * Refresh the ignored users list
+ */
+ private fun refreshIgnoredUsersList() {
+ val ignoredUsersList = mutableListOf() // TODO mSession.dataHandler.ignoredUserIds
+
+ ignoredUsersList.sortWith(Comparator { u1, u2 ->
+ u1.toLowerCase(VectorLocale.applicationLocale).compareTo(u2.toLowerCase(VectorLocale.applicationLocale))
+ })
+
+ val preferenceScreen = preferenceScreen
+
+ preferenceScreen.removePreference(mIgnoredUserSettingsCategory)
+ preferenceScreen.removePreference(mIgnoredUserSettingsCategoryDivider)
+ mIgnoredUserSettingsCategory.removeAll()
+
+ if (ignoredUsersList.size > 0) {
+ preferenceScreen.addPreference(mIgnoredUserSettingsCategoryDivider)
+ preferenceScreen.addPreference(mIgnoredUserSettingsCategory)
+
+ for (userId in ignoredUsersList) {
+ val preference = Preference(activity)
+
+ preference.title = userId
+ preference.key = IGNORED_USER_KEY_BASE + userId
+
+ preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ activity?.let {
+ AlertDialog.Builder(it)
+ .setMessage(getString(R.string.settings_unignore_user, userId))
+ .setPositiveButton(R.string.yes) { _, _ ->
+ displayLoadingView()
+
+ val idsList = ArrayList()
+ idsList.add(userId)
+
+ /* TODO
+ mSession.unIgnoreUsers(idsList, object : ApiCallback {
+ override fun onSuccess(info: Void?) {
+ onCommonDone(null)
+ }
+
+ override fun onNetworkError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ onCommonDone(e.localizedMessage)
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+ })
+ */
+ }
+ .setNegativeButton(R.string.no, null)
+ .show()
+ }
+
+ false
+ }
+
+ mIgnoredUserSettingsCategory.addPreference(preference)
+ }
+ }
+ }
+
+ //==============================================================================================================
+ // pushers list management
+ //==============================================================================================================
+
+ /**
+ * Refresh the pushers list
+ */
+ private fun refreshPushersList() {
+ activity?.let { activity ->
+ /* TODO
+ val pushManager = Matrix.getInstance(activity).pushManager
+ val pushersList = ArrayList(pushManager.mPushersList)
+
+ if (pushersList.isEmpty()) {
+ preferenceScreen.removePreference(mPushersSettingsCategory)
+ preferenceScreen.removePreference(mPushersSettingsDivider)
+ return
+ }
+
+ // check first if there is an update
+ var isNewList = true
+ if (pushersList.size == mDisplayedPushers.size) {
+ isNewList = !mDisplayedPushers.containsAll(pushersList)
+ }
+
+ if (isNewList) {
+ // remove the displayed one
+ mPushersSettingsCategory.removeAll()
+
+ // add new emails list
+ mDisplayedPushers = pushersList
+
+ var index = 0
+
+ for (pusher in mDisplayedPushers) {
+ if (null != pusher.lang) {
+ val isThisDeviceTarget = TextUtils.equals(pushManager.currentRegistrationToken, pusher.pushkey)
+
+ val preference = VectorPreference(activity).apply {
+ mTypeface = if (isThisDeviceTarget) Typeface.BOLD else Typeface.NORMAL
+ }
+ preference.title = pusher.deviceDisplayName
+ preference.summary = pusher.appDisplayName
+ preference.key = PUSHER_PREFERENCE_KEY_BASE + index
+ index++
+ mPushersSettingsCategory.addPreference(preference)
+
+ // the user cannot remove the self device target
+ if (!isThisDeviceTarget) {
+ preference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener {
+ override fun onPreferenceLongClick(preference: Preference): Boolean {
+ AlertDialog.Builder(activity)
+ .setTitle(R.string.dialog_title_confirmation)
+ .setMessage(R.string.settings_delete_notification_targets_confirmation)
+ .setPositiveButton(R.string.remove)
+ { _, _ ->
+ displayLoadingView()
+ pushManager.unregister(mSession, pusher, object : ApiCallback {
+ override fun onSuccess(info: Void?) {
+ refreshPushersList()
+ onCommonDone(null)
+ }
+
+ override fun onNetworkError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ onCommonDone(e.localizedMessage)
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+ })
+ }
+ .setNegativeButton(R.string.cancel, null)
+ .show()
+ return true
+ }
+ }
+ }
+ }
+ }
+ }
+ */
+ }
+ }
+
+ //==============================================================================================================
+ // Email management
+ //==============================================================================================================
+
+ /**
+ * Refresh the emails list
+ */
+ private fun refreshEmailsList() {
+ val currentEmail3PID = emptyList() // TODO ArrayList(mSession.myUser.getlinkedEmails())
+
+ val newEmailsList = ArrayList()
+ for (identifier in currentEmail3PID) {
+ // TODO newEmailsList.add(identifier.address)
+ }
+
+ // check first if there is an update
+ var isNewList = true
+ if (newEmailsList.size == mDisplayedEmails.size) {
+ isNewList = !mDisplayedEmails.containsAll(newEmailsList)
+ }
+
+ if (isNewList) {
+ // remove the displayed one
+ run {
+ var index = 0
+ while (true) {
+ val preference = mUserSettingsCategory.findPreference(EMAIL_PREFERENCE_KEY_BASE + index)
+
+ if (null != preference) {
+ mUserSettingsCategory.removePreference(preference)
+ } else {
+ break
+ }
+ index++
+ }
+ }
+
+ // add new emails list
+ mDisplayedEmails = newEmailsList
+
+ val addEmailBtn = mUserSettingsCategory.findPreference(ADD_EMAIL_PREFERENCE_KEY)
+ ?: return
+
+ var order = addEmailBtn.order
+
+ for ((index, email3PID) in currentEmail3PID.withIndex()) {
+ val preference = VectorPreference(activity!!)
+
+ preference.title = getString(R.string.settings_email_address)
+ preference.summary = "TODO" // email3PID.address
+ preference.key = EMAIL_PREFERENCE_KEY_BASE + index
+ preference.order = order
+
+ preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { pref ->
+ displayDelete3PIDConfirmationDialog(/* TODO email3PID, */ pref.summary)
+ true
+ }
+
+ preference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener {
+ override fun onPreferenceLongClick(preference: Preference): Boolean {
+ activity?.let { copyToClipboard(it, "TODO") } //email3PID.address) }
+ return true
+ }
+ }
+
+ mUserSettingsCategory.addPreference(preference)
+
+ order++
+ }
+
+ addEmailBtn.order = order
+ }
+ }
+
+ /**
+ * A request has been processed.
+ * Display a toast if there is a an error message
+ *
+ * @param errorMessage the error message
+ */
+ private fun onCommonDone(errorMessage: String?) {
+ activity?.runOnUiThread {
+ if (!TextUtils.isEmpty(errorMessage) && errorMessage != null) {
+ activity?.toast(errorMessage!!)
+ }
+ hideLoadingView()
+ }
+ }
+
+ /**
+ * Attempt to add a new email to the account
+ *
+ * @param email the email to add.
+ */
+ private fun addEmail(email: String) {
+ // check first if the email syntax is valid
+ // if email is null , then also its invalid email
+ if (TextUtils.isEmpty(email) || !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
+ activity?.toast(R.string.auth_invalid_email)
+ return
+ }
+
+ // check first if the email syntax is valid
+ if (mDisplayedEmails.indexOf(email) >= 0) {
+ activity?.toast(R.string.auth_email_already_defined)
+ return
+ }
+
+ /* TODO
+ val pid = ThreePid(email, ThreePid.MEDIUM_EMAIL)
+
+ displayLoadingView()
+
+ mSession.myUser.requestEmailValidationToken(pid, object : ApiCallback {
+ override fun onSuccess(info: Void?) {
+ activity?.runOnUiThread { showEmailValidationDialog(pid) }
+ }
+
+ override fun onNetworkError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ if (TextUtils.equals(MatrixError.THREEPID_IN_USE, e.errcode)) {
+ onCommonDone(getString(R.string.account_email_already_used_error))
+ } else {
+ onCommonDone(e.localizedMessage)
+ }
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+ })
+ */
+ }
+
+ /**
+ * Show an email validation dialog to warn the user tho valid his email link.
+ *
+ * @param pid the used pid.
+ */
+ /* TODO
+ private fun showEmailValidationDialog(pid: ThreePid) {
+ activity?.let {
+ AlertDialog.Builder(it)
+ .setTitle(R.string.account_email_validation_title)
+ .setMessage(R.string.account_email_validation_message)
+ .setPositiveButton(R.string._continue) { _, _ ->
+ mSession.myUser.add3Pid(pid, true, object : ApiCallback {
+ override fun onSuccess(info: Void?) {
+ it.runOnUiThread {
+ hideLoadingView()
+ refreshEmailsList()
+ }
+ }
+
+ override fun onNetworkError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ if (TextUtils.equals(e.errcode, MatrixError.THREEPID_AUTH_FAILED)) {
+ it.runOnUiThread {
+ hideLoadingView()
+ it.toast(R.string.account_email_validation_error)
+ }
+ } else {
+ onCommonDone(e.localizedMessage)
+ }
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+ })
+ }
+ .setNegativeButton(R.string.cancel) { _, _ ->
+ hideLoadingView()
+ }
+ .show()
+ }
+ }
+ */
+
+ //==============================================================================================================
+ // Phone number management
+ //==============================================================================================================
+
+ /**
+ * Refresh phone number list
+ */
+ private fun refreshPhoneNumbersList() {
+ /* TODO
+ val currentPhoneNumber3PID = ArrayList(mSession.myUser.getlinkedPhoneNumbers())
+
+ val phoneNumberList = ArrayList()
+ for (identifier in currentPhoneNumber3PID) {
+ phoneNumberList.add(identifier.address)
+ }
+
+ // check first if there is an update
+ var isNewList = true
+ if (phoneNumberList.size == mDisplayedPhoneNumber.size) {
+ isNewList = !mDisplayedPhoneNumber.containsAll(phoneNumberList)
+ }
+
+ if (isNewList) {
+ // remove the displayed one
+ run {
+ var index = 0
+ while (true) {
+ val preference = mUserSettingsCategory.findPreference(PHONE_NUMBER_PREFERENCE_KEY_BASE + index)
+
+ if (null != preference) {
+ mUserSettingsCategory.removePreference(preference)
+ } else {
+ break
+ }
+ index++
+ }
+ }
+
+ // add new phone number list
+ mDisplayedPhoneNumber = phoneNumberList
+
+ val addPhoneBtn = mUserSettingsCategory.findPreference(ADD_PHONE_NUMBER_PREFERENCE_KEY)
+ ?: return
+
+ var order = addPhoneBtn.order
+
+ for ((index, phoneNumber3PID) in currentPhoneNumber3PID.withIndex()) {
+ val preference = VectorPreference(activity!!)
+
+ preference.title = getString(R.string.settings_phone_number)
+ var phoneNumberFormatted = phoneNumber3PID.address
+ try {
+ // Attempt to format phone number
+ val phoneNumber = PhoneNumberUtil.getInstance().parse("+$phoneNumberFormatted", null)
+ phoneNumberFormatted = PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)
+ } catch (e: NumberParseException) {
+ // Do nothing, we will display raw version
+ }
+
+ preference.summary = phoneNumberFormatted
+ preference.key = PHONE_NUMBER_PREFERENCE_KEY_BASE + index
+ preference.order = order
+
+ preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ displayDelete3PIDConfirmationDialog(phoneNumber3PID, preference.summary)
+ true
+ }
+
+ preference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener {
+ override fun onPreferenceLongClick(preference: Preference): Boolean {
+ activity?.let { copyToClipboard(it, phoneNumber3PID.address) }
+ return true
+ }
+ }
+
+ order++
+ mUserSettingsCategory.addPreference(preference)
+ }
+
+ addPhoneBtn.order = order
+ }
+ */
+ }
+
+ //==============================================================================================================
+ // contacts management
+ //==============================================================================================================
+
+ private fun setContactsPreferences() {
+ /* TODO
+ // Permission
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ // on Android >= 23, use the system one
+ mContactSettingsCategory.removePreference(findPreference(ContactsManager.CONTACTS_BOOK_ACCESS_KEY))
+ }
+ // Phonebook country
+ mContactPhonebookCountryPreference.summary = PhoneNumberUtils.getHumanCountryCode(PhoneNumberUtils.getCountryCode(activity))
+
+ mContactPhonebookCountryPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ val intent = CountryPickerActivity.getIntent(activity, true)
+ startActivityForResult(intent, REQUEST_PHONEBOOK_COUNTRY)
+ true
+ }
+ */
+ }
+
+ private fun onPhonebookCountryUpdate(data: Intent?) {
+ /* TODO
+ if (data != null && data.hasExtra(CountryPickerActivity.EXTRA_OUT_COUNTRY_NAME)
+ && data.hasExtra(CountryPickerActivity.EXTRA_OUT_COUNTRY_CODE)) {
+ val countryCode = data.getStringExtra(CountryPickerActivity.EXTRA_OUT_COUNTRY_CODE)
+ if (!TextUtils.equals(countryCode, PhoneNumberUtils.getCountryCode(activity))) {
+ PhoneNumberUtils.setCountryCode(activity, countryCode)
+ mContactPhonebookCountryPreference.summary = data.getStringExtra(CountryPickerActivity.EXTRA_OUT_COUNTRY_NAME)
+ }
+ }
+ */
+ }
+
+ //==============================================================================================================
+ // user interface management
+ //==============================================================================================================
+
+ private fun setUserInterfacePreferences() {
+ // Selected language
+ selectedLanguagePreference.summary = VectorLocale.localeToLocalisedString(VectorLocale.applicationLocale)
+
+ selectedLanguagePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ // TODO startActivityForResult(LanguagePickerActivity.getIntent(activity), REQUEST_LOCALE)
+ true
+ }
+
+ // Text size
+ textSizePreference.summary = FontScale.getFontScaleDescription(activity!!)
+
+ textSizePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ activity?.let { displayTextSizeSelection(it) }
+ true
+ }
+ }
+
+ private fun displayTextSizeSelection(activity: Activity) {
+ val inflater = activity.layoutInflater
+ val layout = inflater.inflate(R.layout.dialog_select_text_size, null)
+
+ val dialog = AlertDialog.Builder(activity)
+ .setTitle(R.string.font_size)
+ .setView(layout)
+ .setPositiveButton(R.string.ok, null)
+ .setNegativeButton(R.string.cancel, null)
+ .show()
+
+ val linearLayout = layout.findViewById(R.id.text_selection_group_view)
+
+ val childCount = linearLayout.childCount
+
+ val scaleText = FontScale.getFontScaleDescription(activity)
+
+ for (i in 0 until childCount) {
+ val v = linearLayout.getChildAt(i)
+
+ if (v is CheckedTextView) {
+ v.isChecked = TextUtils.equals(v.text, scaleText)
+
+ v.setOnClickListener {
+ dialog.dismiss()
+ FontScale.updateFontScale(activity, v.text.toString())
+ activity.startActivity(activity.intent)
+ activity.finish()
+ }
+ }
+ }
+ }
+
+ //==============================================================================================================
+ // background sync management
+ //==============================================================================================================
+
+ /**
+ * Convert a delay in seconds to string
+ *
+ * @param seconds the delay in seconds
+ * @return the text
+ */
+ private fun secondsToText(seconds: Int): String {
+ return if (seconds > 1) {
+ seconds.toString() + " " + getString(R.string.settings_seconds)
+ } else {
+ seconds.toString() + " " + getString(R.string.settings_second)
+ }
+ }
+
+ /**
+ * Refresh the background sync preference
+ */
+ private fun refreshBackgroundSyncPrefs() {
+ /* TODO
+ activity?.let { activity ->
+ val pushManager = Matrix.getInstance(activity).pushManager
+
+ val timeout = pushManager.backgroundSyncTimeOut / 1000
+ val delay = pushManager.backgroundSyncDelay / 1000
+
+ // update the settings
+ PreferenceManager.getDefaultSharedPreferences(activity).edit {
+ putString(PreferencesManager.SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, timeout.toString() + "")
+ putString(PreferencesManager.SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, delay.toString() + "")
+ }
+
+ mSyncRequestTimeoutPreference?.let {
+ it.summary = secondsToText(timeout)
+ it.text = timeout.toString() + ""
+
+ it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+ var newTimeOut = timeout
+
+ try {
+ newTimeOut = Integer.parseInt(newValue as String)
+ } catch (e: Exception) {
+ Timber.e(e, "## refreshBackgroundSyncPrefs : parseInt failed " + e.message)
+ }
+
+ if (newTimeOut != timeout) {
+ pushManager.backgroundSyncTimeOut = newTimeOut * 1000
+
+ activity.runOnUiThread { refreshBackgroundSyncPrefs() }
+ }
+
+ false
+ }
+ }
+
+ mSyncRequestDelayPreference?.let {
+ it.summary = secondsToText(delay)
+ it.text = delay.toString() + ""
+
+ it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+ var newDelay = delay
+
+ try {
+ newDelay = Integer.parseInt(newValue as String)
+ } catch (e: Exception) {
+ Timber.e(e, "## refreshBackgroundSyncPrefs : parseInt failed " + e.message)
+ }
+
+ if (newDelay != delay) {
+ pushManager.backgroundSyncDelay = newDelay * 1000
+
+ activity.runOnUiThread { refreshBackgroundSyncPrefs() }
+ }
+
+ false
+ }
+ }
+ }
+ */
+ }
+
+ //==============================================================================================================
+ // Cryptography
+ //==============================================================================================================
+
+ private fun removeCryptographyPreference() {
+ preferenceScreen.let {
+ it.removePreference(mCryptographyCategory)
+ it.removePreference(mCryptographyCategoryDivider)
+
+ // Also remove keys management section
+ it.removePreference(mCryptographyManageCategory)
+ it.removePreference(mCryptographyManageCategoryDivider)
+ }
+ }
+
+ /**
+ * Build the cryptography preference section.
+ *
+ * @param aMyDeviceInfo the device info
+ */
+ private fun refreshCryptographyPreference(aMyDeviceInfo: DeviceInfo?) {
+ val userId = mSession.sessionParams.credentials.userId
+ val deviceId = mSession.sessionParams.credentials.deviceId
+
+ // device name
+ if (null != aMyDeviceInfo) {
+ cryptoInfoDeviceNamePreference.summary = "TODO" // aMyDeviceInfo.display_name
+
+ cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ displayDeviceRenameDialog(aMyDeviceInfo)
+ true
+ }
+
+ cryptoInfoDeviceNamePreference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener {
+ override fun onPreferenceLongClick(preference: Preference): Boolean {
+ activity?.let { copyToClipboard(it, "TODO") } //aMyDeviceInfo.display_name) }
+ return true
+ }
+ }
+ }
+
+ // crypto section: device ID
+ if (!TextUtils.isEmpty(deviceId)) {
+ cryptoInfoDeviceIdPreference.summary = deviceId
+
+ cryptoInfoDeviceIdPreference.setOnPreferenceClickListener {
+ activity?.let { copyToClipboard(it, deviceId!!) }
+ true
+ }
+
+
+ manageBackupPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ context?.let {
+ // TODO startActivity(KeysBackupManageActivity.intent(it, mSession.myUserId))
+ }
+ false
+ }
+
+ exportPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ exportKeys()
+ true
+ }
+
+ importPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ importKeys()
+ true
+ }
+ }
+
+ // crypto section: device key (fingerprint)
+ if (!TextUtils.isEmpty(deviceId) && !TextUtils.isEmpty(userId)) {
+ /* TODO
+ mSession.crypto?.getDeviceInfo(userId, deviceId, object : SimpleApiCallback() {
+ override fun onSuccess(deviceInfo: MXDeviceInfo?) {
+ if (null != deviceInfo && !TextUtils.isEmpty(deviceInfo.fingerprint()) && null != activity) {
+ cryptoInfoTextPreference.summary = deviceInfo.getFingerprintHumanReadable()
+
+ cryptoInfoTextPreference.setOnPreferenceClickListener {
+ activity?.let { copyToClipboard(it, deviceInfo.fingerprint()) }
+ true
+ }
+ }
+ }
+ })
+ */
+ }
+
+ sendToUnverifiedDevicesPref.isChecked = false
+
+ /* TODO
+ mSession.crypto?.getGlobalBlacklistUnverifiedDevices(object : SimpleApiCallback() {
+ override fun onSuccess(status: Boolean) {
+ sendToUnverifiedDevicesPref.isChecked = status
+ }
+ })
+
+ sendToUnverifiedDevicesPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ mSession.crypto?.getGlobalBlacklistUnverifiedDevices(object : SimpleApiCallback() {
+ override fun onSuccess(status: Boolean) {
+ if (sendToUnverifiedDevicesPref.isChecked != status) {
+ mSession.crypto
+ ?.setGlobalBlacklistUnverifiedDevices(sendToUnverifiedDevicesPref.isChecked, object : SimpleApiCallback() {
+ override fun onSuccess(info: Void?) {
+
+ }
+ })
+ }
+ }
+ })
+
+ true
+ }
+ */
+ }
+
+ //==============================================================================================================
+ // devices list
+ //==============================================================================================================
+
+ private fun removeDevicesPreference() {
+ preferenceScreen.let {
+ it.removePreference(mDevicesListSettingsCategory)
+ it.removePreference(mDevicesListSettingsCategoryDivider)
+ }
+ }
+
+ /**
+ * Force the refresh of the devices list.
+ * The devices list is the list of the devices where the user as looged in.
+ * It can be any mobile device, as any browser.
+ */
+ private fun refreshDevicesList() {
+ if (mSession.isCryptoEnabled() && !TextUtils.isEmpty(mSession.sessionParams.credentials.deviceId)) {
+ // display a spinner while loading the devices list
+ if (0 == mDevicesListSettingsCategory.preferenceCount) {
+ activity?.let {
+ val preference = ProgressBarPreference(it)
+ mDevicesListSettingsCategory.addPreference(preference)
+ }
+ }
+
+ /* TODO
+ mSession.getDevicesList(object : ApiCallback {
+ override fun onSuccess(info: DevicesListResponse) {
+ if (info.devices.isEmpty()) {
+ removeDevicesPreference()
+ } else {
+ buildDevicesSettings(info.devices)
+ }
+ }
+
+ override fun onNetworkError(e: Exception) {
+ removeDevicesPreference()
+ onCommonDone(e.message)
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ removeDevicesPreference()
+ onCommonDone(e.message)
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ removeDevicesPreference()
+ onCommonDone(e.message)
+ }
+ })
+ */
+ } else {
+ removeDevicesPreference()
+ removeCryptographyPreference()
+ }
+ }
+
+ /**
+ * Build the devices portion of the settings.
+ * Each row correspond to a device ID and its corresponding device name. Clicking on the row
+ * display a dialog containing: the device ID, the device name and the "last seen" information.
+ *
+ * @param aDeviceInfoList the list of the devices
+ */
+ private fun buildDevicesSettings(aDeviceInfoList: List) {
+ var preference: VectorPreference
+ var typeFaceHighlight: Int
+ var isNewList = true
+ val myDeviceId = mSession.sessionParams.credentials.deviceId
+
+ /* TODO
+ if (aDeviceInfoList.size == mDevicesNameList.size) {
+ isNewList = !mDevicesNameList.containsAll(aDeviceInfoList)
+ }
+
+ if (isNewList) {
+ var prefIndex = 0
+ mDevicesNameList = aDeviceInfoList
+
+ // sort before display: most recent first
+ DeviceInfo.sortByLastSeen(mDevicesNameList)
+
+ // start from scratch: remove the displayed ones
+ mDevicesListSettingsCategory.removeAll()
+
+ for (deviceInfo in mDevicesNameList) {
+ // set bold to distinguish current device ID
+ if (null != myDeviceId && myDeviceId == deviceInfo.device_id) {
+ mMyDeviceInfo = deviceInfo
+ typeFaceHighlight = Typeface.BOLD
+ } else {
+ typeFaceHighlight = Typeface.NORMAL
+ }
+
+ // add the edit text preference
+ preference = VectorPreference(activity!!).apply {
+ mTypeface = typeFaceHighlight
+ }
+
+ if (null == deviceInfo.device_id && null == deviceInfo.display_name) {
+ continue
+ } else {
+ if (null != deviceInfo.device_id) {
+ preference.title = deviceInfo.device_id
+ }
+
+ // display name parameter can be null (new JSON API)
+ if (null != deviceInfo.display_name) {
+ preference.summary = deviceInfo.display_name
+ }
+ }
+
+ preference.key = DEVICES_PREFERENCE_KEY_BASE + prefIndex
+ prefIndex++
+
+ // onClick handler: display device details dialog
+ preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ displayDeviceDetailsDialog(deviceInfo)
+ true
+ }
+
+ mDevicesListSettingsCategory.addPreference(preference)
+ }
+
+ refreshCryptographyPreference(mMyDeviceInfo)
+ }
+ */
+ }
+
+ /**
+ * Display a dialog containing the device ID, the device name and the "last seen" information.<>
+ * This dialog allow to delete the corresponding device (see [.displayDeviceDeletionDialog])
+ *
+ * @param aDeviceInfo the device information
+ */
+ private fun displayDeviceDetailsDialog(aDeviceInfo: DeviceInfo) {
+
+ activity?.let {
+
+ val builder = AlertDialog.Builder(it)
+ val inflater = it.layoutInflater
+ val layout = inflater.inflate(R.layout.dialog_device_details, null)
+ var textView = layout.findViewById(R.id.device_id)
+
+ textView.text = "TODO"//aDeviceInfo.device_id
+
+ // device name
+ textView = layout.findViewById(R.id.device_name)
+ val displayName = "TODO" // if (TextUtils.isEmpty(aDeviceInfo.display_name)) LABEL_UNAVAILABLE_DATA else aDeviceInfo.display_name
+ textView.text = displayName
+
+ // last seen info
+ textView = layout.findViewById(R.id.device_last_seen)
+ /* TODO
+ if (!TextUtils.isEmpty(aDeviceInfo.last_seen_ip)) {
+ val lastSeenIp = aDeviceInfo.last_seen_ip
+ val dateFormatTime = SimpleDateFormat("HH:mm:ss")
+ val time = dateFormatTime.format(Date(aDeviceInfo.last_seen_ts))
+ val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
+ val lastSeenTime = dateFormat.format(Date(aDeviceInfo.last_seen_ts)) + ", " + time
+ val lastSeenInfo = getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
+ textView.text = lastSeenInfo
+ } else {
+ // hide last time seen section
+ layout.findViewById(R.id.device_last_seen_title).visibility = View.GONE
+ textView.visibility = View.GONE
+ }
+ */
+
+ // title & icon
+ builder.setTitle(R.string.devices_details_dialog_title)
+ .setIcon(android.R.drawable.ic_dialog_info)
+ .setView(layout)
+ .setPositiveButton(R.string.rename) { _, _ -> displayDeviceRenameDialog(aDeviceInfo) }
+
+ /* TODO
+ // disable the deletion for our own device
+ if (!TextUtils.equals(mSession.crypto?.myDevice?.deviceId, aDeviceInfo.device_id)) {
+ builder.setNegativeButton(R.string.delete) { _, _ -> displayDeviceDeletionDialog(aDeviceInfo) }
+ }
+ */
+
+ builder.setNeutralButton(R.string.cancel, null)
+ .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
+ if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
+ dialog.cancel()
+ return@OnKeyListener true
+ }
+ false
+ })
+ .show()
+ }
+ }
+
+ /**
+ * Display an alert dialog to rename a device
+ *
+ * @param aDeviceInfoToRename device info
+ */
+ private fun displayDeviceRenameDialog(aDeviceInfoToRename: DeviceInfo) {
+ activity?.let {
+ val inflater = it.layoutInflater
+ val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
+
+ val input = layout.findViewById(R.id.edit_text)
+ /* TODO
+ input.setText(aDeviceInfoToRename.display_name)
+
+ AlertDialog.Builder(it)
+ .setTitle(R.string.devices_details_device_name)
+ .setView(layout)
+ .setPositiveButton(R.string.ok) { _, _ ->
+ displayLoadingView()
+
+ val newName = input.text.toString()
+
+ mSession.setDeviceName(aDeviceInfoToRename.device_id, newName, object : ApiCallback {
+ override fun onSuccess(info: Void?) {
+ hideLoadingView()
+
+ // search which preference is updated
+ val count = mDevicesListSettingsCategory.preferenceCount
+
+ for (i in 0 until count) {
+ val pref = mDevicesListSettingsCategory.getPreference(i)
+
+ if (TextUtils.equals(aDeviceInfoToRename.device_id, pref.title)) {
+ pref.summary = newName
+ }
+ }
+
+ // detect if the updated device is the current account one
+ if (TextUtils.equals(cryptoInfoDeviceIdPreference.summary, aDeviceInfoToRename.device_id)) {
+ cryptoInfoDeviceNamePreference.summary = newName
+ }
+
+ // Also change the display name in aDeviceInfoToRename, in case of multiple renaming
+ aDeviceInfoToRename.display_name = newName
+ }
+
+ override fun onNetworkError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ onCommonDone(e.localizedMessage)
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ onCommonDone(e.localizedMessage)
+ }
+ })
+ }
+ .setNegativeButton(R.string.cancel, null)
+ .show()
+ */
+ }
+ }
+
+ /**
+ * Try to delete a device.
+ *
+ * @param deviceId the device id
+ */
+ private fun deleteDevice(deviceId: String) {
+ displayLoadingView()
+ /* TODO
+ mSession.deleteDevice(deviceId, mAccountPassword, object : ApiCallback {
+ override fun onSuccess(info: Void?) {
+ hideLoadingView()
+ refreshDevicesList() // force settings update
+ }
+
+ private fun onError(message: String) {
+ mAccountPassword = null
+ onCommonDone(message)
+ }
+
+ override fun onNetworkError(e: Exception) {
+ onError(e.localizedMessage)
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ onError(e.localizedMessage)
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ onError(e.localizedMessage)
+ }
+ })
+ */
+ }
+
+ /**
+ * Display a delete confirmation dialog to remove a device.
+ * The user is invited to enter his password to confirm the deletion.
+ *
+ * @param aDeviceInfoToDelete device info
+ */
+ private fun displayDeviceDeletionDialog(aDeviceInfoToDelete: DeviceInfo) {
+ /*
+ TODO
+ if (aDeviceInfoToDelete.device_id != null) {
+ if (!TextUtils.isEmpty(mAccountPassword)) {
+ deleteDevice(aDeviceInfoToDelete.device_id)
+ } else {
+ activity?.let {
+ val inflater = it.layoutInflater
+ val layout = inflater.inflate(R.layout.dialog_device_delete, null)
+ val passwordEditText = layout.findViewById(R.id.delete_password)
+
+ AlertDialog.Builder(it)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setTitle(R.string.devices_delete_dialog_title)
+ .setView(layout)
+ .setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ ->
+ if (TextUtils.isEmpty(passwordEditText.toString())) {
+ it.toast(R.string.error_empty_field_your_password)
+ return@OnClickListener
+ }
+ mAccountPassword = passwordEditText.text.toString()
+ deleteDevice(aDeviceInfoToDelete.device_id)
+ })
+ .setNegativeButton(R.string.cancel) { _, _ ->
+ hideLoadingView()
+ }
+ .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
+ if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
+ dialog.cancel()
+ hideLoadingView()
+ return@OnKeyListener true
+ }
+ false
+ })
+ .show()
+ }
+ }
+ } else {
+ Timber.e("## displayDeviceDeletionDialog(): sanity check failure")
+ }
+ */
+ }
+
+ /**
+ * Manage the e2e keys export.
+ */
+ private fun exportKeys() {
+ // We need WRITE_EXTERNAL permission
+ /*
+ TODO
+ if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
+ activity?.let { activity ->
+ ExportKeysDialog().show(activity, object : ExportKeysDiaLog.ExportKeyDialogListener {
+ override fun onPassphrase(passphrase: String) {
+ displayLoadingView()
+
+ CommonActivityUtils.exportKeys(mSession, passphrase, object : SimpleApiCallback(activity) {
+ override fun onSuccess(filename: String) {
+ hideLoadingView()
+
+ AlertDialog.Builder(activity)
+ .setMessage(getString(R.string.encryption_export_saved_as, filename))
+ .setCancelable(false)
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+
+ override fun onNetworkError(e: Exception) {
+ super.onNetworkError(e)
+ hideLoadingView()
+ }
+
+ override fun onMatrixError(e: MatrixError) {
+ super.onMatrixError(e)
+ hideLoadingView()
+ }
+
+ override fun onUnexpectedError(e: Exception) {
+ super.onUnexpectedError(e)
+ hideLoadingView()
+ }
+ })
+ }
+ })
+ }
+ }
+ */
+ }
+
+ /**
+ * Manage the e2e keys import.
+ */
+ @SuppressLint("NewApi")
+ private fun importKeys() {
+ // TODO activity?.let { openFileSelection(it, this, false, REQUEST_E2E_FILE_REQUEST_CODE) }
+ }
+
+ /**
+ * Manage the e2e keys import.
+ *
+ * @param intent the intent result
+ */
+ private fun importKeys(intent: Intent?) {
+ // sanity check
+ if (null == intent) {
+ return
+ }
+
+ /*
+ TODO
+ val sharedDataItems = ArrayList(RoomMediaMessage.listRoomMediaMessages(intent))
+ val thisActivity = activity
+
+ if (sharedDataItems.isNotEmpty() && thisActivity != null) {
+ val sharedDataItem = sharedDataItems[0]
+ val dialogLayout = thisActivity.layoutInflater.inflate(R.layout.dialog_import_e2e_keys, null)
+ val builder = AlertDialog.Builder(thisActivity)
+ .setTitle(R.string.encryption_import_room_keys)
+ .setView(dialogLayout)
+
+ val passPhraseEditText = dialogLayout.findViewById(R.id.dialog_e2e_keys_passphrase_edit_text)
+ val importButton = dialogLayout.findViewById