Merge branch 'develop' into feature/dagger [WIP]

This commit is contained in:
ganfra
2019-06-26 20:58:46 +02:00
354 changed files with 12548 additions and 3924 deletions

View File

@ -2,14 +2,24 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.vector.riotredesign">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application>
<receiver android:name=".receiver.OnApplicationUpgradeReceiver">
<receiver android:name=".receiver.OnApplicationUpgradeOrRebootReceiver">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".core.services.AlarmSyncBroadcastReceiver"
android:enabled="true"
android:exported="false" />
</application>
</manifest>

View File

@ -14,24 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.push.fcm;
package im.vector.riotredesign.push.fcm
import android.app.Activity;
import android.content.Context;
import android.app.Activity
import android.content.Context
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import im.vector.riotredesign.core.pushers.PushersManager
public class FcmHelper {
object FcmHelper {
fun isPushSupported(): Boolean = false
/**
* 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;
fun getFcmToken(context: Context): String? {
return null
}
/**
@ -40,8 +40,7 @@ public class FcmHelper {
* @param context android context
* @param token the token to store
*/
public static void storeFcmToken(@NonNull Context context,
@Nullable String token) {
fun storeFcmToken(context: Context, token: String?) {
// No op
}
@ -50,7 +49,7 @@ public class FcmHelper {
*
* @param activity the first launch Activity
*/
public static void ensureFcmTokenIsRetrieved(final Activity activity) {
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager) {
// No op
}
}

View File

@ -16,12 +16,8 @@
package im.vector.riotredesign.push.fcm
import androidx.fragment.app.Fragment
import im.vector.fragments.troubleshoot.TestAccountSettings
import im.vector.matrix.android.api.session.Session
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
import im.vector.riotredesign.features.settings.troubleshoot.*
import im.vector.riotredesign.push.fcm.troubleshoot.TestAutoStartBoot
import im.vector.riotredesign.push.fcm.troubleshoot.TestBackgroundRestrictions

View File

@ -1,5 +1,6 @@
/*
* Copyright 2018 New Vector Ltd
* 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.
@ -14,21 +15,18 @@
* limitations under the License.
*/
package im.vector.riotredesign.receiver;
package im.vector.riotredesign.receiver
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import im.vector.riotredesign.core.services.AlarmSyncBroadcastReceiver
import timber.log.Timber
import timber.log.Timber;
class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() {
public class OnApplicationUpgradeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Timber.v("## onReceive() : Application has been upgraded, restart event stream service.");
// Start Event stream
// TODO EventStreamServiceX.Companion.onApplicationUpgrade(context);
override fun onReceive(context: Context, intent: Intent) {
Timber.v("## onReceive() ${intent.action}")
AlarmSyncBroadcastReceiver.scheduleAlarm(context, 10)
}
}

View File

@ -10,14 +10,10 @@
"client_info": {
"mobilesdk_app_id": "1:912726360885:android:448c9b63161abc9c",
"android_client_info": {
"package_name": "im.vector.riotredesign"
"package_name": "im.vector.alpha"
}
},
"oauth_client": [
{
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
"client_type": 3
@ -29,15 +25,100 @@
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
"other_platform_oauth_client": [
{
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:912726360885:android:3120c24f6ef22f2b",
"android_client_info": {
"package_name": "im.vector.app"
}
},
"oauth_client": [
{
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:912726360885:android:25ef253beaff462e",
"android_client_info": {
"package_name": "im.vector.riotredesign"
}
},
"oauth_client": [
{
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:912726360885:android:bb204b7a7b08a10b",
"android_client_info": {
"package_name": "im.veon"
}
},
"oauth_client": [
{
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}

View File

@ -1,120 +0,0 @@
/*
* 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.riotredesign.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;
}
}

View File

@ -0,0 +1,102 @@
/*
* 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.riotredesign.push.fcm
import android.app.Activity
import android.content.Context
import android.preference.PreferenceManager
import android.widget.Toast
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.firebase.iid.FirebaseInstanceId
import im.vector.riotredesign.R
import im.vector.riotredesign.core.pushers.PushersManager
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.
*/
object FcmHelper {
private val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"
fun isPushSupported(): Boolean = true
/**
* Retrieves the FCM registration token.
*
* @return the FCM token or null if not received from FCM
*/
fun getFcmToken(context: Context): String? {
return PreferenceManager.getDefaultSharedPreferences(context).getString(PREFS_KEY_FCM_TOKEN, null)
}
/**
* Store FCM token to the SharedPrefs
* TODO Store in realm
*
* @param context android context
* @param token the token to store
*/
fun storeFcmToken(context: Context,
token: String?) {
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
*/
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager) {
// if (TextUtils.isEmpty(getFcmToken(activity))) {
//'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().instanceId
.addOnSuccessListener(activity) { instanceIdResult ->
storeFcmToken(activity, instanceIdResult.token)
pushersManager.registerPusherWithFcmKey(instanceIdResult.token)
}
.addOnFailureListener(activity) { e -> Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.message) }
} catch (e: Throwable) {
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.message)
}
} 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 fun checkPlayServices(activity: Activity): Boolean {
val apiAvailability = GoogleApiAvailability.getInstance()
val resultCode = apiAvailability.isGooglePlayServicesAvailable(activity)
return resultCode == ConnectionResult.SUCCESS
}
}

View File

@ -16,15 +16,11 @@
package im.vector.riotredesign.push.fcm
import androidx.fragment.app.Fragment
import im.vector.fragments.troubleshoot.TestAccountSettings
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.features.settings.troubleshoot.*
import im.vector.riotredesign.push.fcm.troubleshoot.TestFirebaseToken
import im.vector.riotredesign.push.fcm.troubleshoot.TestPlayServices
import im.vector.riotredesign.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 {

View File

@ -22,18 +22,27 @@ package im.vector.riotredesign.push.fcm
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import im.vector.matrix.android.api.Matrix
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.core.pushers.PushersManager
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
<<<<<<< HEAD
=======
import im.vector.riotredesign.features.settings.PreferencesManager
import org.koin.android.ext.android.inject
>>>>>>> develop
import timber.log.Timber
/**
@ -41,12 +50,18 @@ import timber.log.Timber
*/
class VectorFirebaseMessagingService : FirebaseMessagingService() {
<<<<<<< HEAD
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
private val notifiableEventResolver by lazy {
NotifiableEventResolver(this)
}
=======
private val notificationDrawerManager by inject<NotificationDrawerManager>()
private val pusherManager by inject<PushersManager>()
>>>>>>> develop
private val notifiableEventResolver by inject<NotifiableEventResolver>()
// UI handler
private val mUIHandler by lazy {
Handler(Looper.getMainLooper())
@ -58,26 +73,27 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
* @param message the message
*/
override fun onMessageReceived(message: RemoteMessage?) {
if (!PreferencesManager.areNotificationEnabledForDevice(applicationContext)) {
Timber.i("Notification are disabled for this device")
return
}
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)
Timber.i("## onMessageReceived() %s", message.data.toString())
Timber.i("## onMessageReceived() from FCM with priority %s", message.priority)
}
//safe guard
/* TODO
val pushManager = Matrix.getInstance(applicationContext).pushManager
if (!pushManager.areDeviceNotificationsAllowed()) {
Timber.i("## onMessageReceived() : the notifications are disabled")
return
mUIHandler.post {
if (ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
//we are in foreground, let the sync do the things?
Timber.v("PUSH received in a foreground state, ignore")
} else {
onMessageReceivedInternal(message.data)
}
}
*/
//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) }
}
/**
@ -87,11 +103,27 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
* you retrieve the token.
*/
override fun onNewToken(refreshedToken: String?) {
if (Matrix.getInstance().currentSession == null) return
Timber.i("onNewToken: FCM Token has been updated")
FcmHelper.storeFcmToken(this, refreshedToken)
// TODO Matrix.getInstance(this)?.pushManager?.resetFCMRegistration(refreshedToken)
if (refreshedToken == null) {
Timber.w("onNewToken:received null token")
} else {
if (PreferencesManager.areNotificationEnabledForDevice(applicationContext)) {
pusherManager.registerPusherWithFcmKey(refreshedToken)
}
}
}
/**
* Called when the FCM server deletes pending messages. This may be due to:
* - Too many messages stored on the FCM server.
* This can occur when an app's servers send a bunch of non-collapsible messages to FCM servers while the device is offline.
* - The device hasn't connected in a long time and the app server has recently (within the last 4 weeks)
* sent a message to the app on that device.
*
* It is recommended that the app do a full sync with the app server after receiving this call.
*/
override fun onDeletedMessages() {
Timber.v("## onDeletedMessages()")
}
@ -102,55 +134,58 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
* @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*/) {
private fun onMessageReceivedInternal(data: Map<String, String>) {
try {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.i("## onMessageReceivedInternal() : $data")
}
val eventId = data["event_id"]
val roomId = data["room_id"]
if (eventId == null || roomId == null) {
Timber.e("## onMessageReceivedInternal() missing eventId and/or roomId")
return
}
// 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
val session = safeGetCurrentSession()
if (VectorApp.isAppInBackground() && !pushManager.isBackgroundSyncAllowed) {
//Notification contains metadata and maybe data information
handleNotificationWithoutSyncingMode(data, session)
if (session == null) {
Timber.w("## Can't sync from push, no current session")
} else {
// Safe guard... (race?)
if (isEventAlreadyKnown(data["event_id"], data["room_id"])) return
//Catch up!!
EventStreamServiceX.onPushReceived(this)
if (isEventAlreadyKnown(eventId, roomId)) {
Timber.i("Ignoring push, event already knwown")
} else {
Timber.v("Requesting background sync")
session.requireBackgroundSync()
}
}
*/
} catch (e: Exception) {
Timber.e(e, "## onMessageReceivedInternal() failed : " + e.message)
}
}
fun safeGetCurrentSession(): Session? {
try {
return Matrix.getInstance().currentSession
} catch (e: Throwable) {
Timber.e(e, "## Failed to get current session")
return null
}
}
// 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
}
}
}
}
*/
val session = safeGetCurrentSession() ?: return false
val room = session.getRoom(roomId) ?: return false
return room.getTimeLineEvent(eventId) != null
} catch (e: Exception) {
Timber.e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined " + e.message)
Timber.e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined")
}
}
@ -186,48 +221,44 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
isPushGatewayEvent = true
)
notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent)
notificationDrawerManager.refreshNotificationDrawer(null)
notificationDrawerManager.refreshNotificationDrawer()
return
} else {
val event = parseEvent(data)
if (event?.roomId == null) {
//unsupported event
Timber.e("Received an event with no room id")
return
val event = parseEvent(data) ?: return
val notifiableEvent = notifiableEventResolver.resolveEvent(event, session)
if (notifiableEvent == null) {
Timber.e("Unsupported notifiable event ${eventId}")
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.e("--> ${event}")
}
} 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}")
if (notifiableEvent is NotifiableMessageEvent) {
if (TextUtils.isEmpty(notifiableEvent.senderName)) {
notifiableEvent.senderName = data["sender_display_name"]
?: data["sender"] ?: ""
}
} 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) ?: ""
}
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)
}
notifiableEvent.isPushGatewayEvent = true
notifiableEvent.matrixID = session.sessionParams.credentials.userId
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
notificationDrawerManager.refreshNotificationDrawer()
}
}
}
private fun findRoomNameBestEffort(data: Map<String, String>, session: Session?): String? {
var roomName: String? = data["room_name"]
val roomName: String? = data["room_name"]
val roomId = data["room_id"]
if (null == roomName && null != roomId) {
// Try to get the room name from our store
@ -256,13 +287,13 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
try {
return Event(eventId = data["event_id"],
sender = data["sender"],
senderId = 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)
Timber.e(e, "buildEvent fails ")
}
return null

View File

@ -3,7 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"
package="im.vector.riotredesign">
<uses-permission android:name="android.permission.INTERNET" />
<application
@ -19,13 +18,18 @@
<activity
android:name=".features.MainActivity"
android:theme="@style/AppTheme.Launcher">
android:theme="@style/AppTheme.Launcher" />
<!-- Activity alias for the launcher Activity (must be declared after the Activity it targets) -->
<activity-alias
android:name=".features.Alias"
android:targetActivity=".features.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</activity-alias>
<activity android:name=".features.home.HomeActivity" />
<activity android:name=".features.login.LoginActivity" />
@ -52,17 +56,37 @@
android:label="@string/encryption_message_recovery" />
<activity
android:name="im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity"
android:name=".features.reactions.EmojiReactionPickerActivity"
android:label="@string/title_activity_emoji_reaction_picker" />
<activity android:name=".features.roomdirectory.RoomDirectoryActivity" />
<activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" />
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
<activity android:name=".features.debug.DebugMenuActivity" />
<!-- Services -->
<service
android:name=".core.services.CallService"
android:exported="false" />
<!--<service-->
<!--android:name="im.vector.matrix.android.internal.session.sync.job.SyncService"-->
<!--android:exported="false" />-->
<service
android:name=".core.services.VectorSyncService"
android:exported="false">
</service>
<!-- Receivers -->
<!-- Exported false, should only be accessible from this app!! -->
<receiver
android:name=".features.notifications.NotificationBroadcastReceiver"
android:enabled="true"
android:exported="false" />
<!-- Providers -->
<provider
android:name="androidx.core.content.FileProvider"
@ -73,7 +97,6 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/riotx_provider_paths" />
</provider>
</application>
</manifest>

View File

@ -21,11 +21,12 @@
div {
padding: 4px;
}
</style>
</head>
<body>
<div>
<p>Riot Android</p>
<p>RiotX Android</p>
<p>Third Party Licenses</p>
</div>
@ -173,6 +174,62 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
</pre>
<ul>
<li>
<b>Tapadoo/Alerter</b>
<br/>
Tapadoo/Alerter is licensed under the MIT License
Copyright 2017 Tapadoo, Dublin.
</li>
</ul>
<pre>
Copyright 2017 Tapadoo, Dublin.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre>
<ul>
<li>
<b>com.github.piasy:BigImageViewer</b>
<br/>
MIT License
Copyright (c) 2018 Piasy
</li>
</ul>
<pre>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</pre>
<ul>
<li>
<b>textdrawable</b>
<br/>
textdrawable is licensed under the MIT License
</li>
</ul>
<h3>
Apache License
@ -192,78 +249,25 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Copyright (c) 2019 The Matrix.org Foundation C.I.C
</li>
<li>
<b>Jitsi Meet (<a
href="https://github.com/jitsi/jitsi-meet">jitsi-meet</a>)</b>
<b>rxkotlin</b>
<br/>
Copyright @ 2018-present 8x8, Inc.
<br/>
Copyright @ 2017-2018 Atlassian Pty Ltd
Copyright io.reactivex.
</li>
<li>
<b>Retrofit</b>
<b>rxandroid</b>
<br/>
Copyright 2013 Square, Inc.
Copyright io.reactivex.
</li>
<li>
<b>okhttp</b>
<b>rxrelay</b>
<br/>
Copyright 2016 Square, Inc.
Copyright 2014 Netflix, Inc.
Copyright 2015 Jake Wharton
</li>
<li>
<b>ShortcutBadger</b>
<b>rxbinding</b>
<br/>
Copyright 2014 Leo Lin
</li>
<li>
<b>html-textview</b>
<br/>
Copyright (C) 2013-2015 Dominik Sch&uuml;rmann
<br/>
Copyright (C) 2013-2015 Juha Kuitunen
<br/>
Copyright (C) 2013 Mohammed Lakkadshaw
<br/>
Copyright (C) 2007 The Android Open Source Project
</li>
<li>
<b>anddown</b>
<br/>
Copyright (c) 2016 CommonsWare, LLC
</li>
<li>
<b>zip4j</b>
<br/>
Copyright 2010 Srikanth Reddy Lingala
</li>
<li>
<b>SwipeBack</b>
<br/>
Copyright 2015 Eric Liu
</li>
<li>
<b>Libphonenumber</b>
<br/>
Copyright 2017 Google
</li>
<li>
<b>Butter Knife</b>
<br/>
Copyright 2013 Jake Wharton
</li>
<li>
<b>FloatingActionButton</b>
<br/>
Copyright (C) 2014 Jerzy Chalupski
</li>
<li>
<b>Spanny</b>
<br/>
Copyright 2015 Pavlovsky Ivan
</li>
<li>
<b>PhotoView</b>
<br/>
Copyright 2018 Chris Banes
Copyright (C) 2015 Jake Wharton
</li>
<li>
<b>Epoxy</b>
@ -271,9 +275,69 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Copyright 2016 Airbnb, Inc.
</li>
<li>
<b>Anko</b>
<b>mvrx</b>
<br/>
Copyright 2016 JetBrains s.r.o.
Copyright 2018 Airbnb, Inc.
</li>
<li>
<b>arrow-core</b>
<br/>
Copyright (C) 2017 The Λrrow Authors
</li>
<li>
<b>material</b>
<br/>
Copyright (C) 2016 Google
</li>
<li>
<b>span</b>
<br/>
Copyright 2018 Jun Gu
</li>
<li>
<b>ru.noties.markwon</b>
<br/>
Copyright 2017 Dimitry Ivanov (mail@dimitryivanov.ru)
</li>
<li>
<b>better-link-movement-method</b>
<br/>
Copyright 2018 Saket Narayan.
</li>
<li>
<b>zxcvbn</b>
<br/>
Copyright (c) 2012-2016 Dan Wheeler and Dropbox, Inc.
</li>
<li>
<b>com.otaliastudios:autocomplete</b>
<br/>
Copyright (c) 2017
</li>
<li>
<b>Butterknife</b>
<br/>
Copyright 2013 Jake Wharton
</li>
<li>
<b>seismic</b>
<br/>
Copyright 2012 Square, Inc.
</li>
<li>
<b>videocache</b>
<br/>
Copyright 2014-2017 Alexey Danilov
</li>
<li>
<b>ShortcutBadger</b>
<br/>
Copyright 2014 Leo Lin
</li>
<li>
<b>FilePicker</b>
<br/>
Copyright (c) 2018, Jaisel Rahman
</li>
</ul>
<pre>
@ -453,25 +517,5 @@ Apache License
of your accepting any such warranty or additional liability.
</pre>
<ul>
<li>
<b>Tapadoo/Alerter</b>
<br/>
Tapadoo/Alerter is licensed under the MIT License
Copyright 2017 Tapadoo, Dublin.
</li>
</ul>
<pre>
Copyright 2017 Tapadoo, Dublin.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre>
</body>
</html>

View File

@ -23,6 +23,10 @@ import android.os.Handler
import android.os.HandlerThread
import androidx.core.provider.FontRequest
import androidx.core.provider.FontsContractCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDex
import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController
@ -32,55 +36,106 @@ import com.github.piasy.biv.loader.glide.GlideImageLoader
import com.jakewharton.threetenabp.AndroidThreeTen
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.riotredesign.core.di.*
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.riotredesign.core.di.ActiveSessionHolder
import im.vector.riotredesign.core.di.DaggerVectorComponent
import im.vector.riotredesign.core.di.HasVectorInjector
import im.vector.riotredesign.core.di.VectorComponent
import im.vector.riotredesign.core.extensions.configureAndStart
import im.vector.riotredesign.core.services.AlarmSyncBroadcastReceiver
import im.vector.riotredesign.features.configuration.VectorConfiguration
import im.vector.riotredesign.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
import im.vector.riotredesign.features.notifications.NotificationUtils
import im.vector.riotredesign.features.notifications.PushRuleTriggerListener
import im.vector.riotredesign.features.rageshake.VectorFileLogger
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotredesign.features.settings.PreferencesManager
import im.vector.riotredesign.features.version.getVersion
import im.vector.riotredesign.push.fcm.FcmHelper
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
import kotlin.system.measureTimeMillis
class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider {
lateinit var appContext: Context
//font thread handler
@Inject lateinit var authenticator: Authenticator
@Inject lateinit var vectorConfiguration: VectorConfiguration
@Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
lateinit var vectorComponent: VectorComponent
private var fontThreadHandler: Handler? = null
// var slowMode = false
override fun onCreate() {
val time = measureTimeMillis {
super.onCreate()
appContext = this
vectorComponent = DaggerVectorComponent.factory().create(this)
vectorComponent.inject(this)
vectorUncaughtExceptionHandler.activate(this)
// Log
VectorFileLogger.init(this)
Timber.plant(Timber.DebugTree(), VectorFileLogger)
if (BuildConfig.DEBUG) {
Stetho.initializeWithDefaults(this)
}
AndroidThreeTen.init(this)
BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks())
val fontRequest = FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",
"Noto Color Emoji Compat",
R.array.com_google_android_gms_fonts_certs
)
FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler())
vectorConfiguration.initConfiguration()
super.onCreate()
appContext = this
vectorComponent = DaggerVectorComponent.factory().create(this)
vectorComponent.inject(this)
vectorUncaughtExceptionHandler.activate(this)
// Log
VectorFileLogger.init(this)
Timber.plant(Timber.DebugTree(), VectorFileLogger)
if (BuildConfig.DEBUG) {
Stetho.initializeWithDefaults(this)
}
Timber.v("On create took $time ms")
logInfo()
AndroidThreeTen.init(this)
BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks())
val fontRequest = FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",
"Noto Color Emoji Compat",
R.array.com_google_android_gms_fonts_certs
)
FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler())
vectorConfiguration.initConfiguration()
NotificationUtils.createNotificationChannels(applicationContext)
if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
lastAuthenticatedSession.configureAndStart(pushRuleTriggerListener)
}
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() {
AlarmSyncBroadcastReceiver.cancelAlarm(appContext)
activeSessionHolder.getActiveSession().also {
it.stopAnyBackgroundSync()
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun entersBackground() {
Timber.i("App entered background") // call persistInfo
notificationDrawerManager.persistInfo()
if (FcmHelper.isPushSupported()) {
//TODO FCM fallback
} else {
//TODO check if notifications are enabled for this device
//We need to use alarm in this mode
if (PreferencesManager.areNotificationEnabledForDevice(applicationContext)) {
AlarmSyncBroadcastReceiver.scheduleAlarm(applicationContext, 4_000L)
Timber.i("Alarm scheduled to restart service")
}
}
}
})
}
override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION)
@ -91,6 +146,20 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
return vectorComponent
}
private fun logInfo() {
val appVersion = getVersion(longFormat = true, useBuildNumber = true)
val sdkVersion = Matrix.getSdkVersion()
val date = SimpleDateFormat("MM-dd HH:mm:ss.SSSZ", Locale.US).format(Date())
Timber.v("----------------------------------------------------------------")
Timber.v("----------------------------------------------------------------")
Timber.v(" Application version: $appVersion")
Timber.v(" SDK version: $sdkVersion")
Timber.v(" Local time: $date")
Timber.v("----------------------------------------------------------------")
Timber.v("----------------------------------------------------------------\n\n\n\n")
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
MultiDex.install(this)

View File

@ -44,6 +44,10 @@ class ActiveSessionHolder @Inject constructor(private val authenticator: Authent
return activeSession.get() != null
}
fun getSafeActiveSession(): Session? {
return activeSession.get()
}
fun getActiveSession(): Session {
return activeSession.get() ?: throw IllegalStateException("You should authenticate before using this")
}

View File

@ -133,7 +133,6 @@ interface ScreenComponent {
fun inject(videoMediaViewerActivity: VideoMediaViewerActivity)
@Component.Factory
interface Factory {
fun create(vectorComponent: VectorComponent,

View File

@ -16,9 +16,6 @@
package im.vector.riotredesign.core.di
import com.squareup.inject.assisted.dagger2.AssistedModule
import dagger.Module
/*
@Module(includes = [AssistedInject_VectorAssistedModule::class])
@AssistedModule

View File

@ -19,34 +19,30 @@ package im.vector.riotredesign.core.dialogs
import android.app.Activity
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.widget.Button
import android.widget.ImageView
import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.showPassword
import im.vector.riotredesign.core.platform.SimpleTextWatcher
class ExportKeysDialog {
var passwordVisible = false
fun show(activity: Activity, exportKeyDialogListener: ExportKeyDialogListener) {
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_export_e2e_keys, null)
val builder = AlertDialog.Builder(activity)
.setTitle(R.string.encryption_export_room_keys)
.setView(dialogLayout)
val passPhrase1EditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_passphrase_edit_text)
val passPhrase2EditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_confirm_passphrase_edit_text)
val passPhrase2Til = dialogLayout.findViewById<TextInputLayout>(R.id.dialog_e2e_keys_confirm_passphrase_til)
val exportButton = dialogLayout.findViewById<Button>(R.id.dialog_e2e_keys_export_button)
val textWatcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
}
val passPhrase1EditText = dialogLayout.findViewById<TextInputEditText>(R.id.exportDialogEt)
val passPhrase2EditText = dialogLayout.findViewById<TextInputEditText>(R.id.exportDialogEtConfirm)
val passPhrase2Til = dialogLayout.findViewById<TextInputLayout>(R.id.exportDialogTilConfirm)
val exportButton = dialogLayout.findViewById<Button>(R.id.exportDialogSubmit)
val textWatcher = object : SimpleTextWatcher() {
override fun afterTextChanged(s: Editable) {
when {
TextUtils.isEmpty(passPhrase1EditText.text) -> {
@ -68,6 +64,14 @@ class ExportKeysDialog {
passPhrase1EditText.addTextChangedListener(textWatcher)
passPhrase2EditText.addTextChangedListener(textWatcher)
val showPassword = dialogLayout.findViewById<ImageView>(R.id.exportDialogShowPassword)
showPassword.setOnClickListener {
passwordVisible = !passwordVisible
passPhrase1EditText.showPassword(passwordVisible)
passPhrase2EditText.showPassword(passwordVisible)
showPassword.setImageResource(if (passwordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
}
val exportDialog = builder.show()
exportButton.setOnClickListener {

View File

@ -20,9 +20,13 @@ package im.vector.riotredesign.core.extensions
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.riotredesign.features.notifications.PushRuleTriggerListener
fun Session.openAndStartSync(){
fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener) {
open()
setFilter(FilterService.FilterPreset.RiotFilter)
startSync()
refreshPushers()
pushRuleTriggerListener.startWithSession(this)
fetchPushRules()
}

View File

@ -18,16 +18,18 @@ package im.vector.riotredesign.core.files
import android.app.DownloadManager
import android.content.Context
import androidx.annotation.WorkerThread
import arrow.core.Try
import okio.Okio
import timber.log.Timber
import java.io.File
/**
* Save a string to a file with Okio
* @return true in case of success
*/
fun saveStringToFile(str: String, file: File): Boolean {
return try {
@WorkerThread
fun writeToFile(str: String, file: File): Try<Unit> {
return Try {
val sink = Okio.sink(file)
val bufferedSink = Okio.buffer(sink)
@ -36,14 +38,25 @@ fun saveStringToFile(str: String, file: File): Boolean {
bufferedSink.close()
sink.close()
true
} catch (e: Exception) {
Timber.e(e, "Error saving file")
false
}
}
/**
* Save a byte array to a file with Okio
*/
@WorkerThread
fun writeToFile(data: ByteArray, file: File): Try<Unit> {
return Try {
val sink = Okio.sink(file)
val bufferedSink = Okio.buffer(sink)
bufferedSink.write(data)
bufferedSink.close()
sink.close()
}
}
fun addEntryToDownloadManager(context: Context,
file: File,

View File

@ -0,0 +1,152 @@
/*
* 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.intent
import android.content.ClipData
import android.content.ClipDescription
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.text.TextUtils
import androidx.core.util.PatternsCompat.WEB_URL
import java.util.*
/**
* Inspired from Riot code: RoomMediaMessage.java
*/
sealed class ExternalIntentData {
/**
* Constructor for a text message.
*
* @param text the text
* @param htmlText the HTML text
* @param format the formatted text format
*/
data class IntentDataText(
val text: CharSequence? = null,
val htmlText: String? = null,
val format: String? = null,
val clipDataItem: ClipData.Item = ClipData.Item(text, htmlText),
val mimeType: String? = if (null == htmlText) ClipDescription.MIMETYPE_TEXT_PLAIN else format
) : ExternalIntentData()
/**
* Clip data
*/
data class IntentDataClipData(
val clipDataItem: ClipData.Item,
val mimeType: String?
) : ExternalIntentData()
/**
* Constructor from a media Uri/
*
* @param uri the media uri
* @param filename the media file name
*/
data class IntentDataUri(
val uri: Uri,
val filename: String? = null
) : ExternalIntentData()
}
fun analyseIntent(intent: Intent): List<ExternalIntentData> {
val externalIntentDataList = ArrayList<ExternalIntentData>()
// chrome adds many items when sharing an web page link
// so, test first the type
if (TextUtils.equals(intent.type, ClipDescription.MIMETYPE_TEXT_PLAIN)) {
var message: String? = intent.getStringExtra(Intent.EXTRA_TEXT)
if (null == message) {
val sequence = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)
if (null != sequence) {
message = sequence.toString()
}
}
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
if (!TextUtils.isEmpty(subject)) {
if (TextUtils.isEmpty(message)) {
message = subject
} else if (WEB_URL.matcher(message!!).matches()) {
message = subject + "\n" + message
}
}
if (!TextUtils.isEmpty(message)) {
externalIntentDataList.add(ExternalIntentData.IntentDataText(message!!, null, intent.type))
return externalIntentDataList
}
}
var clipData: ClipData? = null
var mimetypes: MutableList<String>? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
clipData = intent.clipData
}
// multiple data
if (null != clipData) {
if (null != clipData.description) {
if (0 != clipData.description.mimeTypeCount) {
mimetypes = ArrayList()
for (i in 0 until clipData.description.mimeTypeCount) {
mimetypes.add(clipData.description.getMimeType(i))
}
// if the filter is "accept anything" the mimetype does not make sense
if (1 == mimetypes.size) {
if (mimetypes[0].endsWith("/*")) {
mimetypes = null
}
}
}
}
val count = clipData.itemCount
for (i in 0 until count) {
val item = clipData.getItemAt(i)
var mimetype: String? = null
if (null != mimetypes) {
if (i < mimetypes.size) {
mimetype = mimetypes[i]
} else {
mimetype = mimetypes[0]
}
// uris list is not a valid mimetype
if (TextUtils.equals(mimetype, ClipDescription.MIMETYPE_TEXT_URILIST)) {
mimetype = null
}
}
externalIntentDataList.add(ExternalIntentData.IntentDataClipData(item, mimetype))
}
} else if (null != intent.data) {
externalIntentDataList.add(ExternalIntentData.IntentDataUri(intent.data!!))
}
return externalIntentDataList
}

View File

@ -0,0 +1,44 @@
/*
* 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.intent
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
fun getFilenameFromUri(context: Context, uri: Uri): String? {
var result: String? = null
if (uri.scheme == "content") {
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
try {
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
} finally {
cursor?.close()
}
}
if (result == null) {
result = uri.path
val cut = result.lastIndexOf('/')
if (cut != -1) {
result = result.substring(cut + 1)
}
}
return result
}

View File

@ -0,0 +1,54 @@
/*
* 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.intent
import android.content.Context
import android.net.Uri
import android.webkit.MimeTypeMap
import im.vector.riotredesign.core.utils.getFileExtension
import timber.log.Timber
/**
* Returns the mimetype from a uri.
*
* @param context the context
* @return the mimetype
*/
fun getMimeTypeFromUri(context: Context, uri: Uri): String? {
var mimeType: String? = null
try {
mimeType = context.contentResolver.getType(uri)
// try to find the mimetype from the filename
if (null == mimeType) {
val extension = getFileExtension(uri.toString())
if (extension != null) {
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
}
}
if (null != mimeType) {
// the mimetype is sometimes in uppercase.
mimeType = mimeType.toLowerCase()
}
} catch (e: Exception) {
Timber.e(e, "Failed to open resource input stream")
}
return mimeType
}

View File

@ -0,0 +1,60 @@
/*
* 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.platform
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.Checkable
class CheckableView : View, Checkable {
private var mChecked = false
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun isChecked(): Boolean {
return mChecked
}
override fun setChecked(b: Boolean) {
if (b != mChecked) {
mChecked = b
refreshDrawableState()
}
}
override fun toggle() {
isChecked = !mChecked
}
public override fun onCreateDrawableState(extraSpace: Int): IntArray {
val drawableState = super.onCreateDrawableState(extraSpace + 1)
if (isChecked) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET)
}
return drawableState
}
companion object {
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
}
}

View File

@ -356,7 +356,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
open fun getMenuRes() = -1
@AttrRes
open fun getMenuTint() = R.attr.vctr_icon_tint_on_dark_action_bar_color
open fun getMenuTint() = R.attr.vctr_icon_tint_on_light_action_bar_color
/**
* Return a object containing other themes for this activity

View File

@ -28,6 +28,8 @@ abstract class VectorPreferenceFragment : PreferenceFragmentCompat() {
activity as VectorBaseActivity
}
abstract var titleRes: Int
/* ==========================================================================================
* Life cycle
* ========================================================================================== */
@ -36,6 +38,7 @@ abstract class VectorPreferenceFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(titleRes)
Timber.v("onResume Fragment ${this.javaClass.simpleName}")
}

View File

@ -25,7 +25,7 @@ import android.widget.TextView
import androidx.preference.PreferenceViewHolder
import im.vector.riotredesign.R
// TODO Replace by real Bingrule class
// TODO Replace by real Bingrule class, then delete
class BingRule(rule: BingRule) {
fun shouldNotNotify() = false
fun shouldNotify() = false

View File

@ -39,7 +39,8 @@ open class UserAvatarPreference : Preference {
init {
widgetLayoutResource = R.layout.vector_settings_round_avatar
isIconSpaceReserved = false
// Set to false to remove the space when there is no icon
isIconSpaceReserved = true
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {

View File

@ -37,7 +37,8 @@ class VectorEditTextPreference : EditTextPreference {
init {
dialogLayoutResource = R.layout.dialog_preference_edit_text
isIconSpaceReserved = false
// Set to false to remove the space when there is no icon
isIconSpaceReserved = true
}
// No single line for title

View File

@ -54,7 +54,8 @@ class VectorListPreference : ListPreference {
init {
widgetLayoutResource = R.layout.vector_settings_list_preference_with_warning
isIconSpaceReserved = false
// Set to false to remove the space when there is no icon
isIconSpaceReserved = true
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {

View File

@ -75,7 +75,8 @@ open class VectorPreference : Preference {
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
init {
isIconSpaceReserved = false
// Set to false to remove the space when there is no icon
isIconSpaceReserved = true
}
var isHighlighted = false
@ -156,8 +157,4 @@ open class VectorPreference : Preference {
}
}
}
companion object {
private val LOG_TAG = VectorPreference::class.java.simpleName
}
}

View File

@ -36,7 +36,8 @@ class VectorPreferenceCategory : PreferenceCategory {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
isIconSpaceReserved = false
// Set to false to remove the space when there is no icon
isIconSpaceReserved = true
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
@ -47,6 +48,8 @@ class VectorPreferenceCategory : PreferenceCategory {
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)
if (!isIconSpaceReserved) {
(titleTextView?.parent as? ViewGroup)?.setPadding(0, 0, 0, 0)
}
}
}

View File

@ -37,7 +37,8 @@ class VectorSwitchPreference : SwitchPreference {
constructor(context: Context) : super(context)
init {
isIconSpaceReserved = false
// Set to false to remove the space when there is no icon
isIconSpaceReserved = true
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {

View File

@ -0,0 +1,38 @@
package im.vector.riotredesign.core.pushers
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.AppNameProvider
import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringProvider
private const val DEFAULT_PUSHER_FILE_TAG = "mobile"
class PushersManager(
private val currentSession: Session,
private val localeProvider: LocaleProvider,
private val stringProvider: StringProvider,
private val appNameProvider: AppNameProvider
) {
fun registerPusherWithFcmKey(pushKey: String) {
var profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + Math.abs(currentSession.sessionParams.credentials.userId.hashCode())
currentSession.addHttpPusher(
pushKey,
stringProvider.getString(R.string.pusher_app_id),
profileTag,
localeProvider.current().language,
appNameProvider.getAppName(),
currentSession.sessionParams.credentials.deviceId ?: "MOBILE",
stringProvider.getString(R.string.pusher_http_url),
false,
true
)
}
fun unregisterPusher(pushKey: String, callback: MatrixCallback<Unit>) {
currentSession.removeHttpPusher(pushKey, stringProvider.getString(R.string.pusher_app_id),callback)
}
}

View File

@ -0,0 +1,26 @@
package im.vector.riotredesign.core.resources
import android.content.Context
import timber.log.Timber
class AppNameProvider(private val context: Context) {
fun getAppName(): String {
try {
val appPackageName = context.applicationContext.packageName
val pm = context.packageManager
val appInfo = pm.getApplicationInfo(appPackageName, 0)
var appName = pm.getApplicationLabel(appInfo).toString()
// Use appPackageName instead of appName if appName contains any non-ASCII character
if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) {
appName = appPackageName
}
return appName
} catch (e: Exception) {
Timber.e(e, "## AppNameProvider() : failed " + e.message)
return "RiotXAndroid"
}
}
}

View File

@ -26,6 +26,4 @@ class LocaleProvider @Inject constructor(private val resources: Resources) {
fun current(): Locale {
return ConfigurationCompat.getLocales(resources.configuration)[0]
}
}

View File

@ -0,0 +1,73 @@
package im.vector.riotredesign.core.services
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.PowerManager
import androidx.core.content.ContextCompat
import timber.log.Timber
class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
//Aquire a lock to give enough time for the sync :/
(context.getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "riotx:fdroidSynclock").apply {
acquire((10_000).toLong())
}
}
// This method is called when the BroadcastReceiver is receiving an Intent broadcast.
Timber.d("RestartBroadcastReceiver received intent")
Intent(context, VectorSyncService::class.java).also {
it.action = "SLOW"
context.startService(it)
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ContextCompat.startForegroundService(context, intent)
} else {
context.startService(intent)
}
} catch (ex: Throwable) {
//TODO
Timber.e(ex)
}
}
scheduleAlarm(context, 30_000L)
Timber.i("Alarm scheduled to restart service")
}
companion object {
const val REQUEST_CODE = 0
fun scheduleAlarm(context: Context, delay: Long) {
//Reschedule
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java)
val pIntent = PendingIntent.getBroadcast(context, AlarmSyncBroadcastReceiver.REQUEST_CODE,
intent, PendingIntent.FLAG_UPDATE_CURRENT)
val firstMillis = System.currentTimeMillis() + delay
val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pIntent)
} else {
alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pIntent)
}
}
fun cancelAlarm(context: Context) {
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java)
val pIntent = PendingIntent.getBroadcast(context, AlarmSyncBroadcastReceiver.REQUEST_CODE,
intent, PendingIntent.FLAG_UPDATE_CURRENT)
val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmMgr.cancel(pIntent)
}
}
}

View File

@ -1,36 +0,0 @@
/*
* 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()
}
}

View File

@ -0,0 +1,72 @@
/*
* 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.NotificationManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import im.vector.matrix.android.internal.session.sync.job.SyncService
import im.vector.riotredesign.R
import im.vector.riotredesign.features.notifications.NotificationUtils
import timber.log.Timber
class VectorSyncService : SyncService() {
override fun onCreate() {
Timber.v("VectorSyncService - onCreate ")
super.onCreate()
}
override fun onDestroy() {
Timber.v("VectorSyncService - onDestroy ")
removeForegroundNotif()
super.onDestroy()
}
private fun removeForegroundNotif() {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE)
}
/**
* Service is started only in fdroid mode when no FCM is available
* Otherwise it is bounded
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.v("VectorSyncService - onStartCommand ")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notification = NotificationUtils.buildForegroundServiceNotification(applicationContext, R.string.notification_listening_for_events, false)
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
}
return super.onStartCommand(intent, flags, startId)
}
/**
* If the service is bounded and the service was previously started we can remove foreground notif
*/
override fun onBind(intent: Intent?): IBinder {
Timber.v("VectorSyncService - onBind ")
stopForeground(true)
return super.onBind(intent)
}
override fun onUnbind(intent: Intent?): Boolean {
Timber.v("VectorSyncService - onUnbind ")
return super.onUnbind(intent)
}
}

View File

@ -0,0 +1,62 @@
/*
* 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.ui.list
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.setTextOrHide
/**
* A generic list item.
* Displays an item with a title, and optional description.
* Can display an accessory on the right, that can be an image or an indeterminate progress.
* If provided with an action, will display a button at the bottom of the list item.
*/
@EpoxyModelClass(layout = R.layout.item_generic_footer)
abstract class GenericFooterItem : VectorEpoxyModel<GenericFooterItem.Holder>() {
@EpoxyAttribute
var text: String? = null
@EpoxyAttribute
var style: GenericItem.STYLE = GenericItem.STYLE.NORMAL_TEXT
@EpoxyAttribute
var itemClickAction: GenericItem.Action? = null
override fun bind(holder: Holder) {
holder.text.setTextOrHide(text)
when (style) {
GenericItem.STYLE.BIG_TEXT -> holder.text.textSize = 18f
GenericItem.STYLE.NORMAL_TEXT -> holder.text.textSize = 14f
}
holder.view.setOnClickListener {
itemClickAction?.perform?.run()
}
}
class Holder : VectorEpoxyHolder() {
val text by bind<TextView>(R.id.itemGenericFooterText)
}
}

View File

@ -20,6 +20,7 @@ package im.vector.riotredesign.core.utils
import android.content.Context
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AlertDialog
/**
@ -29,9 +30,13 @@ import androidx.appcompat.app.AlertDialog
*/
fun Context.displayInWebView(url: String) {
val wv = WebView(this)
// Set a WebViewClient to ensure redirection is handled directly in the WebView
wv.webViewClient = WebViewClient()
wv.loadUrl(url)
AlertDialog.Builder(this)
.setView(wv)
.setPositiveButton(android.R.string.ok, null)
.show()
}
}

