Merge branch 'develop' into feature/ons/add_members_fab

This commit is contained in:
Benoit Marty 2020-10-12 18:15:59 +02:00 committed by GitHub
commit dff7f24187
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1006 additions and 180 deletions

View File

@ -29,6 +29,7 @@
<w>signout</w>
<w>signup</w>
<w>ssss</w>
<w>sygnal</w>
<w>threepid</w>
<w>unwedging</w>
</words>

View File

@ -11,17 +11,23 @@ Improvements 🙌:
- Small optimisation of scrolling experience in timeline (#2114)
- Allow user to reset cross signing if he has no way to recover (#2052)
- Create home shortcut for any room (#1525)
- Can't confirm email due to killing by Android (#2021)
- Add a menu item to open the setting in room list and in room (#2171)
- Add a menu item in the timeline as a shortcut to invite user (#2171)
- Drawer: move settings access and add sign out action (#2171)
- Filter room member (and banned users) by name (#2184)
- Implement "Jump to read receipt" and "Mention" actions on the room member profile screen
- Add FAB to room members list (#2226)
- Add Sygnal API implementation to test is Push are correctly received
- Add PushGateway API implementation to test if Push are correctly received
- Cross signing: shouldn't offer to verify with other session when there is not. (#2227)
Bugfix 🐛:
- Improve support for image/audio/video/file selection with intent changes (#1376)
- Fix Splash layout on small screens
- Invalid popup when pressing back (#1635)
- Simplifies draft management and should fix bunch of draft issues (#952, #683)
- Very long topic cannot be fully visible (#1957)
Translations 🗣:
-

View File

@ -0,0 +1,23 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.pushers
import org.matrix.android.sdk.api.failure.Failure
sealed class PushGatewayFailure : Failure.FeatureFailure() {
object PusherRejected : PushGatewayFailure()
}

View File

@ -66,6 +66,21 @@ interface PushersService {
append: Boolean,
withEventIdOnly: Boolean): UUID
/**
* Directly ask the push gateway to send a push to this device
* @param url the push gateway url (full path)
* @param appId the application id
* @param pushkey the FCM token
* @param eventId the eventId which will be sent in the Push message. Use a fake eventId.
* @param callback callback to know if the push gateway has accepted the request. In this case, the app should receive a Push with the provided eventId.
* In case of error, PusherRejected failure can happen. In this case it means that the pushkey is not valid.
*/
fun testPush(url: String,
appId: String,
pushkey: String,
eventId: String,
callback: MatrixCallback<Unit>): Cancelable
/**
* Remove the http pusher
*/

View File

@ -50,7 +50,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
@ -953,7 +953,7 @@ internal class DefaultCryptoService @Inject constructor(
roomEncryptorsStore.get(roomId) ?: /* No encrypting in this room */ return
event.stateKey?.let { userId ->
val roomMember: RoomMemberSummary? = event.content.toModel()
val roomMember: RoomMemberContent? = event.content.toModel()
val membership = roomMember?.membership
if (membership == Membership.JOIN) {
// make sure we are tracking the deviceList for this user.

View File

@ -16,6 +16,9 @@
package org.matrix.android.sdk.internal.crypto.model.rest
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class SendToDeviceBody(
/**
* `Any` should implement [SendToDeviceObject], but we cannot use interface here because of Json serialization

View File

@ -17,7 +17,7 @@
package org.matrix.android.sdk.internal.network
internal object NetworkConstants {
// Homeserver
private const val URI_API_PREFIX_PATH = "_matrix/client"
const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
@ -31,5 +31,9 @@ internal object NetworkConstants {
const val URI_IDENTITY_PREFIX_PATH = "_matrix/identity/v2"
const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/"
// Push Gateway
const val URI_PUSH_GATEWAY_PREFIX_PATH = "_matrix/push/v1/"
// Integration
const val URI_INTEGRATION_MANAGER_PATH = "_matrix/integrations/v1/"
}

View File

@ -113,8 +113,4 @@ constructor(trustPinned: Array<TrustManager>, acceptedTlsVersions: List<TlsVersi
}
return socket
}
companion object {
private val LOG_TAG = TLSSocketFactory::class.java.simpleName
}
}

View File

@ -28,6 +28,7 @@ import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.pushers.gateway.PushGatewayNotifyTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
@ -41,10 +42,23 @@ internal class DefaultPushersService @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
@SessionId private val sessionId: String,
private val getPusherTask: GetPushersTask,
private val pushGatewayNotifyTask: PushGatewayNotifyTask,
private val removePusherTask: RemovePusherTask,
private val taskExecutor: TaskExecutor
) : PushersService {
override fun testPush(url: String,
appId: String,
pushkey: String,
eventId: String,
callback: MatrixCallback<Unit>): Cancelable {
return pushGatewayNotifyTask
.configureWith(PushGatewayNotifyTask.Params(url, appId, pushkey, eventId)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun refreshPushers() {
getPusherTask
.configureWith()

View File

@ -25,6 +25,8 @@ import org.matrix.android.sdk.api.session.pushers.PushersService
import org.matrix.android.sdk.internal.session.notification.DefaultProcessEventForPushTask
import org.matrix.android.sdk.internal.session.notification.DefaultPushRuleService
import org.matrix.android.sdk.internal.session.notification.ProcessEventForPushTask
import org.matrix.android.sdk.internal.session.pushers.gateway.DefaultPushGatewayNotifyTask
import org.matrix.android.sdk.internal.session.pushers.gateway.PushGatewayNotifyTask
import org.matrix.android.sdk.internal.session.room.notification.DefaultSetRoomNotificationStateTask
import org.matrix.android.sdk.internal.session.room.notification.SetRoomNotificationStateTask
import retrofit2.Retrofit
@ -86,4 +88,7 @@ internal abstract class PushersModule {
@Binds
abstract fun bindProcessEventForPushTask(task: DefaultProcessEventForPushTask): ProcessEventForPushTask
@Binds
abstract fun bindPushGatewayNotifyTask(task: DefaultPushGatewayNotifyTask): PushGatewayNotifyTask
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.pushers.gateway
import org.matrix.android.sdk.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
internal interface PushGatewayAPI {
/**
* Ask the Push Gateway to send a push to the current device.
*
* Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#post-matrix-push-v1-notify
*/
@POST(NetworkConstants.URI_PUSH_GATEWAY_PREFIX_PATH + "notify")
fun notify(@Body body: PushGatewayNotifyBody): Call<PushGatewayNotifyResponse>
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.pushers.gateway
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class PushGatewayDevice(
/**
* Required. The app_id given when the pusher was created.
*/
@Json(name = "app_id")
val appId: String,
/**
* Required. The pushkey given when the pusher was created.
*/
@Json(name = "pushkey")
val pushKey: String
)

View File

@ -0,0 +1,32 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.pushers.gateway
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class PushGatewayNotification(
@Json(name = "event_id")
val eventId: String,
/**
* Required. This is an array of devices that the notification should be sent to.
*/
@Json(name = "devices")
val devices: List<PushGatewayDevice>
)

View File

@ -0,0 +1,29 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.pushers.gateway
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class PushGatewayNotifyBody(
/**
* Required. Information about the push notification
*/
@Json(name = "notification")
val notification: PushGatewayNotification
)

View File

@ -0,0 +1,26 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.pushers.gateway
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class PushGatewayNotifyResponse(
@Json(name = "rejected")
val rejectedPushKeys: List<String>
)

View File

@ -0,0 +1,68 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.pushers.gateway
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.session.pushers.PushGatewayFailure
import org.matrix.android.sdk.internal.di.Unauthenticated
import org.matrix.android.sdk.internal.network.NetworkConstants
import org.matrix.android.sdk.internal.network.RetrofitFactory
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface PushGatewayNotifyTask : Task<PushGatewayNotifyTask.Params, Unit> {
data class Params(
val url: String,
val appId: String,
val pushKey: String,
val eventId: String
)
}
internal class DefaultPushGatewayNotifyTask @Inject constructor(
private val retrofitFactory: RetrofitFactory,
@Unauthenticated private val unauthenticatedOkHttpClient: OkHttpClient
) : PushGatewayNotifyTask {
override suspend fun execute(params: PushGatewayNotifyTask.Params) {
val sygnalApi = retrofitFactory.create(
unauthenticatedOkHttpClient,
params.url.substringBefore(NetworkConstants.URI_PUSH_GATEWAY_PREFIX_PATH)
)
.create(PushGatewayAPI::class.java)
val response = executeRequest<PushGatewayNotifyResponse>(null) {
apiCall = sygnalApi.notify(
PushGatewayNotifyBody(
PushGatewayNotification(
eventId = params.eventId,
devices = listOf(
PushGatewayDevice(
params.appId,
params.pushKey
)
)
)
)
)
}
if (response.rejectedPushKeys.contains(params.pushKey)) {
throw PushGatewayFailure.PusherRejected
}
}
}

46
tools/tests/test_push.sh Executable file
View File

@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Some doc
# https://firebase.google.com/docs/cloud-messaging/android/first-message
# http://bulkpush.com/pushnotification/guidedetail/s-4/android-gcm-api-configuration
# http://www.feelzdroid.com/2016/02/android-google-cloud-messaging-push-notifications-gcm-tutorial.html
# Ask for parameter
read -p "Enter the server API key: " SERVER_KEY
echo
echo "Check validity of API key, InvalidRegistration error is OK"
# https://developers.google.com/cloud-messaging/http
curl -H "Authorization: key=$SERVER_KEY" \
-H Content-Type:"application/json" \
-d "{\"registration_ids\":[\"ABC\"]}" \
-s \
https://fcm.googleapis.com/fcm/send \
| python -m json.tool
# should obtain something like this:
# {"multicast_id":5978845027639121780,"success":0,"failure":1,"canonical_ids":0,"results":[{"error":"InvalidRegistration"}]}
read -p "Enter the FCM token: " FCM_TOKEN
# content of the notification
DATA='{"event_id":"$THIS_IS_A_FAKE_EVENT_ID"}'
echo
echo
echo "Send a push, you should see success:1..."
curl -H "Authorization: key=$SERVER_KEY" \
-H Content-Type:"application/json" \
-d "{ \"data\" : $DATA, \"to\":\"$FCM_TOKEN\" }" \
-s \
https://fcm.googleapis.com/fcm/send \
| python -m json.tool
echo
echo
# should obtain something like this:
# {"multicast_id":7967233883611790812,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"0:1472636210339069%84ac25d9f9fd7ecd"}]}

53
tools/tests/test_push_unsafe.sh Executable file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Copy and adaptation of ./test_push.sh, which takes 2 params: server key and fcm token.
# It's unsafe to use it because it takes server key as parameter, that will remain in the terminal history.
# Some doc
# https://firebase.google.com/docs/cloud-messaging/android/first-message
# http://bulkpush.com/pushnotification/guidedetail/s-4/android-gcm-api-configuration
# http://www.feelzdroid.com/2016/02/android-google-cloud-messaging-push-notifications-gcm-tutorial.html
if [[ "$#" -ne 2 ]]; then
echo "Usage: $0 SERVER_KEY FCM_TOKEN" >&2
exit 1
fi
# Get the command line parameters
SERVER_KEY=$1
FCM_TOKEN=$2
echo
echo "Check validity of API key, InvalidRegistration error is OK"
# https://developers.google.com/cloud-messaging/http
curl -H "Authorization: key=$SERVER_KEY" \
-H Content-Type:"application/json" \
-d "{\"registration_ids\":[\"ABC\"]}" \
-s \
https://fcm.googleapis.com/fcm/send \
| python -m json.tool
# should obtain something like this:
# {"multicast_id":5978845027639121780,"success":0,"failure":1,"canonical_ids":0,"results":[{"error":"InvalidRegistration"}]}
# content of the notification
DATA='{"event_id":"$THIS_IS_A_FAKE_EVENT_ID"}'
echo
echo
echo "Send a push, you should see success:1..."
curl -H "Authorization: key=$SERVER_KEY" \
-H Content-Type:"application/json" \
-d "{ \"data\" : $DATA, \"to\":\"$FCM_TOKEN\" }" \
-s \
https://fcm.googleapis.com/fcm/send \
| python -m json.tool
echo
echo
# should obtain something like this:
# {"multicast_id":7967233883611790812,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"0:1472636210339069%84ac25d9f9fd7ecd"}]}

View File

@ -22,17 +22,21 @@ import im.vector.app.fdroid.features.settings.troubleshoot.TestBatteryOptimizati
import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.app.features.settings.troubleshoot.TestAccountSettings
import im.vector.app.features.settings.troubleshoot.TestDeviceSettings
import im.vector.app.features.settings.troubleshoot.TestNotification
import im.vector.app.features.settings.troubleshoot.TestPushRulesSettings
import im.vector.app.features.settings.troubleshoot.TestSystemSettings
import javax.inject.Inject
class NotificationTroubleshootTestManagerFactory @Inject constructor(private val testSystemSettings: TestSystemSettings,
private val testAccountSettings: TestAccountSettings,
private val testDeviceSettings: TestDeviceSettings,
private val testPushRulesSettings: TestPushRulesSettings,
private val testAutoStartBoot: TestAutoStartBoot,
private val testBackgroundRestrictions: TestBackgroundRestrictions,
private val testBatteryOptimization: TestBatteryOptimization) {
class NotificationTroubleshootTestManagerFactory @Inject constructor(
private val testSystemSettings: TestSystemSettings,
private val testAccountSettings: TestAccountSettings,
private val testDeviceSettings: TestDeviceSettings,
private val testPushRulesSettings: TestPushRulesSettings,
private val testAutoStartBoot: TestAutoStartBoot,
private val testBackgroundRestrictions: TestBackgroundRestrictions,
private val testBatteryOptimization: TestBatteryOptimization,
private val testNotification: TestNotification
) {
fun create(fragment: Fragment): NotificationTroubleshootTestManager {
val mgr = NotificationTroubleshootTestManager(fragment)
@ -43,6 +47,7 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor(private val
mgr.addTest(testAutoStartBoot)
mgr.addTest(testBackgroundRestrictions)
mgr.addTest(testBatteryOptimization)
mgr.addTest(testNotification)
return mgr
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright 2020 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.app.gplay.features.settings.troubleshoot
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AppCompatActivity
import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.settings.troubleshoot.TroubleshootTest
import im.vector.app.push.fcm.FcmHelper
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.pushers.PushGatewayFailure
import org.matrix.android.sdk.api.util.Cancelable
import javax.inject.Inject
/**
* Test Push by asking the Push Gateway to send a Push back
*/
class TestPushFromPushGateway @Inject constructor(private val context: AppCompatActivity,
private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter,
private val pushersManager: PushersManager)
: TroubleshootTest(R.string.settings_troubleshoot_test_push_loop_title) {
private var action: Cancelable? = null
override fun perform(activityResultLauncher: ActivityResultLauncher<Intent>) {
val fcmToken = FcmHelper.getFcmToken(context) ?: run {
status = TestStatus.FAILED
return
}
action = pushersManager.testPush(fcmToken, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
description = if (failure is PushGatewayFailure.PusherRejected) {
stringProvider.getString(R.string.settings_troubleshoot_test_push_loop_failed)
} else {
errorFormatter.toHumanReadable(failure)
}
status = TestStatus.FAILED
}
override fun onSuccess(data: Unit) {
// Wait for the push to be received
description = stringProvider.getString(R.string.settings_troubleshoot_test_push_loop_waiting_for_push)
status = TestStatus.RUNNING
}
})
}
override fun onPushReceived() {
description = stringProvider.getString(R.string.settings_troubleshoot_test_push_loop_success)
status = TestStatus.SUCCESS
}
override fun cancel() {
action?.cancel()
}
}

View File

@ -19,10 +19,12 @@
package im.vector.app.gplay.push.fcm
import android.content.Intent
import android.os.Handler
import android.os.Looper
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import im.vector.app.BuildConfig
@ -34,6 +36,7 @@ import im.vector.app.features.badge.BadgeProxy
import im.vector.app.features.notifications.NotifiableEventResolver
import im.vector.app.features.notifications.NotifiableMessageEvent
import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.notifications.SimpleNotifiableEvent
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.push.fcm.FcmHelper
@ -60,11 +63,13 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
override fun onCreate() {
super.onCreate()
notificationDrawerManager = vectorComponent().notificationDrawerManager()
notifiableEventResolver = vectorComponent().notifiableEventResolver()
pusherManager = vectorComponent().pusherManager()
activeSessionHolder = vectorComponent().activeSessionHolder()
vectorPreferences = vectorComponent().vectorPreferences()
with(vectorComponent()) {
notificationDrawerManager = notificationDrawerManager()
notifiableEventResolver = notifiableEventResolver()
pusherManager = pusherManager()
activeSessionHolder = activeSessionHolder()
vectorPreferences = vectorPreferences()
}
}
/**
@ -73,6 +78,13 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
* @param message the message
*/
override fun onMessageReceived(message: RemoteMessage) {
// Diagnostic Push
if (message.data["event_id"] == PushersManager.TEST_EVENT_ID) {
val intent = Intent(NotificationUtils.PUSH_ACTION)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
return
}
if (!vectorPreferences.areNotificationEnabledForDevice()) {
Timber.i("Notification are disabled for this device")
return

View File

@ -19,20 +19,26 @@ import androidx.fragment.app.Fragment
import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.app.features.settings.troubleshoot.TestAccountSettings
import im.vector.app.features.settings.troubleshoot.TestDeviceSettings
import im.vector.app.features.settings.troubleshoot.TestNotification
import im.vector.app.features.settings.troubleshoot.TestPushRulesSettings
import im.vector.app.features.settings.troubleshoot.TestSystemSettings
import im.vector.app.gplay.features.settings.troubleshoot.TestFirebaseToken
import im.vector.app.gplay.features.settings.troubleshoot.TestPlayServices
import im.vector.app.gplay.features.settings.troubleshoot.TestPushFromPushGateway
import im.vector.app.gplay.features.settings.troubleshoot.TestTokenRegistration
import javax.inject.Inject
class NotificationTroubleshootTestManagerFactory @Inject constructor(private val testSystemSettings: TestSystemSettings,
private val testAccountSettings: TestAccountSettings,
private val testDeviceSettings: TestDeviceSettings,
private val testBingRulesSettings: TestPushRulesSettings,
private val testPlayServices: TestPlayServices,
private val testFirebaseToken: TestFirebaseToken,
private val testTokenRegistration: TestTokenRegistration) {
class NotificationTroubleshootTestManagerFactory @Inject constructor(
private val testSystemSettings: TestSystemSettings,
private val testAccountSettings: TestAccountSettings,
private val testDeviceSettings: TestDeviceSettings,
private val testBingRulesSettings: TestPushRulesSettings,
private val testPlayServices: TestPlayServices,
private val testFirebaseToken: TestFirebaseToken,
private val testTokenRegistration: TestTokenRegistration,
private val testPushFromPushGateway: TestPushFromPushGateway,
private val testNotification: TestNotification
) {
fun create(fragment: Fragment): NotificationTroubleshootTestManager {
val mgr = NotificationTroubleshootTestManager(fragment)
@ -43,6 +49,8 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor(private val
mgr.addTest(testPlayServices)
mgr.addTest(testFirebaseToken)
mgr.addTest(testTokenRegistration)
mgr.addTest(testPushFromPushGateway)
mgr.addTest(testNotification)
return mgr
}
}

View File

@ -94,9 +94,11 @@
</activity>
<activity android:name=".features.media.ImageMediaViewerActivity" />
<!-- Add tools:ignore="Instantiatable" for the error reported only by Buildkite :/ -->
<activity
android:name=".features.media.VectorAttachmentViewerActivity"
android:theme="@style/AppTheme.Transparent" />
android:theme="@style/AppTheme.Transparent"
tools:ignore="Instantiatable" />
<activity android:name=".features.media.BigImageViewerActivity" />
<activity
@ -252,6 +254,10 @@
android:name=".features.call.service.CallHeadsUpActionReceiver"
android:exported="false" />
<receiver
android:name=".features.settings.troubleshoot.TestNotificationReceiver"
android:exported="false" />
<!-- Exported false, should only be accessible from this app!! -->
<receiver
android:name=".features.notifications.NotificationBroadcastReceiver"

View File

@ -0,0 +1,94 @@
/*
* Copyright (c) 2020 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.app.core.epoxy
import android.animation.ObjectAnimator
import android.text.TextUtils
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.doOnPreDraw
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.extensions.copyOnLongClick
@EpoxyModelClass(layout = R.layout.item_expandable_textview)
abstract class ExpandableTextItem : VectorEpoxyModel<ExpandableTextItem.Holder>() {
@EpoxyAttribute
lateinit var content: String
@EpoxyAttribute
var maxLines: Int = 3
private var isExpanded = false
private var expandedLines = 0
override fun bind(holder: Holder) {
super.bind(holder)
holder.content.text = content
holder.content.copyOnLongClick()
holder.content.doOnPreDraw {
if (holder.content.lineCount > maxLines) {
expandedLines = holder.content.lineCount
holder.content.maxLines = maxLines
holder.view.setOnClickListener {
if (isExpanded) {
collapse(holder)
} else {
expand(holder)
}
}
holder.arrow.isVisible = true
} else {
holder.arrow.isVisible = false
}
}
}
private fun expand(holder: Holder) {
ObjectAnimator
.ofInt(holder.content, "maxLines", expandedLines)
.setDuration(200)
.start()
holder.content.ellipsize = null
holder.arrow.setImageResource(R.drawable.ic_expand_less)
holder.arrow.contentDescription = holder.view.context.getString(R.string.merged_events_collapse)
isExpanded = true
}
private fun collapse(holder: Holder) {
ObjectAnimator
.ofInt(holder.content, "maxLines", maxLines)
.setDuration(200)
.start()
holder.content.ellipsize = TextUtils.TruncateAt.END
holder.arrow.setImageResource(R.drawable.ic_expand_more)
holder.arrow.contentDescription = holder.view.context.getString(R.string.merged_events_expand)
isExpanded = false
}
class Holder : VectorEpoxyHolder() {
val content by bind<TextView>(R.id.expandableContent)
val arrow by bind<ImageView>(R.id.expandableArrow)
}
}

View File

@ -24,7 +24,6 @@ import org.matrix.android.sdk.api.failure.isInvalidPassword
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import javax.inject.Inject
import javax.net.ssl.SSLException
import javax.net.ssl.SSLPeerUnverifiedException
@ -45,15 +44,12 @@ class DefaultErrorFormatter @Inject constructor(
when (throwable.ioException) {
is SocketTimeoutException ->
stringProvider.getString(R.string.error_network_timeout)
is UnknownHostException ->
// Invalid homeserver?
// TODO Check network state, airplane mode, etc.
stringProvider.getString(R.string.login_error_unknown_host)
is SSLPeerUnverifiedException ->
stringProvider.getString(R.string.login_error_ssl_peer_unverified)
is SSLException ->
stringProvider.getString(R.string.login_error_ssl_other)
else ->
// TODO Check network state, airplane mode, etc.
stringProvider.getString(R.string.error_no_network)
}
}

View File

@ -22,6 +22,7 @@ import im.vector.app.core.resources.AppNameProvider
import im.vector.app.core.resources.LocaleProvider
import im.vector.app.core.resources.StringProvider
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.util.Cancelable
import java.util.UUID
import javax.inject.Inject
import kotlin.math.abs
@ -34,6 +35,17 @@ class PushersManager @Inject constructor(
private val stringProvider: StringProvider,
private val appNameProvider: AppNameProvider
) {
fun testPush(pushKey: String, callback: MatrixCallback<Unit>): Cancelable {
val currentSession = activeSessionHolder.getActiveSession()
return currentSession.testPush(
stringProvider.getString(R.string.pusher_http_url),
stringProvider.getString(R.string.pusher_app_id),
pushKey,
TEST_EVENT_ID,
callback
)
}
fun registerPusherWithFcmKey(pushKey: String): UUID {
val currentSession = activeSessionHolder.getActiveSession()
@ -56,4 +68,8 @@ class PushersManager @Inject constructor(
val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
currentSession.removeHttpPusher(pushKey, stringProvider.getString(R.string.pusher_app_id), callback)
}
companion object {
const val TEST_EVENT_ID = "\$THIS_IS_A_FAKE_EVENT_ID"
}
}

View File

@ -30,7 +30,6 @@ import android.provider.Settings
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import im.vector.app.R
@ -97,15 +96,15 @@ fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = t
* Shows notification settings for the current app.
* In android O will directly opens the notification settings, in lower version it will show the App settings
*/
fun startNotificationSettingsIntent(activity: AppCompatActivity, activityResultLauncher: ActivityResultLauncher<Intent>) {
fun startNotificationSettingsIntent(context: Context, activityResultLauncher: ActivityResultLauncher<Intent>) {
val intent = Intent()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
intent.putExtra(Settings.EXTRA_APP_PACKAGE, activity.packageName)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
} else {
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
intent.putExtra("app_package", activity.packageName)
intent.putExtra("app_uid", activity.applicationInfo?.uid)
intent.putExtra("app_package", context.packageName)
intent.putExtra("app_uid", context.applicationInfo?.uid)
}
activityResultLauncher.launch(intent)
}

View File

@ -76,7 +76,8 @@ data class VerificationBottomSheetViewState(
val userWantsToCancel: Boolean = false,
val userThinkItsNotHim: Boolean = false,
val quadSContainsSecrets: Boolean = true,
val quadSHasBeenReset: Boolean = false
val quadSHasBeenReset: Boolean = false,
val hasAnyOtherSession: Boolean = false
) : MvRxState
class VerificationBottomSheetViewModel @AssistedInject constructor(
@ -119,6 +120,12 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
session.cryptoService().verificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction
}
val hasAnyOtherSession = session.cryptoService()
.getCryptoDeviceInfo(session.myUserId)
.any {
it.deviceId != session.sessionParams.deviceId
}
setState {
copy(
otherUserMxItem = userItem?.toMatrixItem(),
@ -130,7 +137,8 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
roomId = args.roomId,
isMe = args.otherUserId == session.myUserId,
currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(),
quadSContainsSecrets = session.sharedSecretStorageService.isRecoverySetup()
quadSContainsSecrets = session.sharedSecretStorageService.isRecoverySetup(),
hasAnyOtherSession = hasAnyOtherSession
)
}

View File

@ -52,25 +52,32 @@ class VerificationRequestController @Inject constructor(
val matrixItem = viewState?.otherUserMxItem ?: return
if (state.selfVerificationMode) {
bottomSheetVerificationNoticeItem {
id("notice")
notice(stringProvider.getString(R.string.verification_open_other_to_verify))
}
if (state.hasAnyOtherSession) {
bottomSheetVerificationNoticeItem {
id("notice")
notice(stringProvider.getString(R.string.verification_open_other_to_verify))
}
bottomSheetSelfWaitItem {
id("waiting")
}
bottomSheetSelfWaitItem {
id("waiting")
}
dividerItem {
id("sep")
dividerItem {
id("sep")
}
}
if (state.quadSContainsSecrets) {
val subtitle = if (state.hasAnyOtherSession) {
stringProvider.getString(R.string.verification_use_passphrase)
} else {
null
}
bottomSheetVerificationActionItem {
id("passphrase")
title(stringProvider.getString(R.string.verification_cannot_access_other_session))
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
subTitle(stringProvider.getString(R.string.verification_use_passphrase))
subTitle(subtitle)
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
listener { listener?.onClickRecoverFromPassphrase() }
@ -122,13 +129,13 @@ class VerificationRequestController @Inject constructor(
listener { listener?.onClickOnVerificationStart() }
}
}
is Loading -> {
is Loading -> {
bottomSheetVerificationWaitingItem {
id("waiting")
title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName()))
}
}
is Success -> {
is Success -> {
if (!pr.invoke().isReady) {
if (state.isMe) {
bottomSheetVerificationWaitingItem {

View File

@ -27,7 +27,7 @@ sealed class LoginAction : VectorViewModelAction {
data class UpdateSignMode(val signMode: SignMode) : LoginAction()
data class LoginWithToken(val loginToken: String) : LoginAction()
data class WebLoginSuccess(val credentials: Credentials) : LoginAction()
data class InitWith(val loginConfig: LoginConfig) : LoginAction()
data class InitWith(val loginConfig: LoginConfig?) : LoginAction()
data class ResetPassword(val email: String, val newPassword: String) : LoginAction()
object ResetPasswordMailConfirmed : LoginAction()

View File

@ -91,19 +91,19 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
addFirstFragment()
}
// Get config extra
val loginConfig = intent.getParcelableExtra<LoginConfig?>(EXTRA_CONFIG)
if (loginConfig != null && isFirstCreation()) {
// TODO Check this
loginViewModel.handle(LoginAction.InitWith(loginConfig))
}
loginViewModel
.subscribe(this) {
updateWithState(it)
}
loginViewModel.observeViewEvents { handleLoginViewEvents(it) }
// Get config extra
val loginConfig = intent.getParcelableExtra<LoginConfig?>(EXTRA_CONFIG)
if (isFirstCreation()) {
// TODO Check this
loginViewModel.handle(LoginAction.InitWith(loginConfig))
}
}
protected open fun addFirstFragment() {

View File

@ -28,6 +28,8 @@ import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.utils.ensureProtocol
import im.vector.app.core.utils.openUrlInChromeCustomTab
import kotlinx.android.synthetic.main.fragment_login_server_url_form.*
import org.matrix.android.sdk.api.failure.Failure
import java.net.UnknownHostException
import javax.inject.Inject
/**
@ -115,7 +117,13 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
}
override fun onError(throwable: Throwable) {
loginServerUrlFormHomeServerUrlTil.error = errorFormatter.toHumanReadable(throwable)
loginServerUrlFormHomeServerUrlTil.error = if (throwable is Failure.NetworkConnection
&& throwable.ioException is UnknownHostException) {
// Invalid homeserver?
getString(R.string.login_error_homeserver_not_found)
} else {
errorFormatter.toHumanReadable(throwable)
}
}
override fun updateWithState(state: LoginViewState) {

View File

@ -417,6 +417,18 @@ class LoginViewModel @AssistedInject constructor(
private fun handleInitWith(action: LoginAction.InitWith) {
loginConfig = action.loginConfig
// If there is a pending email validation continue on this step
try {
if (registrationWizard?.isRegistrationStarted == true) {
currentThreePid?.let {
handle(LoginAction.PostViewEvent(LoginViewEvents.OnSendEmailSuccess(it)))
}
}
} catch (e: Throwable) {
// NOOP. API is designed to use wizards in a login/registration flow,
// but we need to check the state anyway.
}
}
private fun handleResetPassword(action: LoginAction.ResetPassword) {
@ -672,6 +684,7 @@ class LoginViewModel @AssistedInject constructor(
private fun onSessionCreated(session: Session) {
activeSessionHolder.setActiveSession(session)
authenticationService.reset()
session.configureAndStart(applicationContext)
setState {
copy(
@ -740,7 +753,7 @@ class LoginViewModel @AssistedInject constructor(
override fun onSuccess(data: LoginFlowResult) {
when (data) {
is LoginFlowResult.Success -> {
is LoginFlowResult.Success -> {
val loginMode = when {
// SSO login is taken first
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso

View File

@ -590,6 +590,10 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
}
}
fun displayDiagnosticNotification() {
notificationUtils.displayDiagnosticNotification()
}
companion object {
private const val SUMMARY_NOTIFICATION_ID = 0
private const val ROOM_MESSAGES_NOTIFICATION_ID = 1

View File

@ -27,8 +27,10 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.net.Uri
import android.os.Build
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@ -36,6 +38,7 @@ import androidx.core.app.RemoteInput
import androidx.core.app.TaskStackBuilder
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.fragment.app.Fragment
import im.vector.app.BuildConfig
@ -47,8 +50,8 @@ import im.vector.app.features.call.service.CallHeadsUpActionReceiver
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.room.detail.RoomDetailActivity
import im.vector.app.features.home.room.detail.RoomDetailArgs
import im.vector.app.features.pin.PinLocker
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.troubleshoot.TestNotificationReceiver
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -61,7 +64,6 @@ import kotlin.random.Random
@Singleton
class NotificationUtils @Inject constructor(private val context: Context,
private val stringProvider: StringProvider,
private val pinLocker: PinLocker,
private val vectorPreferences: VectorPreferences) {
companion object {
@ -89,6 +91,8 @@ class NotificationUtils @Inject constructor(private val context: Context,
const val DISMISS_SUMMARY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_SUMMARY_ACTION"
const val DISMISS_ROOM_NOTIF_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_ROOM_NOTIF_ACTION"
private const val TAP_TO_VIEW_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.TAP_TO_VIEW_ACTION"
const val DIAGNOSTIC_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DIAGNOSTIC"
const val PUSH_ACTION = "${BuildConfig.APPLICATION_ID}.PUSH"
/* ==========================================================================================
* IDs for channels
@ -845,6 +849,43 @@ class NotificationUtils @Inject constructor(private val context: Context,
}
}
fun displayDiagnosticNotification() {
val testActionIntent = Intent(context, TestNotificationReceiver::class.java)
testActionIntent.action = DIAGNOSTIC_ACTION
val testPendingIntent = PendingIntent.getBroadcast(
context,
0,
testActionIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
notificationManager.notify(
"DIAGNOSTIC",
888,
NotificationCompat.Builder(context, NOISY_NOTIFICATION_CHANNEL_ID)
.setContentTitle(stringProvider.getString(R.string.app_name))
.setContentText(stringProvider.getString(R.string.settings_troubleshoot_test_push_notification_content))
.setSmallIcon(R.drawable.ic_status_bar)
.setLargeIcon(getBitmap(context, R.drawable.element_logo_green))
.setColor(ContextCompat.getColor(context, R.color.notification_accent_color))
.setPriority(NotificationCompat.PRIORITY_MAX)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setAutoCancel(true)
.setContentIntent(testPendingIntent)
.build()
)
}
private fun getBitmap(context: Context, @DrawableRes drawableRes: Int): Bitmap? {
val drawable = ResourcesCompat.getDrawable(context.resources, drawableRes, null) ?: return null
val canvas = Canvas()
val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
canvas.setBitmap(bitmap)
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
drawable.draw(canvas)
return bitmap
}
/**
* Return true it the user has enabled the do not disturb mode
*/

View File

@ -19,6 +19,7 @@ package im.vector.app.features.roomprofile
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R
import im.vector.app.core.epoxy.expandableTextItem
import im.vector.app.core.epoxy.profiles.buildProfileAction
import im.vector.app.core.epoxy.profiles.buildProfileSection
import im.vector.app.core.resources.ColorProvider
@ -57,6 +58,20 @@ class RoomProfileController @Inject constructor(
return
}
val roomSummary = data.roomSummary() ?: return
// Topic
roomSummary
.topic
.takeIf { it.isNotEmpty() }
?.let {
buildProfileSection(stringProvider.getString(R.string.room_settings_topic))
expandableTextItem {
id("topic")
content(it)
maxLines(2)
}
}
// Security
buildProfileSection(stringProvider.getString(R.string.room_profile_section_security))
val learnMoreSubtitle = if (roomSummary.isEncrypted) {

View File

@ -129,7 +129,6 @@ class RoomProfileFragment @Inject constructor(
private fun setupLongClicks() {
roomProfileNameView.copyOnLongClick()
roomProfileAliasView.copyOnLongClick()
roomProfileTopicView.copyOnLongClick()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -187,7 +186,6 @@ class RoomProfileFragment @Inject constructor(
roomProfileNameView.text = it.displayName
matrixProfileToolbarTitleView.text = it.displayName
roomProfileAliasView.setTextOrHide(it.canonicalAlias)
roomProfileTopicView.setTextOrHide(it.topic)
val matrixItem = it.toMatrixItem()
avatarRenderer.render(matrixItem, roomProfileAvatarView)
avatarRenderer.render(matrixItem, matrixProfileToolbarAvatarImageView)

View File

@ -16,12 +16,16 @@
package im.vector.app.features.settings
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -32,10 +36,13 @@ import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.rageshake.BugReporter
import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.app.features.settings.troubleshoot.TroubleshootTest
import im.vector.app.push.fcm.NotificationTroubleshootTestManagerFactory
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import javax.inject.Inject
class VectorSettingsNotificationsTroubleshootFragment @Inject constructor(
@ -45,12 +52,16 @@ class VectorSettingsNotificationsTroubleshootFragment @Inject constructor(
@BindView(R.id.troubleshoot_test_recycler_view)
lateinit var mRecyclerView: RecyclerView
@BindView(R.id.troubleshoot_bottom_view)
lateinit var mBottomView: ViewGroup
@BindView(R.id.toubleshoot_summ_description)
lateinit var mSummaryDescription: TextView
@BindView(R.id.troubleshoot_summ_button)
lateinit var mSummaryButton: Button
@BindView(R.id.troubleshoot_run_button)
lateinit var mRunButton: Button
@ -82,8 +93,7 @@ class VectorSettingsNotificationsTroubleshootFragment @Inject constructor(
}
private fun startUI() {
mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_running_status,
0, 0)
mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_running_status, 0, 0)
testManager = testManagerFactory.create(this)
testManager?.statusListener = { troubleshootTestManager ->
if (isAdded) {
@ -94,10 +104,10 @@ class VectorSettingsNotificationsTroubleshootFragment @Inject constructor(
mSummaryButton.visibility = View.GONE
mRunButton.visibility = View.VISIBLE
}
TroubleshootTest.TestStatus.RUNNING -> {
// Forces int type because it's breaking lint
val size: Int = troubleshootTestManager.testList.size
val currentTestIndex: Int = troubleshootTestManager.currentTestIndex
TroubleshootTest.TestStatus.RUNNING,
TroubleshootTest.TestStatus.WAITING_FOR_USER -> {
val size = troubleshootTestManager.testListSize
val currentTestIndex = troubleshootTestManager.currentTestIndex
mSummaryDescription.text = getString(
R.string.settings_troubleshoot_diagnostic_running_status,
currentTestIndex,
@ -108,15 +118,7 @@ class VectorSettingsNotificationsTroubleshootFragment @Inject constructor(
}
TroubleshootTest.TestStatus.FAILED -> {
// check if there are quick fixes
var hasQuickFix = false
testManager?.testList?.let {
for (test in it) {
if (test.status == TroubleshootTest.TestStatus.FAILED && test.quickFix != null) {
hasQuickFix = true
break
}
}
}
val hasQuickFix = testManager?.hasQuickFix().orFalse()
if (hasQuickFix) {
mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_failure_status_with_quickfix)
} else {
@ -161,6 +163,39 @@ class VectorSettingsNotificationsTroubleshootFragment @Inject constructor(
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_notification_troubleshoot)
tryOrNull("Unable to register the receiver") {
LocalBroadcastManager.getInstance(requireContext())
.registerReceiver(broadcastReceiverPush, IntentFilter(NotificationUtils.PUSH_ACTION))
}
tryOrNull("Unable to register the receiver") {
LocalBroadcastManager.getInstance(requireContext())
.registerReceiver(broadcastReceiverNotification, IntentFilter(NotificationUtils.DIAGNOSTIC_ACTION))
}
}
override fun onPause() {
super.onPause()
tryOrNull {
LocalBroadcastManager.getInstance(requireContext())
.unregisterReceiver(broadcastReceiverPush)
}
tryOrNull {
LocalBroadcastManager.getInstance(requireContext())
.unregisterReceiver(broadcastReceiverNotification)
}
}
private val broadcastReceiverPush = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
testManager?.onDiagnosticPushReceived()
}
}
private val broadcastReceiverNotification = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
testManager?.onDiagnosticNotificationClicked()
}
}
override fun onAttach(context: Context) {

View File

@ -77,6 +77,16 @@ class NotificationTroubleshootRecyclerViewAdapter(val tests: ArrayList<Troublesh
statusIconImage.visibility = View.VISIBLE
statusIconImage.setImageResource(R.drawable.unit_test)
}
TroubleshootTest.TestStatus.WAITING_FOR_USER -> {
progressBar.visibility = View.INVISIBLE
statusIconImage.visibility = View.VISIBLE
val infoColor = ContextCompat.getColor(context, R.color.vector_info_color)
val drawable = ContextCompat.getDrawable(itemView.context, R.drawable.ic_notification_privacy_warning)?.apply {
ThemeUtils.tintDrawableWithColor(this, infoColor)
}
statusIconImage.setImageDrawable(drawable)
descriptionText.setTextColor(infoColor)
}
TroubleshootTest.TestStatus.RUNNING -> {
progressBar.visibility = View.VISIBLE
statusIconImage.visibility = View.INVISIBLE

View File

@ -23,13 +23,19 @@ import androidx.fragment.app.Fragment
import kotlin.properties.Delegates
class NotificationTroubleshootTestManager(val fragment: Fragment) {
private val testList = ArrayList<TroubleshootTest>()
val testListSize: Int
get() = testList.size
val testList = ArrayList<TroubleshootTest>()
var isCancelled = false
private set
var currentTestIndex by Delegates.observable(0) { _, _, _ ->
statusListener?.invoke(this)
}
private set
val adapter = NotificationTroubleshootRecyclerViewAdapter(testList)
var statusListener: ((NotificationTroubleshootTestManager) -> Unit)? = null
@ -37,6 +43,7 @@ class NotificationTroubleshootTestManager(val fragment: Fragment) {
var diagStatus: TroubleshootTest.TestStatus by Delegates.observable(TroubleshootTest.TestStatus.NOT_STARTED) { _, _, _ ->
statusListener?.invoke(this)
}
private set
fun addTest(test: TroubleshootTest) {
testList.add(test)
@ -79,19 +86,31 @@ class NotificationTroubleshootTestManager(val fragment: Fragment) {
}
fun retry(activityResultLauncher: ActivityResultLauncher<Intent>) {
for (test in testList) {
test.cancel()
test.description = null
test.quickFix = null
test.status = TroubleshootTest.TestStatus.NOT_STARTED
testList.forEach {
it.cancel()
it.description = null
it.quickFix = null
it.status = TroubleshootTest.TestStatus.NOT_STARTED
}
runDiagnostic(activityResultLauncher)
}
fun cancel() {
isCancelled = true
for (test in testList) {
test.cancel()
fun hasQuickFix(): Boolean {
return testList.any { test ->
test.status == TroubleshootTest.TestStatus.FAILED && test.quickFix != null
}
}
fun cancel() {
isCancelled = true
testList.forEach { it.cancel() }
}
fun onDiagnosticPushReceived() {
testList.forEach { it.onPushReceived() }
}
fun onDiagnosticNotificationClicked() {
testList.forEach { it.onNotificationClicked() }
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.troubleshoot
import android.content.Context
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.startNotificationSettingsIntent
import im.vector.app.features.notifications.NotificationUtils
import javax.inject.Inject
/**
* Checks if notifications can be displayed and clicked by the user
*/
class TestNotification @Inject constructor(private val context: Context,
private val notificationUtils: NotificationUtils,
private val stringProvider: StringProvider)
: TroubleshootTest(R.string.settings_troubleshoot_test_notification_title) {
override fun perform(activityResultLauncher: ActivityResultLauncher<Intent>) {
// Display the notification right now
notificationUtils.displayDiagnosticNotification()
description = stringProvider.getString(R.string.settings_troubleshoot_test_notification_notice)
quickFix = object : TroubleshootQuickFix(R.string.open_settings) {
override fun doFix() {
startNotificationSettingsIntent(context, activityResultLauncher)
}
}
status = TestStatus.WAITING_FOR_USER
}
override fun onNotificationClicked() {
description = stringProvider.getString(R.string.settings_troubleshoot_test_notification_notification_clicked)
quickFix = null
status = TestStatus.SUCCESS
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 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.app.features.settings.troubleshoot
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
class TestNotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Internal broadcast to any one interested
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
}
}

View File

@ -15,9 +15,9 @@
*/
package im.vector.app.features.settings.troubleshoot
import android.content.Context
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationManagerCompat
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
@ -27,7 +27,7 @@ import javax.inject.Inject
/**
* Checks if notifications are enable in the system settings for this app.
*/
class TestSystemSettings @Inject constructor(private val context: AppCompatActivity,
class TestSystemSettings @Inject constructor(private val context: Context,
private val stringProvider: StringProvider)
: TroubleshootTest(R.string.settings_troubleshoot_test_system_settings_title) {

View File

@ -25,6 +25,7 @@ abstract class TroubleshootTest(@StringRes val titleResId: Int) {
enum class TestStatus {
NOT_STARTED,
RUNNING,
WAITING_FOR_USER,
FAILED,
SUCCESS
}
@ -51,4 +52,10 @@ abstract class TroubleshootTest(@StringRes val titleResId: Int) {
open fun cancel() {
}
open fun onPushReceived() {
}
open fun onNotificationClicked() {
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
</vector>

View File

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/jitsi_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
<!-- Note: A org.jitsi.meet.sdk.JitsiMeetView will be added here -->
<!-- Note: A org.jitsi.meet.sdk.JitsiMeetView will be added here and so add tools:ignore="UselessParent" -->
<LinearLayout
android:id="@+id/jitsi_progress_layout"
@ -13,7 +14,8 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
android:orientation="vertical"
tools:ignore="UselessParent">
<ProgressBar
android:layout_width="40dp"

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/expandableContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
app:layout_constraintBottom_toTopOf="@+id/expandableArrow"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:maxLines="2"
tools:text="@sample/matrix.json/data/roomTopic" />
<ImageView
android:id="@+id/expandableArrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:autoLink="web"
android:fontFamily="sans-serif"
android:gravity="center"
android:src="@drawable/ic_expand_more"
android:textSize="14sp"
android:textStyle="normal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/expandableContent"
app:tint="?riotx_text_secondary"
tools:ignore="MissingPrefix" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,68 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/bg_attachment_type_selector"
android:orientation="vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:baselineAligned="false"
android:orientation="horizontal"
android:weightSum="2">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton
android:id="@+id/avatarCameraButton"
style="@style/AttachmentTypeSelectorButton"
android:contentDescription="@string/attachment_type_camera"
android:src="@drawable/ic_attachment_camera_white_24dp"
tools:background="@color/riotx_accent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_camera" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton
android:id="@+id/avatarGalleryButton"
style="@style/AttachmentTypeSelectorButton"
android:contentDescription="@string/attachment_type_gallery"
android:src="@drawable/ic_attachment_gallery_white_24dp"
tools:background="@color/riotx_accent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_gallery" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -54,27 +54,9 @@
android:textAppearance="@style/Vector.Toolbar.Title"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/roomProfileTopicView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/roomProfileNameView"
tools:text="@sample/matrix.json/data/roomAlias" />
<TextView
android:id="@+id/roomProfileTopicView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginEnd="40dp"
android:autoLink="web"
android:fontFamily="sans-serif"
android:gravity="center"
android:textSize="14sp"
android:textStyle="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/roomProfileAliasView"
tools:text="@sample/matrix.json/data/roomTopic" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -749,6 +749,15 @@
<string name="settings_troubleshoot_test_token_registration_success">FCM token successfully registered to HomeServer.</string>
<string name="settings_troubleshoot_test_token_registration_failed">Failed to register FCM token to HomeServer:\n%1$s</string>
<string name="settings_troubleshoot_test_push_loop_title">Test Push</string>
<string name="settings_troubleshoot_test_push_loop_waiting_for_push">The application is waiting for the PUSH</string>
<string name="settings_troubleshoot_test_push_loop_success">The application is receiving PUSH</string>
<string name="settings_troubleshoot_test_push_loop_failed">Failed to receive push. Solution could be to reinstall the application.</string>
<string name="settings_troubleshoot_test_push_notification_content">You are viewing the notification! Click me!</string>
<string name="settings_troubleshoot_test_notification_title">Notification Display</string>
<string name="settings_troubleshoot_test_notification_notice">Please click on the notification. If you do not see the notification, please check the system settings.</string>
<string name="settings_troubleshoot_test_notification_notification_clicked">The notification has been clicked!</string>
<string name="settings_troubleshoot_test_foreground_service_started_title">Notifications Service</string>
<string name="settings_troubleshoot_test_foreground_service_startedt_success">Notifications Service is running.</string>
<string name="settings_troubleshoot_test_foreground_service_started_failed">Notifications Service is not running.\nTry to restart the application.</string>