diff --git a/CHANGES.md b/CHANGES.md index 026d7af6cd..754fb35612 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ Improvements 🙌: - PIN code: request PIN code if phone has been locked - 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) Bugfix 🐛: - Improve support for image/audio/video/file selection with intent changes (#1376) diff --git a/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt b/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt new file mode 100644 index 0000000000..171cc7b3d9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt @@ -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) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt b/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt index 1a476913f3..3684a8b3f8 100644 --- a/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt @@ -18,40 +18,19 @@ package im.vector.app.features.home import android.content.Context import android.content.pm.ShortcutManager -import android.graphics.Bitmap import android.os.Build import androidx.core.content.getSystemService -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.util.toMatrixItem import io.reactivex.Observable import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers 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( private val context: Context, private val homeRoomListStore: HomeRoomListDataSource, - private val avatarRenderer: AvatarRenderer, - private val dimensionConverter: DimensionConverter + private val shortcutCreator: ShortcutCreator ) { - 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 { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { @@ -67,19 +46,7 @@ class ShortcutsHandler @Inject constructor( val shortcuts = rooms .filter { room -> room.isFavorite } .take(n = 4) // Android only allows us to create 4 shortcuts - .map { room -> - 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() - } + .map { shortcutCreator.create(it) } ShortcutManagerCompat.removeAllDynamicShortcuts(context) 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) - } - } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt index 8061ad6bfc..1f89380449 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt @@ -26,4 +26,5 @@ sealed class RoomProfileAction: VectorViewModelAction { data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction() data class ChangeRoomAvatar(val uri: Uri, val fileName: String?) : RoomProfileAction() object ShareRoomProfile : RoomProfileAction() + object CreateShortcut : RoomProfileAction() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt index ccfbf6d4fa..160ebd56be 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt @@ -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.StringProvider import im.vector.app.core.ui.list.genericFooterItem +import im.vector.app.features.home.ShortcutCreator import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import javax.inject.Inject @@ -31,6 +32,7 @@ import javax.inject.Inject class RoomProfileController @Inject constructor( private val stringProvider: StringProvider, private val vectorPreferences: VectorPreferences, + private val shortcutCreator: ShortcutCreator, colorProvider: ColorProvider ) : TypedEpoxyController() { @@ -44,6 +46,7 @@ class RoomProfileController @Inject constructor( fun onBannedMemberListClicked() fun onNotificationsClicked() fun onUploadsClicked() + fun createShortcut() fun onSettingsClicked() fun onLeaveRoomClicked() fun onRoomIdClicked() @@ -114,6 +117,16 @@ class RoomProfileController @Inject constructor( icon = R.drawable.ic_room_profile_uploads, 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( id = "leave", title = stringProvider.getString(if (roomSummary.isDirect) { @@ -124,6 +137,7 @@ class RoomProfileController @Inject constructor( dividerColor = dividerColor, divider = false, destructive = true, + icon = R.drawable.ic_room_actions_leave, editable = false, action = { callback?.onLeaveRoomClicked() } ) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index d94a326ba9..477d557bea 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -27,6 +27,7 @@ import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.app.ActivityOptionsCompat +import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.net.toUri import androidx.core.view.ViewCompat import androidx.core.view.isVisible @@ -115,6 +116,7 @@ class RoomProfileFragment @Inject constructor( is RoomProfileViewEvents.Failure -> showFailure(it.throwable) is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink) RoomProfileViewEvents.OnChangeAvatarSuccess -> dismissLoadingDialog() + is RoomProfileViewEvents.OnShortcutReady -> addShortcut(it) }.exhaustive } roomListQuickActionsSharedActionViewModel @@ -232,6 +234,16 @@ class RoomProfileFragment @Inject constructor( 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() { AlertDialog.Builder(requireContext()) .setTitle(R.string.room_participants_leave_prompt_title) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt index 1245016ada..380efd6fcd 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt @@ -16,6 +16,7 @@ package im.vector.app.features.roomprofile +import androidx.core.content.pm.ShortcutInfoCompat import im.vector.app.core.platform.VectorViewEvents /** @@ -27,4 +28,5 @@ sealed class RoomProfileViewEvents : VectorViewEvents { object OnChangeAvatarSuccess : RoomProfileViewEvents() data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents() + data class OnShortcutReady(val shortcutInfo: ShortcutInfoCompat) : RoomProfileViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt index 2d409888e9..12e88c2dec 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt @@ -17,15 +17,20 @@ package im.vector.app.features.roomprofile +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.home.ShortcutCreator 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.session.Session 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 java.util.UUID -class RoomProfileViewModel @AssistedInject constructor(@Assisted private val initialState: RoomProfileViewState, - private val stringProvider: StringProvider, - private val session: Session) - : VectorViewModel(initialState) { +class RoomProfileViewModel @AssistedInject constructor( + @Assisted private val initialState: RoomProfileViewState, + private val stringProvider: StringProvider, + private val shortcutCreator: ShortcutCreator, + private val session: Session +) : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -88,11 +95,24 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini } } - override fun handle(action: RoomProfileAction) = when (action) { - RoomProfileAction.LeaveRoom -> handleLeaveRoom() - is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) - is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile() - is RoomProfileAction.ChangeRoomAvatar -> handleChangeAvatar(action) + override fun handle(action: RoomProfileAction) { + when (action) { + RoomProfileAction.LeaveRoom -> handleLeaveRoom() + is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(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) { diff --git a/vector/src/main/res/drawable/ic_add_to_home_screen_24dp.xml b/vector/src/main/res/drawable/ic_add_to_home_screen_24dp.xml new file mode 100644 index 0000000000..4a0a809578 --- /dev/null +++ b/vector/src/main/res/drawable/ic_add_to_home_screen_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 0edd930498..623e5e205f 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -664,7 +664,7 @@ Direct Chat Leave Conversation Forget - Add Homescreen Shortcut + Add to Home screen Messages