View File

@ -24,7 +24,6 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.riotredesign.core.di.ActiveSessionHolder
import im.vector.riotredesign.core.di.ScreenComponent
import im.vector.riotredesign.core.extensions.openAndStartSync
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.features.home.HomeActivity
import im.vector.riotredesign.features.login.LoginActivity
@ -76,16 +75,12 @@ class MainActivity : VectorBaseActivity() {
}
})
else -> start()
}
}
private fun start() {
val intent = if (authenticator.hasAuthenticatedSessions()) {
if (!sessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!!
sessionHolder.setActiveSession(lastAuthenticatedSession)
lastAuthenticatedSession.openAndStartSync()
}
val intent = if (sessionHolder.hasActiveSession()) {
HomeActivity.newIntent(this)
} else {
LoginActivity.newIntent(this)

View File

@ -0,0 +1,64 @@
/*
* 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.crypto.keys
import android.content.Context
import android.os.Environment
import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.riotredesign.core.files.addEntryToDownloadManager
import im.vector.riotredesign.core.files.writeToFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class KeysExporter(private val session: Session) {
/**
* Export keys and return the file path with the callback
*/
fun export(context: Context, password: String, callback: MatrixCallback<String>) {
session.exportRoomKeys(password, object : MatrixCallback<ByteArray> {
override fun onSuccess(data: ByteArray) {
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
Try {
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt")
writeToFile(data, file)
addEntryToDownloadManager(context, file, "text/plain")
file.absolutePath
}
}
.foldToCallback(callback)
}
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
})
}
}

View File

@ -0,0 +1,83 @@
/*
* 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.crypto.keys
import android.content.Context
import android.net.Uri
import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.riotredesign.core.intent.getMimeTypeFromUri
import im.vector.riotredesign.core.resources.openResource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
class KeysImporter(private val session: Session) {
/**
* Import keys from provided Uri
*/
fun import(context: Context,
uri: Uri,
mimetype: String?,
password: String,
callback: MatrixCallback<ImportRoomKeysResult>) {
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
Try {
val resource = openResource(context, uri, mimetype ?: getMimeTypeFromUri(context, uri))
if (resource?.mContentStream == null) {
throw Exception("Error")
}
val data: ByteArray
try {
data = ByteArray(resource.mContentStream!!.available())
resource.mContentStream!!.read(data)
resource.mContentStream!!.close()
data
} catch (e: Exception) {
try {
resource.mContentStream!!.close()
} catch (e2: Exception) {
Timber.e(e2, "## importKeys()")
}
throw e
}
}
}
.fold(
{
callback.onFailure(it)
},
{ byteArray ->
session.importRoomKeys(byteArray,
password,
null,
callback)
}
)
}
}
}

