diff --git a/CHANGES.md b/CHANGES.md index e4e0ae9df0..3f9bdc85eb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ Changes in RiotX 0.20.0 (2020-XX-XX) =================================================== Features ✨: - - + - Add Direct Shortcuts (#652) Improvements 🙌: - Improve notification accessibility with ticker text (#1226) diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt index 6d85dd8a3e..2edb2c4edf 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt @@ -17,11 +17,13 @@ package im.vector.riotx.features.home import android.content.Context +import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.widget.ImageView import androidx.annotation.AnyThread import androidx.annotation.UiThread import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap import com.amulyakhare.textdrawable.TextDrawable import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.DrawableImageViewTarget @@ -72,6 +74,28 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active .into(target) } + @AnyThread + fun shortcutDrawable(context: Context, glideRequest: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap { + return glideRequest + .asBitmap() + .apply { + val resolvedUrl = resolvedUrl(matrixItem.avatarUrl) + if (resolvedUrl != null) { + load(resolvedUrl) + } else { + val avatarColor = avatarColor(matrixItem, context) + load(TextDrawable.builder() + .beginConfig() + .bold() + .endConfig() + .buildRect(matrixItem.firstLetterOfDisplayName(), avatarColor) + .toBitmap(width = iconSize, height = iconSize)) + } + } + .submit(iconSize, iconSize) + .get() + } + @AnyThread fun getCachedDrawable(glideRequest: GlideRequests, matrixItem: MatrixItem): Drawable { return buildGlideRequest(glideRequest, matrixItem.avatarUrl) @@ -82,10 +106,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active @AnyThread fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable { - val avatarColor = when (matrixItem) { - is MatrixItem.UserItem -> ContextCompat.getColor(context, getColorFromUserId(matrixItem.id)) - else -> ContextCompat.getColor(context, getColorFromRoomId(matrixItem.id)) - } + val avatarColor = avatarColor(matrixItem, context) return TextDrawable.builder() .beginConfig() .bold() @@ -96,11 +117,21 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active // PRIVATE API ********************************************************************************* private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest { - val resolvedUrl = activeSessionHolder.getSafeActiveSession()?.contentUrlResolver() - ?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) - + val resolvedUrl = resolvedUrl(avatarUrl) return glideRequest .load(resolvedUrl) .apply(RequestOptions.circleCropTransform()) } + + private fun resolvedUrl(avatarUrl: String?): String? { + return activeSessionHolder.getSafeActiveSession()?.contentUrlResolver() + ?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) + } + + private fun avatarColor(matrixItem: MatrixItem, context: Context): Int { + return when (matrixItem) { + is MatrixItem.UserItem -> ContextCompat.getColor(context, getColorFromUserId(matrixItem.id)) + else -> ContextCompat.getColor(context, getColorFromRoomId(matrixItem.id)) + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 60c974c291..b6e3cbcd76 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -65,6 +65,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var popupAlertManager: PopupAlertManager + @Inject lateinit var shortcutsHandler: ShortcutsHandler private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { override fun onDrawerStateChanged(newState: Int) { @@ -144,6 +145,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { && activeSessionHolder.getSafeActiveSession()?.hasAlreadySynced() == true) { promptCompleteSecurityIfNeeded() } + + shortcutsHandler.observeRoomsAndBuildShortcuts() + .disposeOnDestroy() } private fun promptCompleteSecurityIfNeeded() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt b/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt new file mode 100644 index 0000000000..657942457e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt @@ -0,0 +1,88 @@ +/* + * 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.riotx.features.home + +import android.content.Context +import android.graphics.Bitmap +import android.os.Build +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import im.vector.matrix.android.api.session.room.model.tag.RoomTag +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.core.glide.GlideApp +import im.vector.riotx.core.utils.DimensionConverter +import im.vector.riotx.features.home.room.detail.RoomDetailActivity +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 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 { + return homeRoomListStore + .observe() + .distinct() + .observeOn(Schedulers.computation()) + .subscribe { rooms -> + val shortcuts = rooms + .filter { room -> room.tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE } } + .take(n = 4) // Android only allows us to create 4 shortcuts + .map { room -> + val intent = RoomDetailActivity.shortcutIntent(context, room.roomId) + val bitmap = avatarRenderer.shortcutDrawable(context, GlideApp.with(context), room.toMatrixItem(), iconSize) + + ShortcutInfoCompat.Builder(context, room.roomId) + .setShortLabel(room.displayName) + .setIcon(bitmap.toProfileImageIcon()) + .setIntent(intent) + .build() + } + + ShortcutManagerCompat.removeAllDynamicShortcuts(context) + ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts) + } + } + + // 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/riotx/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt index fe4d0ae1f7..6507bf6030 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt @@ -44,8 +44,14 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { super.onCreate(savedInstanceState) waitingView = waiting_view if (isFirstCreation()) { - val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) - ?: return + val roomDetailArgs: RoomDetailArgs? = if (intent?.action == ACTION_ROOM_DETAILS_FROM_SHORTCUT) { + RoomDetailArgs(roomId = intent?.extras?.getString(EXTRA_ROOM_ID)!!) + } else { + intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) + } + + if (roomDetailArgs == null) return + currentRoomId = roomDetailArgs.roomId replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, roomDetailArgs) replaceFragment(R.id.roomDetailDrawerContainer, BreadcrumbsFragment::class.java) @@ -110,11 +116,20 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { companion object { const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS" + const val EXTRA_ROOM_ID = "EXTRA_ROOM_ID" + const val ACTION_ROOM_DETAILS_FROM_SHORTCUT = "ROOM_DETAILS_FROM_SHORTCUT" fun newIntent(context: Context, roomDetailArgs: RoomDetailArgs): Intent { return Intent(context, RoomDetailActivity::class.java).apply { putExtra(EXTRA_ROOM_DETAIL_ARGS, roomDetailArgs) } } + + fun shortcutIntent(context: Context, roomId: String): Intent { + return Intent(context, RoomDetailActivity::class.java).apply { + action = ACTION_ROOM_DETAILS_FROM_SHORTCUT + putExtra(EXTRA_ROOM_ID, roomId) + } + } } }