Merge pull request #6644 from vector-im/feature/mna/notification-tap-lls

[Location Share] Open maximized map on tapping on live sharing notification (PSG-616)
This commit is contained in:
Maxime NATUREL 2022-07-29 11:27:53 +02:00 committed by GitHub
commit c7d5ceca5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 133 additions and 35 deletions

1
changelog.d/6642.misc Normal file
View File

@ -0,0 +1 @@
[Location Share] Open maximized map on tapping on live sharing notification

View File

@ -377,7 +377,7 @@
</service> </service>
<service <service
android:name=".features.location.LocationSharingAndroidService" android:name=".features.location.live.tracking.LocationSharingAndroidService"
android:exported="false" android:exported="false"
android:foregroundServiceType="location" /> android:foregroundServiceType="location" />

View File

@ -42,6 +42,7 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.location.live.LiveLocationLabsFlagPromotionBottomSheet import im.vector.app.features.location.live.LiveLocationLabsFlagPromotionBottomSheet
import im.vector.app.features.location.live.duration.ChooseLiveDurationBottomSheet import im.vector.app.features.location.live.duration.ChooseLiveDurationBottomSheet
import im.vector.app.features.location.live.tracking.LocationSharingAndroidService
import im.vector.app.features.location.option.LocationSharingOption import im.vector.app.features.location.option.LocationSharingOption
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem

View File

@ -22,6 +22,7 @@ import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.IBinder import android.os.IBinder
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.location.live.tracking.LocationSharingAndroidService
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn

View File

@ -24,6 +24,7 @@ import im.vector.app.R
import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityLocationSharingBinding import im.vector.app.databinding.ActivityLocationSharingBinding
import im.vector.app.features.MainActivity
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@ -59,10 +60,15 @@ class LocationLiveMapViewActivity : VectorBaseActivity<ActivityLocationSharingBi
private const val EXTRA_LOCATION_LIVE_MAP_VIEW_ARGS = "EXTRA_LOCATION_LIVE_MAP_VIEW_ARGS" private const val EXTRA_LOCATION_LIVE_MAP_VIEW_ARGS = "EXTRA_LOCATION_LIVE_MAP_VIEW_ARGS"
fun getIntent(context: Context, locationLiveMapViewArgs: LocationLiveMapViewArgs): Intent { fun getIntent(context: Context, locationLiveMapViewArgs: LocationLiveMapViewArgs, firstStartMainActivity: Boolean = false): Intent {
return Intent(context, LocationLiveMapViewActivity::class.java).apply { val intent = Intent(context, LocationLiveMapViewActivity::class.java).apply {
putExtra(EXTRA_LOCATION_LIVE_MAP_VIEW_ARGS, locationLiveMapViewArgs) putExtra(EXTRA_LOCATION_LIVE_MAP_VIEW_ARGS, locationLiveMapViewArgs)
} }
return if (firstStartMainActivity) {
MainActivity.getIntentWithNextIntent(context, intent)
} else {
intent
}
} }
} }
} }

View File

@ -0,0 +1,83 @@
/*
* Copyright (c) 2022 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.location.live.tracking
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.TaskStackBuilder
import im.vector.app.R
import im.vector.app.core.extensions.createIgnoredUri
import im.vector.app.core.platform.PendingIntentCompat
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.time.Clock
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.room.detail.RoomDetailActivity
import im.vector.app.features.home.room.detail.arguments.TimelineArgs
import im.vector.app.features.location.live.map.LocationLiveMapViewActivity
import im.vector.app.features.location.live.map.LocationLiveMapViewArgs
import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.themes.ThemeUtils
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LiveLocationNotificationBuilder @Inject constructor(
private val context: Context,
private val stringProvider: StringProvider,
private val clock: Clock,
) {
/**
* Creates a notification that indicates the application is retrieving location even if it is in background or killed.
* @param roomId the id of the room where a live location is shared
*/
fun buildLiveLocationSharingNotification(roomId: String): Notification {
return NotificationCompat.Builder(context, NotificationUtils.SILENT_NOTIFICATION_CHANNEL_ID)
.setContentTitle(stringProvider.getString(R.string.live_location_sharing_notification_title))
.setContentText(stringProvider.getString(R.string.live_location_sharing_notification_description))
.setSmallIcon(R.drawable.ic_attachment_location_live_white)
.setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
.setCategory(NotificationCompat.CATEGORY_LOCATION_SHARING)
.setContentIntent(buildOpenLiveLocationMapIntent(roomId))
.build()
}
private fun buildOpenLiveLocationMapIntent(roomId: String): PendingIntent? {
val homeIntent = HomeActivity.newIntent(context, firstStartMainActivity = false)
val roomIntent = RoomDetailActivity.newIntent(context, TimelineArgs(roomId = roomId, switchToParentSpace = true), firstStartMainActivity = false)
val mapIntent = LocationLiveMapViewActivity.getIntent(
context = context,
locationLiveMapViewArgs = LocationLiveMapViewArgs(roomId = roomId),
firstStartMainActivity = true
)
mapIntent.action = NotificationUtils.TAP_TO_VIEW_ACTION
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
mapIntent.data = createIgnoredUri("openLiveLocationMap?$roomId")
// Recreate the back stack
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(homeIntent)
.addNextIntent(roomIntent)
.addNextIntent(mapIntent)
.getPendingIntent(
clock.epochMillis().toInt(),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
}
}