View File

@ -22,10 +22,13 @@ import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import im.vector.matrix.android.api.MatrixCallback
import im.vector.riotredesign.R
import im.vector.riotredesign.core.dialogs.ExportKeysDialog
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.platform.SimpleFragmentActivity
import im.vector.riotredesign.core.utils.toast
import im.vector.riotredesign.features.crypto.keys.KeysExporter
class KeysBackupSetupActivity : SimpleFragmentActivity() {
@ -63,19 +66,19 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
viewModel.navigateEvent.observeEvent(this) { uxStateEvent ->
when (uxStateEvent) {
KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_2 -> {
KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_2 -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
supportFragmentManager.beginTransaction()
.replace(R.id.container, KeysBackupSetupStep2Fragment.newInstance())
.commit()
}
KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_3 -> {
KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_3 -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
supportFragmentManager.beginTransaction()
.replace(R.id.container, KeysBackupSetupStep3Fragment.newInstance())
.commit()
}
KeysBackupSetupSharedViewModel.NAVIGATE_FINISH -> {
KeysBackupSetupSharedViewModel.NAVIGATE_FINISH -> {
val resultIntent = Intent()
viewModel.keysVersion.value?.version?.let {
resultIntent.putExtra(KEYS_VERSION, it)
@ -83,7 +86,18 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
setResult(RESULT_OK, resultIntent)
finish()
}
KeysBackupSetupSharedViewModel.NAVIGATE_MANUAL_EXPORT -> {
KeysBackupSetupSharedViewModel.NAVIGATE_PROMPT_REPLACE -> {
AlertDialog.Builder(this)
.setTitle(R.string.keys_backup_setup_override_backup_prompt_tile)
.setMessage(R.string.keys_backup_setup_override_backup_prompt_description)
.setPositiveButton(R.string.keys_backup_setup_override_replace) { _, _ ->
viewModel.forceCreateKeyBackup(this)
}.setNegativeButton(R.string.keys_backup_setup_override_stop) { _, _ ->
viewModel.stopAndKeepAfterDetectingExistingOnServer()
}
.show()
}
KeysBackupSetupSharedViewModel.NAVIGATE_MANUAL_EXPORT -> {
exportKeysManually()
}
}
@ -117,49 +131,38 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
})
}
fun exportKeysManually() {
private fun exportKeysManually() {
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) {
notImplemented()
/*
showWaitingView()
CommonActivityUtils.exportKeys(session, passphrase, object : SimpleApiCallback<String>(this@KeysBackupSetupActivity) {
override fun onSuccess(filename: String) {
hideWaitingView()
KeysExporter(session)
.export(this@KeysBackupSetupActivity,
passphrase,
object : MatrixCallback<String> {
AlertDialog.Builder(this@KeysBackupSetupActivity)
.setMessage(getString(R.string.encryption_export_saved_as, filename))
.setCancelable(false)
.setPositiveButton(R.string.ok) { dialog, which ->
val resultIntent = Intent()
resultIntent.putExtra(MANUAL_EXPORT, true)
setResult(RESULT_OK, resultIntent)
finish()
}
.show()
}
override fun onSuccess(data: String) {
hideWaitingView()
override fun onNetworkError(e: Exception) {
super.onNetworkError(e)
hideWaitingView()
}
AlertDialog.Builder(this@KeysBackupSetupActivity)
.setMessage(getString(R.string.encryption_export_saved_as, data))
.setCancelable(false)
.setPositiveButton(R.string.ok) { dialog, which ->
val resultIntent = Intent()
resultIntent.putExtra(MANUAL_EXPORT, true)
setResult(RESULT_OK, resultIntent)
finish()
}
.show()
}
override fun onMatrixError(e: MatrixError) {
super.onMatrixError(e)
hideWaitingView()
}
override fun onUnexpectedError(e: Exception) {
super.onUnexpectedError(e)
hideWaitingView()
}
})
*/
override fun onFailure(failure: Throwable) {
toast(failure.localizedMessage)
hideWaitingView()
}
})
}
})
}
@ -171,8 +174,8 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
AlertDialog.Builder(this)
.setTitle(R.string.keys_backup_setup_skip_title)
.setMessage(R.string.keys_backup_setup_skip_msg)
.setNegativeButton(R.string.stay, null)
.setPositiveButton(R.string.abort) { _, _ ->
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.leave) { _, _ ->
finish()
}
.show()

View File

@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.WaitingViewData
import im.vector.riotredesign.core.utils.LiveEvent
@ -40,6 +41,7 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
companion object {
const val NAVIGATE_TO_STEP_2 = "NAVIGATE_TO_STEP_2"
const val NAVIGATE_TO_STEP_3 = "NAVIGATE_TO_STEP_3"
const val NAVIGATE_PROMPT_REPLACE = "NAVIGATE_PROMPT_REPLACE"
const val NAVIGATE_FINISH = "NAVIGATE_FINISH"
const val NAVIGATE_MANUAL_EXPORT = "NAVIGATE_MANUAL_EXPORT"
}
@ -124,15 +126,8 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
megolmBackupCreationInfo = data
copyHasBeenMade = false
val keyBackup = session?.getKeysBackupService()
if (keyBackup != null) {
createKeysBackup(context, keyBackup)
} else {
loadingStatus.value = null
isCreatingBackupVersion.value = false
prepareRecoverFailError.value = Exception()
}
val keyBackup = session.getKeysBackupService()
createKeysBackup(context, keyBackup)
}
override fun onFailure(failure: Throwable) {
@ -150,18 +145,32 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
}
}
private fun createKeysBackup(context: Context, keysBackup: KeysBackupService) {
fun forceCreateKeyBackup(context: Context) {
val keyBackup = session.getKeysBackupService()
createKeysBackup(context, keyBackup, true)
}
fun stopAndKeepAfterDetectingExistingOnServer() {
loadingStatus.value = null
navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
session.getKeysBackupService().checkAndStartKeysBackup()
}
private fun createKeysBackup(context: Context, keysBackup: KeysBackupService, forceOverride: Boolean = false) {
loadingStatus.value = WaitingViewData(context.getString(R.string.keys_backup_setup_creating_backup), isIndeterminate = true)
creatingBackupError.value = null
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo!!, object : MatrixCallback<KeysVersion> {
override fun onSuccess(data: KeysVersion) {
loadingStatus.value = null
isCreatingBackupVersion.value = false
keysVersion.value = data
navigateEvent.value = LiveEvent(NAVIGATE_TO_STEP_3)
keysBackup.getCurrentVersion(object : MatrixCallback<KeysVersionResult?> {
override fun onSuccess(data: KeysVersionResult?) {
if (data?.version.isNullOrBlank() || forceOverride) {
processOnCreate()
} else {
loadingStatus.value = null
// we should prompt
isCreatingBackupVersion.value = false
navigateEvent.value = LiveEvent(NAVIGATE_PROMPT_REPLACE)
}
}
override fun onFailure(failure: Throwable) {
@ -171,7 +180,26 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
isCreatingBackupVersion.value = false
creatingBackupError.value = failure
}
fun processOnCreate() {
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo!!, object : MatrixCallback<KeysVersion> {
override fun onSuccess(data: KeysVersion) {
loadingStatus.value = null
isCreatingBackupVersion.value = false
keysVersion.value = data
navigateEvent.value = LiveEvent(NAVIGATE_TO_STEP_3)
}
override fun onFailure(failure: Throwable) {
Timber.e(failure, "## createKeyBackupVersion")
loadingStatus.value = null
isCreatingBackupVersion.value = false
creatingBackupError.value = failure
}
})
}
})
}
}

View File

@ -25,15 +25,20 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import arrow.core.Try
import butterknife.BindView
import butterknife.OnClick
import com.google.android.material.bottomsheet.BottomSheetDialog
import im.vector.riotredesign.R
import im.vector.riotredesign.core.di.ScreenComponent
import im.vector.riotredesign.core.files.addEntryToDownloadManager
import im.vector.riotredesign.core.files.saveStringToFile
import im.vector.riotredesign.core.files.writeToFile
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.utils.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
@ -161,30 +166,39 @@ class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
}
private fun exportRecoveryKeyToFile(data: String) {
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt")
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
Try {
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt")
if (saveStringToFile(data, file)) {
addEntryToDownloadManager(requireContext(), file, "text/plain")
writeToFile(data, file)
context?.let {
AlertDialog.Builder(it)
.setMessage(getString(R.string.recovery_key_export_saved_as_warning, file.absolutePath))
.setCancelable(false)
.setPositiveButton(R.string.ok, null)
.show()
addEntryToDownloadManager(requireContext(), file, "text/plain")
file.absolutePath
}
}
.fold(
{ throwable ->
context?.let {
AlertDialog.Builder(it)
.setTitle(R.string.dialog_title_error)
.setMessage(throwable.localizedMessage)
}
},
{ path ->
viewModel.copyHasBeenMade = true
viewModel.copyHasBeenMade = true
} else {
context?.let {
AlertDialog.Builder(it)
.setTitle(R.string.dialog_title_error)
.setMessage(getString(R.string.unknown_error))
.setCancelable(false)
.setPositiveButton(R.string.ok, null)
.show()
}
context?.let {
AlertDialog.Builder(it)
.setMessage(getString(R.string.recovery_key_export_saved_as_warning, path))
}
}
)
?.setCancelable(false)
?.setPositiveButton(R.string.ok, null)
?.show()
}
}

View File

@ -39,11 +39,15 @@ import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.core.platform.OnBackPressed
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.core.pushers.PushersManager
import im.vector.riotredesign.features.crypto.keysrequest.KeyRequestHandler
import im.vector.riotredesign.features.crypto.verification.IncomingVerificationRequestHandler
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
import im.vector.riotredesign.features.rageshake.BugReporter
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker
import im.vector.riotredesign.features.workers.signout.SignOutViewModel
import im.vector.riotredesign.push.fcm.FcmHelper
import kotlinx.android.synthetic.main.activity_home.*
import javax.inject.Inject
@ -66,6 +70,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
@Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler
// TODO Move this elsewhere
@Inject lateinit var keyRequestHandler: KeyRequestHandler
@Inject lateinit var pushManager: PushersManager
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
private var progress: ProgressDialog? = null
@ -84,8 +90,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
homeNavigator.activity = this
navigationViewModel = ViewModelProviders.of(this, viewModelFactory).get(HomeNavigationViewModel::class.java)
FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager)
navigationViewModel = ViewModelProviders.of(this).get(HomeNavigationViewModel::class.java)
drawerLayout.addDrawerListener(drawerListener)
if (isFirstCreation()) {
val homeDrawerFragment = HomeDrawerFragment.newInstance()
@ -111,6 +117,20 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
is Navigation.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
}
}
if (intent.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)) {
notificationDrawerManager.clearAllEvents()
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) {
notificationDrawerManager.clearAllEvents()
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
}
}
override fun onDestroy() {
@ -132,6 +152,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
.setNegativeButton(R.string.no) { _, _ -> bugReporter.deleteCrashFile(this) }
.show()
}
//Force remote backup state update to update the banner if needed
ViewModelProviders.of(this).get(SignOutViewModel::class.java).refreshRemoteStateIfNeeded()
}
override fun configure(toolbar: Toolbar) {
@ -143,7 +166,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.sliding_menu_sign_out -> {
SignOutUiWorker(this).perform(activeSessionHolder.getActiveSession())
SignOutUiWorker(this, notificationDrawerManager).perform(activeSessionHolder.getActiveSession())
return true
}
}
@ -182,10 +205,14 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
companion object {
fun newIntent(context: Context): Intent {
return Intent(context, HomeActivity::class.java)
}
private const val EXTRA_CLEAR_EXISTING_NOTIFICATION = "EXTRA_CLEAR_EXISTING_NOTIFICATION"
fun newIntent(context: Context, clearNotification: Boolean = false): Intent {
return Intent(context, HomeActivity::class.java)
.apply {
putExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, clearNotification)
}
}
}
}

View File

@ -17,12 +17,9 @@
package im.vector.riotredesign.features.home
import androidx.core.view.GravityCompat
import androidx.fragment.app.FragmentManager
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.di.ScreenScope
import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.features.navigation.Navigator
import kotlinx.android.synthetic.main.activity_home.*
import timber.log.Timber
import javax.inject.Inject
@ -37,43 +34,11 @@ class HomeNavigator @Inject constructor() {
fun openSelectedGroup(groupSummary: GroupSummary) {
Timber.v("Open selected group ${groupSummary.groupId}")
activity?.let {
it.drawerLayout?.closeDrawer(GravityCompat.START)
val args = HomeDetailParams(groupSummary.groupId, groupSummary.displayName, groupSummary.avatarUrl)
val homeDetailFragment = HomeDetailFragment.newInstance(args)
it.drawerLayout?.closeDrawer(GravityCompat.START)
it.replaceFragment(homeDetailFragment, R.id.homeDetailFragmentContainer)
}
}
fun openRoomDetail(roomId: String,
eventId: String?,
navigator: Navigator) {
Timber.v("Open room detail $roomId - $eventId")
activity?.let {
//TODO enable eventId permalink. It doesn't work enough at the moment.
it.drawerLayout?.closeDrawer(GravityCompat.START)
navigator.openRoom(roomId, it)
}
}
fun openGroupDetail(groupId: String) {
Timber.v("Open group detail $groupId")
}
fun openUserDetail(userId: String) {
Timber.v("Open user detail $userId")
}
// Private Methods *****************************************************************************
private fun clearBackStack(fragmentManager: FragmentManager) {
if (fragmentManager.backStackEntryCount > 0) {
val first = fragmentManager.getBackStackEntryAt(0)
fragmentManager.popBackStack(first.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
}
private fun isRoot(roomId: String): Boolean {
return rootRoomId == roomId
}
}
}

View File

@ -0,0 +1,87 @@
/*
* 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.home
import android.content.Context
import android.net.Uri
import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.features.navigation.Navigator
class PermalinkHandler(private val session: Session,
private val navigator: Navigator) {
fun launch(context: Context, deepLink: String?, navigateToRoomInterceptor: NavigateToRoomInterceptor? = null): Boolean {
val uri = deepLink?.let { Uri.parse(it) }
return launch(context, uri, navigateToRoomInterceptor)
}
fun launch(context: Context, deepLink: Uri?, navigateToRoomInterceptor: NavigateToRoomInterceptor? = null): Boolean {
if (deepLink == null) {
return false
}
return when (val permalinkData = PermalinkParser.parse(deepLink)) {
is PermalinkData.EventLink -> {
if (navigateToRoomInterceptor?.navToRoom(permalinkData.roomIdOrAlias, permalinkData.eventId) != true) {
openRoom(context, permalinkData.roomIdOrAlias, permalinkData.eventId)
}
true
}
is PermalinkData.RoomLink -> {
if (navigateToRoomInterceptor?.navToRoom(permalinkData.roomIdOrAlias) != true) {
openRoom(context, permalinkData.roomIdOrAlias)
}
true
}
is PermalinkData.GroupLink -> {
navigator.openGroupDetail(permalinkData.groupId, context)
true
}
is PermalinkData.UserLink -> {
navigator.openUserDetail(permalinkData.userId, context)
true
}
is PermalinkData.FallbackLink -> {
false
}
}
}
/**
* Open room either joined, or not unknown
*/
private fun openRoom(context: Context, roomIdOrAlias: String, eventId: String? = null) {
if (session.getRoom(roomIdOrAlias) != null) {
navigator.openRoom(context, roomIdOrAlias, eventId)
} else {
navigator.openNotJoinedRoom(context, roomIdOrAlias, eventId)
}
}
}
interface NavigateToRoomInterceptor {
/**
* Return true if the navigation has been intercepted
*/
fun navToRoom(roomId: String, eventId: String? = null): Boolean
}

