Import settings from Riot - not all fonctional of course
|
@ -11,6 +11,7 @@ buildscript {
|
||||||
} }
|
} }
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
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 "com.airbnb.okreplay:gradle-plugin:1.4.0"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.6.2'
|
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.6.2'
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
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.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.group.GroupService
|
||||||
import im.vector.matrix.android.api.session.room.RoomService
|
import im.vector.matrix.android.api.session.room.RoomService
|
||||||
import im.vector.matrix.android.api.session.user.UserService
|
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.
|
* This interface defines interactions with a session.
|
||||||
* An instance of a session will be provided by the SDK.
|
* 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
|
* The params associated to the session
|
||||||
|
@ -69,5 +70,4 @@ interface Session : RoomService, GroupService, UserService {
|
||||||
// Not used at the moment
|
// Not used at the moment
|
||||||
interface Listener
|
interface Listener
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -74,10 +74,12 @@ android {
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
resValue "bool", "debug_mode", "true"
|
resValue "bool", "debug_mode", "true"
|
||||||
|
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
release {
|
release {
|
||||||
resValue "bool", "debug_mode", "false"
|
resValue "bool", "debug_mode", "false"
|
||||||
|
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
|
||||||
|
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
@ -181,6 +183,9 @@ dependencies {
|
||||||
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
|
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
|
||||||
implementation 'com.airbnb.android:mvrx:0.7.0'
|
implementation 'com.airbnb.android:mvrx:0.7.0'
|
||||||
|
|
||||||
|
// Work
|
||||||
|
implementation "android.arch.work:work-runtime-ktx:1.0.0"
|
||||||
|
|
||||||
// FP
|
// FP
|
||||||
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
||||||
|
|
||||||
|
@ -209,14 +214,23 @@ dependencies {
|
||||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||||
|
|
||||||
|
// Badge for compatibility
|
||||||
|
implementation 'me.leolin:ShortcutBadger:1.1.2@aar'
|
||||||
|
|
||||||
// DI
|
// DI
|
||||||
implementation "org.koin:koin-android:$koin_version"
|
implementation "org.koin:koin-android:$koin_version"
|
||||||
implementation "org.koin:koin-android-scope:$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
|
// TESTS
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'androidx.test:runner:1.1.1'
|
androidTestImplementation 'androidx.test:runner:1.1.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.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'
|
||||||
|
}
|
||||||
|
|
15
vector/src/fdroid/AndroidManifest.xml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="im.vector">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
|
||||||
|
<receiver android:name=".receiver.OnApplicationUpgradeReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
56
vector/src/fdroid/java/im/vector/push/fcm/FcmHelper.java
Executable file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
19
vector/src/gplay/AndroidManifest.xml
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
|
||||||
|
<!-- Firebase components -->
|
||||||
|
<meta-data
|
||||||
|
android:name="firebase_analytics_collection_deactivated"
|
||||||
|
android:value="true" />
|
||||||
|
|
||||||
|
<service android:name="im.vector.push.fcm.VectorFirebaseMessagingService">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
46
vector/src/gplay/google-services.json
Normal file
|
@ -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"
|
||||||
|
}
|
120
vector/src/gplay/java/im/vector/push/fcm/FcmHelper.java
Executable file
|
@ -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<InstanceIdResult>() {
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
271
vector/src/gplay/java/im/vector/push/fcm/VectorFirebaseMessagingService.kt
Executable file
|
@ -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<NotificationDrawerManager>()
|
||||||
|
|
||||||
|
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<String, String> /*, 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<String, String>, 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<String, String>, 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<String, String>?): 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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<Void> {
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -32,6 +32,15 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".features.rageshake.BugReportActivity"
|
android:name=".features.rageshake.BugReportActivity"
|
||||||
android:label="@string/title_activity_bug_report" />
|
android:label="@string/title_activity_bug_report" />
|
||||||
|
<activity
|
||||||
|
android:name=".features.settings.VectorSettingsActivity"
|
||||||
|
android:label="@string/title_activity_settings"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".core.services.CallService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -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.VisibleRoomStore
|
||||||
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
|
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
|
||||||
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
|
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
|
||||||
|
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
|
||||||
import org.koin.dsl.module.module
|
import org.koin.dsl.module.module
|
||||||
|
|
||||||
class AppModule(private val context: Context) {
|
class AppModule(private val context: Context) {
|
||||||
|
@ -64,6 +65,10 @@ class AppModule(private val context: Context) {
|
||||||
RoomSummaryComparator()
|
RoomSummaryComparator()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
single {
|
||||||
|
NotificationDrawerManager(context)
|
||||||
|
}
|
||||||
|
|
||||||
factory {
|
factory {
|
||||||
Matrix.getInstance().currentSession!!
|
Matrix.getInstance().currentSession!!
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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<View>(R.id.search_edit_frame))?.let {
|
||||||
|
val searchEditFrameParams = it.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
searchEditFrameParams.leftMargin = 0
|
||||||
|
it.layoutParams = searchEditFrameParams
|
||||||
|
}
|
||||||
|
|
||||||
|
(findViewById<View>(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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<TextView>(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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<TextView>(android.R.id.title)?.setSingleLine(false)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "onBindView " + e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onBindViewHolder(holder)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ImageView>(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
163
vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreference.kt
Executable file
|
@ -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<TextView>(android.R.id.title)
|
||||||
|
val summary = itemView.findViewById<TextView>(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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<TextView>(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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<TextView>(android.R.id.title)?.setSingleLine(false)
|
||||||
|
|
||||||
|
super.onBindViewHolder(holder)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Session>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<PushSimulatorWorker>()
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
*
|
||||||
|
* <b>Android M++</b>
|
||||||
|
* On android M+, the keystore can generates and store AES keys via API. But below API M this functionality
|
||||||
|
* is not available.
|
||||||
|
*
|
||||||
|
* <b>Android [K-M[</b>
|
||||||
|
* For android >=KITKAT and <M, we use the keystore to generate and store a private/public key pair. Then for each secret, a
|
||||||
|
* random secret key in generated to perform encryption.
|
||||||
|
* This secret key is encrypted with the public RSA key and stored with the encrypted secret.
|
||||||
|
* In order to decrypt the encrypted secret key will be retrieved then decrypted with the RSA private key.
|
||||||
|
*
|
||||||
|
* <b>Older androids</b>
|
||||||
|
* 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:
|
||||||
|
* <code>
|
||||||
|
* 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)
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
* 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 <M, as symetric key gen is not available, will use an asymetric key generated
|
||||||
|
* in the keystore to encrypted a random symetric key. The encrypted symetric key is returned
|
||||||
|
* in the bytearray (in can be stored anywhere, it is encrypted)
|
||||||
|
* On older version a key in generated from alias with random salt.
|
||||||
|
*
|
||||||
|
* The secret is encrypted using the following method: AES/GCM/NoPadding
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun securelyStoreString(secret: String, keyAlias: String, context: Context): ByteArray? {
|
||||||
|
if (Build.VERSION.SDK_INT >= 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 <T> 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 <T> loadSecureObjectM(keyAlias: String, file: File): T? {
|
||||||
|
// FileInputStream(file).use {
|
||||||
|
// return loadSecureObjectM<T>(keyAlias, it)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun <T> 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 <T> 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 <T> 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<ByteArray, ByteArray> {
|
||||||
|
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<ByteArray, ByteArray, ByteArray> {
|
||||||
|
|
||||||
|
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<ByteArray, ByteArray, ByteArray> {
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import android.provider.Settings
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.features.notifications.supportNotificationChannels
|
||||||
import im.vector.riotredesign.features.settings.VectorLocale
|
import im.vector.riotredesign.features.settings.VectorLocale
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -124,10 +125,6 @@ fun startNotificationSettingsIntent(fragment: Fragment, requestCode: Int) {
|
||||||
fragment.startActivityForResult(intent, requestCode)
|
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.
|
* Shows notification system settings for the given channel id.
|
||||||
*/
|
*/
|
||||||
|
@ -184,3 +181,8 @@ fun startImportTextFromFileIntent(fragment: Fragment, requestCode: Int) {
|
||||||
fun Context.toast(resId: Int) {
|
fun Context.toast(resId: Int) {
|
||||||
Toast.makeText(this, resId, Toast.LENGTH_SHORT).show()
|
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()
|
||||||
|
}
|
||||||
|
|
|
@ -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.<br></br>
|
||||||
|
* The refresh is only effective if the device is:
|
||||||
|
* * offline * does not support FCM
|
||||||
|
* * FCM registration failed
|
||||||
|
* <br></br>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)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.home.room.detail.LoadingRoomDetailFragment
|
||||||
import im.vector.riotredesign.features.rageshake.BugReporter
|
import im.vector.riotredesign.features.rageshake.BugReporter
|
||||||
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
|
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
|
||||||
|
import im.vector.riotredesign.features.settings.VectorSettingsActivity
|
||||||
import kotlinx.android.synthetic.main.activity_home.*
|
import kotlinx.android.synthetic.main.activity_home.*
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.android.scope.ext.android.bindScope
|
import org.koin.android.scope.ext.android.bindScope
|
||||||
|
@ -101,12 +102,18 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable {
|
||||||
drawerToggle.syncState()
|
drawerToggle.syncState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getMenuRes() = R.menu.home
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
drawerLayout.openDrawer(GravityCompat.START)
|
drawerLayout.openDrawer(GravityCompat.START)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
R.id.sliding_menu_settings -> {
|
||||||
|
startActivity(VectorSettingsActivity.getIntent(this, "TODO"))
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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<String, IconCompat>()
|
||||||
|
|
||||||
|
// URLs to load
|
||||||
|
private val toLoad = HashSet<String>()
|
||||||
|
|
||||||
|
// Black list of URLs (broken URL, etc.)
|
||||||
|
private val blacklist = HashSet<String>()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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<NotificationDrawerManager>()
|
||||||
|
|
||||||
|
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<Void>() {
|
||||||
|
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<Void?> {
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, ArrayList<NotifiableMessageEvent>> = HashMap()
|
||||||
|
val simpleEvents: ArrayList<NotifiableEvent> = ArrayList()
|
||||||
|
val notifications: ArrayList<Notification> = 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<NotifiableMessageEvent>): 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<NotifiableEvent> {
|
||||||
|
try {
|
||||||
|
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
||||||
|
if (file.exists()) {
|
||||||
|
FileInputStream(file).use {
|
||||||
|
val events: ArrayList<NotifiableEvent>? = 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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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<String> 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<String> 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<String> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Session>()
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Session>()
|
||||||
|
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<Parcelable>(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?
|
||||||
|
|
||||||
|
}
|
|
@ -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<Session>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* 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.troubleshoot
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import butterknife.BindView
|
||||||
|
import butterknife.ButterKnife
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.features.themes.ThemeUtils
|
||||||
|
|
||||||
|
class NotificationTroubleshootRecyclerViewAdapter(val tests: ArrayList<TroubleshootTest>)
|
||||||
|
: RecyclerView.Adapter<NotificationTroubleshootRecyclerViewAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
val itemView = inflater.inflate(viewType, parent, false)
|
||||||
|
return ViewHolder(itemView)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int = R.layout.item_notification_troubleshoot
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val test = tests[position]
|
||||||
|
holder.bind(test)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = tests.size
|
||||||
|
|
||||||
|
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
@BindView(R.id.troubleshootTestTitle)
|
||||||
|
lateinit var titleText: TextView
|
||||||
|
@BindView(R.id.troubleshootTestDescription)
|
||||||
|
lateinit var descriptionText: TextView
|
||||||
|
@BindView(R.id.troubleshootStatusIcon)
|
||||||
|
lateinit var statusIconImage: ImageView
|
||||||
|
@BindView(R.id.troubleshootProgressBar)
|
||||||
|
lateinit var progressBar: ProgressBar
|
||||||
|
@BindView(R.id.troubleshootTestButton)
|
||||||
|
lateinit var fixButton: Button
|
||||||
|
|
||||||
|
init {
|
||||||
|
ButterKnife.bind(this, itemView)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(test: TroubleshootTest) {
|
||||||
|
|
||||||
|
val context = itemView.context
|
||||||
|
titleText.setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary))
|
||||||
|
descriptionText.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_default_text_hint_color))
|
||||||
|
|
||||||
|
when (test.status) {
|
||||||
|
TroubleshootTest.TestStatus.NOT_STARTED -> {
|
||||||
|
titleText.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_default_text_hint_color))
|
||||||
|
descriptionText.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_default_text_hint_color))
|
||||||
|
|
||||||
|
progressBar.visibility = View.INVISIBLE
|
||||||
|
statusIconImage.visibility = View.VISIBLE
|
||||||
|
statusIconImage.setImageResource(R.drawable.unit_test)
|
||||||
|
}
|
||||||
|
TroubleshootTest.TestStatus.RUNNING -> {
|
||||||
|
progressBar.visibility = View.VISIBLE
|
||||||
|
statusIconImage.visibility = View.INVISIBLE
|
||||||
|
|
||||||
|
}
|
||||||
|
TroubleshootTest.TestStatus.FAILED -> {
|
||||||
|
progressBar.visibility = View.INVISIBLE
|
||||||
|
statusIconImage.visibility = View.VISIBLE
|
||||||
|
statusIconImage.setImageResource(R.drawable.unit_test_ko)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
statusIconImage.imageTintList = null
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptionText.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_highlighted_message_text_color))
|
||||||
|
}
|
||||||
|
TroubleshootTest.TestStatus.SUCCESS -> {
|
||||||
|
progressBar.visibility = View.INVISIBLE
|
||||||
|
statusIconImage.visibility = View.VISIBLE
|
||||||
|
statusIconImage.setImageResource(R.drawable.unit_test_ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val quickFix = test.quickFix
|
||||||
|
if (quickFix != null) {
|
||||||
|
fixButton.setText(test.quickFix!!.title)
|
||||||
|
fixButton.setOnClickListener { _ ->
|
||||||
|
test.quickFix!!.doFix()
|
||||||
|
}
|
||||||
|
fixButton.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
fixButton.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
titleText.setText(test.titleResId)
|
||||||
|
val description = test.description
|
||||||
|
if (description == null) {
|
||||||
|
descriptionText.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
descriptionText.visibility = View.VISIBLE
|
||||||
|
descriptionText.text = description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* 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.troubleshoot
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
class NotificationTroubleshootTestManager(val fragment: Fragment) {
|
||||||
|
|
||||||
|
val testList = ArrayList<TroubleshootTest>()
|
||||||
|
var isCancelled = false
|
||||||
|
|
||||||
|
var currentTestIndex by Delegates.observable(0) { _, _, _ ->
|
||||||
|
statusListener?.invoke(this)
|
||||||
|
}
|
||||||
|
val adapter = NotificationTroubleshootRecyclerViewAdapter(testList)
|
||||||
|
|
||||||
|
|
||||||
|
var statusListener: ((NotificationTroubleshootTestManager) -> Unit)? = null
|
||||||
|
|
||||||
|
var diagStatus: TroubleshootTest.TestStatus by Delegates.observable(TroubleshootTest.TestStatus.NOT_STARTED) { _, _, _ ->
|
||||||
|
statusListener?.invoke(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun addTest(test: TroubleshootTest) {
|
||||||
|
testList.add(test)
|
||||||
|
test.manager = this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runDiagnostic() {
|
||||||
|
if (isCancelled) return
|
||||||
|
currentTestIndex = 0
|
||||||
|
val handler = Handler(Looper.getMainLooper())
|
||||||
|
diagStatus = if (testList.size > 0) TroubleshootTest.TestStatus.RUNNING else TroubleshootTest.TestStatus.SUCCESS
|
||||||
|
var isAllGood = true
|
||||||
|
for ((index, test) in testList.withIndex()) {
|
||||||
|
test.statusListener = {
|
||||||
|
if (!isCancelled) {
|
||||||
|
adapter.notifyItemChanged(index)
|
||||||
|
if (it.isFinished()) {
|
||||||
|
isAllGood = isAllGood && (it.status == TroubleshootTest.TestStatus.SUCCESS)
|
||||||
|
currentTestIndex++
|
||||||
|
if (currentTestIndex < testList.size) {
|
||||||
|
val troubleshootTest = testList[currentTestIndex]
|
||||||
|
troubleshootTest.status = TroubleshootTest.TestStatus.RUNNING
|
||||||
|
//Cosmetic: Start with a small delay for UI/UX reason (better animation effect) for non async tests
|
||||||
|
handler.postDelayed({
|
||||||
|
if (fragment.isAdded) {
|
||||||
|
troubleshootTest.perform()
|
||||||
|
}
|
||||||
|
}, 600)
|
||||||
|
} else {
|
||||||
|
//we are done, test global status?
|
||||||
|
diagStatus = if (isAllGood) TroubleshootTest.TestStatus.SUCCESS else TroubleshootTest.TestStatus.FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fragment.isAdded) {
|
||||||
|
testList.firstOrNull()?.perform()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retry() {
|
||||||
|
for (test in testList) {
|
||||||
|
test.cancel()
|
||||||
|
test.description = null
|
||||||
|
test.quickFix = null
|
||||||
|
test.status = TroubleshootTest.TestStatus.NOT_STARTED
|
||||||
|
}
|
||||||
|
runDiagnostic()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
isCancelled = true
|
||||||
|
for (test in testList) {
|
||||||
|
test.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val REQ_CODE_FIX = 9099
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.fragments.troubleshoot
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the main pushRule (RULE_ID_DISABLE_ALL) is correctly setup
|
||||||
|
*/
|
||||||
|
class TestAccountSettings(val fragment: Fragment, val session: Session) : TroubleshootTest(R.string.settings_troubleshoot_test_account_settings_title) {
|
||||||
|
|
||||||
|
override fun perform() {
|
||||||
|
/*
|
||||||
|
TODO
|
||||||
|
val defaultRule = session?.dataHandler?.bingRulesManager?.pushRules()?.findDefaultRule(BingRule.RULE_ID_DISABLE_ALL)
|
||||||
|
if (defaultRule != null) {
|
||||||
|
if (!defaultRule.isEnabled) {
|
||||||
|
description = fragment.getString(R.string.settings_troubleshoot_test_account_settings_success)
|
||||||
|
quickFix = null
|
||||||
|
status = TestStatus.SUCCESS
|
||||||
|
} else {
|
||||||
|
description = fragment.getString(R.string.settings_troubleshoot_test_account_settings_failed)
|
||||||
|
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_account_settings_quickfix) {
|
||||||
|
override fun doFix() {
|
||||||
|
if (manager?.diagStatus == TestStatus.RUNNING) return //wait before all is finished
|
||||||
|
session?.dataHandler?.bingRulesManager?.updateEnableRuleStatus(defaultRule, !defaultRule.isEnabled,
|
||||||
|
object : BingRulesManager.onBingRuleUpdateListener {
|
||||||
|
|
||||||
|
override fun onBingRuleUpdateSuccess() {
|
||||||
|
manager?.retry()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBingRuleUpdateFailure(errorMessage: String) {
|
||||||
|
manager?.retry()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status = TestStatus.FAILED
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//should not happen?
|
||||||
|
status = TestStatus.FAILED
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
status = TestStatus.FAILED
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* 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.troubleshoot
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.features.settings.PreferencesManager
|
||||||
|
import im.vector.riotredesign.features.settings.VectorSettingsFragmentInteractionListener
|
||||||
|
|
||||||
|
class TestBingRulesSettings(val fragment: Fragment, val session: Session) : TroubleshootTest(R.string.settings_troubleshoot_test_bing_settings_title) {
|
||||||
|
|
||||||
|
private val testedRules = emptyArray<String>()
|
||||||
|
/* TODO
|
||||||
|
arrayOf(BingRule.RULE_ID_CONTAIN_DISPLAY_NAME,
|
||||||
|
BingRule.RULE_ID_CONTAIN_USER_NAME,
|
||||||
|
BingRule.RULE_ID_ONE_TO_ONE_ROOM,
|
||||||
|
BingRule.RULE_ID_ALL_OTHER_MESSAGES_ROOMS)
|
||||||
|
*/
|
||||||
|
|
||||||
|
val ruleSettingsName = arrayOf(R.string.settings_containing_my_display_name,
|
||||||
|
R.string.settings_containing_my_user_name,
|
||||||
|
R.string.settings_messages_in_one_to_one,
|
||||||
|
R.string.settings_messages_in_group_chat)
|
||||||
|
|
||||||
|
override fun perform() {
|
||||||
|
val pushRules = null // TODO session.dataHandler.pushRules()
|
||||||
|
if (pushRules == null) {
|
||||||
|
description = fragment.getString(R.string.settings_troubleshoot_test_bing_settings_failed_to_load_rules)
|
||||||
|
status = TestStatus.FAILED
|
||||||
|
} else {
|
||||||
|
var oneOrMoreRuleIsOff = false
|
||||||
|
var oneOrMoreRuleAreSilent = false
|
||||||
|
for ((index, ruleId) in testedRules.withIndex()) {
|
||||||
|
/* TODO
|
||||||
|
pushRules.findDefaultRule(ruleId)?.let { rule ->
|
||||||
|
if (!rule.isEnabled || rule.shouldNotNotify()) {
|
||||||
|
//off
|
||||||
|
oneOrMoreRuleIsOff = true
|
||||||
|
} else if (rule.notificationSound == null) {
|
||||||
|
//silent
|
||||||
|
oneOrMoreRuleAreSilent = true
|
||||||
|
} else {
|
||||||
|
//noisy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oneOrMoreRuleIsOff) {
|
||||||
|
description = fragment.getString(R.string.settings_troubleshoot_test_bing_settings_failed)
|
||||||
|
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_bing_settings_quickfix) {
|
||||||
|
override fun doFix() {
|
||||||
|
val activity = fragment.activity
|
||||||
|
if (activity is VectorSettingsFragmentInteractionListener) {
|
||||||
|
activity.requestHighlightPreferenceKeyOnResume(PreferencesManager.SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY)
|
||||||
|
}
|
||||||
|
activity?.supportFragmentManager?.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status = TestStatus.FAILED
|
||||||
|
} else {
|
||||||
|
if (oneOrMoreRuleAreSilent) {
|
||||||
|
description = fragment.getString(R.string.settings_troubleshoot_test_bing_settings_success_with_warn)
|
||||||
|
} else {
|
||||||
|
description = null
|
||||||
|
}
|
||||||
|
status = TestStatus.SUCCESS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.riotredesign.features.settings.troubleshoot
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if notifications are enable in the system settings for this app.
|
||||||
|
*/
|
||||||
|
class TestDeviceSettings(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_device_settings_title) {
|
||||||
|
|
||||||
|
override fun perform() {
|
||||||
|
/* TODO
|
||||||
|
val pushManager = Matrix.getInstance(fragment.activity).pushManager
|
||||||
|
if (pushManager.areDeviceNotificationsAllowed()) {
|
||||||
|
description = fragment.getString(R.string.settings_troubleshoot_test_device_settings_success)
|
||||||
|
quickFix = null
|
||||||
|
status = TestStatus.SUCCESS
|
||||||
|
} else {
|
||||||
|
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_device_settings_quickfix) {
|
||||||
|
override fun doFix() {
|
||||||
|
pushManager.setDeviceNotificationsAllowed(true)
|
||||||
|
manager?.retry()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
description = fragment.getString(R.string.settings_troubleshoot_test_device_settings_failed)
|
||||||
|
status = TestStatus.FAILED
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
status = TestStatus.FAILED
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* 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.troubleshoot
|
||||||
|
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.utils.startNotificationSettingsIntent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if notifications are enable in the system settings for this app.
|
||||||
|
*/
|
||||||
|
class TestSystemSettings(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_system_settings_title) {
|
||||||
|
|
||||||
|
override fun perform() {
|
||||||
|
if (NotificationManagerCompat.from(fragment.context!!).areNotificationsEnabled()) {
|
||||||
|
description = fragment.getString(R.string.settings_troubleshoot_test_system_settings_success)
|
||||||
|
quickFix = null
|
||||||
|
status = TestStatus.SUCCESS
|
||||||
|
} else {
|
||||||
|
description = fragment.getString(R.string.settings_troubleshoot_test_system_settings_failed)
|
||||||
|
quickFix = object : TroubleshootQuickFix(R.string.open_settings) {
|
||||||
|
override fun doFix() {
|
||||||
|
if (manager?.diagStatus == TestStatus.RUNNING) return //wait before all is finished
|
||||||
|
startNotificationSettingsIntent(fragment, NotificationTroubleshootTestManager.REQ_CODE_FIX)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
status = TestStatus.FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.features.settings.troubleshoot
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
abstract class TroubleshootTest(@StringRes val titleResId: Int) {
|
||||||
|
|
||||||
|
enum class TestStatus {
|
||||||
|
NOT_STARTED,
|
||||||
|
RUNNING,
|
||||||
|
FAILED,
|
||||||
|
SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String? = null
|
||||||
|
|
||||||
|
var status: TestStatus by Delegates.observable(TestStatus.NOT_STARTED) { _, _, _ ->
|
||||||
|
statusListener?.invoke(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusListener: ((TroubleshootTest) -> Unit)? = null
|
||||||
|
|
||||||
|
var manager: NotificationTroubleshootTestManager? = null
|
||||||
|
|
||||||
|
abstract fun perform()
|
||||||
|
|
||||||
|
fun isFinished(): Boolean = (status == TestStatus.FAILED || status == TestStatus.SUCCESS)
|
||||||
|
|
||||||
|
var quickFix: TroubleshootQuickFix? = null
|
||||||
|
|
||||||
|
|
||||||
|
abstract class TroubleshootQuickFix(@StringRes val title: Int) {
|
||||||
|
abstract fun doFix()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun cancel() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
9
vector/src/main/res/anim/anim_slide_in_bottom.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<translate
|
||||||
|
android:duration="200"
|
||||||
|
android:fromYDelta="20%"
|
||||||
|
android:toYDelta="0%" />
|
||||||
|
</set>
|
9
vector/src/main/res/anim/anim_slide_nothing.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<translate
|
||||||
|
android:duration="200"
|
||||||
|
android:fromYDelta="000%"
|
||||||
|
android:toYDelta="000%" />
|
||||||
|
</set>
|
14
vector/src/main/res/anim/anim_slide_out_bottom.xml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<translate
|
||||||
|
android:duration="200"
|
||||||
|
android:fromYDelta="0%"
|
||||||
|
android:toYDelta="20%" />
|
||||||
|
<alpha
|
||||||
|
android:duration="200"
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0" />
|
||||||
|
|
||||||
|
</set>
|
16
vector/src/main/res/anim/unread_marker_anim.xml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<scale xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="1500"
|
||||||
|
android:fromXScale="1"
|
||||||
|
android:fromYScale="1"
|
||||||
|
android:pivotX="50%p"
|
||||||
|
android:pivotY="50%"
|
||||||
|
android:toXScale="0"
|
||||||
|
android:toYScale="0" />
|
||||||
|
|
||||||
|
<alpha
|
||||||
|
android:duration="1500"
|
||||||
|
android:fromAlpha="1"
|
||||||
|
android:toAlpha="0" />
|
||||||
|
</set>
|
BIN
vector/src/main/res/drawable-hdpi/unit_test.png
Normal file
After Width: | Height: | Size: 684 B |
BIN
vector/src/main/res/drawable-hdpi/unit_test_ko.png
Normal file
After Width: | Height: | Size: 577 B |
BIN
vector/src/main/res/drawable-hdpi/unit_test_ok.png
Normal file
After Width: | Height: | Size: 838 B |
BIN
vector/src/main/res/drawable-mdpi/unit_test.png
Normal file
After Width: | Height: | Size: 411 B |
BIN
vector/src/main/res/drawable-mdpi/unit_test_ko.png
Normal file
After Width: | Height: | Size: 388 B |
BIN
vector/src/main/res/drawable-mdpi/unit_test_ok.png
Normal file
After Width: | Height: | Size: 548 B |
BIN
vector/src/main/res/drawable-xhdpi/unit_test.png
Normal file
After Width: | Height: | Size: 893 B |
BIN
vector/src/main/res/drawable-xhdpi/unit_test_ko.png
Normal file
After Width: | Height: | Size: 742 B |
BIN
vector/src/main/res/drawable-xhdpi/unit_test_ok.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/ic_add_black.png
Executable file
After Width: | Height: | Size: 114 B |
BIN
vector/src/main/res/drawable-xxhdpi/ic_eye_black.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/ic_eye_closed_black.png
Normal file
After Width: | Height: | Size: 3 KiB |
After Width: | Height: | Size: 4.3 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/ic_material_done_all_white.png
Executable file
After Width: | Height: | Size: 398 B |
BIN
vector/src/main/res/drawable-xxhdpi/ic_material_done_white.png
Executable file
After Width: | Height: | Size: 255 B |
BIN
vector/src/main/res/drawable-xxhdpi/ic_settings.png
Executable file
After Width: | Height: | Size: 1.5 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/icon_notif_important.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/incoming_call_notification_transparent.png
Executable file
After Width: | Height: | Size: 684 B |
BIN
vector/src/main/res/drawable-xxhdpi/logo_transparent.png
Executable file
After Width: | Height: | Size: 1.6 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/main_alias_icon.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/unit_test.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/unit_test_ko.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/unit_test_ok.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/vector_notification_accept_invitation.png
Executable file
After Width: | Height: | Size: 473 B |
BIN
vector/src/main/res/drawable-xxhdpi/vector_notification_open.png
Normal file
After Width: | Height: | Size: 318 B |
BIN
vector/src/main/res/drawable-xxhdpi/vector_notification_quick_reply.png
Executable file
After Width: | Height: | Size: 269 B |
BIN
vector/src/main/res/drawable-xxhdpi/vector_notification_reject_invitation.png
Executable file
After Width: | Height: | Size: 309 B |