Create home shortcut for any room (#1525)

This commit is contained in:
Benoit Marty 2020-10-01 18:33:33 +02:00 committed by Benoit Marty
parent 28b039fde3
commit 92e021a3d7
10 changed files with 151 additions and 55 deletions

View File

@ -9,6 +9,7 @@ Improvements 🙌:
- PIN code: request PIN code if phone has been locked - PIN code: request PIN code if phone has been locked
- Small optimisation of scrolling experience in timeline (#2114) - Small optimisation of scrolling experience in timeline (#2114)
- Allow user to reset cross signing if he has no way to recover (#2052) - Allow user to reset cross signing if he has no way to recover (#2052)
- Create home shortcut for any room (#1525)
Bugfix 🐛: Bugfix 🐛:
- Improve support for image/audio/video/file selection with intent changes (#1376) - Improve support for image/audio/video/file selection with intent changes (#1376)

View File

@ -0,0 +1,78 @@
/*
* 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.home
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import androidx.annotation.WorkerThread
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import im.vector.app.core.glide.GlideApp
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.RoomDetailActivity
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
private val useAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
private const val adaptiveIconSizeDp = 108
private const val adaptiveIconOuterSidesDp = 18
class ShortcutCreator @Inject constructor(
private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val dimensionConverter: DimensionConverter
) {
private val adaptiveIconSize = dimensionConverter.dpToPx(adaptiveIconSizeDp)
private val adaptiveIconOuterSides = dimensionConverter.dpToPx(adaptiveIconOuterSidesDp)
private val iconSize by lazy {
if (useAdaptiveIcon) {
adaptiveIconSize - adaptiveIconOuterSides
} else {
dimensionConverter.dpToPx(72)
}
}
fun canCreateShortcut(): Boolean {
return ShortcutManagerCompat.isRequestPinShortcutSupported(context)
}
@WorkerThread
fun create(roomSummary: RoomSummary): ShortcutInfoCompat {
val intent = RoomDetailActivity.shortcutIntent(context, roomSummary.roomId)
val bitmap = try {
avatarRenderer.shortcutDrawable(GlideApp.with(context), roomSummary.toMatrixItem(), iconSize)
} catch (failure: Throwable) {
null
}
return ShortcutInfoCompat.Builder(context, roomSummary.roomId)
.setShortLabel(roomSummary.displayName)
.setIcon(bitmap?.toProfileImageIcon())
.setIntent(intent)
.build()
}
private fun Bitmap.toProfileImageIcon(): IconCompat {
return if (useAdaptiveIcon) {
IconCompat.createWithAdaptiveBitmap(this)
} else {
IconCompat.createWithBitmap(this)
}
}
}

View File

@ -18,40 +18,19 @@ package im.vector.app.features.home
import android.content.Context import android.content.Context
import android.content.pm.ShortcutManager import android.content.pm.ShortcutManager
import android.graphics.Bitmap
import android.os.Build import android.os.Build
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import im.vector.app.core.glide.GlideApp
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.RoomDetailActivity
import org.matrix.android.sdk.api.util.toMatrixItem
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import javax.inject.Inject import javax.inject.Inject
private val useAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
private const val adaptiveIconSizeDp = 108
private const val adaptiveIconOuterSidesDp = 18
class ShortcutsHandler @Inject constructor( class ShortcutsHandler @Inject constructor(
private val context: Context, private val context: Context,
private val homeRoomListStore: HomeRoomListDataSource, private val homeRoomListStore: HomeRoomListDataSource,
private val avatarRenderer: AvatarRenderer, private val shortcutCreator: ShortcutCreator
private val dimensionConverter: DimensionConverter
) { ) {
private val adaptiveIconSize = dimensionConverter.dpToPx(adaptiveIconSizeDp)
private val adaptiveIconOuterSides = dimensionConverter.dpToPx(adaptiveIconOuterSidesDp)
private val iconSize by lazy {
if (useAdaptiveIcon) {
adaptiveIconSize - adaptiveIconOuterSides
} else {
dimensionConverter.dpToPx(72)
}
}
fun observeRoomsAndBuildShortcuts(): Disposable { fun observeRoomsAndBuildShortcuts(): Disposable {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
@ -67,19 +46,7 @@ class ShortcutsHandler @Inject constructor(
val shortcuts = rooms val shortcuts = rooms
.filter { room -> room.isFavorite } .filter { room -> room.isFavorite }
.take(n = 4) // Android only allows us to create 4 shortcuts .take(n = 4) // Android only allows us to create 4 shortcuts
.map { room -> .map { shortcutCreator.create(it) }
val intent = RoomDetailActivity.shortcutIntent(context, room.roomId)
val bitmap = try {
avatarRenderer.shortcutDrawable(GlideApp.with(context), room.toMatrixItem(), iconSize)
} catch (failure: Throwable) {
null
}
ShortcutInfoCompat.Builder(context, room.roomId)
.setShortLabel(room.displayName)
.setIcon(bitmap?.toProfileImageIcon())
.setIntent(intent)
.build()
}
ShortcutManagerCompat.removeAllDynamicShortcuts(context) ShortcutManagerCompat.removeAllDynamicShortcuts(context)
ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts) ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts)
@ -104,14 +71,4 @@ class ShortcutsHandler @Inject constructor(
} }
} }
} }
// PRIVATE API *********************************************************************************
private fun Bitmap.toProfileImageIcon(): IconCompat {
return if (useAdaptiveIcon) {
IconCompat.createWithAdaptiveBitmap(this)
} else {
IconCompat.createWithBitmap(this)
}
}
} }

View File

@ -26,4 +26,5 @@ sealed class RoomProfileAction: VectorViewModelAction {
data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction() data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction()
data class ChangeRoomAvatar(val uri: Uri, val fileName: String?) : RoomProfileAction() data class ChangeRoomAvatar(val uri: Uri, val fileName: String?) : RoomProfileAction()
object ShareRoomProfile : RoomProfileAction() object ShareRoomProfile : RoomProfileAction()
object CreateShortcut : RoomProfileAction()
} }

View File

@ -24,6 +24,7 @@ import im.vector.app.core.epoxy.profiles.buildProfileSection
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.features.home.ShortcutCreator
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import javax.inject.Inject import javax.inject.Inject
@ -31,6 +32,7 @@ import javax.inject.Inject
class RoomProfileController @Inject constructor( class RoomProfileController @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val shortcutCreator: ShortcutCreator,
colorProvider: ColorProvider colorProvider: ColorProvider
) : TypedEpoxyController<RoomProfileViewState>() { ) : TypedEpoxyController<RoomProfileViewState>() {
@ -44,6 +46,7 @@ class RoomProfileController @Inject constructor(
fun onBannedMemberListClicked() fun onBannedMemberListClicked()
fun onNotificationsClicked() fun onNotificationsClicked()
fun onUploadsClicked() fun onUploadsClicked()
fun createShortcut()
fun onSettingsClicked() fun onSettingsClicked()
fun onLeaveRoomClicked() fun onLeaveRoomClicked()
fun onRoomIdClicked() fun onRoomIdClicked()
@ -114,6 +117,16 @@ class RoomProfileController @Inject constructor(
icon = R.drawable.ic_room_profile_uploads, icon = R.drawable.ic_room_profile_uploads,
action = { callback?.onUploadsClicked() } action = { callback?.onUploadsClicked() }
) )
if (shortcutCreator.canCreateShortcut()) {
buildProfileAction(
id = "shortcut",
title = stringProvider.getString(R.string.room_settings_add_homescreen_shortcut),
dividerColor = dividerColor,
editable = false,
icon = R.drawable.ic_add_to_home_screen_24dp,
action = { callback?.createShortcut() }
)
}
buildProfileAction( buildProfileAction(
id = "leave", id = "leave",
title = stringProvider.getString(if (roomSummary.isDirect) { title = stringProvider.getString(if (roomSummary.isDirect) {

View File

@ -27,6 +27,7 @@ import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -115,6 +116,7 @@ class RoomProfileFragment @Inject constructor(
is RoomProfileViewEvents.Failure -> showFailure(it.throwable) is RoomProfileViewEvents.Failure -> showFailure(it.throwable)
is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink) is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink)
RoomProfileViewEvents.OnChangeAvatarSuccess -> dismissLoadingDialog() RoomProfileViewEvents.OnChangeAvatarSuccess -> dismissLoadingDialog()
is RoomProfileViewEvents.OnShortcutReady -> addShortcut(it)
}.exhaustive }.exhaustive
} }
roomListQuickActionsSharedActionViewModel roomListQuickActionsSharedActionViewModel
@ -232,6 +234,16 @@ class RoomProfileFragment @Inject constructor(
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomUploads) roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomUploads)
} }
override fun createShortcut() {
// Ask the view model to prepare it...
roomProfileViewModel.handle(RoomProfileAction.CreateShortcut)
}
private fun addShortcut(onShortcutReady: RoomProfileViewEvents.OnShortcutReady) {
// ... and propose the user to add it
ShortcutManagerCompat.requestPinShortcut(requireContext(), onShortcutReady.shortcutInfo, null)
}
override fun onLeaveRoomClicked() { override fun onLeaveRoomClicked() {
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setTitle(R.string.room_participants_leave_prompt_title) .setTitle(R.string.room_participants_leave_prompt_title)

View File

@ -16,6 +16,7 @@
package im.vector.app.features.roomprofile package im.vector.app.features.roomprofile
import androidx.core.content.pm.ShortcutInfoCompat
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
/** /**
@ -27,4 +28,5 @@ sealed class RoomProfileViewEvents : VectorViewEvents {
object OnChangeAvatarSuccess : RoomProfileViewEvents() object OnChangeAvatarSuccess : RoomProfileViewEvents()
data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents() data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents()
data class OnShortcutReady(val shortcutInfo: ShortcutInfoCompat) : RoomProfileViewEvents()
} }

View File

@ -17,15 +17,20 @@
package im.vector.app.features.roomprofile package im.vector.app.features.roomprofile
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.ShortcutCreator
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
@ -36,10 +41,12 @@ import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap import org.matrix.android.sdk.rx.unwrap
import java.util.UUID import java.util.UUID
class RoomProfileViewModel @AssistedInject constructor(@Assisted private val initialState: RoomProfileViewState, class RoomProfileViewModel @AssistedInject constructor(
private val stringProvider: StringProvider, @Assisted private val initialState: RoomProfileViewState,
private val session: Session) private val stringProvider: StringProvider,
: VectorViewModel<RoomProfileViewState, RoomProfileAction, RoomProfileViewEvents>(initialState) { private val shortcutCreator: ShortcutCreator,
private val session: Session
) : VectorViewModel<RoomProfileViewState, RoomProfileAction, RoomProfileViewEvents>(initialState) {
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -88,11 +95,24 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini
} }
} }
override fun handle(action: RoomProfileAction) = when (action) { override fun handle(action: RoomProfileAction) {
RoomProfileAction.LeaveRoom -> handleLeaveRoom() when (action) {
is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) RoomProfileAction.LeaveRoom -> handleLeaveRoom()
is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile() is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
is RoomProfileAction.ChangeRoomAvatar -> handleChangeAvatar(action) is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile()
is RoomProfileAction.ChangeRoomAvatar -> handleChangeAvatar(action)
RoomProfileAction.CreateShortcut -> handleCreateShortcut()
}.exhaustive
}
private fun handleCreateShortcut() {
viewModelScope.launch(Dispatchers.IO) {
withState { state ->
state.roomSummary()
?.let { shortcutCreator.create(it) }
?.let { _viewEvents.post(RoomProfileViewEvents.OnShortcutReady(it)) }
}
}
} }
private fun handleChangeNotificationMode(action: RoomProfileAction.ChangeRoomNotificationState) { private fun handleChangeNotificationMode(action: RoomProfileAction.ChangeRoomNotificationState) {

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18,1.01L8,1C6.9,1 6,1.9 6,3V6C6,6.55 6.45,7 7,7C7.55,7 8,6.55 8,6V4.5H18V19.5H8V18C8,17.45 7.55,17 7,17C6.45,17 6,17.45 6,18V21C6,22.1 6.9,23 8,23H18C19.1,23 20,22.1 20,21V3C20,1.9 19.1,1.01 18,1.01Z"
android:fillColor="#000000"/>
<path
android:pathData="M11,14.45C11,15 10.55,15.45 10,15.45C9.45,15.45 9,15 9,14.45V11.86L4.11,16.75C3.72,17.14 3.09,17.14 2.7,16.75C2.31,16.36 2.31,15.73 2.7,15.34L7.59,10.45H5C4.45,10.45 4,10 4,9.45C4,8.9 4.45,8.45 5,8.45H10C10.55,8.45 11,8.9 11,9.45V14.45Z"
android:fillColor="#000000"/>
</vector>

View File

@ -664,7 +664,7 @@
<string name="room_settings_direct_chat">Direct Chat</string> <string name="room_settings_direct_chat">Direct Chat</string>
<string name="room_settings_leave_conversation">Leave Conversation</string> <string name="room_settings_leave_conversation">Leave Conversation</string>
<string name="room_settings_forget">Forget</string> <string name="room_settings_forget">Forget</string>
<string name="room_settings_add_homescreen_shortcut">Add Homescreen Shortcut</string> <string name="room_settings_add_homescreen_shortcut">Add to Home screen</string>
<!-- home sliding menu --> <!-- home sliding menu -->
<string name="room_sliding_menu_messages">Messages</string> <string name="room_sliding_menu_messages">Messages</string>