View File

@ -54,8 +54,8 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
groupController.callback = this
stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(groupController)
stateView.contentView = groupListEpoxyRecyclerView
groupListEpoxyRecyclerView.setController(groupController)
viewModel.subscribe { renderState(it) }
viewModel.openGroupLiveData.observeEvent(this) {
homeNavigator.openSelectedGroup(it)

View File

@ -90,7 +90,8 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
private fun observeGroupSummaries() {
session
.rx().liveGroupSummaries()
.rx()
.liveGroupSummaries()
.map {
val myUser = session.getUser(session.sessionParams.credentials.userId)
val allCommunityGroup = GroupSummary(
@ -100,6 +101,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
listOf(allCommunityGroup) + it
}
.execute { async ->
// TODO Phase2 Handle the case where the selected group is deleted on another client
val newSelectedGroup = selectedGroup ?: async()?.firstOrNull()
copy(asyncGroups = async, selectedGroup = newSelectedGroup)
}

View File

@ -30,8 +30,9 @@ sealed class RoomDetailActions {
data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val opposite: String) : RoomDetailActions()
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions()
object AcceptInvite : RoomDetailActions()
object RejectInvite : RoomDetailActions()

View File

@ -57,13 +57,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.R
@ -72,6 +66,7 @@ import im.vector.riotredesign.core.dialogs.DialogListItem
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.extensions.hideKeyboard
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.extensions.setTextOrHide
import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
@ -86,9 +81,7 @@ import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandP
import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotredesign.features.command.Command
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.HomePermalinkHandler
import im.vector.riotredesign.features.home.getColorFromUserId
import im.vector.riotredesign.features.home.*
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerActions
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerView
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel
@ -106,6 +99,7 @@ import im.vector.riotredesign.features.media.ImageContentRenderer
import im.vector.riotredesign.features.media.ImageMediaViewerActivity
import im.vector.riotredesign.features.media.VideoContentRenderer
import im.vector.riotredesign.features.media.VideoMediaViewerActivity
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity
import im.vector.riotredesign.features.settings.PreferencesManager
import kotlinx.android.parcel.Parcelize
@ -179,10 +173,12 @@ class RoomDetailFragment :
@Inject lateinit var commandAutocompletePolicy: CommandAutocompletePolicy
@Inject lateinit var autocompleteCommandPresenter: AutocompleteCommandPresenter
@Inject lateinit var autocompleteUserPresenter: AutocompleteUserPresenter
@Inject lateinit var homePermalinkHandler: HomePermalinkHandler
@Inject lateinit var permalinkHandler: PermalinkHandler
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var roomDetailViewModelFactory: RoomDetailViewModel.Factory
@Inject lateinit var textComposerViewModelFactory: TextComposerViewModel.Factory
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
override fun getLayoutResId() = R.layout.fragment_room_detail
@ -216,6 +212,11 @@ class RoomDetailFragment :
handleActions(it)
}
roomDetailViewModel.navigateToEvent.observeEvent(this) {
//
scrollOnHighlightedEventCallback.scheduleScrollTo(it)
}
roomDetailViewModel.selectSubscribe(
RoomDetailViewState::sendMode,
RoomDetailViewState::selectedEvent,
@ -239,25 +240,25 @@ class RoomDetailFragment :
}
//switch to expanded bar
composerLayout.composerRelatedMessageTitle.apply {
text = event.senderName
setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.sender)))
text = event.getDisambiguatedDisplayName()
setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId)))
}
//TODO this is used at several places, find way to refactor?
val messageContent: MessageContent? =
event.annotations?.editSummary?.aggregatedContent?.toModel()
?: event.root.content.toModel()
?: event.root.content.toModel()
val nonFormattedBody = messageContent?.body ?: ""
var formattedBody: CharSequence? = null
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
val parser = Parser.builder().build()
val document = parser.parse(messageContent.formattedBody
?: messageContent.body)
?: messageContent.body)
formattedBody = Markwon.builder(requireContext())
.usePlugin(HtmlPlugin.create()).build().render(document)
}
composerLayout.composerRelatedMessageContent.text = formattedBody
?: nonFormattedBody
?: nonFormattedBody
if (mode == SendMode.EDIT) {
@ -272,8 +273,8 @@ class RoomDetailFragment :
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_reply))
}
avatarRenderer.render(event.senderAvatar, event.root.sender
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
avatarRenderer.render(event.senderAvatar, event.root.senderId
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
composerLayout.expand {
@ -289,6 +290,18 @@ class RoomDetailFragment :
}
}
override fun onResume() {
super.onResume()
notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId)
}
override fun onPause() {
super.onPause()
notificationDrawerManager.setCurrentRoom(null)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK && data != null) {
@ -296,9 +309,9 @@ class RoomDetailFragment :
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
REACTION_SELECT_REQUEST_CODE -> {
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
?: return
?: return
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
?: return
?: return
//TODO check if already reacted with that?
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
}
@ -314,12 +327,14 @@ class RoomDetailFragment :
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController)
recyclerView.layoutManager = layoutManager
recyclerView.itemAnimator = null
recyclerView.setHasFixedSize(true)
timelineEventController.addModelBuildListener {
it.dispatchTo(stateRestorer)
it.dispatchTo(scrollOnNewMessageCallback)
it.dispatchTo(scrollOnHighlightedEventCallback)
}
recyclerView.addOnScrollListener(
@ -484,7 +499,7 @@ class RoomDetailFragment :
val summary = state.asyncRoomSummary()
val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) {
timelineEventController.setTimeline(state.timeline)
timelineEventController.setTimeline(state.timeline, state.eventId)
inviteView.visibility = View.GONE
val uid = session.sessionParams.credentials.userId
@ -503,12 +518,7 @@ class RoomDetailFragment :
state.asyncRoomSummary()?.let {
roomToolbarTitleView.text = it.displayName
avatarRenderer.render(it, roomToolbarAvatarImageView)
if (it.topic.isNotEmpty()) {
roomToolbarSubtitleView.visibility = View.VISIBLE
roomToolbarSubtitleView.text = it.topic
} else {
roomToolbarSubtitleView.visibility = View.GONE
}
roomToolbarSubtitleView.setTextOrHide(it.topic)
}
}
@ -551,20 +561,49 @@ class RoomDetailFragment :
// TimelineEventController.Callback ************************************************************
override fun onUrlClicked(url: String) {
homePermalinkHandler.launch(url)
override fun onUrlClicked(url: String): Boolean {
return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
override fun navToRoom(roomId: String, eventId: String?): Boolean {
// Same room?
if (roomId == roomDetailArgs.roomId) {
// Navigation to same room
if (eventId == null) {
showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room))
} else {
// Highlight and scroll to this event
roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, timelineEventController.searchPositionOfEvent(eventId)))
}
return true
}
// Not handled
return false
}
})
}
override fun onUrlLongClicked(url: String): Boolean {
// Copy the url to the clipboard
copyToClipboard(requireContext(), url)
return true
}
override fun onEventVisible(event: TimelineEvent) {
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event))
}
override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) {
vectorBaseActivity.notImplemented("encrypted message click")
}
override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) {
// TODO Use navigator
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
startActivity(intent)
}
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
// TODO Use navigator
val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
startActivity(intent)
}
@ -593,7 +632,7 @@ class RoomDetailFragment :
}
override fun onAvatarClicked(informationData: MessageInformationData) {
vectorBaseActivity.notImplemented()
vectorBaseActivity.notImplemented("Click on user avatar")
}
@SuppressLint("SetTextI18n")
@ -636,7 +675,7 @@ class RoomDetailFragment :
}
MessageMenuViewModel.ACTION_VIEW_REACTIONS -> {
val messageInformationData = actionData.data as? MessageInformationData
?: return
?: return
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, messageInformationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
}
@ -694,9 +733,9 @@ class RoomDetailFragment :
.show()
}
MessageMenuViewModel.ACTION_QUICK_REACT -> {
//eventId,ClickedOn,Opposite
(actionData.data as? Triple<String, String, String>)?.let { (eventId, clickedOn, opposite) ->
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, opposite))
//eventId,ClickedOn,Add
(actionData.data as? Triple<String, String, Boolean>)?.let { (eventId, clickedOn, add) ->
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, add))
}
}
MessageMenuViewModel.ACTION_EDIT -> {
@ -775,7 +814,7 @@ class RoomDetailFragment :
imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT)
}
fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
val snack = Snackbar.make(view!!, message, duration)
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
snack.show()

View File

