Commit 92c0e717 authored by Valere's avatar Valere

Allow room widget flow

parent f72f7a5f
......@@ -28,7 +28,6 @@ import android.content.IntentFilter;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
......
......@@ -55,7 +55,7 @@ class StickerPickerActivity : AbstractWidgetActivity() {
}
override fun canScalarTokenBeProvided(): Boolean {
return widgetManager.isScalarUrl(this, mWidgetUrl)
return widgetManager.isScalarUrl( mWidgetUrl)
}
/**
......
......@@ -30,6 +30,7 @@ import androidx.core.view.isVisible
import butterknife.BindView
import butterknife.ButterKnife
import butterknife.Unbinder
import com.airbnb.mvrx.BaseMvRxActivity
import im.vector.BuildConfig
import im.vector.R
import im.vector.VectorApp
......@@ -45,7 +46,7 @@ import org.matrix.androidsdk.core.Log
/**
* Parent class for all Activities in Vector application
*/
abstract class VectorAppCompatActivity : AppCompatActivity() {
abstract class VectorAppCompatActivity : BaseMvRxActivity() {
private var LOG_TAG = VectorAppCompatActivity::class.java.simpleName
......
......@@ -137,7 +137,6 @@ import im.vector.view.VectorAutoCompleteTextView;
import im.vector.view.VectorOngoingConferenceCallView;
import im.vector.view.VectorPendingCallView;
import im.vector.widgets.Widget;
import im.vector.widgets.WidgetManagerProvider;
import im.vector.widgets.WidgetsManager;
/**
......
/*
* Copyright 2019 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.fragments.roomwidgets
import android.os.Build
import android.os.Parcelable
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.BulletSpan
import android.widget.ImageView
import android.widget.TextView
import butterknife.BindView
import butterknife.OnClick
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.R
import im.vector.extensions.withArgs
import im.vector.fragments.VectorBaseBottomSheetDialogFragment
import im.vector.util.VectorUtils
import im.vector.widgets.Widget
import kotlinx.android.parcel.Parcelize
class RoomWidgetPermissionBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun getLayoutResId(): Int = R.layout.bottom_sheet_room_widget_permission
private val viewModel: RoomWidgetPermissionViewModel by fragmentViewModel()
@BindView(R.id.bottom_sheet_widget_permission_shared_info)
lateinit var sharedInfoTextView: TextView
@BindView(R.id.bottom_sheet_widget_permission_owner_id)
lateinit var authorIdText: TextView
@BindView(R.id.bottom_sheet_widget_permission_owner_display_name)
lateinit var authorNameText: TextView
@BindView(R.id.bottom_sheet_widget_permission_owner_avatar)
lateinit var authorAvatarView: ImageView
private val sharedActivityViewModel: RoomWidgetViewModel by activityViewModel()
override fun invalidate() = withState(viewModel) { state ->
authorIdText.text = state.authorId
authorNameText.text = state.authorName ?: ""
VectorUtils.loadUserAvatar(requireContext(), viewModel.session, authorAvatarView,
state.authorAvatarUrl, state.authorId, state.authorName)
val infoBuilder = SpannableStringBuilder()
.append(getString(R.string.room_widget_permission_shared_info_title, "'${state.widgetDomain
?: ""}'"))
infoBuilder.append("\n")
state.permissionsList?.forEach {
infoBuilder.append("\n")
val bulletPoint = getString(it)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
infoBuilder.append(bulletPoint, BulletSpan(resources.getDimension(R.dimen.quote_gap).toInt()), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
} else {
val start = infoBuilder.length
infoBuilder.append(bulletPoint)
infoBuilder.setSpan(BulletSpan(resources.getDimension(R.dimen.quote_gap).toInt()), start, bulletPoint.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
infoBuilder.append("\n")
sharedInfoTextView.text = infoBuilder
}
@OnClick(R.id.bottom_sheet_widget_permission_decline_button)
fun doDecline() {
viewModel.blockWidget {
dismiss()
sharedActivityViewModel.doFinish()
}
}
@OnClick(R.id.bottom_sheet_widget_permission_continue_button)
fun doAccept() {
viewModel.allowWidget {
dismiss()
}
}
@Parcelize
data class FragArgs(
val widget: Widget,
val mxId: String
) : Parcelable
companion object {
fun newInstance(matrixId: String, widget: Widget) = RoomWidgetPermissionBottomSheet().withArgs {
putParcelable(MvRx.KEY_ARG, FragArgs(widget, matrixId))
}
}
}
\ No newline at end of file
/*
* Copyright 2019 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.fragments.roomwidgets
import com.airbnb.mvrx.BaseMvRxViewModel
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.Matrix
import im.vector.R
import im.vector.widgets.Widget
import org.matrix.androidsdk.MXSession
import org.matrix.androidsdk.core.callback.ApiCallback
import org.matrix.androidsdk.core.model.MatrixError
import java.net.URL
data class RoomWidgetPermissionViewState(
val authorId: String = "",
val authorAvatarUrl: String? = null,
val authorName: String? = null,
val widgetDomain: String? = null,
//List of @StringRes
val permissionsList: List<Int>? = null
) : MvRxState
class RoomWidgetPermissionViewModel(val session: MXSession, val widget: Widget, initialState: RoomWidgetPermissionViewState)
: BaseMvRxViewModel<RoomWidgetPermissionViewState>(initialState, false) {
init {
val room = session.dataHandler.getRoom(widget.roomId)
val creator = room.getMember(widget.widgetEvent.sender)
var domain: String?
try {
domain = URL(widget.url).host
} catch (e: Throwable) {
domain = null
}
//TODO check from widget urls the perms that should be shown?
//For now put all
val infoShared = listOf<Int>(
R.string.room_widget_permission_ip_address,
R.string.room_widget_permission_useragent,
R.string.room_widget_permission_widget_id,
R.string.room_widget_permission_room_id,
R.string.room_widget_permission_matrix_profile
)
setState {
copy(
authorName = creator?.displayname,
authorId = widget.widgetEvent.sender,
authorAvatarUrl = creator?.getAvatarUrl(),
widgetDomain = domain,
permissionsList = infoShared
)
}
}
fun allowWidget(onFinished: (() -> Unit)) {
session.integrationManager.setWidgetAllowed(widget.widgetEvent?.eventId
?: "", true, object : ApiCallback<Void?> {
override fun onSuccess(info: Void?) {
onFinished()
}
override fun onUnexpectedError(e: Exception?) {
//TODO.. make the button with a loading state?
}
override fun onNetworkError(e: Exception?) {
//TODO.. make the button with a loading state?
}
override fun onMatrixError(e: MatrixError?) {
//TODO.. make the button with a loading state?
}
})
}
fun blockWidget(onFinished: (() -> Unit)) {
session.integrationManager.setWidgetAllowed(widget.widgetEvent?.eventId
?: "", false, object : ApiCallback<Void?> {
override fun onSuccess(info: Void?) {
onFinished()
}
override fun onUnexpectedError(e: Exception?) {
//TODO.. make the button with a loading state?
}
override fun onNetworkError(e: Exception?) {
//TODO.. make the button with a loading state?
}
override fun onMatrixError(e: MatrixError?) {
//TODO.. make the button with a loading state?
}
})
}
companion object : MvRxViewModelFactory<RoomWidgetPermissionViewModel, RoomWidgetPermissionViewState> {
override fun create(viewModelContext: ViewModelContext, state: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel? {
val args = viewModelContext.args<RoomWidgetPermissionBottomSheet.FragArgs>()
val session = Matrix.getMXSession(viewModelContext.activity, args.mxId)
return RoomWidgetPermissionViewModel(session, args.widget, state)
}
}
}
\ No newline at end of file
/*
* Copyright 2019 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.fragments.roomwidgets
import android.content.Context
import android.text.TextUtils
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.*
import im.vector.Matrix
import im.vector.R
import im.vector.VectorApp
import im.vector.activity.WidgetActivity
import im.vector.ui.arch.LiveEvent
import im.vector.widgets.Widget
import im.vector.widgets.WidgetsManager
import org.matrix.androidsdk.MXSession
import org.matrix.androidsdk.core.callback.ApiCallback
import org.matrix.androidsdk.core.model.MatrixError
import org.matrix.androidsdk.data.Room
import org.matrix.androidsdk.features.integrationmanager.IntegrationManager
enum class WidgetState {
UNKWNOWN,
WIDGET_NOT_ALLOWED,
WIDGET_ALLOWED,
CLOSING_WIDGET
}
data class RoomWidgetViewModelState(
val status: WidgetState = WidgetState.UNKWNOWN,
val formattedURL: Async<String> = Uninitialized,
val webviewLoadedUrl: Async<String> = Uninitialized,
val widgetName: String = "",
val authorName: String? = null,
val authorId: String? = null,
val canManageWidgets: Boolean = false
) : MvRxState
class RoomWidgetViewModel(initialState: RoomWidgetViewModelState, val widget: Widget)
: BaseMvRxViewModel<RoomWidgetViewModelState>(initialState, false) {
companion object : MvRxViewModelFactory<RoomWidgetViewModel, RoomWidgetViewModelState> {
const val NAVIGATE_FINISH = "NAVIGATE_FINISH"
//private val LOG_TAG = KeysBackupSetupSharedViewModel::class.java.name
override fun create(viewModelContext: ViewModelContext, state: RoomWidgetViewModelState): RoomWidgetViewModel? {
return (viewModelContext.activity.intent?.extras?.getSerializable(WidgetActivity.EXTRA_WIDGET_ID) as? Widget)?.let {
RoomWidgetViewModel(state, it)
} ?: super.create(viewModelContext, state)
}
}
var navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()
var loadWebURLEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()
var toastMessageEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()
private var room: Room? = null
var session: MXSession? = null
var widgetsManager: WidgetsManager? = null
/**
* Widget events listener
*/
private val mWidgetListener = WidgetsManager.onWidgetUpdateListener { widget ->
if (TextUtils.equals(widget.widgetId, widget.widgetId)) {
if (!widget.isActive) {
doFinish()
}
}
}
init {
setState {
copy(widgetName = widget.humanName)
}
configure()
session?.integrationManager?.addListener(object : IntegrationManager.IntegrationManagerManagerListener {
override fun onIntegrationManagerChange(managerConfig: IntegrationManager) {
refreshPermissionStatus()
}
})
}
fun webviewStartedToLoad(url: String?) = withState {
//Only do it for first load
setState {
copy(webviewLoadedUrl = Loading())
}
}
fun webviewLoadingError(url: String?, reason: String?) = withState {
setState {
copy(webviewLoadedUrl = Fail(Throwable(reason)))
}
}
fun webviewLoadSuccess(url: String?) = withState {
setState {
copy(webviewLoadedUrl = Success(url ?: ""))
}
}
private fun configure() {
val applicationContext = VectorApp.getInstance().applicationContext
val matrix = Matrix.getInstance(applicationContext)
session = matrix.getSession(widget.sessionId)
if (session == null) {
//defensive code
doFinish()
return
}
room = session?.dataHandler?.getRoom(widget.roomId)
if (room == null) {
//defensive code
doFinish()
return
}
widgetsManager = matrix
.getWidgetManagerProvider(session)?.getWidgetManager(applicationContext)
setState {
copy(canManageWidgets = WidgetsManager.checkWidgetPermission(session, room) == null)
}
widgetsManager?.addListener(mWidgetListener)
refreshPermissionStatus(applicationContext)
}
private fun refreshPermissionStatus(applicationContext: Context = VectorApp.getInstance().applicationContext) {
//If it was added by me, consider it as allowed
if (widget.widgetEvent.getSender() == session?.myUserId) {
onWidgetAllowed(applicationContext)
return
}
val isAllowed = session
?.integrationManager
?.getKnownWidgetPermissions()
?.findLast { it.stateEventId == widget.widgetEvent.eventId }
?.allowed
?: false
if (!isAllowed) {
setState {
copy(status = WidgetState.WIDGET_NOT_ALLOWED)
}
} else {
//we can start loading the widget then
onWidgetAllowed(applicationContext)
}
}
fun doCloseWidget(context: Context) {
AlertDialog.Builder(context)
.setMessage(R.string.widget_delete_message_confirmation)
.setPositiveButton(R.string.remove) { _, _ ->
// showWaitingView()
setState {
copy(status = WidgetState.CLOSING_WIDGET)
}
widgetsManager?.closeWidget(session, room, widget.widgetId, object : ApiCallback<Void> {
override fun onSuccess(info: Void?) {
doFinish()
}
private fun onError(errorMessage: String) {
toastMessageEvent.postValue(LiveEvent(errorMessage))
//TODO should not be handled with this state
setState {
copy(status = WidgetState.WIDGET_ALLOWED)
}
}
override fun onNetworkError(e: Exception) {
onError(e.localizedMessage)
}
override fun onMatrixError(e: MatrixError) {
onError(e.localizedMessage)
}
override fun onUnexpectedError(e: Exception) {
onError(e.localizedMessage)
}
})
}
.setNegativeButton(R.string.cancel, null)
.show()
}
fun doFinish() {
navigateEvent.postValue(LiveEvent(NAVIGATE_FINISH))
}
private fun onWidgetAllowed(applicationContext: Context = VectorApp.getInstance().applicationContext) {
setState {
copy(
status = WidgetState.WIDGET_ALLOWED,
formattedURL = Loading()
)
}
if (widgetsManager != null) {
widgetsManager!!.getFormattedWidgetUrl(applicationContext, widget, object : ApiCallback<String> {
override fun onSuccess(url: String) {
loadWebURLEvent.postValue(LiveEvent(url))
setState {
//We use a live event to trigger the webview load
copy(
status = WidgetState.WIDGET_ALLOWED,
formattedURL = Success(url)
)
}
}
private fun onError(errorMessage: String) {
setState {
copy(
status = WidgetState.WIDGET_ALLOWED,
formattedURL = Fail(Throwable(errorMessage))
)
}
}
override fun onNetworkError(e: Exception) {
onError(e.localizedMessage)
}
override fun onMatrixError(e: MatrixError) {
onError(e.localizedMessage)
}
override fun onUnexpectedError(e: Exception) {
onError(e.localizedMessage)
}
})
} else {
setState {
copy(
status = WidgetState.WIDGET_ALLOWED,
formattedURL = Success(widget.url)
)
}
loadWebURLEvent.postValue(LiveEvent(widget.url))
}
}
override fun onCleared() {
super.onCleared()
widgetsManager?.removeListener(mWidgetListener)
}
}
\ No newline at end of file
......@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.widgets
/**
......
......@@ -85,7 +85,7 @@ public class WidgetsManager {
/**
* Widget error code
*/
public class WidgetError extends MatrixError {
public static class WidgetError extends MatrixError {
public static final String WIDGET_NOT_ENOUGH_POWER_ERROR_CODE = "WIDGET_NOT_ENOUGH_POWER_ERROR_CODE";
public static final String WIDGET_CREATION_FAILED_ERROR_CODE = "WIDGET_CREATION_FAILED_ERROR_CODE";
......@@ -236,7 +236,7 @@ public class WidgetsManager {
* @return an error if the user cannot act on widgets in this room. Else, null.
*/
public WidgetError checkWidgetPermission(MXSession session, Room room) {
public static WidgetError checkWidgetPermission(MXSession session, Room room) {
WidgetError error = null;
if ((null != room) && (null != room.getState()) && (null != room.getState().getPowerLevels())) {
......@@ -489,7 +489,7 @@ public class WidgetsManager {
* @param callback the callback
*/
public void getFormattedWidgetUrl(final Context context, final Widget widget, final ApiCallback<String> callback) {
if (isScalarUrl(context, widget.getUrl())) {
if (isScalarUrl(widget.getUrl())) {
getScalarToken(context, Matrix.getInstance(context).getSession(widget.getSessionId()), new SimpleApiCallback<String>(callback) {
@Override
public void onSuccess(String token) {
......@@ -509,11 +509,10 @@ public class WidgetsManager {
/**
* Return true if the url is allowed to receive the scalar token in parameter
*
* @param context
* @param url
* @return true if the url is allowed to receive the scalar token in parameter
*/
public boolean isScalarUrl(Context context, String url) {
public boolean isScalarUrl(String url) {
List<String> allowed = config.getWhiteListedUrls();
for (String allowedUrl : allowed) {
if (url.startsWith(allowedUrl)) {
......
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"