View File

@ -14,18 +14,20 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.location package im.vector.app.features.location.live.tracking
import android.content.Intent import android.content.Intent
import android.os.Binder import android.os.Binder
import android.os.IBinder import android.os.IBinder
import android.os.Parcelable import android.os.Parcelable
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.services.VectorAndroidService import im.vector.app.core.services.VectorAndroidService
import im.vector.app.features.location.LocationData
import im.vector.app.features.location.LocationTracker
import im.vector.app.features.location.live.GetLiveLocationShareSummaryUseCase import im.vector.app.features.location.live.GetLiveLocationShareSummaryUseCase
import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.redaction.CheckIfEventIsRedactedUseCase import im.vector.app.features.redaction.CheckIfEventIsRedactedUseCase
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -54,7 +56,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
val durationMillis: Long val durationMillis: Long
) : Parcelable ) : Parcelable
@Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var liveLocationNotificationBuilder: LiveLocationNotificationBuilder
@Inject lateinit var locationTracker: LocationTracker @Inject lateinit var locationTracker: LocationTracker
@Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase @Inject lateinit var getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase
@ -62,13 +64,11 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
private val binder = LocalBinder() private val binder = LocalBinder()
/** private val liveInfoSet = linkedSetOf<LiveInfo>()
* Keep track of a map between beacon event Id starting the live and RoomArgs.
*/
private val roomArgsMap = mutableMapOf<String, RoomArgs>()
var callback: Callback? = null var callback: Callback? = null
private val jobs = mutableListOf<Job>() private val jobs = mutableListOf<Job>()
private var startInProgress = false private var startInProgress = false
private var foregroundModeStarted = false
private val _roomIdsOfActiveLives = MutableSharedFlow<Set<String>>(replay = 1) private val _roomIdsOfActiveLives = MutableSharedFlow<Set<String>>(replay = 1)
val roomIdsOfActiveLives = _roomIdsOfActiveLives.asSharedFlow() val roomIdsOfActiveLives = _roomIdsOfActiveLives.asSharedFlow()
@ -76,7 +76,6 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Timber.i("onCreate") Timber.i("onCreate")
initLocationTracking() initLocationTracking()
} }
@ -102,8 +101,13 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
if (roomArgs != null) { if (roomArgs != null) {
// Show a sticky notification // Show a sticky notification
val notification = notificationUtils.buildLiveLocationSharingNotification() val notification = liveLocationNotificationBuilder.buildLiveLocationSharingNotification(roomArgs.roomId)
startForeground(roomArgs.roomId.hashCode(), notification) if (foregroundModeStarted) {
NotificationManagerCompat.from(this).notify(FOREGROUND_SERVICE_NOTIFICATION_ID, notification)
} else {
startForeground(FOREGROUND_SERVICE_NOTIFICATION_ID, notification)
foregroundModeStarted = true
}
// Send beacon info state event // Send beacon info state event
launchWithActiveSession { session -> launchWithActiveSession { session ->
@ -148,15 +152,24 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
private fun stopSharingLocation(beaconEventId: String) { private fun stopSharingLocation(beaconEventId: String) {
Timber.i("stopSharingLocation for beacon $beaconEventId") Timber.i("stopSharingLocation for beacon $beaconEventId")
removeRoomArgs(beaconEventId) removeRoomArgs(beaconEventId)
updateNotification()
tryToDestroyMe() tryToDestroyMe()
} }
private fun updateNotification() {
if (liveInfoSet.isNotEmpty()) {
val roomId = liveInfoSet.last().roomArgs.roomId
val notification = liveLocationNotificationBuilder.buildLiveLocationSharingNotification(roomId)
NotificationManagerCompat.from(this).notify(FOREGROUND_SERVICE_NOTIFICATION_ID, notification)
}
}
private fun onLocationUpdate(locationData: LocationData) { private fun onLocationUpdate(locationData: LocationData) {
Timber.i("onLocationUpdate. Uncertainty: ${locationData.uncertainty}") Timber.i("onLocationUpdate. Uncertainty: ${locationData.uncertainty}")
// Emit location update to all rooms in which live location sharing is active // Emit location update to all rooms in which live location sharing is active
roomArgsMap.toMap().forEach { item -> liveInfoSet.toSet().forEach { liveInfo ->
sendLiveLocation(item.value.roomId, item.key, locationData) sendLiveLocation(liveInfo.roomArgs.roomId, liveInfo.beaconEventId, locationData)
} }
} }
@ -183,7 +196,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
} }
private fun tryToDestroyMe() { private fun tryToDestroyMe() {
if (startInProgress.not() && roomArgsMap.isEmpty()) { if (startInProgress.not() && liveInfoSet.isEmpty()) {
Timber.i("Destroying self, time is up for all rooms") Timber.i("Destroying self, time is up for all rooms")
stopSelf() stopSelf()
} }
@ -199,13 +212,14 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
private fun addRoomArgs(beaconEventId: String, roomArgs: RoomArgs) { private fun addRoomArgs(beaconEventId: String, roomArgs: RoomArgs) {
Timber.i("adding roomArgs for beaconEventId: $beaconEventId") Timber.i("adding roomArgs for beaconEventId: $beaconEventId")
roomArgsMap[beaconEventId] = roomArgs liveInfoSet.removeAll { it.beaconEventId == beaconEventId }
liveInfoSet.add(LiveInfo(beaconEventId, roomArgs))
launchWithActiveSession { _roomIdsOfActiveLives.emit(getRoomIdsOfActiveLives()) } launchWithActiveSession { _roomIdsOfActiveLives.emit(getRoomIdsOfActiveLives()) }
} }
private fun removeRoomArgs(beaconEventId: String) { private fun removeRoomArgs(beaconEventId: String) {
Timber.i("removing roomArgs for beaconEventId: $beaconEventId") Timber.i("removing roomArgs for beaconEventId: $beaconEventId")
roomArgsMap.remove(beaconEventId) liveInfoSet.removeAll { it.beaconEventId == beaconEventId }
launchWithActiveSession { _roomIdsOfActiveLives.emit(getRoomIdsOfActiveLives()) } launchWithActiveSession { _roomIdsOfActiveLives.emit(getRoomIdsOfActiveLives()) }
} }
@ -234,7 +248,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
} }
fun getRoomIdsOfActiveLives(): Set<String> { fun getRoomIdsOfActiveLives(): Set<String> {
return roomArgsMap.map { it.value.roomId }.toSet() return liveInfoSet.map { it.roomArgs.roomId }.toSet()
} }
override fun onBind(intent: Intent?): IBinder { override fun onBind(intent: Intent?): IBinder {
@ -251,5 +265,11 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
companion object { companion object {
const val EXTRA_ROOM_ARGS = "EXTRA_ROOM_ARGS" const val EXTRA_ROOM_ARGS = "EXTRA_ROOM_ARGS"
private const val FOREGROUND_SERVICE_NOTIFICATION_ID = 300
} }
private data class LiveInfo(
val beaconEventId: String,
val roomArgs: RoomArgs
)
} }

View File

@ -105,7 +105,7 @@ class NotificationUtils @Inject constructor(
const val SMART_REPLY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.SMART_REPLY_ACTION" const val SMART_REPLY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.SMART_REPLY_ACTION"
const val DISMISS_SUMMARY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_SUMMARY_ACTION" const val DISMISS_SUMMARY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_SUMMARY_ACTION"
const val DISMISS_ROOM_NOTIF_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_ROOM_NOTIF_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 TAP_TO_VIEW_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.TAP_TO_VIEW_ACTION"
const val DIAGNOSTIC_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DIAGNOSTIC" const val DIAGNOSTIC_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DIAGNOSTIC"
const val PUSH_ACTION = "${BuildConfig.APPLICATION_ID}.PUSH" const val PUSH_ACTION = "${BuildConfig.APPLICATION_ID}.PUSH"
@ -118,7 +118,7 @@ class NotificationUtils @Inject constructor(
private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID" private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID"
private const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2"
private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2" private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2"
fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
@ -540,20 +540,6 @@ class NotificationUtils @Inject constructor(
return builder.build() return builder.build()
} }
/**
* Creates a notification that indicates the application is retrieving location even if it is in background or killed.
*/
fun buildLiveLocationSharingNotification(): Notification {
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
.setContentTitle(stringProvider.getString(R.string.live_location_sharing_notification_title))
.setContentText(stringProvider.getString(R.string.live_location_sharing_notification_description))
.setSmallIcon(R.drawable.ic_attachment_location_live_white)
.setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
.setCategory(NotificationCompat.CATEGORY_LOCATION_SHARING)
.setContentIntent(buildOpenHomePendingIntentForSummary())
.build()
}
/** /**
* Creates a notification that indicates the application is capturing the screen. * Creates a notification that indicates the application is capturing the screen.
*/ */