~evilham/unchat-android

bc889cbcf4b829335d61aa03144cd415370b65ca — Benoit Marty 3 years ago e3b88d1 + 835a369
Merge pull request #2444 from vector-im/feature/bca/deeplink_mxto

Fix issues with matrix.to deep linking
M matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt => matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt +5 -0
@@ 36,6 36,11 @@ interface UserService {
    fun getUser(userId: String): User?

    /**
     * Try to resolve user from known users, or using profile api
     */
    fun resolveUser(userId: String, callback: MatrixCallback<User>)

    /**
     * Search list of users on server directory.
     * @param search the searched term
     * @param limit the max number of users to return

M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt => matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt +31 -0
@@ 19,10 19,13 @@ package org.matrix.android.sdk.internal.session.user
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.session.user.UserService
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateIgnoredUserIdsTask
import org.matrix.android.sdk.internal.session.user.model.SearchUserTask
import org.matrix.android.sdk.internal.task.TaskExecutor


@@ 32,12 35,40 @@ import javax.inject.Inject
internal class DefaultUserService @Inject constructor(private val userDataSource: UserDataSource,
                                                      private val searchUserTask: SearchUserTask,
                                                      private val updateIgnoredUserIdsTask: UpdateIgnoredUserIdsTask,
                                                      private val getProfileInfoTask: GetProfileInfoTask,
                                                      private val taskExecutor: TaskExecutor) : UserService {

    override fun getUser(userId: String): User? {
        return userDataSource.getUser(userId)
    }

    override fun resolveUser(userId: String, callback: MatrixCallback<User>) {
        val known = getUser(userId)
        if (known != null) {
            callback.onSuccess(known)
        } else {
            val params = GetProfileInfoTask.Params(userId)
            getProfileInfoTask
                    .configureWith(params) {
                        this.callback = object : MatrixCallback<JsonDict> {
                            override fun onSuccess(data: JsonDict) {
                                callback.onSuccess(
                                        User(
                                                userId,
                                                data[ProfileService.DISPLAY_NAME_KEY] as? String,
                                                data[ProfileService.AVATAR_URL_KEY] as? String)
                                )
                            }

                            override fun onFailure(failure: Throwable) {
                                callback.onFailure(failure)
                            }
                        }
                    }
                    .executeBy(taskExecutor)
        }
    }

    override fun getUserLive(userId: String): LiveData<Optional<User>> {
        return userDataSource.getUserLive(userId)
    }

M vector/src/main/AndroidManifest.xml => vector/src/main/AndroidManifest.xml +3 -3
@@ 81,7 81,8 @@
                android:resource="@xml/shortcuts" />
        </activity-alias>

        <activity android:name=".features.home.HomeActivity" />
        <activity android:name=".features.home.HomeActivity"
            android:launchMode="singleTask"/>
        <activity
            android:name=".features.login.LoginActivity"
            android:launchMode="singleTask"


@@ 189,10 190,9 @@
        <activity
            android:name=".features.signout.soft.SoftLogoutActivity"
            android:windowSoftInputMode="adjustResize" />
        <activity android:name=".features.permalink.PermalinkHandlerActivity">
        <activity android:name=".features.permalink.PermalinkHandlerActivity" android:launchMode="singleTask">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />


M vector/src/main/java/im/vector/app/features/home/HomeActivity.kt => vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +47 -3
@@ 18,6 18,7 @@ package im.vector.app.features.home

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.view.MenuItem


@@ 38,8 39,12 @@ import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.utils.toast
import im.vector.app.features.disclaimer.showDisclaimerDialog
import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.permalink.NavigationInterceptor
import im.vector.app.features.permalink.PermalinkHandler
import im.vector.app.features.popup.DefaultVectorAlert
import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.popup.VerificationVectorAlert


@@ 50,10 55,12 @@ import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import im.vector.app.features.workers.signout.ServerBackupStatusViewState
import im.vector.app.push.fcm.FcmHelper
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import org.matrix.android.sdk.api.session.InitialSyncProgressService
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.util.MatrixItem
import timber.log.Timber
import javax.inject.Inject


@@ 64,7 71,8 @@ data class HomeActivityArgs(
        val accountCreation: Boolean
) : Parcelable

class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory {
class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory,
        NavigationInterceptor {

    private lateinit var sharedActionViewModel: HomeSharedActionViewModel



@@ 82,6 90,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
    @Inject lateinit var popupAlertManager: PopupAlertManager
    @Inject lateinit var shortcutsHandler: ShortcutsHandler
    @Inject lateinit var unknownDeviceViewModelFactory: UnknownDeviceDetectorSharedViewModel.Factory
    @Inject lateinit var permalinkHandler: PermalinkHandler

    private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
        override fun onDrawerStateChanged(newState: Int) {


@@ 145,6 154,28 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet

        shortcutsHandler.observeRoomsAndBuildShortcuts()
                .disposeOnDestroy()

        if (isFirstCreation()) {
            handleIntent(intent)
        }
    }

    private fun handleIntent(intent: Intent?) {
        intent?.dataString?.let { deepLink ->
            if (!deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE)) return@let

            permalinkHandler.launch(this, deepLink,
                    navigationInterceptor = this,
                    buildTask = true)
                    // .delay(500, TimeUnit.MILLISECONDS)
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe { isHandled ->
                        if (!isHandled) {
                            toast(R.string.permalink_malformed)
                        }
                    }
                    .disposeOnDestroy()
        }
    }

    private fun renderState(state: HomeActivityViewState) {


@@ 270,6 301,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
        if (intent?.getParcelableExtra<HomeActivityArgs>(MvRx.KEY_ARG)?.clearNotification == true) {
            notificationDrawerManager.clearAllEvents()
        }
        handleIntent(intent)
    }

    override fun onDestroy() {


@@ 313,11 345,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
                bugReporter.openBugReportScreen(this, false)
                return true
            }
            R.id.menu_home_filter     -> {
            R.id.menu_home_filter -> {
                navigator.openRoomsFiltering(this)
                return true
            }
            R.id.menu_home_setting    -> {
            R.id.menu_home_setting -> {
                navigator.openSettings(this)
                return true
            }


@@ 334,6 366,18 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
        }
    }

    override fun navToMemberProfile(userId: String, deepLink: Uri): Boolean {
        val listener = object : MatrixToBottomSheet.InteractionListener {
            override fun navigateToRoom(roomId: String) {
                navigator.openRoom(this@HomeActivity, roomId)
            }
        }
        // TODO check if there is already one??
        MatrixToBottomSheet.withLink(deepLink.toString(), listener)
                .show(supportFragmentManager, "HA#MatrixToBottomSheet")
        return true
    }

    companion object {
        fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent {
            val args = HomeActivityArgs(

M vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt => vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +1 -1
@@ 1460,7 1460,7 @@ class RoomDetailFragment @Inject constructor(
                        return false
                    }

                    override fun navToMemberProfile(userId: String): Boolean {
                    override fun navToMemberProfile(userId: String, deepLink: Uri): Boolean {
                        openRoomMemberProfile(userId)
                        return true
                    }

A vector/src/main/java/im/vector/app/features/matrixto/MatrixToAction.kt => vector/src/main/java/im/vector/app/features/matrixto/MatrixToAction.kt +24 -0
@@ 0,0 1,24 @@
/*
 * 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.matrixto

import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.util.MatrixItem

sealed class MatrixToAction : VectorViewModelAction {
    data class StartChattingWithUser(val matrixItem: MatrixItem) : MatrixToAction()
}

M vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt => vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt +92 -12
@@ 17,23 17,37 @@
package im.vector.app.features.matrixto

import android.os.Bundle
import android.os.Parcelable
import android.view.View
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.features.home.AvatarRenderer
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.bottom_sheet_matrix_to_card.*
import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject

class MatrixToBottomSheet(private val matrixItem: MatrixItem) : VectorBaseBottomSheetDialogFragment() {
class MatrixToBottomSheet : VectorBaseBottomSheetDialogFragment() {

    @Parcelize
    data class MatrixToArgs(
            val matrixToLink: String
    ) : Parcelable

    @Inject lateinit var avatarRenderer: AvatarRenderer

    interface InteractionListener {
        fun didTapStartMessage(matrixItem: MatrixItem)
    }
    @Inject
    lateinit var matrixToBottomSheetViewModelFactory: MatrixToBottomSheetViewModel.Factory

    override fun injectWith(injector: ScreenComponent) {
        injector.inject(this)


@@ 43,21 57,87 @@ class MatrixToBottomSheet(private val matrixItem: MatrixItem) : VectorBaseBottom

    override fun getLayoutResId() = R.layout.bottom_sheet_matrix_to_card

    private val viewModel by fragmentViewModel(MatrixToBottomSheetViewModel::class)

    interface InteractionListener {
        fun navigateToRoom(roomId: String)
    }

    override fun invalidate() = withState(viewModel) { state ->
        super.invalidate()
        when (val item = state.matrixItem) {
            Uninitialized -> {
                matrixToCardContentLoading.isVisible = false
                matrixToCardUserContentVisibility.isVisible = false
            }
            is Loading -> {
                matrixToCardContentLoading.isVisible = true
                matrixToCardUserContentVisibility.isVisible = false
            }
            is Success -> {
                matrixToCardContentLoading.isVisible = false
                matrixToCardUserContentVisibility.isVisible = true
                matrixToCardNameText.setTextOrHide(item.invoke().displayName)
                matrixToCardUserIdText.setTextOrHide(item.invoke().id)
                avatarRenderer.render(item.invoke(), matrixToCardAvatar)
            }
            is Fail -> {
                // TODO display some error copy?
                dismiss()
            }
        }

        when (state.startChattingState) {
            Uninitialized -> {
                matrixToCardButtonLoading.isVisible = false
                matrixToCardSendMessageButton.isVisible = false
            }
            is Success -> {
                matrixToCardButtonLoading.isVisible = false
                matrixToCardSendMessageButton.isVisible = true
            }
            is Fail -> {
                matrixToCardButtonLoading.isVisible = false
                matrixToCardSendMessageButton.isVisible = true
                // TODO display some error copy?
                dismiss()
            }
            is Loading -> {
                matrixToCardButtonLoading.isVisible = true
                matrixToCardSendMessageButton.isInvisible = true
            }
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        matrixToCardSendMessageButton.debouncedClicks {
            interactionListener?.didTapStartMessage(matrixItem)
            dismiss()
            withState(viewModel) {
                it.matrixItem.invoke()?.let { item ->
                    viewModel.handle(MatrixToAction.StartChattingWithUser(item))
                }
            }
        }

        matrixToCardNameText.setTextOrHide(matrixItem.displayName)
        matrixToCardUserIdText.setTextOrHide(matrixItem.id)
        avatarRenderer.render(matrixItem, matrixToCardAvatar)
        viewModel.observeViewEvents {
            when (it) {
                is MatrixToViewEvents.NavigateToRoom -> {
                    interactionListener?.navigateToRoom(it.roomId)
                    dismiss()
                }
                MatrixToViewEvents.Dismiss -> dismiss()
            }
        }
    }

    companion object {
        fun create(matrixItem: MatrixItem, listener: InteractionListener?): MatrixToBottomSheet {
            return MatrixToBottomSheet(matrixItem).apply {
        fun withLink(matrixToLink: String, listener: InteractionListener?): MatrixToBottomSheet {
            return MatrixToBottomSheet().apply {
                arguments = Bundle().apply {
                    putParcelable(MvRx.KEY_ARG, MatrixToBottomSheet.MatrixToArgs(
                            matrixToLink = matrixToLink
                    ))
                }
                interactionListener = listener
            }
        }

A vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt => vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt +33 -0
@@ 0,0 1,33 @@
/*
 * 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.matrixto

import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.util.MatrixItem

data class MatrixToBottomSheetState(
        val deepLink: String,
        val matrixItem: Async<MatrixItem> = Uninitialized,
        val startChattingState: Async<Unit> = Uninitialized
) : MvRxState {

    constructor(args: MatrixToBottomSheet.MatrixToArgs) : this(
            deepLink = args.matrixToLink
    )
}

A vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt => vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt +166 -0
@@ 0,0 1,166 @@
/*
 * 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.matrixto

import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
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.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.util.awaitCallback

class MatrixToBottomSheetViewModel @AssistedInject constructor(
        @Assisted initialState: MatrixToBottomSheetState,
        private val session: Session,
        private val stringProvider: StringProvider,
        private val rawService: RawService) : VectorViewModel<MatrixToBottomSheetState, MatrixToAction, MatrixToViewEvents>(initialState) {

    @AssistedInject.Factory
    interface Factory {
        fun create(initialState: MatrixToBottomSheetState): MatrixToBottomSheetViewModel
    }

    init {
        setState {
            copy(matrixItem = Loading())
        }
        viewModelScope.launch(Dispatchers.IO) {
            resolveLink(initialState)
        }
    }

    private suspend fun resolveLink(initialState: MatrixToBottomSheetState) {
        val permalinkData = PermalinkParser.parse(initialState.deepLink)
        if (permalinkData is PermalinkData.FallbackLink) {
            setState {
                copy(
                        matrixItem = Fail(IllegalArgumentException(stringProvider.getString(R.string.permalink_malformed))),
                        startChattingState = Uninitialized
                )
            }
            return
        }

        when (permalinkData) {
            is PermalinkData.UserLink -> {
                val user = resolveUser(permalinkData.userId)
                setState {
                    copy(
                            matrixItem = Success(user.toMatrixItem()),
                            startChattingState = Success(Unit)
                    )
                }
            }
            is PermalinkData.RoomLink -> {
                // not yet supported
                _viewEvents.post(MatrixToViewEvents.Dismiss)
            }
            is PermalinkData.GroupLink -> {
                // not yet supported
                _viewEvents.post(MatrixToViewEvents.Dismiss)
            }
            is PermalinkData.FallbackLink -> {
                _viewEvents.post(MatrixToViewEvents.Dismiss)
            }
        }
    }

    private suspend fun resolveUser(userId: String): User {
        return tryOrNull {
            awaitCallback<User> {
                session.resolveUser(userId, it)
            }
        }
        // Create raw user in case the user is not searchable
                ?: User(userId, null, null)
    }

    companion object : MvRxViewModelFactory<MatrixToBottomSheetViewModel, MatrixToBottomSheetState> {
        override fun create(viewModelContext: ViewModelContext, state: MatrixToBottomSheetState): MatrixToBottomSheetViewModel? {
            val fragment: MatrixToBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()

            return fragment.matrixToBottomSheetViewModelFactory.create(state)
        }
    }

    override fun handle(action: MatrixToAction) {
        when (action) {
            is MatrixToAction.StartChattingWithUser -> handleStartChatting(action)
        }.exhaustive
    }

    private fun handleStartChatting(action: MatrixToAction.StartChattingWithUser) {
        val mxId = action.matrixItem.id
        val existing = session.getExistingDirectRoomWithUser(mxId)
        if (existing != null) {
            // navigate to this room
            _viewEvents.post(MatrixToViewEvents.NavigateToRoom(existing))
        } else {
            setState {
                copy(startChattingState = Loading())
            }
            // we should create the room then navigate
            viewModelScope.launch(Dispatchers.IO) {
                val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
                        ?.isE2EByDefault()
                        ?: true

                val roomParams = CreateRoomParams()
                        .apply {
                            invitedUserIds.add(mxId)
                            setDirectMessage()
                            enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault
                        }

                val roomId = try {
                    awaitCallback<String> { session.createRoom(roomParams, it) }
                } catch (failure: Throwable) {
                    setState {
                        copy(startChattingState = Fail(Exception(stringProvider.getString(R.string.invite_users_to_room_failure))))
                    }
                    return@launch
                }
                setState {
                    // we can hide this button has we will navigate out
                    copy(startChattingState = Uninitialized)
                }
                _viewEvents.post(MatrixToViewEvents.NavigateToRoom(roomId))
            }
        }
    }
}

A vector/src/main/java/im/vector/app/features/matrixto/MatrixToViewEvents.kt => vector/src/main/java/im/vector/app/features/matrixto/MatrixToViewEvents.kt +24 -0
@@ 0,0 1,24 @@
/*
 * 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.matrixto

import im.vector.app.core.platform.VectorViewEvents

sealed class MatrixToViewEvents : VectorViewEvents {
    data class NavigateToRoom(val roomId: String) : MatrixToViewEvents()
    object Dismiss : MatrixToViewEvents()
}

M vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt => vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt +4 -3
@@ 63,13 63,14 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
                .subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread())
                .flatMap { permalinkData ->
                    handlePermalink(permalinkData, context, navigationInterceptor, buildTask)
                    handlePermalink(permalinkData, deepLink, context, navigationInterceptor, buildTask)
                }
                .onErrorReturnItem(false)
    }

    private fun handlePermalink(
            permalinkData: PermalinkData,
            rawLink: Uri,
            context: Context,
            navigationInterceptor: NavigationInterceptor?,
            buildTask: Boolean


@@ 96,7 97,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
                Single.just(true)
            }
            is PermalinkData.UserLink     -> {
                if (navigationInterceptor?.navToMemberProfile(permalinkData.userId) != true) {
                if (navigationInterceptor?.navToMemberProfile(permalinkData.userId, rawLink) != true) {
                    navigator.openRoomMemberProfile(userId = permalinkData.userId, roomId = null, context = context, buildTask = buildTask)
                }
                Single.just(true)


@@ 175,7 176,7 @@ interface NavigationInterceptor {
    /**
     * Return true if the navigation has been intercepted
     */
    fun navToMemberProfile(userId: String): Boolean {
    fun navToMemberProfile(userId: String, deepLink: Uri): Boolean {
        return false
    }
}

M vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt => vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt +17 -14
@@ 23,11 23,9 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.toast
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.LoadingFragment
import im.vector.app.features.login.LoginActivity
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
import javax.inject.Inject

class PermalinkHandlerActivity : VectorBaseActivity() {


@@ 45,23 43,28 @@ class PermalinkHandlerActivity : VectorBaseActivity() {
        if (isFirstCreation()) {
            replaceFragment(R.id.simpleFragmentContainer, LoadingFragment::class.java)
        }
        handleIntent()
    }

    private fun handleIntent() {
        // If we are not logged in, open login screen.
        // In the future, we might want to relaunch the process after login.
        if (!sessionHolder.hasActiveSession()) {
            startLoginActivity()
            return
        }
        val uri = intent.dataString
        permalinkHandler.launch(this, uri, buildTask = true)
                .delay(500, TimeUnit.MILLISECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe { isHandled ->
                    if (!isHandled) {
                        toast(R.string.permalink_malformed)
                    }
                    finish()
                }
                .disposeOnDestroy()
        // We forward intent to HomeActivity (singleTask) to avoid the dueling app problem
        // https://stackoverflow.com/questions/25884954/deep-linking-and-multiple-app-instances
        intent.setClass(this, HomeActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
        startActivity(intent)

        finish()
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        handleIntent()
    }

    private fun startLoginActivity() {

M vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt => vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt +3 -4
@@ 36,7 36,6 @@ import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.features.matrixto.MatrixToBottomSheet
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_simple.*
import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject
import kotlin.reflect.KClass



@@ 72,7 71,7 @@ class UserCodeActivity
                UserCodeState.Mode.SCAN      -> showFragment(ScanUserCodeFragment::class, Bundle.EMPTY)
                is UserCodeState.Mode.RESULT -> {
                    showFragment(ShowUserCodeFragment::class, Bundle.EMPTY)
                    MatrixToBottomSheet.create(mode.matrixItem, this).show(supportFragmentManager, "MatrixToBottomSheet")
                    MatrixToBottomSheet.withLink(mode.rawLink, this).show(supportFragmentManager, "MatrixToBottomSheet")
                }
            }
        }


@@ 104,8 103,8 @@ class UserCodeActivity
        }
    }

    override fun didTapStartMessage(matrixItem: MatrixItem) {
        sharedViewModel.handle(UserCodeActions.StartChattingWithUser(matrixItem))
    override fun navigateToRoom(roomId: String) {
        navigator.openRoom(this, roomId)
    }

    override fun onBackPressed() = withState(sharedViewModel) {

M vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt => vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt +26 -14
@@ 30,6 30,7 @@ import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.permalinks.PermalinkData


@@ 72,12 73,12 @@ class UserCodeSharedViewModel @AssistedInject constructor(

    override fun handle(action: UserCodeActions) {
        when (action) {
            UserCodeActions.DismissAction              -> _viewEvents.post(UserCodeShareViewEvents.Dismiss)
            is UserCodeActions.SwitchMode              -> setState { copy(mode = action.mode) }
            is UserCodeActions.DecodedQRCode           -> handleQrCodeDecoded(action)
            is UserCodeActions.StartChattingWithUser   -> handleStartChatting(action)
            UserCodeActions.DismissAction -> _viewEvents.post(UserCodeShareViewEvents.Dismiss)
            is UserCodeActions.SwitchMode -> setState { copy(mode = action.mode) }
            is UserCodeActions.DecodedQRCode -> handleQrCodeDecoded(action)
            is UserCodeActions.StartChattingWithUser -> handleStartChatting(action)
            UserCodeActions.CameraPermissionNotGranted -> _viewEvents.post(UserCodeShareViewEvents.CameraPermissionNotGranted)
            UserCodeActions.ShareByText                -> handleShareByText()
            UserCodeActions.ShareByText -> handleShareByText()
        }
    }



@@ 139,22 140,33 @@ class UserCodeSharedViewModel @AssistedInject constructor(
        _viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen)
        viewModelScope.launch(Dispatchers.IO) {
            when (linkedId) {
                is PermalinkData.RoomLink     -> TODO()
                is PermalinkData.UserLink     -> {
                    val user = session.getUser(linkedId.userId) ?: awaitCallback<List<User>> {
                        session.searchUsersDirectory(linkedId.userId, 10, emptySet(), it)
                    }.firstOrNull { it.userId == linkedId.userId }
                is PermalinkData.RoomLink -> {
                    // not yet supported
                    _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented)))
                }
                is PermalinkData.UserLink -> {
                    val user = tryOrNull {
                        awaitCallback<User> {
                            session.resolveUser(linkedId.userId, it)
                        }
                    }
                    // Create raw Uxid in case the user is not searchable
                    ?: User(linkedId.userId, null, null)
                            ?: User(linkedId.userId, null, null)

                    setState {
                        copy(
                                mode = UserCodeState.Mode.RESULT(user.toMatrixItem())
                                mode = UserCodeState.Mode.RESULT(user.toMatrixItem(), action.code)
                        )
                    }
                }
                is PermalinkData.GroupLink    -> TODO()
                is PermalinkData.FallbackLink -> TODO()
                is PermalinkData.GroupLink -> {
                    // not yet supported
                    _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented)))
                }
                is PermalinkData.FallbackLink -> {
                    // not yet supported
                    _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented)))
                }
            }
            _viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen)
        }

M vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt => vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt +1 -1
@@ 28,7 28,7 @@ data class UserCodeState(
    sealed class Mode {
        object SHOW : Mode()
        object SCAN : Mode()
        data class RESULT(val matrixItem: MatrixItem) : Mode()
        data class RESULT(val matrixItem: MatrixItem, val rawLink: String) : Mode()
    }

    constructor(args: UserCodeActivity.Args) : this(

M vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml => vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml +32 -2
@@ 3,13 3,24 @@
    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:layout_height="wrap_content"
    android:minHeight="200dp">

    <ProgressBar
        android:id="@+id/matrixToCardContentLoading"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:visibility="visible" />

    <ImageView
        android:id="@+id/matrixToCardAvatar"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_marginStart="@dimen/layout_horizontal_margin"
        android:layout_marginTop="@dimen/layout_vertical_margin_big"
        android:elevation="4dp"
        android:transitionName="profile"


@@ 63,4 74,23 @@
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/matrixToCardUserIdText" />

    <ProgressBar
        android:id="@+id/matrixToCardButtonLoading"
        style="?android:attr/progressBarStyleSmall"
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="@id/matrixToCardSendMessageButton"
        app:layout_constraintEnd_toEndOf="@id/matrixToCardSendMessageButton"
        app:layout_constraintStart_toStartOf="@id/matrixToCardSendMessageButton"
        app:layout_constraintTop_toTopOf="@id/matrixToCardSendMessageButton"
        tools:visibility="visible" />

    <androidx.constraintlayout.widget.Group
        android:id="@+id/matrixToCardUserContentVisibility"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="matrixToCardAvatar,matrixToCardNameText,matrixToCardUserIdText"
        tools:visibility="visible" />

</androidx.constraintlayout.widget.ConstraintLayout>