@ -44,6 +44,7 @@ import im.vector.riotredesign.features.home.room.detail.timeline.helper.Timeline
import io.reactivex.rxkotlin.subscribeBy
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
@ -62,7 +63,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} else {
TimelineDisplayableEvents.DISPLAYABLE_TYPES
}
private val timeline = room.createTimeline(eventId, allowedTypes)
private var timeline = room.createTimeline(eventId, allowedTypes)
@AssistedInject.Factory
interface Factory {
@ -110,6 +111,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
else -> Timber.e("Unhandled Action: $action")
}
}
@ -140,6 +143,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>>
get() = _sendMessageResultLiveData
private val _navigateToEvent = MutableLiveData<LiveEvent<String>>()
val navigateToEvent: LiveData<LiveEvent<String>>
get() = _navigateToEvent
// PRIVATE METHODS *****************************************************************************
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
@ -213,7 +221,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
SendMode.EDIT -> {
room.editTextMessage(state.selectedEvent?.root?.eventId
?: "", action.text, action.autoMarkdown)
?: "", action.text, action.autoMarkdown)
setState {
copy(
sendMode = SendMode.REGULAR,
@ -225,7 +233,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
SendMode.QUOTE -> {
val messageContent: MessageContent? =
state.selectedEvent?.annotations?.editSummary?.aggregatedContent?.toModel()
?: state.selectedEvent?.root?.content.toModel()
?: state.selectedEvent?.root?.content.toModel()
val textMsg = messageContent?.body
val finalText = legacyRiotQuoteText(textMsg, action.text)
@ -294,7 +302,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
_nonBlockingPopAlert.postValue(LiveEvent(
Pair(R.string.last_edited_info_message, listOf(
lastReplace.senderName ?: "?",
lastReplace.getDisambiguatedDisplayName(),
dateFormat.format(Date(lastReplace.root.originServerTs ?: 0)))
))
)
@ -345,7 +353,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) {
room.updateQuickReaction(action.selectedReaction, action.opposite, action.targetEventId, session.sessionParams.credentials.userId)
if (action.add) {
room.sendReaction(action.selectedReaction, action.targetEventId)
} else {
room.undoReaction(action.selectedReaction, action.targetEventId, session.sessionParams.credentials.userId)
}
}
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
@ -415,6 +427,56 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
}
private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) {
val targetEventId = action.eventId
if (action.position != null) {
// Event is already in RAM
withState {
if (it.eventId == targetEventId) {
// ensure another click on the same permalink will also do a scroll
setState {
copy(
eventId = null
)
}
}
setState {
copy(
eventId = targetEventId
)
}
}
_navigateToEvent.postValue(LiveEvent(targetEventId))
} else {
// change timeline
timeline.dispose()
timeline = room.createTimeline(targetEventId, allowedTypes)
timeline.start()
withState {
if (it.eventId == targetEventId) {
// ensure another click on the same permalink will also do a scroll
setState {
copy(
eventId = null
)
}
}
setState {
copy(
eventId = targetEventId,
timeline = this@RoomDetailViewModel.timeline
)
}
}
_navigateToEvent.postValue(LiveEvent(targetEventId))
}
}
private fun observeEventDisplayedActions() {
// We are buffering scroll events for one second
@ -441,7 +503,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun observeInvitationState() {
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
if (summary.membership == Membership.INVITE) {
summary.lastMessage?.sender?.let { senderId ->
summary.lastMessage?.senderId?.let { senderId ->
session.getUser(senderId)
}?.also {
setState { copy(asyncInviter = Success(it)) }

View File

@ -21,7 +21,6 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineData
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.user.model.User
@ -46,7 +45,6 @@ data class RoomDetailViewState(
val timeline: Timeline? = null,
val asyncInviter: Async<User> = Uninitialized,
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val asyncTimelineData: Async<TimelineData> = Uninitialized,
val sendMode: SendMode = SendMode.REGULAR,
val selectedEvent: TimelineEvent? = null
) : MvRxState {

View File

@ -0,0 +1,50 @@
/*
* 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.home.room.detail
import androidx.recyclerview.widget.LinearLayoutManager
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import java.util.concurrent.atomic.AtomicReference
class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutManager,
private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback {
private val scheduledEventId = AtomicReference<String?>()
override fun onChanged(position: Int, count: Int, tag: Any?) {
val eventId = scheduledEventId.get() ?: return
val positionToScroll = timelineEventController.searchPositionOfEvent(eventId)
if (positionToScroll != null) {
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
// Do not scroll it item is already visible
if (positionToScroll !in firstVisibleItem..lastVisibleItem) {
// Note: Offset will be from the bottom, since the layoutManager is reversed
layoutManager.scrollToPositionWithOffset(positionToScroll, 120)
}
scheduledEventId.set(null)
}
}
fun scheduleScrollTo(eventId: String?) {
scheduledEventId.set(eventId)
}
}

View File

@ -50,9 +50,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
private val backgroundHandler: Handler
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback {
interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback {
fun onEventVisible(event: TimelineEvent)
fun onUrlClicked(url: String)
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
fun onFileMessageClicked(messageFileContent: MessageFileContent)
@ -75,6 +75,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
fun onMemberNameClicked(informationData: MessageInformationData)
}
interface UrlClickCallback {
fun onUrlClicked(url: String): Boolean
fun onUrlLongClicked(url: String): Boolean
}
private val collapsedEventIds = linkedSetOf<String>()
private val mergeItemCollapseStates = HashMap<String, Boolean>()
private val modelCache = arrayListOf<CacheItemData?>()
@ -87,39 +92,43 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
private val listUpdateCallback = object : ListUpdateCallback {
@Synchronized
override fun onChanged(position: Int, count: Int, payload: Any?) {
assertUpdateCallbacksAllowed()
(position until (position + count)).forEach {
modelCache[it] = null
synchronized(modelCache) {
assertUpdateCallbacksAllowed()
(position until (position + count)).forEach {
modelCache[it] = null
}
requestModelBuild()
}
requestModelBuild()
}
@Synchronized
override fun onMoved(fromPosition: Int, toPosition: Int) {
assertUpdateCallbacksAllowed()
val model = modelCache.removeAt(fromPosition)
modelCache.add(toPosition, model)
requestModelBuild()
synchronized(modelCache) {
assertUpdateCallbacksAllowed()
val model = modelCache.removeAt(fromPosition)
modelCache.add(toPosition, model)
requestModelBuild()
}
}
@Synchronized
override fun onInserted(position: Int, count: Int) {
assertUpdateCallbacksAllowed()
(0 until count).forEach {
modelCache.add(position, null)
synchronized(modelCache) {
assertUpdateCallbacksAllowed()
(0 until count).forEach {
modelCache.add(position, null)
}
requestModelBuild()
}
requestModelBuild()
}
@Synchronized
override fun onRemoved(position: Int, count: Int) {
assertUpdateCallbacksAllowed()
(0 until count).forEach {
modelCache.removeAt(position)
synchronized(modelCache) {
assertUpdateCallbacksAllowed()
(0 until count).forEach {
modelCache.removeAt(position)
}
requestModelBuild()
}
requestModelBuild()
}
}
@ -127,13 +136,37 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
requestModelBuild()
}
fun setTimeline(timeline: Timeline?) {
fun setTimeline(timeline: Timeline?, eventIdToHighlight: String?) {
if (this.timeline != timeline) {
this.timeline = timeline
this.timeline?.listener = this
// Clear cache
synchronized(modelCache) {
for (i in 0 until modelCache.size) {
modelCache[i] = null
}
}
}
if (this.eventIdToHighlight != eventIdToHighlight) {
// Clear cache to force a refresh
synchronized(modelCache) {
for (i in 0 until modelCache.size) {
if (modelCache[i]?.eventId == eventIdToHighlight
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
modelCache[i] = null
}
}
}
this.eventIdToHighlight = eventIdToHighlight
requestModelBuild()
}
}
private var eventIdToHighlight: String? = null
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
timelineMediaSizeProvider.recyclerView = recyclerView
@ -173,28 +206,29 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
require(inSubmitList || Looper.myLooper() == backgroundHandler.looper)
}
@Synchronized
private fun getModels(): List<EpoxyModel<*>> {
(0 until modelCache.size).forEach { position ->
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
// We then are sure we always have items up to date.
if (modelCache[position] == null
|| modelCache[position]?.mergedHeaderModel != null
|| modelCache[position]?.formattedDayModel != null) {
modelCache[position] = buildItemModels(position, currentSnapshot)
}
}
return modelCache
.map {
val eventModel = if (it == null || collapsedEventIds.contains(it.localId)) {
null
} else {
it.eventModel
}
listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel)
synchronized(modelCache) {
(0 until modelCache.size).forEach { position ->
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
// We then are sure we always have items up to date.
if (modelCache[position] == null
|| modelCache[position]?.mergedHeaderModel != null
|| modelCache[position]?.formattedDayModel != null) {
modelCache[position] = buildItemModels(position, currentSnapshot)
}
.flatten()
.filterNotNull()
}
return modelCache
.map {
val eventModel = if (it == null || collapsedEventIds.contains(it.localId)) {
null
} else {
it.eventModel
}
listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel)
}
.flatten()
.filterNotNull()
}
}
@ -205,14 +239,14 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
val eventModel = timelineItemFactory.create(event, nextEvent, callback).also {
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also {
it.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
}
val mergedHeaderModel = buildMergedHeaderItem(event, nextEvent, items, addDaySeparator, currentPosition)
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
return CacheItemData(event.localId, eventModel, mergedHeaderModel, daySeparatorItem)
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem)
}
private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? {
@ -224,6 +258,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
}
}
// TODO Phase 3 Handle the case where the eventId we have to highlight is merged
private fun buildMergedHeaderItem(event: TimelineEvent,
nextEvent: TimelineEvent?,
items: List<TimelineEvent>,
@ -241,7 +276,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
val senderAvatar = mergedEvent.senderAvatar()
val senderName = mergedEvent.senderName()
MergedHeaderItem.Data(
userId = mergedEvent.root.sender ?: "",
userId = mergedEvent.root.senderId ?: "",
avatarUrl = senderAvatar,
memberName = senderName ?: "",
eventId = mergedEvent.localId
@ -273,10 +308,23 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
addIf(shouldAdd, this@TimelineEventController)
}
fun searchPositionOfEvent(eventId: String): Int? {
synchronized(modelCache) {
// Search in the cache
modelCache.forEachIndexed { idx, cacheItemData ->
if (cacheItemData?.eventId == eventId) {
return idx
}
}
return null
}
}
}
private data class CacheItemData(
val localId: String,
val eventId: String?,
val eventModel: EpoxyModel<*>? = null,
val mergedHeaderModel: MergedHeaderItem? = null,
val formattedDayModel: DaySeparatorItem? = null

View File

@ -101,8 +101,9 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
.commit()
}
quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener {
override fun didQuickReactWith(clikedOn: String, opposite: String, reactions: List<String>, eventId: String) {
actionHandlerModel.fireAction(MessageMenuViewModel.ACTION_QUICK_REACT, Triple(eventId, clikedOn, opposite))
override fun didQuickReactWith(clikedOn: String, add: Boolean, eventId: String) {
actionHandlerModel.fireAction(MessageMenuViewModel.ACTION_QUICK_REACT, Triple(eventId, clikedOn, add))
dismiss()
}
}

View File

@ -55,7 +55,8 @@ data class MessageActionState(
/**
* Information related to an event and used to display preview in contextual bottomsheet.
*/
class MessageActionsViewModel @AssistedInject constructor(@Assisted initialState: MessageActionState,
class MessageActionsViewModel @AssistedInject constructor(@Assisted
initialState: MessageActionState,
private val eventHtmlRenderer: EventHtmlRenderer,
private val session: Session,
private val noticeEventFormatter: NoticeEventFormatter
@ -91,11 +92,11 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted initialState
when (event.root.type) {
EventType.MESSAGE -> {
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel()
?: event.root.content.toModel()
?: event.root.content.toModel()
body = messageContent?.body
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
body = eventHtmlRenderer.render(messageContent.formattedBody
?: messageContent.body)
?: messageContent.body)
}
}
EventType.STATE_ROOM_NAME,
@ -109,7 +110,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted initialState
}
}
return state.copy(
userId = event.root.sender ?: "",
userId = event.root.senderId ?: "",
senderName = informationData.memberName?.toString() ?: "",
messageBody = body,
ts = dateFormat.format(Date(originTs ?: 0)),

View File

@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import org.json.JSONObject
@ -52,7 +53,8 @@ data class MessageMenuState(
* Manages list actions for a given message (copy / paste / forward...)
*/
class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: MessageMenuState,
private val session: Session) : VectorViewModel<MessageMenuState>(initialState) {
private val session: Session,
private val stringProvider: StringProvider) : VectorViewModel<MessageMenuState>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -159,10 +161,14 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
this.add(SimpleAction(VIEW_SOURCE, R.string.view_source, R.drawable.ic_view_source, JSONObject(event.root.toContent()).toString(4)))
if (event.isEncrypted()) {
this.add(SimpleAction(VIEW_DECRYPTED_SOURCE, R.string.view_decrypted_source, R.drawable.ic_view_source, state.eventId))
val decryptedContent = event.root.mClearEvent?.toContent()?.let {
JSONObject(it).toString(4)
} ?: stringProvider.getString(R.string.encryption_information_decryption_error)
this.add(SimpleAction(VIEW_DECRYPTED_SOURCE, R.string.view_decrypted_source, R.drawable.ic_view_source, decryptedContent))
}
this.add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, state.eventId))
if (session.sessionParams.credentials.userId != event.root.sender && event.root.getClearType() == EventType.MESSAGE) {
if (session.sessionParams.credentials.userId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
//not sent by me
this.add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, state.eventId))
}
@ -207,12 +213,12 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
}
}
private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
//TODO if user is admin or moderator
return event.root.sender == myUserId
}
private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
//TODO if user is admin or moderator
return event.root.senderId == myUserId
}
private fun canViewReactions(event: TimelineEvent): Boolean {
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
@ -221,16 +227,17 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
}
private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
//TODO if user is admin or moderator
val messageContent = event.root.content.toModel<MessageContent>()
return event.root.sender == myUserId && (
messageContent?.type == MessageType.MSGTYPE_TEXT
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
)
}
private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
//TODO if user is admin or moderator
val messageContent = event.root.content.toModel<MessageContent>()
return event.root.senderId == myUserId && (
messageContent?.type == MessageType.MSGTYPE_TEXT
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
)
}
private fun canCopy(type: String?): Boolean {

View File

@ -19,9 +19,6 @@ import android.graphics.Typeface
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.transition.TransitionManager
import butterknife.BindView
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@ -29,6 +26,7 @@ import im.vector.riotredesign.EmojiCompatFontProvider
import im.vector.riotredesign.R
import im.vector.riotredesign.core.di.ScreenComponent
import im.vector.riotredesign.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.adapter_item_action_quick_reaction.*
import javax.inject.Inject
/**
@ -38,21 +36,6 @@ class QuickReactionFragment : VectorBaseFragment() {
private val viewModel: QuickReactionViewModel by fragmentViewModel(QuickReactionViewModel::class)
@BindView(R.id.root_layout)
lateinit var rootLayout: ConstraintLayout
@BindView(R.id.quick_react_1_text)
lateinit var quickReact1Text: TextView
@BindView(R.id.quick_react_2_text)
lateinit var quickReact2Text: TextView
@BindView(R.id.quick_react_3_text)
lateinit var quickReact3Text: TextView
@BindView(R.id.quick_react_4_text)
lateinit var quickReact4Text: TextView
var interactionListener: InteractionListener? = null
@Inject lateinit var fontProvider: EmojiCompatFontProvider
@ -64,77 +47,35 @@ class QuickReactionFragment : VectorBaseFragment() {
injector.inject(this)
}
lateinit var textViews: List<TextView>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
quickReact1Text.text = QuickReactionViewModel.AGREE_POSITIVE
quickReact2Text.text = QuickReactionViewModel.AGREE_NEGATIVE
quickReact3Text.text = QuickReactionViewModel.LIKE_POSITIVE
quickReact4Text.text = QuickReactionViewModel.LIKE_NEGATIVE
listOf(quickReact1Text, quickReact2Text, quickReact3Text, quickReact4Text).forEach {
it.typeface = fontProvider.typeface ?: Typeface.DEFAULT
}
//configure click listeners
quickReact1Text.setOnClickListener {
viewModel.toggleAgree(true)
}
quickReact2Text.setOnClickListener {
viewModel.toggleAgree(false)
}
quickReact3Text.setOnClickListener {
viewModel.toggleLike(true)
}
quickReact4Text.setOnClickListener {
viewModel.toggleLike(false)
textViews = listOf(quickReaction0, quickReaction1, quickReaction2, quickReaction3,
quickReaction4, quickReaction5, quickReaction6, quickReaction7)
textViews.forEachIndexed { index, textView ->
textView.typeface = fontProvider.typeface ?: Typeface.DEFAULT
textView.setOnClickListener {
viewModel.didSelect(index)
}
}
}
override fun invalidate() = withState(viewModel) {
TransitionManager.beginDelayedTransition(rootLayout)
when (it.agreeTriggleState) {
TriggleState.NONE -> {
quickReact1Text.alpha = 1f
quickReact2Text.alpha = 1f
}
TriggleState.FIRST -> {
quickReact1Text.alpha = 1f
quickReact2Text.alpha = 0.2f
}
TriggleState.SECOND -> {
quickReact1Text.alpha = 0.2f
quickReact2Text.alpha = 1f
}
}
when (it.likeTriggleState) {
TriggleState.NONE -> {
quickReact3Text.alpha = 1f
quickReact4Text.alpha = 1f
}
TriggleState.FIRST -> {
quickReact3Text.alpha = 1f
quickReact4Text.alpha = 0.2f
}
TriggleState.SECOND -> {
quickReact3Text.alpha = 0.2f
quickReact4Text.alpha = 1f
}
it.quickStates.forEachIndexed { index, qs ->
textViews[index].text = qs.reaction
textViews[index].alpha = if (qs.isSelected) 0.2f else 1f
}
if (it.selectionResult != null) {
val clikedOn = it.selectionResult.first
interactionListener?.didQuickReactWith(clikedOn, QuickReactionViewModel.getOpposite(clikedOn)
?: "", it.selectionResult.second, it.eventId)
if (it.result != null) {
interactionListener?.didQuickReactWith(it.result.reaction, it.result.isSelected, it.eventId)
}
}
interface InteractionListener {
fun didQuickReactWith(clikedOn: String, opposite: String, reactions: List<String>, eventId: String)
fun didQuickReactWith(clikedOn: String, add: Boolean, eventId: String)
}
companion object {

View File

@ -28,27 +28,23 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInf
/**
* Quick reactions state, it's a toggle with 3rd state
*/
enum class TriggleState {
NONE,
FIRST,
SECOND
}
data class ToggleState(
val reaction: String,
val isSelected: Boolean
)
data class QuickReactionState(
val roomId: String,
val eventId: String,
val informationData: MessageInformationData,
val agreeTriggleState: TriggleState = TriggleState.NONE,
val likeTriggleState: TriggleState = TriggleState.NONE,
val quickStates: List<ToggleState> = emptyList(),
val result: ToggleState? = null
/** Pair of 'clickedOn' and current toggles state*/
val selectionResult: Pair<String, List<String>>? = null
) : MvRxState {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
}
/**
* Quick reaction view model
*/
@ -62,20 +58,7 @@ class QuickReactionViewModel @AssistedInject constructor(@Assisted initialState:
companion object : MvRxViewModelFactory<QuickReactionViewModel, QuickReactionState> {
const val AGREE_POSITIVE = "👍"
const val AGREE_NEGATIVE = "👎"
const val LIKE_POSITIVE = "🙂"
const val LIKE_NEGATIVE = "😔"
fun getOpposite(reaction: String): String? {
return when (reaction) {
AGREE_POSITIVE -> AGREE_NEGATIVE
AGREE_NEGATIVE -> AGREE_POSITIVE
LIKE_POSITIVE -> LIKE_NEGATIVE
LIKE_NEGATIVE -> LIKE_POSITIVE
else -> null
}
}
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
override fun create(viewModelContext: ViewModelContext, state: QuickReactionState): QuickReactionViewModel? {
val fragment: QuickReactionFragment = (viewModelContext as FragmentViewModelContext).fragment()
@ -89,84 +72,18 @@ class QuickReactionViewModel @AssistedInject constructor(@Assisted initialState:
private fun reduceState(state: QuickReactionState): QuickReactionState {
val event = session.getRoom(state.roomId)?.getTimeLineEvent(state.eventId) ?: return state
var agreeTriggle: TriggleState = TriggleState.NONE
var likeTriggle: TriggleState = TriggleState.NONE
event.annotations?.reactionsSummary?.forEach {
//it.addedByMe
if (it.addedByMe) {
if (AGREE_POSITIVE == it.key) {
agreeTriggle = TriggleState.FIRST
} else if (AGREE_NEGATIVE == it.key) {
agreeTriggle = TriggleState.SECOND
}
if (LIKE_POSITIVE == it.key) {
likeTriggle = TriggleState.FIRST
} else if (LIKE_NEGATIVE == it.key) {
likeTriggle = TriggleState.SECOND
}
}
val summary = event.annotations?.reactionsSummary
val quickReactions = quickEmojis.map { emoji ->
ToggleState(emoji, summary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
}
return state.copy(
agreeTriggleState = agreeTriggle,
likeTriggleState = likeTriggle
)
return state.copy(quickStates = quickReactions)
}
fun toggleAgree(isFirst: Boolean) = withState {
if (isFirst) {
setState {
val newTriggle = if (it.agreeTriggleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST
copy(
agreeTriggleState = newTriggle,
selectionResult = Pair(AGREE_POSITIVE, getReactions(this, newTriggle, null))
)
}
} else {
setState {
val newTriggle = if (it.agreeTriggleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND
copy(
agreeTriggleState = agreeTriggleState,
selectionResult = Pair(AGREE_NEGATIVE, getReactions(this, newTriggle, null))
)
}
fun didSelect(index: Int) = withState {
val isSelected = it.quickStates[index].isSelected
setState {
copy(result = ToggleState(it.quickStates[index].reaction, !isSelected))
}
}
fun toggleLike(isFirst: Boolean) = withState {
if (isFirst) {
setState {
val newTriggle = if (it.likeTriggleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST
copy(
likeTriggleState = newTriggle,
selectionResult = Pair(LIKE_POSITIVE, getReactions(this, null, newTriggle))
)
}
} else {
setState {
val newTriggle = if (it.likeTriggleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND
copy(
likeTriggleState = newTriggle,
selectionResult = Pair(LIKE_NEGATIVE, getReactions(this, null, newTriggle))
)
}
}
}
private fun getReactions(state: QuickReactionState, newState1: TriggleState?, newState2: TriggleState?): List<String> {
return ArrayList<String>(4).apply {
when (newState2 ?: state.likeTriggleState) {
TriggleState.FIRST -> add(LIKE_POSITIVE)
TriggleState.SECOND -> add(LIKE_NEGATIVE)
else -> {
}
}
when (newState1 ?: state.agreeTriggleState) {
TriggleState.FIRST -> add(AGREE_POSITIVE)
TriggleState.SECOND -> add(AGREE_NEGATIVE)
else -> {
}
}
}
}
}

View File

@ -46,13 +46,14 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
@AssistedInject.Factory
interface Factory {
fun create(initialState: DisplayReactionsViewState): ViewReactionViewModel
}
companion object : MvRxViewModelFactory<ViewReactionViewModel, DisplayReactionsViewState> {
companion object : MvRxViewModelFactory<ViewReactionViewModel, DisplayReactionsViewState> {
override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionViewModel? {
val fragment: ViewReactionBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewReactionViewModelFactory.create(state)
}
override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionViewModel? {
val fragment: ViewReactionBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewReactionViewModelFactory.create(state)
}
}
@ -76,7 +77,7 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
ReactionInfo(
event.root.eventId!!,
it.key,
event.root.sender ?: "",
event.root.senderId ?: "",
event.senderName,
timelineDateFormatter.formatMessageHour(localDate)
)

View File

@ -23,13 +23,15 @@ import javax.inject.Inject
class DefaultItemFactory @Inject constructor(){
fun create(event: TimelineEvent, exception: Exception? = null): DefaultItem? {
fun create(event: TimelineEvent, highlight: Boolean, exception: Exception? = null): DefaultItem? {
val text = if (exception == null) {
"${event.root.getClearType()} events are not yet handled"
} else {
"an exception occurred when rendering the event ${event.root.eventId}"
}
return DefaultItem_().text(text)
return DefaultItem_()
.text(text)
.highlighted(highlight)
}
}

View File

@ -16,31 +16,37 @@
package im.vector.riotredesign.features.home.room.detail.timeline.factory
import android.graphics.Typeface
import android.text.Spannable
import android.text.SpannableString
import android.text.style.StyleSpan
import android.view.View
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.resources.ColorProvider
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.core.utils.DebouncedClickListener
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem_
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
import javax.inject.Inject
import im.vector.riotredesign.features.home.room.detail.timeline.util.MessageInformationDataFactory
import me.gujun.android.span.span
// This class handles timeline event who haven't been successfully decrypted
class EncryptedItemFactory @Inject constructor(private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer) {
// This class handles timeline events who haven't been successfully decrypted
class EncryptedItemFactory(private val messageInformationDataFactory: MessageInformationDataFactory,
private val colorProvider: ColorProvider,
private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer) {
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
highlight: Boolean,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
event.root.eventId ?: return null
fun create(timelineEvent: TimelineEvent): VectorEpoxyModel<*>? {
return when {
EventType.ENCRYPTED == timelineEvent.root.getClearType() -> {
val cryptoError = timelineEvent.root.mCryptoError
EventType.ENCRYPTED == event.root.getClearType() -> {
val cryptoError = event.root.mCryptoError
val errorDescription =
if (cryptoError?.code == MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE) {
stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id)
@ -49,23 +55,34 @@ class EncryptedItemFactory @Inject constructor(private val stringProvider: Strin
}
val message = stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription)
val spannableStr = SpannableString(message)
spannableStr.setSpan(StyleSpan(Typeface.ITALIC), 0, message.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
val spannableStr = span(message) {
textStyle = "italic"
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
}
// TODO This is not correct format for error, change it
val informationData = MessageInformationData(
eventId = timelineEvent.root.eventId ?: "?",
senderId = timelineEvent.root.sender ?: "",
sendState = timelineEvent.sendState,
avatarUrl = timelineEvent.senderAvatar(),
memberName = timelineEvent.senderName(),
showInformation = false
)
val informationData = messageInformationDataFactory.create(event, nextEvent)
return NoticeItem_()
.avatarRenderer(avatarRenderer)
.noticeText(spannableStr)
return MessageTextItem_()
.message(spannableStr)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
.urlClickCallback(callback)
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEncryptedMessageClicked(informationData, view)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, null, view)
?: false
}
}
else -> null
else -> null
}
}
}
}

View File

@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventConte
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
@ -34,11 +35,14 @@ import javax.inject.Inject
class EncryptionItemFactory @Inject constructor(private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer) {
fun create(event: TimelineEvent): NoticeItem? {
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.BaseCallback?): NoticeItem? {
val text = buildNoticeText(event.root, event.senderName) ?: return null
val informationData = MessageInformationData(
eventId = event.root.eventId ?: "?",
senderId = event.root.sender ?: "",
senderId = event.root.senderId ?: "",
sendState = event.sendState,
avatarUrl = event.senderAvatar(),
memberName = event.senderName(),
@ -48,6 +52,8 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri
.avatarRenderer(avatarRenderer)
.noticeText(text)
.informationData(informationData)
.highlighted(highlight)
.baseCallback(callback)
}
private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {

View File

@ -25,7 +25,6 @@ import android.text.style.RelativeSizeSpan
import android.view.View
import im.vector.matrix.android.api.permalinks.MatrixLinkify
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
@ -35,18 +34,16 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.EmojiCompatFontProvider
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.core.linkify.VectorLinkify
import im.vector.riotredesign.core.resources.ColorProvider
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.core.utils.DebouncedClickListener
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.getColorFromUserId
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.home.room.detail.timeline.item.*
import im.vector.riotredesign.features.home.room.detail.timeline.util.MessageInformationDataFactory
import im.vector.riotredesign.features.html.EventHtmlRenderer
import im.vector.riotredesign.features.media.ImageContentRenderer
import im.vector.riotredesign.features.media.VideoContentRenderer
@ -57,56 +54,26 @@ class MessageItemFactory @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val colorProvider: ColorProvider,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val timelineDateFormatter: TimelineDateFormatter,
private val htmlRenderer: EventHtmlRenderer,
private val stringProvider: StringProvider,
private val emojiCompatFontProvider: EmojiCompatFontProvider,
private val imageContentRenderer: ImageContentRenderer,
private val messageInformationDataFactory: MessageInformationDataFactory,
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder) {
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
highlight: Boolean,
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
event.root.eventId ?: return null
val eventId = event.root.eventId ?: return null
val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
?: false
val showInformation = addDaySeparator
|| event.senderAvatar != nextEvent?.senderAvatar
|| event.senderName != nextEvent?.senderName
|| nextEvent?.root?.getClearType() != EventType.MESSAGE
|| isNextMessageReceivedMoreThanOneHourAgo
val time = timelineDateFormatter.formatMessageHour(date)
val avatarUrl = event.senderAvatar
val memberName = event.senderName ?: event.root.sender ?: ""
val formattedMemberName = span(memberName) {
textColor = colorProvider.getColor(getColorFromUserId(event.root.sender
?: ""))
}
val hasBeenEdited = event.annotations?.editSummary != null
val informationData = MessageInformationData(eventId = eventId,
senderId = event.root.sender ?: "",
sendState = event.sendState,
time = time,
avatarUrl = avatarUrl,
memberName = formattedMemberName,
showInformation = showInformation,
orderedReactionList = event.annotations?.reactionsSummary?.map {
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
},
hasBeenEdited = hasBeenEdited
)
val informationData = messageInformationDataFactory.create(event, nextEvent)
if (event.root.unsignedData?.redactedEvent != null) {
//message is redacted
return buildRedactedItem(informationData, callback)
return buildRedactedItem(informationData, highlight, callback)
}
val messageContent: MessageContent =
@ -124,31 +91,33 @@ class MessageItemFactory @Inject constructor(
return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
informationData,
hasBeenEdited,
event.annotations?.editSummary,
highlight,
callback)
is MessageTextContent -> buildTextMessageItem(event.sendState,
messageContent,
informationData,
hasBeenEdited,
event.annotations?.editSummary,
highlight,
callback
)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback)
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback)
else -> buildNotHandledMessageItem(messageContent)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback)
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback)
else -> buildNotHandledMessageItem(messageContent, highlight)
}
}
private fun buildAudioMessageItem(messageContent: MessageAudioContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageFileItem? {
return MessageFileItem_()
.avatarRenderer(avatarRenderer)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
.filename(messageContent.body)
.iconRes(R.drawable.filetype_audio)
@ -170,10 +139,12 @@ class MessageItemFactory @Inject constructor(
private fun buildFileMessageItem(messageContent: MessageFileContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageFileItem? {
return MessageFileItem_()
.avatarRenderer(avatarRenderer)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
.filename(messageContent.body)
.reactionPillCallback(callback)
@ -193,13 +164,16 @@ class MessageItemFactory @Inject constructor(
}))
}
private fun buildNotHandledMessageItem(messageContent: MessageContent): DefaultItem? {
private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? {
val text = "${messageContent.type} message events are not yet handled"
return DefaultItem_().text(text)
return DefaultItem_()
.text(text)
.highlighted(highlight)
}
private fun buildImageMessageItem(messageContent: MessageImageContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
@ -219,6 +193,7 @@ class MessageItemFactory @Inject constructor(
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.playable(messageContent.info?.mimeType == "image/gif")
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
.mediaData(data)
.reactionPillCallback(callback)
@ -239,6 +214,7 @@ class MessageItemFactory @Inject constructor(
private fun buildVideoMessageItem(messageContent: MessageVideoContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
@ -263,6 +239,7 @@ class MessageItemFactory @Inject constructor(
.avatarRenderer(avatarRenderer)
.playable(true)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
.mediaData(thumbnailData)
.reactionPillCallback(callback)
@ -281,8 +258,8 @@ class MessageItemFactory @Inject constructor(
private fun buildTextMessageItem(sendState: SendState,
messageContent: MessageTextContent,
informationData: MessageInformationData,
hasBeenEdited: Boolean,
editSummary: EditAggregatedSummary?,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageTextItem? {
val bodyToUse = messageContent.formattedBody?.let {
@ -293,7 +270,7 @@ class MessageItemFactory @Inject constructor(
return MessageTextItem_()
.apply {
if (hasBeenEdited) {
if (informationData.hasBeenEdited) {
val spannable = annotateWithEdited(linkifiedBody, callback, informationData, editSummary)
message(spannable)
} else {
@ -302,7 +279,9 @@ class MessageItemFactory @Inject constructor(
}
.avatarRenderer(avatarRenderer)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
.urlClickCallback(callback)
.reactionPillCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
//click on the text
@ -352,6 +331,7 @@ class MessageItemFactory @Inject constructor(
private fun buildNoticeMessageItem(messageContent: MessageNoticeContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageTextItem? {
val message = messageContent.body.let {
@ -366,8 +346,10 @@ class MessageItemFactory @Inject constructor(
.avatarRenderer(avatarRenderer)
.message(message)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
.reactionPillCallback(callback)
.urlClickCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.memberClickListener(
DebouncedClickListener(View.OnClickListener { view ->
@ -385,8 +367,8 @@ class MessageItemFactory @Inject constructor(
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
informationData: MessageInformationData,
hasBeenEdited: Boolean,
editSummary: EditAggregatedSummary?,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageTextItem? {
val message = messageContent.body.let {
@ -395,7 +377,7 @@ class MessageItemFactory @Inject constructor(
}
return MessageTextItem_()
.apply {
if (hasBeenEdited) {
if (informationData.hasBeenEdited) {
val spannable = annotateWithEdited(message, callback, informationData, editSummary)
message(spannable)
} else {
@ -404,8 +386,10 @@ class MessageItemFactory @Inject constructor(
}
.avatarRenderer(avatarRenderer)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
.reactionPillCallback(callback)
.urlClickCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.cellClickListener(
DebouncedClickListener(View.OnClickListener { view ->
@ -418,10 +402,12 @@ class MessageItemFactory @Inject constructor(
}
private fun buildRedactedItem(informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): RedactedMessageItem? {
return RedactedMessageItem_()
.avatarRenderer(avatarRenderer)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
}

View File

@ -31,11 +31,12 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
private val avatarRenderer: AvatarRenderer) {
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?): NoticeItem? {
val formattedText = eventFormatter.format(event) ?: return null
val informationData = MessageInformationData(
eventId = event.root.eventId ?: "?",
senderId = event.root.sender ?: "",
senderId = event.root.senderId ?: "",
sendState = event.sendState,
avatarUrl = event.senderAvatar(),
memberName = event.senderName(),
@ -45,6 +46,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
return NoticeItem_()
.avatarRenderer(avatarRenderer)
.noticeText(formattedText)
.highlighted(highlight)
.informationData(informationData)
.baseCallback(callback)
}

View File

@ -38,11 +38,13 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
eventIdToHighlight: String?,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
val highlight = event.root.eventId == eventIdToHighlight
val computedModel = try {
when (event.root.getClearType()) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
// State and call
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
@ -50,38 +52,38 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.STATE_HISTORY_VISIBILITY,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> noticeItemFactory.create(event, callback)
EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback)
// Crypto
EventType.ENCRYPTION -> encryptionItemFactory.create(event)
EventType.ENCRYPTED -> encryptedItemFactory.create(event)
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
EventType.ENCRYPTED -> encryptedItemFactory.create(event, nextEvent, highlight, callback)
// Unhandled event types (yet)
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER,
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event)
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event, highlight)
else -> {
//These are just for debug to display hidden event, they should be filtered out in normal mode
if (TimelineDisplayableEvents.DEBUG_HIDDEN_EVENT) {
val informationData = MessageInformationData(eventId = event.root.eventId
?: "?",
senderId = event.root.sender
?: "",
sendState = event.sendState,
time = "",
avatarUrl = null,
memberName = "",
showInformation = false
?: "?",
senderId = event.root.senderId ?: "",
sendState = event.sendState,
time = "",
avatarUrl = null,
memberName = "",
showInformation = false
)
val messageContent = event.root.content.toModel<MessageContent>()
?: MessageDefaultContent("", "", null, null)
?: MessageDefaultContent("", "", null, null)
MessageTextItem_()
.informationData(informationData)
.message("{ \"type\": ${event.root.type} }")
.highlighted(highlight)
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
} else {
Timber.w("Ignored event (type: ${event.root.type}")
@ -91,7 +93,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
}
} catch (e: Exception) {
Timber.e(e, "failed to create message item")
defaultItemFactory.create(event, e)
defaultItemFactory.create(event, highlight, e)
}
return (computedModel ?: EmptyItem_())
}

View File

@ -20,12 +20,7 @@ import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.api.session.room.model.*
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
@ -38,13 +33,29 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
fun format(timelineEvent: TimelineEvent): CharSequence? {
return when (val type = timelineEvent.root.getClearType()) {
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderName)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderName)
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName())
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderName)
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.senderName)
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
else -> {
Timber.v("Type $type not handled by this formatter")
null
}
}
}
fun format(event: Event, senderName: String?): CharSequence? {
return when (val type = event.getClearType()) {
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName)
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName)
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(event, senderName)
else -> {
Timber.v("Type $type not handled by this formatter")
null
@ -72,7 +83,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility
?: return null
?: return null
val formattedVisibility = when (historyVisibility) {
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
@ -117,12 +128,12 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
if (!TextUtils.equals(eventContent?.displayName, prevEventContent?.displayName)) {
val displayNameText = when {
prevEventContent?.displayName.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_display_name_set, event.sender, eventContent?.displayName)
stringProvider.getString(R.string.notice_display_name_set, event.senderId, eventContent?.displayName)
eventContent?.displayName.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_display_name_removed, event.sender, prevEventContent?.displayName)
stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName)
else ->
stringProvider.getString(R.string.notice_display_name_changed_from,
event.sender, prevEventContent?.displayName, eventContent?.displayName)
event.senderId, prevEventContent?.displayName, eventContent?.displayName)
}
displayText.append(displayNameText)
}
@ -140,8 +151,8 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
}
private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val senderDisplayName = senderName ?: event.sender
val targetDisplayName = eventContent?.displayName ?: event.sender
val senderDisplayName = senderName ?: event.senderId
val targetDisplayName = eventContent?.displayName ?: event.senderId
return when {
Membership.INVITE == eventContent?.membership -> {
// TODO get userId
@ -149,7 +160,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
when {
eventContent.thirdPartyInvite != null ->
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
TextUtils.equals(event.stateKey, selfUserId) ->
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
event.stateKey.isNullOrEmpty() ->
@ -162,7 +173,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
stringProvider.getString(R.string.notice_room_join, senderDisplayName)
Membership.LEAVE == eventContent?.membership ->
// 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked
return if (TextUtils.equals(event.sender, event.stateKey)) {
return if (TextUtils.equals(event.senderId, event.stateKey)) {
if (prevEventContent?.membership == Membership.INVITE) {
stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
} else {

View File

@ -22,7 +22,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.riotredesign.R
import im.vector.riotredesign.core.di.ActiveSessionHolder

View File

@ -1,26 +0,0 @@
/*
* 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.home.room.detail.timeline.helper
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
object RoomMemberEventHelper {
}

View File

@ -85,12 +85,11 @@ fun TimelineEvent.senderAvatar(): String? {
fun TimelineEvent.senderName(): String? {
// We might have no senderName when user leave, so we try to get it from prevContent
return senderName
?: if (root.type == EventType.STATE_ROOM_MEMBER) {
root.prevContent.toModel<RoomMember>()?.displayName
} else {
null
}
return when {
senderName != null -> getDisambiguatedDisplayName()
root.type == EventType.STATE_ROOM_MEMBER -> root.prevContent.toModel<RoomMember>()?.displayName
else -> null
}
}
fun TimelineEvent.canBeMerged(): Boolean {

View File

@ -85,7 +85,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
override fun bind(holder: H) {
super.bind(holder)
if (informationData.showInformation) {
holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply {
val size = dpToPx(avatarStyle.avatarSizeDP, holder.view.context)
height = size

View File

@ -19,20 +19,28 @@ import android.view.View
import android.view.ViewStub
import androidx.annotation.IdRes
import androidx.constraintlayout.widget.Guideline
import com.airbnb.epoxy.EpoxyAttribute
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.platform.CheckableView
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() {
var avatarStyle: AvatarStyle = Companion.AvatarStyle.SMALL
var avatarStyle: AvatarStyle = AvatarStyle.SMALL
// To use for instance when opening a permalink with an eventId
@EpoxyAttribute
var highlighted: Boolean = false
override fun bind(holder: H) {
super.bind(holder)
//optimize?
val px = dpToPx(avatarStyle.avatarSizeDP, holder.view.context)
val px = dpToPx(avatarStyle.avatarSizeDP + 8, holder.view.context)
holder.leftGuideline.setGuidelineBegin(px)
holder.checkableBackground.isChecked = highlighted
}
@ -46,6 +54,7 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
abstract class BaseHolder : VectorEpoxyHolder() {
val leftGuideline by bind<Guideline>(R.id.messageStartGuideline)
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
@IdRes
abstract fun getStubId(): Int

View File

@ -25,6 +25,7 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.utils.containsOnlyEmojis
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.html.PillImageSpan
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@ -41,15 +42,18 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
override lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute
override lateinit var informationData: MessageInformationData
@EpoxyAttribute
var urlClickCallback: TimelineEventController.UrlClickCallback? = null
val mvmtMethod = BetterLinkMovementMethod.newInstance().also {
it.setOnLinkClickListener { textView, url ->
//Return false to let android manage the click on the link
false
// TODO Move this instantiation somewhere else?
private val mvmtMethod = BetterLinkMovementMethod.newInstance().also {
it.setOnLinkClickListener { _, url ->
//Return false to let android manage the click on the link, or true if the link is handled by the application
urlClickCallback?.onUrlClicked(url) == true
}
it.setOnLinkLongClickListener { textView, url ->
it.setOnLinkLongClickListener { _, url ->
//Long clicks are handled by parent, return true to block android to do something with url
true
urlClickCallback?.onUrlLongClicked(url) == true
}
}

View File

@ -41,11 +41,9 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
var baseCallback: TimelineEventController.BaseCallback? = null
private var longClickListener = View.OnLongClickListener {
baseCallback?.onEventLongClicked(informationData, null, it)
baseCallback != null
return@OnLongClickListener baseCallback?.onEventLongClicked(informationData, null, it) == true
}
override fun bind(holder: Holder) {
super.bind(holder)
holder.noticeTextView.text = noticeText

View File

@ -0,0 +1,77 @@
/*
* 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.home.room.detail.timeline.util
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.core.resources.ColorProvider
import im.vector.riotredesign.features.home.getColorFromUserId
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotredesign.features.home.room.detail.timeline.item.ReactionInfoData
import me.gujun.android.span.span
import javax.inject.Inject
/**
* This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline
*/
class MessageInformationDataFactory @Inject constructor(private val timelineDateFormatter: TimelineDateFormatter,
private val colorProvider: ColorProvider) {
fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData {
// Non nullability has been tested before
val eventId = event.root.eventId!!
val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
?: false
val showInformation =
addDaySeparator
|| event.senderAvatar != nextEvent?.senderAvatar
|| event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName()
|| (nextEvent?.root?.getClearType() != EventType.MESSAGE && nextEvent?.root?.getClearType() != EventType.ENCRYPTED)
|| isNextMessageReceivedMoreThanOneHourAgo
val time = timelineDateFormatter.formatMessageHour(date)
val avatarUrl = event.senderAvatar
val memberName = event.getDisambiguatedDisplayName()
val formattedMemberName = span(memberName) {
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId
?: ""))
}
val hasBeenEdited = event.annotations?.editSummary != null
return MessageInformationData(
eventId = eventId,
senderId = event.root.senderId ?: "",
sendState = event.sendState,
time = time,
avatarUrl = avatarUrl,
memberName = formattedMemberName,
showInformation = showInformation,
orderedReactionList = event.annotations?.reactionsSummary?.map {
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
},
hasBeenEdited = hasBeenEdited
)
}
}

View File

@ -23,11 +23,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.*
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
@ -82,7 +78,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O
setupRecyclerView()
roomListViewModel.subscribe { renderState(it) }
roomListViewModel.openRoomLiveData.observeEvent(this) {
navigator.openRoom(it, requireActivity())
navigator.openRoom(requireActivity(), it)
}
createChatFabMenu.listener = this
@ -103,25 +99,26 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O
}
// Hide FAB when list is scrolling
epoxyRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
createChatFabMenu.removeCallbacks(showFabRunnable)
epoxyRecyclerView.addOnScrollListener(
object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
createChatFabMenu.removeCallbacks(showFabRunnable)
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
createChatFabMenu.postDelayed(showFabRunnable, 1000)
}
RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> {
when (roomListParams.displayMode) {
DisplayMode.HOME -> createChatFabMenu.hide()
DisplayMode.PEOPLE -> createChatRoomButton.hide()
else -> createGroupRoomButton.hide()
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
createChatFabMenu.postDelayed(showFabRunnable, 1000)
}
RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> {
when (roomListParams.displayMode) {
DisplayMode.HOME -> createChatFabMenu.hide()
DisplayMode.PEOPLE -> createChatRoomButton.hide()
else -> createGroupRoomButton.hide()
}
}
}
}
}
}
})
})
}
@ -230,7 +227,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O
return super.onBackPressed()
}
// RoomSummaryController.Callback **************************************************************
// RoomSummaryController.Callback **************************************************************
override fun onRoomSelected(room: RoomSummary) {
roomListViewModel.accept(RoomListActions.SelectRoom(room))

View File

@ -1,70 +0,0 @@
/*
* 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.home.room.list
import im.vector.matrix.android.api.session.room.model.RoomSummary
class RoomSummaryComparator
: Comparator<RoomSummary> {
override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int {
val retValue: Int
var leftHighlightCount = 0
var rightHighlightCount = 0
var leftNotificationCount = 0
var rightNotificationCount = 0
var rightTimestamp = 0L
var leftTimestamp = 0L
if (null != leftRoomSummary) {
leftHighlightCount = leftRoomSummary.highlightCount
leftNotificationCount = leftRoomSummary.notificationCount
leftTimestamp = leftRoomSummary.lastMessage?.originServerTs ?: 0
}
if (null != rightRoomSummary) {
rightHighlightCount = rightRoomSummary.highlightCount
rightNotificationCount = rightRoomSummary.notificationCount
rightTimestamp = rightRoomSummary.lastMessage?.originServerTs ?: 0
}
if (rightRoomSummary?.lastMessage == null) {
retValue = -1
} else if (leftRoomSummary?.lastMessage == null) {
retValue = 1
} else if (rightHighlightCount > 0 && leftHighlightCount == 0) {
retValue = 1
} else if (rightHighlightCount == 0 && leftHighlightCount > 0) {
retValue = -1
} else if (rightNotificationCount > 0 && leftNotificationCount == 0) {
retValue = 1
} else if (rightNotificationCount == 0 && leftNotificationCount > 0) {
retValue = -1
} else {
val deltaTimestamp = rightTimestamp - leftTimestamp
if (deltaTimestamp > 0) {
retValue = 1
} else if (deltaTimestamp < 0) {
retValue = -1
} else {
retValue = 0
}
}
return retValue
}
}

View File

@ -18,56 +18,31 @@ package im.vector.riotredesign.features.home.room.list.widget
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.isVisible
import androidx.transition.ChangeTransform
import androidx.transition.Transition
import androidx.transition.TransitionManager
import com.google.android.material.floatingactionbutton.FloatingActionButton
import im.vector.riotredesign.R
import im.vector.riotredesign.core.animations.ANIMATION_DURATION_SHORT
import im.vector.riotredesign.core.animations.SimpleTransitionListener
import im.vector.riotredesign.core.animations.VectorFullTransitionSet
import im.vector.riotredesign.features.themes.ThemeUtils
import kotlinx.android.synthetic.main.merge_fab_menu_view.view.*
import kotlinx.android.synthetic.main.motion_fab_menu_merge.view.*
class FabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
defStyleAttr: Int = 0) : MotionLayout(context, attrs, defStyleAttr) {
var listener: Listener? = null
private var isFabMenuOpened = false
init {
inflate(context, R.layout.merge_fab_menu_view, this)
inflate(context, R.layout.motion_fab_menu_merge, this)
}
override fun onFinishInflate() {
super.onFinishInflate()
// Collapse
ConstraintSet().also {
it.clone(context, R.layout.constraint_set_fab_menu_close)
it.applyTo(this)
}
createRoomItemChat.isVisible = false
createRoomItemChatLabel.isVisible = false
createRoomItemGroup.isVisible = false
createRoomItemGroupLabel.isVisible = false
// Collapse end
// Tint label background
listOf(createRoomItemChatLabel, createRoomItemGroupLabel)
.forEach {
it.setBackgroundResource(ThemeUtils.getResourceId(context, R.drawable.vector_label_background_light))
}
createRoomButton.setOnClickListener {
toggleFabMenu()
}
listOf(createRoomItemChat, createRoomItemChatLabel)
.forEach {
it.setOnClickListener {
@ -89,68 +64,25 @@ class FabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSe
}
fun show() {
isVisible = true
createRoomButton.show()
}
fun hide() {
createRoomButton.hide()
}
private fun openFabMenu() {
if (isFabMenuOpened) {
return
}
toggleFabMenu()
createRoomButton.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton?) {
super.onHidden(fab)
isVisible = false
}
})
}
private fun closeFabMenu() {
if (!isFabMenuOpened) {
return
}
toggleFabMenu()
}
private fun toggleFabMenu() {
isFabMenuOpened = !isFabMenuOpened
TransitionManager.beginDelayedTransition(parent as? ViewGroup ?: this,
VectorFullTransitionSet().apply {
duration = ANIMATION_DURATION_SHORT
ChangeTransform()
addListener(object : SimpleTransitionListener() {
override fun onTransitionEnd(transition: Transition) {
// Hide the view after the transition for a better visual effect
createRoomItemChat.isVisible = isFabMenuOpened
createRoomItemChatLabel.isVisible = isFabMenuOpened
createRoomItemGroup.isVisible = isFabMenuOpened
createRoomItemGroupLabel.isVisible = isFabMenuOpened
}
})
})
if (isFabMenuOpened) {
// Animate manually the rotation for a better effect
createRoomButton.animate().setDuration(ANIMATION_DURATION_SHORT).rotation(135f)
ConstraintSet().also {
it.clone(context, R.layout.constraint_set_fab_menu_open)
it.applyTo(this)
}
} else {
createRoomButton.animate().setDuration(ANIMATION_DURATION_SHORT).rotation(0f)
ConstraintSet().also {
it.clone(context, R.layout.constraint_set_fab_menu_close)
it.applyTo(this)
}
}
transitionToStart()
}
fun onBackPressed(): Boolean {
if (isFabMenuOpened) {
if (currentState == R.id.constraint_set_fab_menu_open) {
closeFabMenu()
return true
}

View File

@ -23,19 +23,18 @@ import android.view.View
import android.widget.Toast
import arrow.core.Try
import com.jakewharton.rxbinding2.widget.RxTextView
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.riotredesign.R
import im.vector.riotredesign.core.di.ActiveSessionHolder
import im.vector.riotredesign.core.di.ScreenComponent
import im.vector.riotredesign.core.extensions.openAndStartSync
import im.vector.riotredesign.core.extensions.configureAndStart
import im.vector.riotredesign.core.extensions.showPassword
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.features.home.HomeActivity
import im.vector.riotredesign.features.notifications.PushRuleTriggerListener
import io.reactivex.Observable
import io.reactivex.functions.Function3
import io.reactivex.rxkotlin.subscribeBy
@ -50,6 +49,7 @@ class LoginActivity : VectorBaseActivity() {
@Inject lateinit var authenticator: Authenticator
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
private var passwordShown = false
@ -82,7 +82,7 @@ class LoginActivity : VectorBaseActivity() {
authenticator.authenticate(homeServerConnectionConfig, login, password, object : MatrixCallback<Session> {
override fun onSuccess(data: Session) {
activeSessionHolder.setActiveSession(data)
data.openAndStartSync()
data.configureAndStart(pushRuleTriggerListener)
goToHome()
}

View File

@ -19,6 +19,9 @@ package im.vector.riotredesign.features.navigation
import android.content.Context
import android.content.Intent
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.core.utils.toast
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import im.vector.riotredesign.features.debug.DebugMenuActivity
@ -27,18 +30,27 @@ import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotredesign.features.roomdirectory.roompreview.RoomPreviewActivity
import im.vector.riotredesign.features.settings.VectorSettingsActivity
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DefaultNavigator @Inject constructor() : Navigator {
override fun openRoom(roomId: String, context: Context) {
val args = RoomDetailArgs(roomId)
override fun openRoom(context: Context, roomId: String, eventId: String?) {
val args = RoomDetailArgs(roomId, eventId)
val intent = RoomDetailActivity.newIntent(context, args)
context.startActivity(intent)
}
override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String?) {
if (context is VectorBaseActivity) {
context.notImplemented("Open not joined room")
} else {
context.toast(R.string.not_implemented)
}
}
override fun openRoomPreview(publicRoom: PublicRoom, context: Context) {
val intent = RoomPreviewActivity.getIntent(context, publicRoom)
context.startActivity(intent)
@ -65,4 +77,12 @@ class DefaultNavigator @Inject constructor() : Navigator {
override fun openKeysBackupManager(context: Context) {
context.startActivity(KeysBackupManageActivity.intent(context))
}
override fun openGroupDetail(groupId: String, context: Context) {
Timber.v("Open group detail $groupId")
}
override fun openUserDetail(userId: String, context: Context) {
Timber.v("Open user detail $userId")
}
}

View File

@ -21,7 +21,9 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
interface Navigator {
fun openRoom(roomId: String, context: Context)
fun openRoom(context: Context, roomId: String, eventId: String? = null)
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String? = null)
fun openRoomPreview(publicRoom: PublicRoom, context: Context)
@ -34,4 +36,8 @@ interface Navigator {
fun openKeysBackupSetup(context: Context, showManualExport: Boolean)
fun openKeysBackupManager(context: Context)
fun openGroupDetail(groupId: String, context: Context)
fun openUserDetail(userId: String, context: Context)
}

View File

@ -0,0 +1,124 @@
/*
* 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.graphics.Bitmap
import android.os.Handler
import android.os.HandlerThread
import androidx.annotation.WorkerThread
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import timber.log.Timber
/**
* FIXME It works, but it does not refresh the notification, when it's already displayed
*/
class BitmapLoader(val context: Context,
val listener: BitmapLoaderListener) {
/**
* Avatar Url -> Icon
*/
private val cache = HashMap<String, Bitmap>()
// 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("BitmapLoader", Thread.MIN_PRIORITY)
private var backgroundHandler: Handler
init {
handlerThread.start()
backgroundHandler = Handler(handlerThread.looper)
}
/**
* Get icon of a room.
* If already in cache, use it, else load it and call BitmapLoaderListener.onBitmapsLoaded() when ready
*/
fun getRoomBitmap(path: String?): Bitmap? {
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 {
loadRoomBitmap(path)
}
}
}
}
return null
}
@WorkerThread
private fun loadRoomBitmap(path: String) {
val bitmap = path.let {
try {
Glide.with(context)
.asBitmap()
.load(path)
.format(DecodeFormat.PREFER_ARGB_8888)
.submit()
.get()
} catch (e: Exception) {
Timber.e(e, "decodeFile failed")
null
}
}
synchronized(cache) {
if (bitmap == null) {
// Add to the blacklist
blacklist.add(path)
} else {
cache[path] = bitmap
}
toLoad.remove(path)
if (toLoad.isEmpty()) {
uiHandler.post {
listener.onBitmapsLoaded()
}
}
}
}
interface BitmapLoaderListener {
fun onBitmapsLoaded()
}
}

View File

@ -15,16 +15,21 @@
*/
package im.vector.riotredesign.features.notifications
import android.content.Context
import androidx.core.app.NotificationCompat
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotredesign.core.preference.BingRule
// TODO Remove
class RoomState {
}
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import timber.log.Timber
/**
* The notifiable event resolver is able to create a NotifiableEvent (view model for notifications) from an sdk Event.
@ -32,146 +37,137 @@ class RoomState {
* 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) {
class NotifiableEventResolver(private val stringProvider: StringProvider,
private val noticeEventFormatter: NoticeEventFormatter) {
//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
}
fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session): NotifiableEvent? {
val roomID = event.roomId ?: return null
val eventId = event.eventId ?: return null
val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null
when (event.getClearType()) {
EventType.MESSAGE -> {
return resolveMessageEvent(event, bingRule, session, store)
EventType.MESSAGE -> {
return resolveMessageEvent(timelineEvent, session)
}
EventType.ENCRYPTED -> {
val messageEvent = resolveMessageEvent(event, bingRule, session, store)
EventType.ENCRYPTED -> {
val messageEvent = resolveMessageEvent(timelineEvent, session)
messageEvent?.lockScreenVisibility = NotificationCompat.VISIBILITY_PRIVATE
return messageEvent
}
EventType.STATE_ROOM_MEMBER -> {
return resolveStateRoomEvent(event, bingRule, session, store)
return resolveStateRoomEvent(event, session)
}
else -> {
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.getClearType())
}
//TODO Better event text display
val bodyPreview = event.type
return SimpleNotifiableEvent(
session.sessionParams.credentials.userId,
eventId = event.eventId!!,
noisy = false,//will be updated
timestamp = event.originServerTs ?: System.currentTimeMillis(),
description = bodyPreview,
title = stringProvider.getString(R.string.notification_unknown_new_event),
soundName = null,
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
private fun resolveMessageEvent(event: TimelineEvent, session: Session): NotifiableEvent? {
//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)*/)
val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/)
if (room == null) {
Timber.e("## Unable to resolve room for eventId [${event.eventId}] and roomID [${event.roomId}]")
Timber.e("## Unable to resolve room for eventId [${event}]")
// 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 body =
event.annotations?.editSummary?.aggregatedContent?.toModel<MessageContent>()?.body
?: event.root.getClearContent().toModel<MessageContent>()?.body
?: stringProvider.getString(R.string.notification_unknown_new_event)
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
val senderDisplayName = event.senderName
val notifiableEvent = NotifiableMessageEvent(
eventId = event.eventId,
timestamp = event.originServerTs,
noisy = noisy,
eventId = event.root.eventId!!,
timestamp = event.root.originServerTs ?: 0,
noisy = false,//will be updated
senderName = senderDisplayName,
senderId = event.sender,
senderId = event.root.senderId,
body = body,
roomId = event.roomId,
roomId = event.root.roomId!!,
roomName = roomName)
notifiableEvent.matrixID = session.myUserId
notifiableEvent.soundName = soundName
notifiableEvent.matrixID = session.sessionParams.credentials.userId
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 body = event.annotations?.editSummary?.aggregatedContent?.toModel<MessageContent>()?.body
?: event.root.getClearContent().toModel<MessageContent>()?.body
?: stringProvider.getString(R.string.notification_unknown_new_event)
val roomName = room.roomSummary?.displayName ?: ""
val senderDisplayName = event.senderName ?: ""
val notifiableEvent = NotifiableMessageEvent(
eventId = event.eventId,
timestamp = event.originServerTs,
noisy = noisy,
eventId = event.root.eventId!!,
timestamp = event.root.originServerTs ?: 0,
noisy = false,//will be updated
senderName = senderDisplayName,
senderId = event.sender,
senderId = event.root.senderId,
body = body,
roomId = event.roomId,
roomId = event.root.roomId!!,
roomName = roomName,
roomIsDirect = room.isDirect)
roomIsDirect = room.roomSummary?.isDirect ?: false)
notifiableEvent.matrixID = session.myUserId
notifiableEvent.soundName = soundName
notifiableEvent.matrixID = session.sessionParams.credentials.userId
notifiableEvent.soundName = null
// Get the avatars URL
// TODO They will be not displayed the first time (known limitation)
notifiableEvent.roomAvatarPath = session.contentUrlResolver()
.resolveThumbnail(room.roomSummary?.avatarUrl,
250,
250,
ContentUrlResolver.ThumbnailMethod.SCALE)
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)
}
}
notifiableEvent.senderAvatarPath = session.contentUrlResolver()
.resolveThumbnail(event.senderAvatar,
250,
250,
ContentUrlResolver.ThumbnailMethod.SCALE)
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)
private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? {
val content = event.content?.toModel<RoomMember>() ?: return null
val roomId = event.roomId ?: return null
val dName = event.senderId?.let { session.getUser(it)?.displayName }
if (Membership.INVITE == content.membership) {
val body = noticeEventFormatter.format(event, dName)
?: stringProvider.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,
session.sessionParams.credentials.userId,
eventId = event.eventId!!,
roomId = roomId,
timestamp = event.originServerTs ?: 0,
noisy = false,//will be set later
title = stringProvider.getString(R.string.notification_new_invitation),
description = body.toString(),
soundName = null, //will be set later
type = event.getClearType(),
isPushGatewayEvent = false)
} else {
@ -182,6 +178,6 @@ class NotifiableEventResolver(val context: Context) {
//TODO generic handling?
}
return null
} */
}
}

View File

@ -0,0 +1,41 @@
/*
* 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 im.vector.matrix.android.api.pushrules.Action
data class NotificationAction(
val shouldNotify: Boolean,
val highlight: Boolean = false,
val soundName: String? = null
) {
companion object {
fun extractFrom(ruleActions: List<Action>): NotificationAction {
var shouldNotify = false
var highlight = false
var sound: String? = null
ruleActions.forEach {
if (it.type == Action.Type.NOTIFY) shouldNotify = true
if (it.type == Action.Type.DONT_NOTIFY) shouldNotify = false
if (it.type == Action.Type.SET_TWEAK) {
if (it.tweak_action == "highlight") highlight = it.boolValue ?: false
if (it.tweak_action == "sound") sound = it.stringValue
}
}
return NotificationAction(shouldNotify, highlight, sound)
}
}
}

View File

@ -20,9 +20,14 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.app.RemoteInput
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.Room
import im.vector.riotredesign.R
import im.vector.riotredesign.core.di.ActiveSessionHolder
import timber.log.Timber
import java.util.*
import javax.inject.Inject
/**
@ -31,22 +36,22 @@ import javax.inject.Inject
class NotificationBroadcastReceiver : BroadcastReceiver() {
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null || context == null) return
Timber.v("ReplyNotificationBroadcastReceiver received : $intent")
Timber.v("NotificationBroadcastReceiver received : $intent")
when (intent.action) {
NotificationUtils.SMART_REPLY_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 ->
NotificationUtils.DISMISS_SUMMARY_ACTION ->
notificationDrawerManager.clearAllEvents()
NotificationUtils.MARK_ROOM_READ_ACTION ->
NotificationUtils.MARK_ROOM_READ_ACTION ->
intent.getStringExtra(KEY_ROOM_ID)?.let {
notificationDrawerManager.clearMessageEventOfRoom(it)
handleMarkAsRead(context, it)
@ -55,67 +60,61 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
}
private fun handleMarkAsRead(context: Context, roomId: String) {
/*
TODO
Matrix.getInstance(context)?.defaultSession?.let { session ->
session.dataHandler
?.getRoom(roomId)
?.markAllAsRead(object : SimpleApiCallback<Unit>() {
override fun onSuccess(void: Void?) {
// Ignore
}
})
activeSessionHolder.getActiveSession().let { session ->
session.getRoom(roomId)
?.markAllAsRead(object : MatrixCallback<Unit> {})
}
*/
}
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)) {
if (message.isNullOrBlank() || roomId.isBlank()) {
//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)
activeSessionHolder.getActiveSession().let { session ->
session.getRoom(roomId)?.let { room ->
sendMatrixEvent(message, session, room, context)
}
}
*/
}
private fun sendMatrixEvent(message: String, session: Session, roomId: String, room: Room, context: Context?) {
/*
TODO
private fun sendMatrixEvent(message: String, session: Session, room: Room, context: Context?) {
val mxMessage = Message()
mxMessage.msgtype = Message.MSGTYPE_TEXT
mxMessage.body = message
room.sendTextMessage(message)
// Create a new event to be displayed in the notification drawer, right now
val notifiableMessageEvent = NotifiableMessageEvent(
// Generate a Fake event id
UUID.randomUUID().toString(),
false,
System.currentTimeMillis(),
session.getUser(session.sessionParams.credentials.userId)?.displayName
?: context?.getString(R.string.notification_sender_me),
session.sessionParams.credentials.userId,
message,
room.roomId,
room.roomSummary?.displayName ?: room.roomId,
room.roomSummary?.isDirect == true
)
notifiableMessageEvent.outGoingMessage = true
notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent)
notificationDrawerManager.refreshNotificationDrawer()
/*
// TODO Error cannot be managed the same way than in Riot
val event = Event(mxMessage, session.credentials.userId, roomId)
room.storeOutgoingEvent(event)
room.sendEvent(event, object : MatrixCallback<Void?> {
override fun onSuccess(info: Void?) {
Timber.v("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) {

View File

@ -18,14 +18,15 @@ 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.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.R
import im.vector.riotredesign.core.di.ActiveSessionHolder
import im.vector.riotredesign.core.utils.SecretStoringUtils
import im.vector.riotredesign.features.settings.PreferencesManager
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
@ -38,16 +39,15 @@ import javax.inject.Singleton
* 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.
*/
@Singleton
class NotificationDrawerManager @Inject constructor(val context: Context) {
class NotificationDrawerManager @Inject constructor(private val context: Context,
private val activeSessionHolder: ActiveSessionHolder,
private val outdatedDetector: OutdatedEventDetector?) {
//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)
@ -57,31 +57,17 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
object : IconLoader.IconLoaderListener {
override fun onIconsLoaded() {
// Force refresh
refreshNotificationDrawer(null)
refreshNotificationDrawer()
}
})
/**
* 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)
private var bitmapLoader = BitmapLoader(context,
object : BitmapLoader.BitmapLoaderListener {
override fun onBitmapsLoaded() {
// Force refresh
refreshNotificationDrawer()
}
}
*/
}
}
})
/**
Should be called as soon as a new event is ready to be displayed.
@ -90,6 +76,10 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
Events might be grouped and there might not be one notification per event!
*/
fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
if (!PreferencesManager.areNotificationEnabledForDevice(context)) {
Timber.i("Notification are disabled for this device")
return
}
//If we support multi session, event list should be per userId
//Currently only manage single session
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
@ -116,7 +106,6 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
} else {
eventList.add(notifiableEvent)
}
}
}
@ -127,7 +116,7 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
synchronized(eventList) {
eventList.clear()
}
refreshNotificationDrawer(null)
refreshNotificationDrawer()
}
/** Clear all known message events for this room and refresh the notification drawer */
@ -143,12 +132,12 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
}
NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID)
}
refreshNotificationDrawer(null)
refreshNotificationDrawer()
}
/**
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.
* 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
@ -181,17 +170,11 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
}
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
}
fun refreshNotificationDrawer() {
val session = activeSessionHolder.getActiveSession()
val user = session.getUser(session.sessionParams.credentials.userId)
val myUserDisplayName = user?.displayName ?: ""
val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(user?.avatarUrl, avatarSize, avatarSize, ContentUrlResolver.ThumbnailMethod.SCALE)
synchronized(eventList) {
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ")
@ -242,22 +225,23 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
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 roomEventGroupInfo = RoomEventGroupInfo(
roomId = roomId,
isDirect = events[0].roomIsDirect,
roomDisplayName = roomName)
val style = NotificationCompat.MessagingStyle(Person.Builder()
.setName(myUserDisplayName)
.setIcon(iconLoader.getUserIcon(myUserAvatarUrl))
.setKey(events[0].matrixID)
.build())
roomGroup.roomDisplayName = roomName
style.isGroupConversation = !roomGroup.isDirect
style.isGroupConversation = !roomEventGroupInfo.isDirect
if (!roomGroup.isDirect) {
style.conversationTitle = roomName
if (!roomEventGroupInfo.isDirect) {
style.conversationTitle = roomEventGroupInfo.roomDisplayName
}
val largeBitmap = getRoomBitmap(events)
@ -266,10 +250,10 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
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
roomEventGroupInfo.shouldBing = roomEventGroupInfo.shouldBing || event.noisy
roomEventGroupInfo.customSound = event.soundName
}
roomGroup.hasNewEvent = roomGroup.hasNewEvent || !event.hasBeenDisplayed
roomEventGroupInfo.hasNewEvent = roomEventGroupInfo.hasNewEvent || !event.hasBeenDisplayed
val senderPerson = Person.Builder()
.setName(event.senderName)
@ -279,7 +263,7 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
if (event.outGoingMessage && event.outGoingMessageFailed) {
style.addMessage(context.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson)
roomGroup.hasSmartReplyError = true
roomEventGroupInfo.hasSmartReplyError = true
} else {
style.addMessage(event.body, event.timestamp, senderPerson)
}
@ -300,7 +284,7 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
summaryInboxStyle.addLine(roomName)
}
if (firstTime || roomGroup.hasNewEvent) {
if (firstTime || roomEventGroupInfo.hasNewEvent) {
//Should update displayed notification
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId need refresh")
val lastMessageTimestamp = events.last().timestamp
@ -309,14 +293,14 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
globalLastMessageTimestamp = lastMessageTimestamp
}
NotificationUtils.buildMessagesListNotification(context, style, roomGroup, largeBitmap, lastMessageTimestamp, myUserDisplayName)
NotificationUtils.buildMessagesListNotification(context, style, roomEventGroupInfo, 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
summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing
} else {
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId is up to date")
}
@ -397,19 +381,10 @@ class NotificationDrawerManager @Inject constructor(val context: Context) {
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")
}
val roomAvatarPath = events.last().roomAvatarPath
?: events.last().senderAvatarPath
}
return null
return bitmapLoader.getRoomBitmap(roomAvatarPath)
}
private fun shouldIgnoreMessageEventInRoom(roomId: String?): Boolean {

View File

@ -39,6 +39,8 @@ 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.home.room.detail.RoomDetailActivity
import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
import im.vector.riotredesign.features.settings.PreferencesManager
import timber.log.Timber
import java.util.*
@ -180,7 +182,7 @@ object NotificationUtils {
* @return the polling thread listener notification
*/
@SuppressLint("NewApi")
fun buildForegroundServiceNotification(context: Context, @StringRes subTitleResId: Int): Notification {
fun buildForegroundServiceNotification(context: Context, @StringRes subTitleResId: Int, withProgress: Boolean = true): 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
@ -190,16 +192,21 @@ object NotificationUtils {
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)
.setSmallIcon(R.drawable.sync)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setColor(accentColor)
.setContentIntent(pi)
.apply {
if (withProgress) {
setProgress(0, 0, true)
}
}
// hide the notification from the status bar
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
builder.priority = NotificationCompat.PRIORITY_MIN
}
// PRIORITY_MIN should not be used with Service#startForeground(int, Notification)
builder.priority = NotificationCompat.PRIORITY_LOW
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// builder.priority = NotificationCompat.PRIORITY_MIN
// }
val notification = builder.build()
@ -220,7 +227,7 @@ object NotificationUtils {
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)
Timber.e(ex, "## buildNotification(): Exception - setLatestEventInfo() Msg=")
}
}
@ -540,27 +547,21 @@ object NotificationUtils {
}
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)
val roomIntentTap = RoomDetailActivity.newIntent(context, RoomDetailArgs(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))
.addNextIntentWithParentStack(Intent(context, HomeActivity::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)
val intent = HomeActivity.newIntent(context, clearNotification = true)
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)
}

