Unverified Commit 219b14fb authored by Valere's avatar Valere Committed by GitHub

Merge pull request #3387 from vector-im/feature/room_widgets_perms

Feature/room widgets perms
parents f72f7a5f a765cdb7
......@@ -5,7 +5,7 @@ MatrixSdk 🚀:
- Upgrade to version 0.X.Y.
Features ✨:
-
- Privacy / Room Widget permissions (#3378)
Improvementss 🙌:
-
......
......@@ -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;
......
......@@ -31,6 +31,7 @@ import im.vector.Matrix
import im.vector.R
import im.vector.activity.util.INTEGRATION_MANAGER_ACTIVITY_REQUEST_CODE
import im.vector.activity.util.TERMS_REQUEST_CODE
import im.vector.fragments.roomwidgets.WebviewPermissionUtils
import im.vector.types.JsonDict
import im.vector.types.WidgetEventData
import im.vector.util.AssetReader
......@@ -194,7 +195,7 @@ abstract class AbstractWidgetActivity : VectorAppCompatActivity() {
// Permission requests
it.webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
runOnUiThread { request.grant(request.resources) }
WebviewPermissionUtils.promptForPermissions(R.string.room_widget_resource_permission_title, request, this@AbstractWidgetActivity)
}
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
......
/*
* 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.activity
/**
* A fragment should implement this interface if it wants to intercept backPressed events.
* Any activity extending VectorAppCompatActivity will propagate back pressed event to child
* fragment that implements it.
*/
interface HandleBackParticipant {
/**
* Returns true, if the on back pressed event has been handled by this Fragment.
* Otherwise return false
*/
fun onBackPressed(): Boolean
}
\ No newline at end of file
......@@ -55,7 +55,7 @@ class StickerPickerActivity : AbstractWidgetActivity() {
}
override fun canScalarTokenBeProvided(): Boolean {
return widgetManager.isScalarUrl(this, mWidgetUrl)
return widgetManager.isScalarUrl( mWidgetUrl)
}
/**
......
......@@ -24,17 +24,20 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.annotation.*
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
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
import im.vector.activity.interfaces.Restorable
import im.vector.dialogs.ConsentNotGivenHelper
import im.vector.fragments.VectorBaseFragment
import im.vector.fragments.VectorBaseMvRxFragment
import im.vector.receiver.DebugReceiver
import im.vector.ui.themes.ActivityOtherThemes
import im.vector.ui.themes.ThemeUtils
......@@ -45,7 +48,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
......@@ -151,6 +154,27 @@ abstract class VectorAppCompatActivity : AppCompatActivity() {
}
}
override fun onBackPressed() {
val handled = recursivelyDispatchOnBackPressed(supportFragmentManager)
if (!handled) {
super.onBackPressed()
}
}
private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
val reverseOrder = fm.fragments.filter { it is VectorBaseFragment || it is VectorBaseMvRxFragment }.reversed()
for (f in reverseOrder) {
val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager)
if (handledByChildFragments) {
return true
}
if (f is HandleBackParticipant && f.onBackPressed()) {
return true
}
}
return false
}
override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration?) {
super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig)
......
......@@ -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;
/**
......@@ -925,7 +924,7 @@ public class VectorRoomActivity extends MXCActionBarActivity implements
}
new AlertDialog.Builder(VectorRoomActivity.this)
.setSingleChoiceItems(widgetNames.toArray(CharSequences), 0, new DialogInterface.OnClickListener() {
.setItems(widgetNames.toArray(CharSequences), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface d, int n) {
d.cancel();
......
/*
* 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.DialogInterface
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()
//optimistic dismiss
dismiss()
sharedActivityViewModel.doFinish()
}
@OnClick(R.id.bottom_sheet_widget_permission_continue_button)
fun doAccept() {
viewModel.allowWidget()
//optimistic dismiss
dismiss()
}
override fun onCancel(dialog: DialogInterface?) {
super.onCancel(dialog)
sharedActivityViewModel.doFinish()
}
@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.Log
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) {
fun allowWidget(onFinished: (() -> Unit)? = null) {
session.integrationManager.setWidgetAllowed(widget.widgetEvent?.eventId
?: "", true, object : ApiCallback<Void?> {
override fun onSuccess(info: Void?) {
onFinished?.invoke()
}
override fun onUnexpectedError(e: Exception) {
Log.e(LOG_TAG, e.message)
}
override fun onNetworkError(e: Exception) {
Log.e(LOG_TAG, e.message)
}
override fun onMatrixError(e: MatrixError) {
Log.e(LOG_TAG, e.message)
}
})
}
fun blockWidget(onFinished: (() -> Unit)? = null) {
session.integrationManager.setWidgetAllowed(widget.widgetEvent?.eventId
?: "", false, object : ApiCallback<Void?> {
override fun onSuccess(info: Void?) {
onFinished?.invoke()
}
override fun onUnexpectedError(e: Exception) {
Log.e(LOG_TAG, e.message)
}
override fun onNetworkError(e: Exception) {
Log.e(LOG_TAG, e.message)
}
override fun onMatrixError(e: MatrixError) {
Log.e(LOG_TAG, e.message)
}
})
}
companion object : MvRxViewModelFactory<RoomWidgetPermissionViewModel, RoomWidgetPermissionViewState> {
val LOG_TAG = RoomWidgetPermissionViewModel::class.simpleName
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)
}
override fun initialState(viewModelContext: ViewModelContext): RoomWidgetPermissionViewState? {
val args = viewModelContext.args<RoomWidgetPermissionBottomSheet.FragArgs>()
val session = Matrix.getMXSession(viewModelContext.activity, args.mxId)
val widget = args.widget
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_display_name,
R.string.room_widget_permission_avatar_url,
R.string.room_widget_permission_user_id,
R.string.room_widget_permission_theme,
R.string.room_widget_permission_widget_id,
R.string.room_widget_permission_room_id
)
return RoomWidgetPermissionViewState(
authorName = creator?.displayname,
authorId = widget.widgetEvent.sender,
authorAvatarUrl = creator?.getAvatarUrl(),
widgetDomain = domain,
permissionsList = infoShared
)
}
}
}
\ 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.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.webkit.PermissionRequest
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import im.vector.R
object WebviewPermissionUtils {
@SuppressLint("NewApi")
fun promptForPermissions(@StringRes title: Int, request: PermissionRequest, activity: Activity) {
if (activity.isFinishing || activity.isDestroyed) return
val allowedPermissions = request.resources.map {
it to false
}.toMutableList()
AlertDialog.Builder(activity)
.setTitle(title)
.setMultiChoiceItems(
request.resources.map { webPermissionToHumanReadable(it, activity) }.toTypedArray()
, null
) { dialog, which, isChecked ->
allowedPermissions[which] = allowedPermissions[which].first to isChecked
}
.setPositiveButton(R.string.room_widget_resource_grant_permission) { dialog, wich ->
request.grant(allowedPermissions.mapNotNull { perm ->
perm.first.takeIf { perm.second }
}.toTypedArray())
}
.setNegativeButton(R.string.room_widget_resource_decline_permission) { dialog, wich ->
request.deny()
}
.show()
}
private fun webPermissionToHumanReadable(permission: String, context: Context): String {
return when (permission) {
PermissionRequest.RESOURCE_AUDIO_CAPTURE -> context.getString(R.string.room_widget_webview_access_microphone)
PermissionRequest.RESOURCE_VIDEO_CAPTURE -> context.getString(R.string.room_widget_webview_access_camera)
PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID -> context.getString(R.string.room_widget_webview_read_protected_media)
else -> permission
}
}
}
\ No newline at end of file
......@@ -19,7 +19,6 @@ import android.app.Activity
import android.os.Bundle
import android.view.ViewGroup
import android.widget.Button
import android.widget.ProgressBar
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
......@@ -48,8 +47,8 @@ class AcceptTermsFragment : VectorBaseFragment(), TermsController.Listener {
@BindView(R.id.terms_bottom_accept)
lateinit var acceptButton: Button
@BindView(R.id.termsLoadingIndicator)
lateinit var progressBar: ProgressBar
@BindView(R.id.waitOverlay)
lateinit var waitingModalOverlay: ViewGroup
@BindView(R.id.termsBottomBar)
lateinit var bottomBar: ViewGroup
......@@ -73,10 +72,10 @@ class AcceptTermsFragment : VectorBaseFragment(), TermsController.Listener {
when (terms) {
is MxAsync.Loading -> {
bottomBar.isVisible = false
progressBar.isVisible = true
waitingModalOverlay.isVisible = true
}
is MxAsync.Error -> {
progressBar.isVisible = false
waitingModalOverlay.isVisible = false
terms.stringResId.let { stringRes ->
AlertDialog.Builder(requireActivity())
.setMessage(stringRes)
......@@ -88,20 +87,23 @@ class AcceptTermsFragment : VectorBaseFragment(), TermsController.Listener {
}
is MxAsync.Success -> {
updateState(terms.value)
progressBar.isVisible = false
waitingModalOverlay.isVisible = false
bottomBar.isVisible = true
acceptButton.isEnabled = terms.value.all { it.accepted }
}
else -> {
waitingModalOverlay.isVisible = false
}
}
})
viewModel.acceptTerms.observe(this, Observer { request ->
when (request) {
is MxAsync.Loading -> {
progressBar.isVisible = true
waitingModalOverlay.isVisible = true
}
is MxAsync.Error -> {
progressBar.isVisible = false
waitingModalOverlay.isVisible = false
request.stringResId.let { stringRes ->
AlertDialog.Builder(requireActivity())
.setMessage(stringRes)
......@@ -110,9 +112,14 @@ class AcceptTermsFragment : VectorBaseFragment(), TermsController.Listener {
}
}
is MxAsync.Success -> {
waitingModalOverlay.isVisible = false
activity?.setResult(Activity.RESULT_OK)
activity?.finish()
}
else -> {
waitingModalOverlay.isVisible = false
}
}
})
}
......
......@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.widgets
/**
......
/*
* 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.widgets;
import org.matrix.androidsdk.core.model.MatrixError;
/**
* Widget error code
*/
public 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";
/**
* Create a widget error
*
* @param code the error code (see XX_ERROR_CODE)
* @param detailedErrorDescription the detailed error description
*/
public WidgetError(String code, String detailedErrorDescription) {
errcode = code;
error = detailedErrorDescription;
}
}
......@@ -82,25 +82,6 @@ public class WidgetsManager {
return config.getUiUrl();
}
/**
* Widget error code
*/
public 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";
/**
* Create a widget error
*
* @param code the error code (see XX_ERROR_CODE)
* @param detailedErrorDescription the detailed error description
*/
public WidgetError(String code, String detailedErrorDescription) {
errcode = code;
error = detailedErrorDescription;
}
}
/**
* Pending widget creation callback
......@@ -236,7 +217,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 +470,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 +490,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)) {
......