View File

@ -15,9 +15,10 @@
*/
package im.vector.riotredesign.features.notifications
import android.content.Context
import im.vector.matrix.android.api.session.Session
import javax.inject.Inject
class OutdatedEventDetector(val context: Context) {
class OutdatedEventDetector @Inject constructor(private val session: Session) {
/**
* Returns true if the given event is outdated.
@ -28,20 +29,8 @@ class OutdatedEventDetector(val context: Context) {
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.v("Notifiable Event $eventID is read, and should be removed")
return true
}
}
}
}
*/
val room = session.getRoom(roomID) ?: return false
return room.isEventRead(eventID)
}
return false
}

View File

@ -0,0 +1,77 @@
/*
* 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 im.vector.matrix.android.api.pushrules.Action
import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PushRuleTriggerListener @Inject constructor(
private val resolver: NotifiableEventResolver,
private val drawerManager: NotificationDrawerManager
) : PushRuleService.PushRuleListener {
var session: Session? = null
override fun onMatchRule(event: Event, actions: List<Action>) {
Timber.v("Push rule match for event ${event.eventId}")
if (session == null) {
Timber.e("Called without active session")
return
}
val notificationAction = NotificationAction.extractFrom(actions)
if (notificationAction.shouldNotify) {
val resolveEvent = resolver.resolveEvent(event, session!!)
if (resolveEvent == null) {
Timber.v("## Failed to resolve event")
//TODO
} else {
resolveEvent.noisy = !notificationAction.soundName.isNullOrBlank()
Timber.v("New event to notify $resolveEvent tweaks:$notificationAction")
drawerManager.onNotifiableEventReceived(resolveEvent)
}
} else {
Timber.v("Matched push rule is set to not notify")
}
}
override fun batchFinish() {
drawerManager.refreshNotificationDrawer()
}
fun startWithSession(session: Session) {
if (this.session != null) {
stop()
}
this.session = session
session.addPushRuleListener(this)
}
fun stop() {
session?.removePushRuleListener(this)
session = null
drawerManager.clearAllEvents()
}
}

View File

@ -20,15 +20,14 @@ 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
val roomId: String,
val roomDisplayName: String = "",
val isDirect: Boolean = false
) {
var roomDisplayName: String = ""
var roomAvatarPath: String? = null
//An event in the list has not yet been display
// 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
// 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
}
var hasSmartReplyError: Boolean = false
}

View File

@ -19,10 +19,8 @@ package im.vector.riotredesign.features.rageshake
import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.*
import android.widget.Toast
import androidx.core.view.isVisible
import butterknife.BindView
import butterknife.OnCheckedChanged
import butterknife.OnTextChanged
import im.vector.riotredesign.R
@ -37,36 +35,6 @@ import javax.inject.Inject
*/
class BugReportActivity : VectorBaseActivity() {
/* ==========================================================================================
* UI
* ========================================================================================== */
@BindView(R.id.bug_report_edit_text)
lateinit var mBugReportText: EditText
@BindView(R.id.bug_report_button_include_logs)
lateinit var mIncludeLogsButton: CheckBox
@BindView(R.id.bug_report_button_include_crash_logs)
lateinit var mIncludeCrashLogsButton: CheckBox
@BindView(R.id.bug_report_button_include_screenshot)
lateinit var mIncludeScreenShotButton: CheckBox
@BindView(R.id.bug_report_screenshot_preview)
lateinit var mScreenShotPreview: ImageView
@BindView(R.id.bug_report_progress_view)
lateinit var mProgressBar: ProgressBar
@BindView(R.id.bug_report_progress_text_view)
lateinit var mProgressTextView: TextView
@BindView(R.id.bug_report_scrollview)
lateinit var mScrollView: View
@BindView(R.id.bug_report_mask_view)
lateinit var mMaskView: View
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
@ -78,11 +46,11 @@ class BugReportActivity : VectorBaseActivity() {
configureToolbar(bugReportToolbar)
if (bugReporter.screenshot != null) {
mScreenShotPreview.setImageBitmap(bugReporter.screenshot)
bug_report_screenshot_preview.setImageBitmap(bugReporter.screenshot)
} else {
mScreenShotPreview.isVisible = false
mIncludeScreenShotButton.isChecked = false
mIncludeScreenShotButton.isEnabled = false
bug_report_screenshot_preview.isVisible = false
bug_report_button_include_screenshot.isChecked = false
bug_report_button_include_screenshot.isEnabled = false
}
}
@ -90,8 +58,8 @@ class BugReportActivity : VectorBaseActivity() {
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
menu.findItem(R.id.ic_action_send_bug_report)?.let {
val isValid = mBugReportText.text.toString().trim().length > 10
&& !mMaskView.isVisible
val isValid = bug_report_edit_text.text.toString().trim().length > 10
&& !bug_report_mask_view.isVisible
it.isEnabled = isValid
it.icon.alpha = if (isValid) 255 else 100
@ -115,22 +83,22 @@ class BugReportActivity : VectorBaseActivity() {
* Send the bug report
*/
private fun sendBugReport() {
mScrollView.alpha = 0.3f
mMaskView.isVisible = true
bug_report_scrollview.alpha = 0.3f
bug_report_mask_view.isVisible = true
invalidateOptionsMenu()
mProgressTextView.isVisible = true
mProgressTextView.text = getString(R.string.send_bug_report_progress, 0.toString() + "")
bug_report_progress_text_view.isVisible = true
bug_report_progress_text_view.text = getString(R.string.send_bug_report_progress, "0")
mProgressBar.isVisible = true
mProgressBar.progress = 0
bug_report_progress_view.isVisible = true
bug_report_progress_view.progress = 0
bugReporter.sendBugReport(this,
mIncludeLogsButton.isChecked,
mIncludeCrashLogsButton.isChecked,
mIncludeScreenShotButton.isChecked,
mBugReportText.text.toString(),
bug_report_button_include_logs.isChecked,
bug_report_button_include_crash_logs.isChecked,
bug_report_button_include_screenshot.isChecked,
bug_report_edit_text.text.toString(),
object : BugReporter.IMXBugReportListener {
override fun onUploadFailed(reason: String?) {
try {
@ -142,10 +110,10 @@ class BugReportActivity : VectorBaseActivity() {
Timber.e(e, "## onUploadFailed() : failed to display the toast " + e.message)
}
mMaskView.isVisible = false
mProgressBar.isVisible = false
mProgressTextView.isVisible = false
mScrollView.alpha = 1.0f
bug_report_mask_view.isVisible = false
bug_report_progress_view.isVisible = false
bug_report_progress_text_view.isVisible = false
bug_report_scrollview.alpha = 1.0f
invalidateOptionsMenu()
}
@ -155,17 +123,10 @@ class BugReportActivity : VectorBaseActivity() {
}
override fun onProgress(progress: Int) {
var progress = progress
if (progress > 100) {
Timber.e("## onProgress() : progress > 100")
progress = 100
} else if (progress < 0) {
Timber.e("## onProgress() : progress < 0")
progress = 0
}
val myProgress = progress.coerceIn(0, 100)
mProgressBar.progress = progress
mProgressTextView.text = getString(R.string.send_bug_report_progress, progress.toString() + "")
bug_report_progress_view.progress = myProgress
bug_report_progress_text_view.text = getString(R.string.send_bug_report_progress, "$myProgress")
}
override fun onUploadSucceed() {
@ -180,7 +141,6 @@ class BugReportActivity : VectorBaseActivity() {
} catch (e: Exception) {
Timber.e(e, "## onUploadSucceed() : failed to dismiss the dialog " + e.message)
}
}
})
}
@ -196,7 +156,7 @@ class BugReportActivity : VectorBaseActivity() {
@OnCheckedChanged(R.id.bug_report_button_include_screenshot)
internal fun onSendScreenshotChanged() {
mScreenShotPreview.isVisible = mIncludeScreenShotButton.isChecked && bugReporter.screenshot != null
bug_report_screenshot_preview.isVisible = bug_report_button_include_screenshot.isChecked && bugReporter.screenshot != null
}
override fun onBackPressed() {
@ -205,12 +165,4 @@ class BugReportActivity : VectorBaseActivity() {
super.onBackPressed()
}
/* ==========================================================================================
* Companion
* ========================================================================================== */
companion object {
private val LOG_TAG = BugReportActivity::class.java.simpleName
}
}

View File

@ -35,6 +35,7 @@ import im.vector.riotredesign.core.extensions.toOnOff
import im.vector.riotredesign.core.utils.getDeviceLocale
import im.vector.riotredesign.features.settings.VectorLocale
import im.vector.riotredesign.features.themes.ThemeUtils
import im.vector.riotredesign.features.version.getVersion
import okhttp3.*
import org.json.JSONException
import org.json.JSONObject
@ -198,13 +199,11 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes
var deviceId = "undefined"
var userId = "undefined"
var matrixSdkVersion = "undefined"
var olmVersion = "undefined"
activeSessionHolder.getActiveSession().let { session ->
userId = session.sessionParams.credentials.userId
deviceId = session.sessionParams.credentials.deviceId ?: "undefined"
// TODO matrixSdkVersion = session.getVersion(true);
olmVersion = session.getCryptoVersion(context, true)
}
@ -216,9 +215,9 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes
.addFormDataPart("user_agent", Matrix.getInstance(context).getUserAgent())
.addFormDataPart("user_id", userId)
.addFormDataPart("device_id", deviceId)
// TODO .addFormDataPart("version", Matrix.getInstance(context).getVersion(true, false))
.addFormDataPart("version", getVersion(longFormat = true, useBuildNumber = false))
.addFormDataPart("branch_name", context.getString(R.string.git_branch_name))
.addFormDataPart("matrix_sdk_version", matrixSdkVersion)
.addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion())
.addFormDataPart("olm_version", olmVersion)
.addFormDataPart("device", Build.MODEL.trim())
.addFormDataPart("lazy_loading", true.toOnOff())

View File

@ -20,7 +20,9 @@ import android.content.Context
import android.os.Build
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.features.version.getVersion
import timber.log.Timber
import java.io.PrintWriter
import java.io.StringWriter
@ -35,8 +37,8 @@ class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter
private const val PREFS_CRASH_KEY = "PREFS_CRASH_KEY"
}
private var vectorVersion: String = ""
private var matrixSdkVersion: String = ""
private val vectorVersion = getVersion(longFormat = true, useBuildNumber = true)
private val matrixSdkVersion = Matrix.getSdkVersion()
private var previousHandler: Thread.UncaughtExceptionHandler? = null
@ -116,12 +118,6 @@ class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter
previousHandler?.uncaughtException(thread, throwable)
}
// TODO Call me
fun setVersions(vectorVersion: String, matrixSdkVersion: String) {
this.vectorVersion = vectorVersion
this.matrixSdkVersion = matrixSdkVersion
}
/**
* Tells if the application crashed
*

View File

@ -123,7 +123,7 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
when (joinState) {
JoinState.JOINED -> {
navigator.openRoom(publicRoom.roomId, requireActivity())
navigator.openRoom(requireActivity(), publicRoom.roomId)
}
JoinState.NOT_JOINED,
JoinState.JOINING_ERROR -> {

View File

@ -98,7 +98,7 @@ class CreateRoomFragment : VectorBaseFragment(), CreateRoomController.Listener {
val async = state.asyncCreateRoomRequest
if (async is Success) {
// Navigate to freshly created room
navigator.openRoom(async(), requireActivity())
navigator.openRoom(requireActivity(), async())
navigationViewModel.goTo(RoomDirectoryActivity.Navigation.Close)
} else {

View File

@ -110,7 +110,7 @@ class RoomPreviewNoPreviewFragment : VectorBaseFragment() {
// Quit this screen
requireActivity().finish()
// Open room
navigator.openRoom(roomPreviewData.roomId, requireActivity())
navigator.openRoom(requireActivity(), roomPreviewData.roomId)
}
}
}

View File

@ -40,21 +40,23 @@ 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_SDK_VERSION_PREFERENCE_KEY = "SETTINGS_SDK_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";
//TODO delete
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_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY = "SETTINGS_OTHER_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";
@ -241,6 +243,10 @@ public class PreferencesManager {
editor.apply();
}
public static boolean areNotificationEnabledForDevice(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY, true);
}
/**
* Tells if we have already asked the user to disable battery optimisations on android >= M devices.
*

View File

@ -26,6 +26,7 @@ import im.vector.riotredesign.R
import im.vector.riotredesign.core.di.ScreenComponent
import im.vector.riotredesign.core.platform.VectorBaseActivity
import kotlinx.android.synthetic.main.activity_vector_settings.*
import timber.log.Timber
import javax.inject.Inject
/**
@ -36,7 +37,6 @@ class VectorSettingsActivity : VectorBaseActivity(),
FragmentManager.OnBackStackChangedListener,
VectorSettingsFragmentInteractionListener {
private lateinit var vectorSettingsPreferencesFragment: VectorSettingsPreferencesFragment
override fun getLayoutRes() = R.layout.activity_vector_settings
@ -53,14 +53,15 @@ class VectorSettingsActivity : VectorBaseActivity(),
override fun initUiAndData() {
configureToolbar(settingsToolbar)
var vectorSettingsPreferencesFragment: Fragment? = null
if (isFirstCreation()) {
vectorSettingsPreferencesFragment = VectorSettingsPreferencesFragment.newInstance(session.sessionParams.credentials.userId)
vectorSettingsPreferencesFragment = VectorSettingsPreferencesFragmentV2.newInstance()
// display the fragment
supportFragmentManager.beginTransaction()
.replace(R.id.vector_settings_page, vectorSettingsPreferencesFragment, FRAGMENT_TAG)
.commit()
} else {
vectorSettingsPreferencesFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as VectorSettingsPreferencesFragment
vectorSettingsPreferencesFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG)
}
@ -82,19 +83,33 @@ class VectorSettingsActivity : VectorBaseActivity(),
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat?, pref: Preference?): Boolean {
var oFragment: Fragment? = null
if (PreferencesManager.SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY == pref?.key) {
if ("Legacy" == pref?.title) {
oFragment = VectorSettingsPreferencesFragment.newInstance(session.sessionParams.credentials.userId)
} else 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)
} else {
try {
pref?.fragment?.let {
oFragment = supportFragmentManager.fragmentFactory
.instantiate(
classLoader,
it, pref.extras)
}
} catch (e: Throwable) {
showSnackbar(getString(R.string.not_implemented))
Timber.e(e)
}
}
if (oFragment != null) {
oFragment.setTargetFragment(caller, 0)
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())
.setCustomAnimations(R.anim.right_in, R.anim.fade_out,
R.anim.fade_in, R.anim.right_out)
.replace(R.id.vector_settings_page, oFragment!!, pref?.title.toString())
.addToBackStack(null)
.commit()
return true

Some files were not shown because too many files have changed in this diff Show More