Merge pull request #619 from vector-im/feature/attachments

Feature/attachments
This commit is contained in:
Benoit Marty 2019-10-23 10:09:57 +02:00 committed by GitHub
commit 70a14f6350
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1609 additions and 252 deletions

2
.gitignore vendored
View File

@ -14,3 +14,5 @@
/tmp /tmp
ktlint ktlint
.idea/copyright/New_vector.xml
.idea/copyright/profiles_settings.xml

View File

@ -8,6 +8,8 @@ Improvements:
- Persist active tab between sessions (#503) - Persist active tab between sessions (#503)
- Do not upload file too big for the homeserver (#587) - Do not upload file too big for the homeserver (#587)
- Handle read markers (#84) - Handle read markers (#84)
- Attachments: start using system pickers (#52)
- Attachments: start handling incoming share (#58)
- Mark all messages as read (#396) - Mark all messages as read (#396)
- Add ability to report content (#515) - Add ability to report content (#515)

View File

@ -316,7 +316,7 @@ dependencies {
implementation 'me.leolin:ShortcutBadger:1.1.22@aar' implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
// File picker // File picker
implementation 'com.github.jaiselrahman:FilePicker:1.2.2' implementation 'com.kbeanie:multipicker:1.6@aar'
// DI // DI
implementation "com.google.dagger:dagger:$daggerVersion" implementation "com.google.dagger:dagger:$daggerVersion"

View File

@ -4,6 +4,7 @@
package="im.vector.riotx"> package="im.vector.riotx">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<application <application
android:name=".VectorApplication" android:name=".VectorApplication"
@ -79,6 +80,22 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".features.share.IncomingShareActivity">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<data android:mimeType="*/*" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
</intent-filter>
</activity>
<!-- Services --> <!-- Services -->
<service <service

View File

@ -339,11 +339,6 @@ SOFTWARE.
<br/> <br/>
Copyright 2014 Leo Lin Copyright 2014 Leo Lin
</li> </li>
<li>
<b>FilePicker</b>
<br/>
Copyright (c) 2018, Jaisel Rahman
</li>
<li> <li>
<b>diff-match-patch</b> <b>diff-match-patch</b>
<br/> <br/>
@ -359,6 +354,11 @@ SOFTWARE.
<br/> <br/>
Copyright 2017 Gabriel Ittner. Copyright 2017 Gabriel Ittner.
</li> </li>
<li>
<b>Android-multipicker-library</b>
<br/>
Copyright 2018 Kumar Bibek
</li>
</ul> </ul>
<pre> <pre>
Apache License Apache License

View File

@ -0,0 +1,27 @@
/*
* 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.riotx
import arrow.core.Option
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.core.utils.RxStore
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ActiveSessionObservableStore @Inject constructor() : RxStore<Option<Session>>()

View File

@ -14,56 +14,59 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.home package im.vector.riotx
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import arrow.core.Option import arrow.core.Option
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxState
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.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.features.home.HomeRoomListObservableStore
import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID
import im.vector.riotx.features.home.group.SelectedGroupStore import im.vector.riotx.features.home.group.SelectedGroupStore
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.addTo
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
data class EmptyState(val isEmpty: Boolean = true) : MvRxState /**
* This class handles the global app state. At the moment, it only manages room list.
* It requires to be added to ProcessLifecycleOwner.get().lifecycle
*/
@Singleton
class AppStateHandler @Inject constructor(
private val sessionObservableStore: ActiveSessionObservableStore,
private val homeRoomListStore: HomeRoomListObservableStore,
private val selectedGroupStore: SelectedGroupStore) : LifecycleObserver {
class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState: EmptyState, private val compositeDisposable = CompositeDisposable()
private val session: Session,
private val selectedGroupStore: SelectedGroupStore,
private val homeRoomListStore: HomeRoomListObservableStore
) : VectorViewModel<EmptyState>(initialState) {
@AssistedInject.Factory @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
interface Factory { fun entersForeground() {
fun create(initialState: EmptyState): HomeActivityViewModel observeRoomsAndGroup()
} }
companion object : MvRxViewModelFactory<HomeActivityViewModel, EmptyState> { @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun entersBackground() {
@JvmStatic compositeDisposable.clear()
override fun create(viewModelContext: ViewModelContext, state: EmptyState): HomeActivityViewModel? {
val homeActivity: HomeActivity = (viewModelContext as ActivityViewModelContext).activity()
return homeActivity.homeActivityViewModelFactory.create(state)
}
} }
init { private fun observeRoomsAndGroup() {
observeRoomAndGroup()
}
private fun observeRoomAndGroup() {
Observable Observable
.combineLatest<List<RoomSummary>, Option<GroupSummary>, List<RoomSummary>>( .combineLatest<List<RoomSummary>, Option<GroupSummary>, List<RoomSummary>>(
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS), sessionObservableStore.observe()
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
it.orNull()?.rx()?.liveRoomSummaries()
?: Observable.just(emptyList())
}
.throttleLast(300, TimeUnit.MILLISECONDS),
selectedGroupStore.observe(), selectedGroupStore.observe(),
BiFunction { rooms, selectedGroupOption -> BiFunction { rooms, selectedGroupOption ->
val selectedGroup = selectedGroupOption.orNull() val selectedGroup = selectedGroupOption.orNull()
@ -83,7 +86,7 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState:
.filter { !it.isDirect } .filter { !it.isDirect }
.filter { .filter {
selectedGroup?.groupId == ALL_COMMUNITIES_GROUP_ID selectedGroup?.groupId == ALL_COMMUNITIES_GROUP_ID
|| selectedGroup?.roomIds?.contains(it.roomId) ?: true || selectedGroup?.roomIds?.contains(it.roomId) ?: true
} }
filteredDirectRooms + filteredGroupRooms filteredDirectRooms + filteredGroupRooms
} }
@ -91,6 +94,6 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState:
.subscribe { .subscribe {
homeRoomListStore.post(it) homeRoomListStore.post(it)
} }
.disposeOnClear() .addTo(compositeDisposable)
} }
} }

View File

@ -74,6 +74,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
@Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var versionProvider: VersionProvider @Inject lateinit var versionProvider: VersionProvider
@Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var notificationUtils: NotificationUtils
@Inject lateinit var appStateHandler: AppStateHandler
lateinit var vectorComponent: VectorComponent lateinit var vectorComponent: VectorComponent
private var fontThreadHandler: Handler? = null private var fontThreadHandler: Handler? = null
@ -134,6 +135,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder) FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder)
} }
}) })
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
// This should be done as early as possible // This should be done as early as possible
initKnownEmojiHashSet(appContext) initKnownEmojiHashSet(appContext)
} }

View File

@ -16,8 +16,10 @@
package im.vector.riotx.core.di package im.vector.riotx.core.di
import arrow.core.Option
import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.riotx.ActiveSessionObservableStore
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
@ -26,6 +28,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class ActiveSessionHolder @Inject constructor(private val authenticator: Authenticator, class ActiveSessionHolder @Inject constructor(private val authenticator: Authenticator,
private val sessionObservableStore: ActiveSessionObservableStore,
private val keyRequestHandler: KeyRequestHandler, private val keyRequestHandler: KeyRequestHandler,
private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler
) { ) {
@ -34,12 +37,14 @@ class ActiveSessionHolder @Inject constructor(private val authenticator: Authent
fun setActiveSession(session: Session) { fun setActiveSession(session: Session) {
activeSession.set(session) activeSession.set(session)
sessionObservableStore.post(Option.fromNullable(session))
keyRequestHandler.start(session) keyRequestHandler.start(session)
incomingVerificationRequestHandler.start(session) incomingVerificationRequestHandler.start(session)
} }
fun clearActiveSession() { fun clearActiveSession() {
activeSession.set(null) activeSession.set(null)
sessionObservableStore.post(Option.empty())
keyRequestHandler.stop() keyRequestHandler.stop()
incomingVerificationRequestHandler.stop() incomingVerificationRequestHandler.stop()
} }

View File

@ -67,6 +67,7 @@ import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
import im.vector.riotx.features.settings.* import im.vector.riotx.features.settings.*
import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment
import im.vector.riotx.features.share.IncomingShareActivity
import im.vector.riotx.features.ui.UiStateRepository import im.vector.riotx.features.ui.UiStateRepository
@Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class]) @Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class])
@ -183,6 +184,8 @@ interface ScreenComponent {
fun inject(reactionButton: ReactionButton) fun inject(reactionButton: ReactionButton)
fun inject(incomingShareActivity: IncomingShareActivity)
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create(vectorComponent: VectorComponent, fun create(vectorComponent: VectorComponent,

View File

@ -48,3 +48,10 @@ fun EditText.showPassword(visible: Boolean, updateCursor: Boolean = true) {
} }
if (updateCursor) setSelection(text?.length ?: 0) if (updateCursor) setSelection(text?.length ?: 0)
} }
fun View.getMeasurements(): Pair<Int, Int> {
measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
val width = measuredWidth
val height = measuredHeight
return width to height
}

View File

@ -0,0 +1,40 @@
/*
* 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.riotx.core.utils
import android.app.Activity
import android.graphics.Rect
import android.view.View
import android.view.ViewTreeObserver
class KeyboardStateUtils(activity: Activity) : ViewTreeObserver.OnGlobalLayoutListener {
private val contentView: View = activity.findViewById<View>(android.R.id.content).also {
it.viewTreeObserver.addOnGlobalLayoutListener(this)
}
var isKeyboardShowing: Boolean = false
private set
override fun onGlobalLayout() {
val rect = Rect()
contentView.getWindowVisibleDisplayFrame(rect)
val screenHeight = contentView.rootView.height
val keypadHeight = screenHeight - rect.bottom
isKeyboardShowing = keypadHeight > screenHeight * 0.15
}
}

View File

@ -22,6 +22,7 @@ import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -51,8 +52,9 @@ const val PERMISSIONS_FOR_MEMBER_DETAILS = PERMISSION_READ_CONTACTS
const val PERMISSIONS_FOR_ROOM_AVATAR = PERMISSION_CAMERA const val PERMISSIONS_FOR_ROOM_AVATAR = PERMISSION_CAMERA
const val PERMISSIONS_FOR_VIDEO_RECORDING = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO const val PERMISSIONS_FOR_VIDEO_RECORDING = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO
const val PERMISSIONS_FOR_WRITING_FILES = PERMISSION_WRITE_EXTERNAL_STORAGE const val PERMISSIONS_FOR_WRITING_FILES = PERMISSION_WRITE_EXTERNAL_STORAGE
const val PERMISSIONS_FOR_PICKING_CONTACT = PERMISSION_READ_CONTACTS
private const val PERMISSIONS_EMPTY = PERMISSION_BYPASSED const val PERMISSIONS_EMPTY = PERMISSION_BYPASSED
// Request code to ask permission to the system (arbitrary values) // Request code to ask permission to the system (arbitrary values)
const val PERMISSION_REQUEST_CODE = 567 const val PERMISSION_REQUEST_CODE = 567
@ -64,6 +66,7 @@ const val PERMISSION_REQUEST_CODE_VIDEO_CALL = 572
const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573 const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573
const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574 const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575 const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
/** /**
* Log the used permissions statuses. * Log the used permissions statuses.
@ -98,8 +101,9 @@ fun logPermissionStatuses(context: Context) {
*/ */
fun checkPermissions(permissionsToBeGrantedBitMap: Int, fun checkPermissions(permissionsToBeGrantedBitMap: Int,
activity: Activity, activity: Activity,
requestCode: Int = PERMISSION_REQUEST_CODE): Boolean { requestCode: Int,
return checkPermissions(permissionsToBeGrantedBitMap, activity, null, requestCode) @StringRes rationaleMessage: Int = 0): Boolean {
return checkPermissions(permissionsToBeGrantedBitMap, activity, null, requestCode, rationaleMessage)
} }
/** /**
@ -111,8 +115,9 @@ fun checkPermissions(permissionsToBeGrantedBitMap: Int,
*/ */
fun checkPermissions(permissionsToBeGrantedBitMap: Int, fun checkPermissions(permissionsToBeGrantedBitMap: Int,
fragment: Fragment, fragment: Fragment,
requestCode: Int = PERMISSION_REQUEST_CODE): Boolean { requestCode: Int,
return checkPermissions(permissionsToBeGrantedBitMap, fragment.activity, fragment, requestCode) @StringRes rationaleMessage: Int = 0): Boolean {
return checkPermissions(permissionsToBeGrantedBitMap, fragment.activity, fragment, requestCode, rationaleMessage)
} }
/** /**
@ -136,7 +141,9 @@ fun checkPermissions(permissionsToBeGrantedBitMap: Int,
private fun checkPermissions(permissionsToBeGrantedBitMap: Int, private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
activity: Activity?, activity: Activity?,
fragment: Fragment?, fragment: Fragment?,
requestCode: Int): Boolean { requestCode: Int,
@StringRes rationaleMessage: Int
): Boolean {
var isPermissionGranted = false var isPermissionGranted = false
// sanity check // sanity check
@ -159,7 +166,6 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
val permissionListAlreadyDenied = ArrayList<String>() val permissionListAlreadyDenied = ArrayList<String>()
val permissionsListToBeGranted = ArrayList<String>() val permissionsListToBeGranted = ArrayList<String>()
var isRequestPermissionRequired = false var isRequestPermissionRequired = false
var explanationMessage = ""
// retrieve the permissions to be granted according to the request code bit map // retrieve the permissions to be granted according to the request code bit map
if (PERMISSION_CAMERA == permissionsToBeGrantedBitMap and PERMISSION_CAMERA) { if (PERMISSION_CAMERA == permissionsToBeGrantedBitMap and PERMISSION_CAMERA) {
@ -199,58 +205,11 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
} }
// if some permissions were already denied: display a dialog to the user before asking again. // if some permissions were already denied: display a dialog to the user before asking again.
if (!permissionListAlreadyDenied.isEmpty()) { if (permissionListAlreadyDenied.isNotEmpty() && rationaleMessage != 0) {
if (permissionsToBeGrantedBitMap == PERMISSIONS_FOR_VIDEO_IP_CALL || permissionsToBeGrantedBitMap == PERMISSIONS_FOR_AUDIO_IP_CALL) {
// Permission request for VOIP call
if (permissionListAlreadyDenied.contains(Manifest.permission.CAMERA)
&& permissionListAlreadyDenied.contains(Manifest.permission.RECORD_AUDIO)) {
// Both missing
explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera_and_audio)
} else if (permissionListAlreadyDenied.contains(Manifest.permission.RECORD_AUDIO)) {
// Audio missing
explanationMessage += activity.getString(R.string.permissions_rationale_msg_record_audio)
explanationMessage += activity.getString(R.string.permissions_rationale_msg_record_audio_explanation)
} else if (permissionListAlreadyDenied.contains(Manifest.permission.CAMERA)) {
// Camera missing
explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera)
explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera_explanation)
}
} else {
permissionListAlreadyDenied.forEach {
when (it) {
Manifest.permission.CAMERA -> {
if (explanationMessage.isNotEmpty()) {
explanationMessage += "\n\n"
}
explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera)
}
Manifest.permission.RECORD_AUDIO -> {
if (explanationMessage.isNotEmpty()) {
explanationMessage += "\n\n"
}
explanationMessage += activity.getString(R.string.permissions_rationale_msg_record_audio)
}
Manifest.permission.WRITE_EXTERNAL_STORAGE -> {
if (explanationMessage.isNotEmpty()) {
explanationMessage += "\n\n"
}
explanationMessage += activity.getString(R.string.permissions_rationale_msg_storage)
}
Manifest.permission.READ_CONTACTS -> {
if (!explanationMessage.isEmpty()) {
explanationMessage += "\n\n"
}
explanationMessage += activity.getString(R.string.permissions_rationale_msg_contacts)
}
else -> Timber.v("## checkPermissions(): already denied permission not supported")
}
}
}
// display the dialog with the info text // display the dialog with the info text
AlertDialog.Builder(activity) AlertDialog.Builder(activity)
.setTitle(R.string.permissions_rationale_popup_title) .setTitle(R.string.permissions_rationale_popup_title)
.setMessage(explanationMessage) .setMessage(rationaleMessage)
.setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() } .setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() }
.setPositiveButton(R.string.ok) { _, _ -> .setPositiveButton(R.string.ok) { _, _ ->
if (permissionsListToBeGranted.isNotEmpty()) { if (permissionsListToBeGranted.isNotEmpty()) {

View File

@ -0,0 +1,233 @@
/*
* 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.riotx.features.attachments
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.TargetApi
import android.content.Context
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.util.Pair
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewAnimationUtils
import android.view.animation.Animation
import android.view.animation.AnimationSet
import android.view.animation.OvershootInterpolator
import android.view.animation.ScaleAnimation
import android.view.animation.TranslateAnimation
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.core.view.doOnNextLayout
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import im.vector.riotx.R
import im.vector.riotx.core.extensions.getMeasurements
import im.vector.riotx.core.utils.PERMISSIONS_EMPTY
import im.vector.riotx.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import kotlin.math.max
private const val ANIMATION_DURATION = 250
/**
* This class is the view presenting choices for picking attachments.
* It will return result through [Callback].
*/
class AttachmentTypeSelectorView(context: Context,
inflater: LayoutInflater,
var callback: Callback?)
: PopupWindow(context) {
interface Callback {
fun onTypeSelected(type: Type)
}
private val iconColorGenerator = ColorGenerator.MATERIAL
private var galleryButton: ImageButton
private var cameraButton: ImageButton
private var fileButton: ImageButton
private var stickersButton: ImageButton
private var audioButton: ImageButton
private var contactButton: ImageButton
private var anchor: View? = null
init {
val root = FrameLayout(context)
val layout = inflater.inflate(R.layout.view_attachment_type_selector, root, true)
galleryButton = layout.findViewById<ImageButton>(R.id.attachmentGalleryButton).configure(Type.GALLERY)
cameraButton = layout.findViewById<ImageButton>(R.id.attachmentCameraButton).configure(Type.CAMERA)
fileButton = layout.findViewById<ImageButton>(R.id.attachmentFileButton).configure(Type.FILE)
stickersButton = layout.findViewById<ImageButton>(R.id.attachmentStickersButton).configure(Type.STICKER)
audioButton = layout.findViewById<ImageButton>(R.id.attachmentAudioButton).configure(Type.AUDIO)
contactButton = layout.findViewById<ImageButton>(R.id.attachmentContactButton).configure(Type.CONTACT)
contentView = root
width = LinearLayout.LayoutParams.MATCH_PARENT
height = LinearLayout.LayoutParams.WRAP_CONTENT
animationStyle = 0
@Suppress("DEPRECATION")
setBackgroundDrawable(BitmapDrawable())
inputMethodMode = INPUT_METHOD_NOT_NEEDED
isFocusable = true
isTouchable = true
}
fun show(anchor: View, isKeyboardOpen: Boolean) {
this.anchor = anchor
val anchorCoordinates = IntArray(2)
anchor.getLocationOnScreen(anchorCoordinates)
if (isKeyboardOpen) {
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] + anchor.height)
} else {
val contentViewHeight = if (contentView.height == 0) {
contentView.getMeasurements().second
} else {
contentView.height
}
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] - contentViewHeight)
}
contentView.doOnNextLayout {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
animateWindowInCircular(anchor, contentView)
} else {
animateWindowInTranslate(contentView)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
animateButtonIn(galleryButton, ANIMATION_DURATION / 2)
animateButtonIn(cameraButton, ANIMATION_DURATION / 2)
animateButtonIn(fileButton, ANIMATION_DURATION / 4)
animateButtonIn(audioButton, ANIMATION_DURATION / 2)
animateButtonIn(contactButton, ANIMATION_DURATION / 4)
animateButtonIn(stickersButton, 0)
}
}
override fun dismiss() {
val capturedAnchor = anchor
if (capturedAnchor != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
animateWindowOutCircular(capturedAnchor, contentView)
} else {
animateWindowOutTranslate(contentView)
}
}
private fun animateButtonIn(button: View, delay: Int) {
val animation = AnimationSet(true)
val scale = ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f)
animation.addAnimation(scale)
animation.interpolator = OvershootInterpolator(1f)
animation.duration = ANIMATION_DURATION.toLong()
animation.startOffset = delay.toLong()
button.startAnimation(animation)
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private fun animateWindowInCircular(anchor: View, contentView: View) {
val coordinates = getClickCoordinates(anchor, contentView)
val animator = ViewAnimationUtils.createCircularReveal(contentView,
coordinates.first,
coordinates.second,
0f,
max(contentView.width, contentView.height).toFloat())
animator.duration = ANIMATION_DURATION.toLong()
animator.start()
}
private fun animateWindowInTranslate(contentView: View) {
val animation = TranslateAnimation(0f, 0f, contentView.height.toFloat(), 0f)
animation.duration = ANIMATION_DURATION.toLong()
getContentView().startAnimation(animation)
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private fun animateWindowOutCircular(anchor: View, contentView: View) {
val coordinates = getClickCoordinates(anchor, contentView)
val animator = ViewAnimationUtils.createCircularReveal(getContentView(),
coordinates.first,
coordinates.second,
max(getContentView().width, getContentView().height).toFloat(),
0f)
animator.duration = ANIMATION_DURATION.toLong()
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super@AttachmentTypeSelectorView.dismiss()
}
})
animator.start()
}
private fun animateWindowOutTranslate(contentView: View) {
val animation = TranslateAnimation(0f, 0f, 0f, (contentView.top + contentView.height).toFloat())
animation.duration = ANIMATION_DURATION.toLong()
animation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {}
override fun onAnimationEnd(animation: Animation) {
super@AttachmentTypeSelectorView.dismiss()
}
override fun onAnimationRepeat(animation: Animation) {}
})
getContentView().startAnimation(animation)
}
private fun getClickCoordinates(anchor: View, contentView: View): Pair<Int, Int> {
val anchorCoordinates = IntArray(2)
anchor.getLocationOnScreen(anchorCoordinates)
val contentCoordinates = IntArray(2)
contentView.getLocationOnScreen(contentCoordinates)
val x = anchorCoordinates[0] - contentCoordinates[0] + anchor.width / 2
val y = anchorCoordinates[1] - contentCoordinates[1]
return Pair(x, y)
}
private fun ImageButton.configure(type: Type): ImageButton {
this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type.ordinal))
this.setOnClickListener(TypeClickListener(type))
return this
}
private inner class TypeClickListener(private val type: Type) : View.OnClickListener {
override fun onClick(v: View) {
dismiss()
callback?.onTypeSelected(type)
}
}
/**
* The all possible types to pick with their required permissions.
*/
enum class Type(val permissionsBit: Int) {
CAMERA(PERMISSIONS_EMPTY),
GALLERY(PERMISSIONS_FOR_WRITING_FILES),
FILE(PERMISSIONS_FOR_WRITING_FILES),
STICKER(PERMISSIONS_EMPTY),
AUDIO(PERMISSIONS_FOR_WRITING_FILES),
CONTACT(PERMISSIONS_FOR_PICKING_CONTACT)
}
}

View File

@ -0,0 +1,193 @@
/*
* 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.riotx.features.attachments
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import com.kbeanie.multipicker.api.Picker.*
import com.kbeanie.multipicker.core.PickerManager
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.riotx.core.platform.Restorable
import timber.log.Timber
private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"
private const val PENDING_TYPE_KEY = "PENDING_TYPE_KEY"
/**
* This class helps to handle attachments by providing simple methods.
* The process is asynchronous and you must implement [Callback] methods to get the data or a failure.
*/
class AttachmentsHelper private constructor(private val context: Context,
private val pickerManagerFactory: PickerManagerFactory) : Restorable {
companion object {
fun create(fragment: Fragment, callback: Callback): AttachmentsHelper {
return AttachmentsHelper(fragment.requireContext(), FragmentPickerManagerFactory(fragment, callback))
}
fun create(activity: Activity, callback: Callback): AttachmentsHelper {
return AttachmentsHelper(activity, ActivityPickerManagerFactory(activity, callback))
}
}
interface Callback {
fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
if (BuildConfig.LOG_PRIVATE_DATA) {
Timber.v("On contact attachment ready: $contactAttachment")
}
}
fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>)
fun onAttachmentsProcessFailed()
}
// Capture path allows to handle camera image picking. It must be restored if the activity gets killed.
private var capturePath: String? = null
// The pending type is set if we have to handle permission request. It must be restored if the activity gets killed.
var pendingType: AttachmentTypeSelectorView.Type? = null
private val imagePicker by lazy {
pickerManagerFactory.createImagePicker()
}
private val videoPicker by lazy {
pickerManagerFactory.createVideoPicker()
}
private val cameraImagePicker by lazy {
pickerManagerFactory.createCameraImagePicker()
}
private val filePicker by lazy {
pickerManagerFactory.createFilePicker()
}
private val audioPicker by lazy {
pickerManagerFactory.createAudioPicker()
}
private val contactPicker by lazy {
pickerManagerFactory.createContactPicker()
}
// Restorable
override fun onSaveInstanceState(outState: Bundle) {
capturePath?.also {
outState.putString(CAPTURE_PATH_KEY, it)
}
pendingType?.also {
outState.putSerializable(PENDING_TYPE_KEY, it)
}
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
capturePath = savedInstanceState?.getString(CAPTURE_PATH_KEY)
if (capturePath != null) {
cameraImagePicker.reinitialize(capturePath)
}
pendingType = savedInstanceState?.getSerializable(PENDING_TYPE_KEY) as? AttachmentTypeSelectorView.Type
}
// Public Methods
/**
* Starts the process for handling file picking
*/
fun selectFile() {
filePicker.pickFile()
}
/**
* Starts the process for handling image picking
*/
fun selectGallery() {
imagePicker.pickImage()
}
/**
* Starts the process for handling audio picking
*/
fun selectAudio() {
audioPicker.pickAudio()
}
/**
* Starts the process for handling capture image picking
*/
fun openCamera() {
capturePath = cameraImagePicker.pickImage()
}
/**
* Starts the process for handling contact picking
*/
fun selectContact() {
contactPicker.pickContact()
}
/**
* This methods aims to handle on activity result data.
*
* @return true if it can handle the data, false otherwise
*/
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (resultCode == Activity.RESULT_OK) {
val pickerManager = getPickerManagerForRequestCode(requestCode)
if (pickerManager != null) {
pickerManager.submit(data)
return true
}
}
return false
}
/**
* This methods aims to handle share intent.
*
* @return true if it can handle the intent data, false otherwise
*/
fun handleShareIntent(intent: Intent): Boolean {
val type = intent.resolveType(context) ?: return false
if (type.startsWith("image")) {
imagePicker.submit(intent)
} else if (type.startsWith("video")) {
videoPicker.submit(intent)
} else if (type.startsWith("audio")) {
videoPicker.submit(intent)
} else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) {
filePicker.submit(intent)
} else {
return false
}
return true
}
private fun getPickerManagerForRequestCode(requestCode: Int): PickerManager? {
return when (requestCode) {
PICK_IMAGE_DEVICE -> imagePicker
PICK_IMAGE_CAMERA -> cameraImagePicker
PICK_FILE -> filePicker
PICK_CONTACT -> contactPicker
PICK_AUDIO -> audioPicker
else -> null
}
}
}

View File

@ -0,0 +1,89 @@
/*
* 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.riotx.features.attachments
import com.kbeanie.multipicker.api.entity.*
import im.vector.matrix.android.api.session.content.ContentAttachmentData
fun ChosenContact.toContactAttachment(): ContactAttachment {
return ContactAttachment(
displayName = displayName,
photoUri = photoUri,
emails = emails.toList(),
phones = phones.toList()
)
}
fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
type = mapType(),
size = size,
date = createdAt?.time ?: System.currentTimeMillis(),
name = displayName
)
}
fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
type = mapType(),
size = size,
date = createdAt?.time ?: System.currentTimeMillis(),
name = displayName,
duration = duration
)
}
fun ChosenFile.mapType(): ContentAttachmentData.Type {
return when {
mimeType.startsWith("image/") -> ContentAttachmentData.Type.IMAGE
mimeType.startsWith("video/") -> ContentAttachmentData.Type.VIDEO
mimeType.startsWith("audio/") -> ContentAttachmentData.Type.AUDIO
else -> ContentAttachmentData.Type.FILE
}
}
fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
type = mapType(),
name = displayName,
size = size,
height = height.toLong(),
width = width.toLong(),
exifOrientation = orientation,
date = createdAt?.time ?: System.currentTimeMillis()
)
}
fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
type = ContentAttachmentData.Type.VIDEO,
size = size,
date = createdAt?.time ?: System.currentTimeMillis(),
height = height.toLong(),
width = width.toLong(),
duration = duration,
name = displayName
)
}

View File

@ -0,0 +1,92 @@
/*
* 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.riotx.features.attachments
import com.kbeanie.multipicker.api.callbacks.AudioPickerCallback
import com.kbeanie.multipicker.api.callbacks.ContactPickerCallback
import com.kbeanie.multipicker.api.callbacks.FilePickerCallback
import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback
import com.kbeanie.multipicker.api.callbacks.VideoPickerCallback
import com.kbeanie.multipicker.api.entity.*
/**
* This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback]
*/
class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback)
: ImagePickerCallback,
FilePickerCallback,
VideoPickerCallback,
AudioPickerCallback,
ContactPickerCallback {
override fun onContactChosen(contact: ChosenContact?) {
if (contact == null) {
callback.onAttachmentsProcessFailed()
} else {
val contactAttachment = contact.toContactAttachment()
callback.onContactAttachmentReady(contactAttachment)
}
}
override fun onAudiosChosen(audios: MutableList<ChosenAudio>?) {
if (audios.isNullOrEmpty()) {
callback.onAttachmentsProcessFailed()
} else {
val attachments = audios.map {
it.toContentAttachmentData()
}
callback.onContentAttachmentsReady(attachments)
}
}
override fun onFilesChosen(files: MutableList<ChosenFile>?) {
if (files.isNullOrEmpty()) {
callback.onAttachmentsProcessFailed()
} else {
val attachments = files.map {
it.toContentAttachmentData()
}
callback.onContentAttachmentsReady(attachments)
}
}
override fun onImagesChosen(images: MutableList<ChosenImage>?) {
if (images.isNullOrEmpty()) {
callback.onAttachmentsProcessFailed()
} else {
val attachments = images.map {
it.toContentAttachmentData()
}
callback.onContentAttachmentsReady(attachments)
}
}
override fun onVideosChosen(videos: MutableList<ChosenVideo>?) {
if (videos.isNullOrEmpty()) {
callback.onAttachmentsProcessFailed()
} else {
val attachments = videos.map {
it.toContentAttachmentData()
}
callback.onContentAttachmentsReady(attachments)
}
}
override fun onError(error: String?) {
callback.onAttachmentsProcessFailed()
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.riotx.features.attachments
/**
* Data class holding values of a picked contact
* Can be send as a text message waiting for the protocol to handle contact.
*/
data class ContactAttachment(
val displayName: String,
val photoUri: String?,
val phones: List<String> = emptyList(),
val emails: List<String> = emptyList()
) {
fun toHumanReadable(): String {
return buildString {
append(displayName)
phones.concatIn(this)
emails.concatIn(this)
}
}
private fun List<String>.concatIn(stringBuilder: StringBuilder) {
if (isNotEmpty()) {
stringBuilder.append("\n")
for (i in 0 until size - 1) {
val value = get(i)
stringBuilder.append(value).append("\n")
}
stringBuilder.append(last())
}
}
}

View File

@ -0,0 +1,134 @@
/*
* 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.riotx.features.attachments
import android.app.Activity
import androidx.fragment.app.Fragment
import com.kbeanie.multipicker.api.AudioPicker
import com.kbeanie.multipicker.api.CameraImagePicker
import com.kbeanie.multipicker.api.ContactPicker
import com.kbeanie.multipicker.api.FilePicker
import com.kbeanie.multipicker.api.ImagePicker
import com.kbeanie.multipicker.api.VideoPicker
/**
* Factory for creating different pickers. It allows to use with fragment or activity builders.
*/
interface PickerManagerFactory {
fun createImagePicker(): ImagePicker
fun createCameraImagePicker(): CameraImagePicker
fun createVideoPicker(): VideoPicker
fun createFilePicker(): FilePicker
fun createAudioPicker(): AudioPicker
fun createContactPicker(): ContactPicker
}
class ActivityPickerManagerFactory(private val activity: Activity, callback: AttachmentsHelper.Callback) : PickerManagerFactory {
private val attachmentsPickerCallback = AttachmentsPickerCallback(callback)
override fun createImagePicker(): ImagePicker {
return ImagePicker(activity).also {
it.setImagePickerCallback(attachmentsPickerCallback)
it.allowMultiple()
}
}
override fun createCameraImagePicker(): CameraImagePicker {
return CameraImagePicker(activity).also {
it.setImagePickerCallback(attachmentsPickerCallback)
}
}
override fun createVideoPicker(): VideoPicker {
return VideoPicker(activity).also {
it.setVideoPickerCallback(attachmentsPickerCallback)
it.allowMultiple()
}
}
override fun createFilePicker(): FilePicker {
return FilePicker(activity).also {
it.allowMultiple()
it.setFilePickerCallback(attachmentsPickerCallback)
}
}
override fun createAudioPicker(): AudioPicker {
return AudioPicker(activity).also {
it.allowMultiple()
it.setAudioPickerCallback(attachmentsPickerCallback)
}
}
override fun createContactPicker(): ContactPicker {
return ContactPicker(activity).also {
it.setContactPickerCallback(attachmentsPickerCallback)
}
}
}
class FragmentPickerManagerFactory(private val fragment: Fragment, callback: AttachmentsHelper.Callback) : PickerManagerFactory {
private val attachmentsPickerCallback = AttachmentsPickerCallback(callback)
override fun createImagePicker(): ImagePicker {
return ImagePicker(fragment).also {
it.setImagePickerCallback(attachmentsPickerCallback)
it.allowMultiple()
}
}
override fun createCameraImagePicker(): CameraImagePicker {
return CameraImagePicker(fragment).also {
it.setImagePickerCallback(attachmentsPickerCallback)
}
}
override fun createVideoPicker(): VideoPicker {
return VideoPicker(fragment).also {
it.setVideoPickerCallback(attachmentsPickerCallback)
it.allowMultiple()
}
}
override fun createFilePicker(): FilePicker {
return FilePicker(fragment).also {
it.allowMultiple()
it.setFilePickerCallback(attachmentsPickerCallback)
}
}
override fun createAudioPicker(): AudioPicker {
return AudioPicker(fragment).also {
it.allowMultiple()
it.setAudioPickerCallback(attachmentsPickerCallback)
}
}
override fun createContactPicker(): ContactPicker {
return ContactPicker(fragment).also {
it.setContactPickerCallback(attachmentsPickerCallback)
}
}
}

View File

@ -130,7 +130,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
} }
private fun exportKeysManually() { private fun exportKeysManually() {
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) { if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) {
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener { ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) { override fun onPassphrase(passphrase: String) {
showWaitingView() showWaitingView()

View File

@ -135,7 +135,13 @@ class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
} }
dialog.findViewById<View>(R.id.keys_backup_setup_save)?.setOnClickListener { dialog.findViewById<View>(R.id.keys_backup_setup_save)?.setOnClickListener {
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) { val permissionsChecked = checkPermissions(
PERMISSIONS_FOR_WRITING_FILES,
this,
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
R.string.permissions_rationale_msg_keys_backup_export
)
if (permissionsChecked) {
exportRecoveryKeyToFile(recoveryKey) exportRecoveryKeyToFile(recoveryKey)
} }
dialog.dismiss() dialog.dismiss()

View File

@ -27,7 +27,6 @@ import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.viewModel
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
@ -55,11 +54,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
object OpenGroup : Navigation() object OpenGroup : Navigation()
} }
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
private lateinit var navigationViewModel: HomeNavigationViewModel private lateinit var navigationViewModel: HomeNavigationViewModel
@Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var homeActivityViewModelFactory: HomeActivityViewModel.Factory
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var pushManager: PushersManager @Inject lateinit var pushManager: PushersManager
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager

View File

@ -16,7 +16,7 @@
package im.vector.riotx.features.home.room.detail package im.vector.riotx.features.home.room.detail
import com.jaiselrahman.filepicker.model.MediaFile import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.Timeline
@ -26,7 +26,7 @@ sealed class RoomDetailActions {
data class SaveDraft(val draft: String) : RoomDetailActions() data class SaveDraft(val draft: String) : RoomDetailActions()
data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions() data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions() data class SendMedia(val attachments: List<ContentAttachmentData>) : RoomDetailActions()
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions() data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions() data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions() data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()

View File

@ -52,14 +52,12 @@ import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.jaiselrahman.filepicker.activity.FilePickerActivity
import com.jaiselrahman.filepicker.config.Configurations
import com.jaiselrahman.filepicker.model.MediaFile
import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
@ -82,6 +80,11 @@ import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.ui.views.JumpToReadMarkerView import im.vector.riotx.core.ui.views.JumpToReadMarkerView
import im.vector.riotx.core.ui.views.NotificationAreaView import im.vector.riotx.core.ui.views.NotificationAreaView
import im.vector.riotx.core.utils.* import im.vector.riotx.core.utils.*
import im.vector.riotx.core.utils.Debouncer
import im.vector.riotx.core.utils.createUIHandler
import im.vector.riotx.features.attachments.AttachmentTypeSelectorView
import im.vector.riotx.features.attachments.AttachmentsHelper
import im.vector.riotx.features.attachments.ContactAttachment
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
@ -112,6 +115,7 @@ import im.vector.riotx.features.media.VideoMediaViewerActivity
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.fragment_room_detail.*
@ -125,20 +129,20 @@ import javax.inject.Inject
@Parcelize @Parcelize
data class RoomDetailArgs( data class RoomDetailArgs(
val roomId: String, val roomId: String,
val eventId: String? = null val eventId: String? = null,
val sharedData: SharedData? = null
) : Parcelable ) : Parcelable
private const val CAMERA_VALUE_TITLE = "attachment" private const val REACTION_SELECT_REQUEST_CODE = 0
private const val REQUEST_FILES_REQUEST_CODE = 0
private const val TAKE_IMAGE_REQUEST_CODE = 1
private const val REACTION_SELECT_REQUEST_CODE = 2
class RoomDetailFragment : class RoomDetailFragment :
VectorBaseFragment(), VectorBaseFragment(),
TimelineEventController.Callback, TimelineEventController.Callback,
AutocompleteUserPresenter.Callback, AutocompleteUserPresenter.Callback,
VectorInviteView.Callback, VectorInviteView.Callback,
JumpToReadMarkerView.Callback { JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback,
AttachmentsHelper.Callback {
companion object { companion object {
@ -199,9 +203,12 @@ class RoomDetailFragment :
private lateinit var actionViewModel: ActionsHandler private lateinit var actionViewModel: ActionsHandler
private lateinit var layoutManager: LinearLayoutManager private lateinit var layoutManager: LinearLayoutManager
private lateinit var attachmentsHelper: AttachmentsHelper
private lateinit var keyboardStateUtils: KeyboardStateUtils
@BindView(R.id.composerLayout) @BindView(R.id.composerLayout)
lateinit var composerLayout: TextComposerView lateinit var composerLayout: TextComposerView
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
private var lockSendButton = false private var lockSendButton = false
@ -212,6 +219,8 @@ class RoomDetailFragment :
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java) actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
attachmentsHelper = AttachmentsHelper.create(this, this).register()
keyboardStateUtils = KeyboardStateUtils(requireActivity())
setupToolbar(roomToolbar) setupToolbar(roomToolbar)
setupRecyclerView() setupRecyclerView()
setupComposer() setupComposer()
@ -275,6 +284,14 @@ class RoomDetailFragment :
roomDetailViewModel.requestLiveData.observeEvent(this) { roomDetailViewModel.requestLiveData.observeEvent(this) {
displayRoomDetailActionResult(it) displayRoomDetailActionResult(it)
} }
if (savedInstanceState == null) {
when (val sharedData = roomDetailArgs.sharedData) {
is SharedData.Text -> roomDetailViewModel.process(RoomDetailActions.SendMessage(sharedData.text, false))
is SharedData.Attachments -> roomDetailViewModel.process(RoomDetailActions.SendMedia(sharedData.attachmentData))
null -> Timber.v("No share data to process")
}
}
} }
override fun onDestroy() { override fun onDestroy() {
@ -404,7 +421,7 @@ class RoomDetailFragment :
if (text != composerLayout.composerEditText.text.toString()) { if (text != composerLayout.composerEditText.text.toString()) {
// Ignore update to avoid saving a draft // Ignore update to avoid saving a draft
composerLayout.composerEditText.setText(text) composerLayout.composerEditText.setText(text)
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
} }
} }
@ -423,13 +440,14 @@ class RoomDetailFragment :
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK && data != null) { if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
when (requestCode) { when (requestCode) {
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REACTION_SELECT_REQUEST_CODE -> {
REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) ?: return ?: return
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
?: return
// TODO check if already reacted with that? // TODO check if already reacted with that?
roomDetailViewModel.process(RoomDetailActions.SendReaction(eventId, reaction)) roomDetailViewModel.process(RoomDetailActions.SendReaction(eventId, reaction))
} }
@ -598,47 +616,27 @@ class RoomDetailFragment :
composerLayout.composerRelatedMessageCloseButton.setOnClickListener { composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
roomDetailViewModel.process(RoomDetailActions.ExitSpecialMode(composerLayout.composerEditText.text.toString())) roomDetailViewModel.process(RoomDetailActions.ExitSpecialMode(composerLayout.composerEditText.text.toString()))
} }
composerLayout.callback = object : TextComposerView.Callback {
override fun onRichContentSelected(contentUri: Uri): Boolean {
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
data = contentUri
}
val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
if (!isHandled) {
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
}
return isHandled
}
}
} }
private fun setupAttachmentButton() { private fun setupAttachmentButton() {
composerLayout.attachmentButton.setOnClickListener { composerLayout.attachmentButton.setOnClickListener {
val intent = Intent(requireContext(), FilePickerActivity::class.java) if (!::attachmentTypeSelector.isInitialized) {
intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder() attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this)
.setCheckPermission(true)
.setShowFiles(true)
.setShowAudios(true)
.setSkipZeroSizeFiles(true)
.build())
startActivityForResult(intent, REQUEST_FILES_REQUEST_CODE)
/*
val items = ArrayList<DialogListItem>()
// Send file
items.add(DialogListItem.SendFile)
// Send voice
if (vectorPreferences.isSendVoiceFeatureEnabled()) {
items.add(DialogListItem.SendVoice.INSTANCE)
} }
attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing)
// Send sticker
//items.add(DialogListItem.SendSticker)
// Camera
//if (vectorPreferences.useNativeCamera()) {
items.add(DialogListItem.TakePhoto)
items.add(DialogListItem.TakeVideo)
//} else {
// items.add(DialogListItem.TakePhotoVideo.INSTANCE)
// }
val adapter = DialogSendItemAdapter(requireContext(), items)
AlertDialog.Builder(requireContext())
.setAdapter(adapter) { _, position ->
onSendChoiceClicked(items[position])
}
.setNegativeButton(R.string.cancel, null)
.show()
*/
} }
} }
@ -646,38 +644,6 @@ class RoomDetailFragment :
inviteView.callback = this inviteView.callback = this
} }
/* private fun onSendChoiceClicked(dialogListItem: DialogListItem) {
Timber.v("On send choice clicked: $dialogListItem")
when (dialogListItem) {
is DialogListItem.SendFile -> {
// launchFileIntent
}
is DialogListItem.SendVoice -> {
//launchAudioRecorderIntent()
}
is DialogListItem.SendSticker -> {
//startStickerPickerActivity()
}
is DialogListItem.TakePhotoVideo ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
// launchCamera()
}
is DialogListItem.TakePhoto ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) {
openCamera(requireActivity(), CAMERA_VALUE_TITLE, TAKE_IMAGE_REQUEST_CODE)
}
is DialogListItem.TakeVideo ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA)) {
// launchNativeVideoRecorder()
}
}
} */
private fun handleMediaIntent(data: Intent) {
val files: ArrayList<MediaFile> = data.getParcelableArrayListExtra(FilePickerActivity.MEDIA_FILES)
roomDetailViewModel.process(RoomDetailActions.SendMedia(files))
}
private fun renderState(state: RoomDetailViewState) { private fun renderState(state: RoomDetailViewState) {
readMarkerHelper.updateWith(state) readMarkerHelper.updateWith(state)
renderRoomSummary(state) renderRoomSummary(state)
@ -941,11 +907,16 @@ class RoomDetailFragment :
if (allGranted(grantResults)) { if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) { if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) {
val action = roomDetailViewModel.pendingAction val action = roomDetailViewModel.pendingAction
if (action != null) { if (action != null) {
roomDetailViewModel.pendingAction = null roomDetailViewModel.pendingAction = null
roomDetailViewModel.process(action) roomDetailViewModel.process(action)
} }
} else if (requestCode == PERMISSION_REQUEST_CODE_PICK_ATTACHMENT) {
val pendingType = attachmentsHelper.pendingType
if (pendingType != null) {
attachmentsHelper.pendingType = null
launchAttachmentProcess(pendingType)
}
} }
} }
} }
@ -1164,21 +1135,21 @@ class RoomDetailFragment :
val myDisplayName = session.getUser(session.myUserId)?.displayName val myDisplayName = session.getUser(session.myUserId)?.displayName
if (myDisplayName == text) { if (myDisplayName == text) {
// current user // current user
if (composerLayout.composerEditText.text.isBlank()) { if (composerLayout.composerEditText.text.isNullOrBlank()) {
composerLayout.composerEditText.append(Command.EMOTE.command + " ") composerLayout.composerEditText.append(Command.EMOTE.command + " ")
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
// vibrate = true // vibrate = true
} }
} else { } else {
// another user // another user
if (composerLayout.composerEditText.text.isBlank()) { if (composerLayout.composerEditText.text.isNullOrBlank()) {
// Ensure displayName will not be interpreted as a Slash command // Ensure displayName will not be interpreted as a Slash command
if (text.startsWith("/")) { if (text.startsWith("/")) {
composerLayout.composerEditText.append("\\") composerLayout.composerEditText.append("\\")
} }
composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ") composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ")
} else { } else {
composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ") composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
} }
// vibrate = true // vibrate = true
@ -1227,4 +1198,41 @@ class RoomDetailFragment :
override fun onClearReadMarkerClicked() { override fun onClearReadMarkerClicked() {
roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead) roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead)
} }
// AttachmentTypeSelectorView.Callback
override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) {
if (checkPermissions(type.permissionsBit, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)) {
launchAttachmentProcess(type)
} else {
attachmentsHelper.pendingType = type
}
}
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
when (type) {
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera()
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile()
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery()
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio()
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact()
AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
}
}
// AttachmentsHelper.Callback
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
roomDetailViewModel.process(RoomDetailActions.SendMedia(attachments))
}
override fun onAttachmentsProcessFailed() {
Toast.makeText(requireContext(), R.string.error_attachment, Toast.LENGTH_SHORT).show()
}
override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
super.onContactAttachmentReady(contactAttachment)
val formattedContact = contactAttachment.toHumanReadable()
roomDetailViewModel.process(RoomDetailActions.SendMessage(formattedContact, false))
}
} }

View File

@ -16,7 +16,6 @@
package im.vector.riotx.features.home.room.detail package im.vector.riotx.features.home.room.detail
import android.net.Uri
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -27,7 +26,6 @@ import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isImageMessage import im.vector.matrix.android.api.session.events.model.isImageMessage
import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.isTextMessage
@ -49,8 +47,6 @@ import im.vector.matrix.rx.unwrap
import im.vector.riotx.BuildConfig import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.images.ImageTools
import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent
@ -69,7 +65,6 @@ import java.util.concurrent.TimeUnit
class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState, class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState,
userPreferencesProvider: UserPreferencesProvider, userPreferencesProvider: UserPreferencesProvider,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val imageTools: ImageTools,
private val session: Session private val session: Session
) : VectorViewModel<RoomDetailViewState>(initialState) { ) : VectorViewModel<RoomDetailViewState>(initialState) {
@ -471,32 +466,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
private fun handleSendMedia(action: RoomDetailActions.SendMedia) { private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
val attachments = action.mediaFiles.map { val attachments = action.attachments
val pathWithScheme = if (it.path.startsWith("/")) {
"file://" + it.path
} else {
it.path
}
val uri = Uri.parse(pathWithScheme)
val nameWithExtension = getFilenameFromUri(null, uri)
ContentAttachmentData(
size = it.size,
duration = it.duration,
date = it.date,
height = it.height,
width = it.width,
exifOrientation = imageTools.getOrientationForBitmap(uri),
name = nameWithExtension ?: it.name,
path = it.path,
mimeType = it.mimeType,
type = ContentAttachmentData.Type.values()[it.mediaType]
)
}
val homeServerCapabilities = session.getHomeServerCapabilities() val homeServerCapabilities = session.getHomeServerCapabilities()
val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize
if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) { if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {
@ -505,7 +476,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} else { } else {
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
null -> room.sendMedias(attachments) null -> room.sendMedias(attachments)
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name
?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
} }
} }
} }

View File

@ -0,0 +1,58 @@
/*
* 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.riotx.features.home.room.detail.composer
import android.content.Context
import android.net.Uri
import android.os.Build
import android.util.AttributeSet
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle)
: AppCompatEditText(context, attrs, defStyleAttr) {
interface Callback {
fun onRichContentSelected(contentUri: Uri): Boolean
}
var callback: Callback? = null
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
val ic: InputConnection = super.onCreateInputConnection(editorInfo)
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("*/*"))
val callback =
InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, _ ->
val lacksPermission = (flags and
InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && lacksPermission) {
try {
inputContentInfo.requestPermission()
} catch (e: Exception) {
return@OnCommitContentListener false
}
}
callback?.onRichContentSelected(inputContentInfo.contentUri) ?: false
}
return InputConnectionCompat.createWrapper(ic, editorInfo, callback)
}
}

View File

@ -17,9 +17,9 @@
package im.vector.riotx.features.home.room.detail.composer package im.vector.riotx.features.home.room.detail.composer
import android.content.Context import android.content.Context
import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
@ -39,6 +39,10 @@ import im.vector.riotx.R
class TextComposerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, class TextComposerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) { defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
interface Callback : ComposerEditText.Callback
var callback: Callback? = null
@BindView(R.id.composer_related_message_sender) @BindView(R.id.composer_related_message_sender)
lateinit var composerRelatedMessageTitle: TextView lateinit var composerRelatedMessageTitle: TextView
@BindView(R.id.composer_related_message_preview) @BindView(R.id.composer_related_message_preview)
@ -50,11 +54,11 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
@BindView(R.id.composer_related_message_close) @BindView(R.id.composer_related_message_close)
lateinit var composerRelatedMessageCloseButton: ImageButton lateinit var composerRelatedMessageCloseButton: ImageButton
@BindView(R.id.composerEditText) @BindView(R.id.composerEditText)
lateinit var composerEditText: EditText lateinit var composerEditText: ComposerEditText
@BindView(R.id.composer_avatar_view) @BindView(R.id.composer_avatar_view)
lateinit var composerAvatarImageView: ImageView lateinit var composerAvatarImageView: ImageView
var currentConstraintSetId: Int = -1 private var currentConstraintSetId: Int = -1
private val animationDuration = 100L private val animationDuration = 100L
@ -62,6 +66,11 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
inflate(context, R.layout.merge_composer_layout, this) inflate(context, R.layout.merge_composer_layout, this)
ButterKnife.bind(this) ButterKnife.bind(this)
collapse(false) collapse(false)
composerEditText.callback = object : Callback, ComposerEditText.Callback {
override fun onRichContentSelected(contentUri: Uri): Boolean {
return callback?.onRichContentSelected(contentUri) ?: false
}
}
} }
fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {

View File

@ -42,9 +42,7 @@ class FilteredRoomsActivity : VectorBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
configureToolbar(filteredRoomsToolbar) configureToolbar(filteredRoomsToolbar)
if (isFirstCreation()) { if (isFirstCreation()) {
roomListFragment = RoomListFragment.newInstance(RoomListParams(RoomListFragment.DisplayMode.FILTERED)) roomListFragment = RoomListFragment.newInstance(RoomListParams(RoomListFragment.DisplayMode.FILTERED))
replaceFragment(roomListFragment, R.id.filteredRoomsFragmentContainer, FRAGMENT_TAG) replaceFragment(roomListFragment, R.id.filteredRoomsFragmentContainer, FRAGMENT_TAG)
@ -58,12 +56,10 @@ class FilteredRoomsActivity : VectorBaseActivity() {
} }
override fun onQueryTextChange(newText: String): Boolean { override fun onQueryTextChange(newText: String): Boolean {
// TODO Create a viewModel and remove this public fun
roomListFragment.filterRoomsWith(newText) roomListFragment.filterRoomsWith(newText)
return true return true
} }
}) })
// Open the keyboard immediately // Open the keyboard immediately
filteredRoomsSearchView.requestFocus() filteredRoomsSearchView.requestFocus()
} }

View File

@ -32,6 +32,7 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListFragment.Displa
RoomListFragment.DisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN RoomListFragment.DisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
RoomListFragment.DisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN RoomListFragment.DisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN
RoomListFragment.DisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN RoomListFragment.DisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN
RoomListFragment.DisplayMode.SHARE -> roomSummary.membership == Membership.JOIN
} }
} }
} }

View File

@ -41,13 +41,15 @@ import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.room.list.widget.FabMenuView import im.vector.riotx.features.home.room.list.widget.FabMenuView
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.share.SharedData
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_list.* import kotlinx.android.synthetic.main.fragment_room_list.*
import javax.inject.Inject import javax.inject.Inject
@Parcelize @Parcelize
data class RoomListParams( data class RoomListParams(
val displayMode: RoomListFragment.DisplayMode val displayMode: RoomListFragment.DisplayMode,
val sharedData: SharedData? = null
) : Parcelable ) : Parcelable
class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener { class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
@ -56,7 +58,8 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
HOME(R.string.bottom_action_home), HOME(R.string.bottom_action_home),
PEOPLE(R.string.bottom_action_people_x), PEOPLE(R.string.bottom_action_people_x),
ROOMS(R.string.bottom_action_rooms), ROOMS(R.string.bottom_action_rooms),
FILTERED(/* Not used */ R.string.bottom_action_rooms) FILTERED(/* Not used */ 0),
SHARE(/* Not used */ 0)
} }
companion object { companion object {
@ -106,7 +109,12 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
setupRecyclerView() setupRecyclerView()
roomListViewModel.subscribe { renderState(it) } roomListViewModel.subscribe { renderState(it) }
roomListViewModel.openRoomLiveData.observeEventFirstThrottle(this, 800L) { roomListViewModel.openRoomLiveData.observeEventFirstThrottle(this, 800L) {
navigator.openRoom(requireActivity(), it) if (roomListParams.displayMode == DisplayMode.SHARE) {
val sharedData = roomListParams.sharedData ?: return@observeEventFirstThrottle
navigator.openRoomForSharing(requireActivity(), it, sharedData)
} else {
navigator.openRoom(requireActivity(), it)
}
} }
createChatFabMenu.listener = this createChatFabMenu.listener = this
@ -121,10 +129,10 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
private fun setupCreateRoomButton() { private fun setupCreateRoomButton() {
when (roomListParams.displayMode) { when (roomListParams.displayMode) {
DisplayMode.HOME -> createChatFabMenu.isVisible = true DisplayMode.HOME -> createChatFabMenu.isVisible = true
DisplayMode.PEOPLE -> createChatRoomButton.isVisible = true DisplayMode.PEOPLE -> createChatRoomButton.isVisible = true
DisplayMode.ROOMS -> createGroupRoomButton.isVisible = true DisplayMode.ROOMS -> createGroupRoomButton.isVisible = true
DisplayMode.FILTERED -> Unit // No button in this mode else -> Unit // No button in this mode
} }
createChatRoomButton.setOnClickListener { createChatRoomButton.setOnClickListener {
@ -147,10 +155,10 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
RecyclerView.SCROLL_STATE_DRAGGING, RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> { RecyclerView.SCROLL_STATE_SETTLING -> {
when (roomListParams.displayMode) { when (roomListParams.displayMode) {
DisplayMode.HOME -> createChatFabMenu.hide() DisplayMode.HOME -> createChatFabMenu.hide()
DisplayMode.PEOPLE -> createChatRoomButton.hide() DisplayMode.PEOPLE -> createChatRoomButton.hide()
DisplayMode.ROOMS -> createGroupRoomButton.hide() DisplayMode.ROOMS -> createGroupRoomButton.hide()
DisplayMode.FILTERED -> Unit else -> Unit
} }
} }
} }
@ -187,10 +195,10 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
private val showFabRunnable = Runnable { private val showFabRunnable = Runnable {
if (isAdded) { if (isAdded) {
when (roomListParams.displayMode) { when (roomListParams.displayMode) {
DisplayMode.HOME -> createChatFabMenu.show() DisplayMode.HOME -> createChatFabMenu.show()
DisplayMode.PEOPLE -> createChatRoomButton.show() DisplayMode.PEOPLE -> createChatRoomButton.show()
DisplayMode.ROOMS -> createGroupRoomButton.show() DisplayMode.ROOMS -> createGroupRoomButton.show()
DisplayMode.FILTERED -> Unit else -> Unit
} }
} }
} }
@ -235,7 +243,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
} }
.isNullOrEmpty() .isNullOrEmpty()
val emptyState = when (roomListParams.displayMode) { val emptyState = when (roomListParams.displayMode) {
DisplayMode.HOME -> { DisplayMode.HOME -> {
if (hasNoRoom) { if (hasNoRoom) {
StateView.State.Empty( StateView.State.Empty(
getString(R.string.room_list_catchup_welcome_title), getString(R.string.room_list_catchup_welcome_title),
@ -249,19 +257,19 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
getString(R.string.room_list_catchup_empty_body)) getString(R.string.room_list_catchup_empty_body))
} }
} }
DisplayMode.PEOPLE -> DisplayMode.PEOPLE ->
StateView.State.Empty( StateView.State.Empty(
getString(R.string.room_list_people_empty_title), getString(R.string.room_list_people_empty_title),
ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_chat), ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_chat),
getString(R.string.room_list_people_empty_body) getString(R.string.room_list_people_empty_body)
) )
DisplayMode.ROOMS -> DisplayMode.ROOMS ->
StateView.State.Empty( StateView.State.Empty(
getString(R.string.room_list_rooms_empty_title), getString(R.string.room_list_rooms_empty_title),
ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_group), ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_group),
getString(R.string.room_list_rooms_empty_body) getString(R.string.room_list_rooms_empty_body)
) )
DisplayMode.FILTERED -> else ->
// Always display the content in this mode, because if the footer // Always display the content in this mode, because if the footer
StateView.State.Content StateView.State.Content
} }

View File

@ -230,6 +230,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
RoomListFragment.DisplayMode.PEOPLE -> chronologicalRoomComparator RoomListFragment.DisplayMode.PEOPLE -> chronologicalRoomComparator
RoomListFragment.DisplayMode.ROOMS -> chronologicalRoomComparator RoomListFragment.DisplayMode.ROOMS -> chronologicalRoomComparator
RoomListFragment.DisplayMode.FILTERED -> chronologicalRoomComparator RoomListFragment.DisplayMode.FILTERED -> chronologicalRoomComparator
RoomListFragment.DisplayMode.SHARE -> chronologicalRoomComparator
} }
return RoomSummaries().apply { return RoomSummaries().apply {

View File

@ -16,6 +16,7 @@
package im.vector.riotx.features.navigation package im.vector.riotx.features.navigation
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
@ -33,6 +34,7 @@ import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -46,6 +48,13 @@ class DefaultNavigator @Inject constructor() : Navigator {
context.startActivity(intent) context.startActivity(intent)
} }
override fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData) {
val args = RoomDetailArgs(roomId, null, sharedData)
val intent = RoomDetailActivity.newIntent(activity, args)
activity.startActivity(intent)
activity.finish()
}
override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String?) { override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String?) {
if (context is VectorBaseActivity) { if (context is VectorBaseActivity) {
context.notImplemented("Open not joined room") context.notImplemented("Open not joined room")

View File

@ -16,13 +16,17 @@
package im.vector.riotx.features.navigation package im.vector.riotx.features.navigation
import android.app.Activity
import android.content.Context import android.content.Context
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotx.features.share.SharedData
interface Navigator { interface Navigator {
fun openRoom(context: Context, roomId: String, eventId: String? = null) fun openRoom(context: Context, roomId: String, eventId: String? = null)
fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData)
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String? = null) fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String? = null)
fun openRoomPreview(publicRoom: PublicRoom, context: Context) fun openRoomPreview(publicRoom: PublicRoom, context: Context)

View File

@ -211,7 +211,7 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() {
*/ */
private fun exportKeys() { private fun exportKeys() {
// We need WRITE_EXTERNAL permission // We need WRITE_EXTERNAL permission
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) { if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) {
activity?.let { activity -> activity?.let { activity ->
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener { ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) { override fun onPassphrase(passphrase: String) {

View File

@ -0,0 +1,118 @@
/*
* 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.riotx.features.share
import android.content.ClipDescription
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import com.kbeanie.multipicker.utils.IntentUtils
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.attachments.AttachmentsHelper
import im.vector.riotx.features.home.LoadingFragment
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.home.room.list.RoomListParams
import im.vector.riotx.features.login.LoginActivity
import kotlinx.android.synthetic.main.activity_incoming_share.*
import javax.inject.Inject
class IncomingShareActivity :
VectorBaseActivity(), AttachmentsHelper.Callback {
@Inject lateinit var sessionHolder: ActiveSessionHolder
private lateinit var roomListFragment: RoomListFragment
private lateinit var attachmentsHelper: AttachmentsHelper
override fun getLayoutRes(): Int {
return R.layout.activity_incoming_share
}
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// If we are not logged in, stop the sharing process and open login screen.
// In the future, we might want to relaunch the sharing process after login.
if (!sessionHolder.hasActiveSession()) {
startLoginActivity()
return
}
configureToolbar(incomingShareToolbar)
if (isFirstCreation()) {
val loadingDetail = LoadingFragment.newInstance()
replaceFragment(loadingDetail, R.id.shareRoomListFragmentContainer)
}
attachmentsHelper = AttachmentsHelper.create(this, this).register()
if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) {
var isShareManaged = attachmentsHelper.handleShareIntent(
IntentUtils.getPickerIntentForSharing(intent)
)
if (!isShareManaged) {
isShareManaged = handleTextShare(intent)
}
if (!isShareManaged) {
cannotManageShare()
}
} else {
cannotManageShare()
}
}
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Attachments(attachments))
roomListFragment = RoomListFragment.newInstance(roomListParams)
replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer)
}
override fun onAttachmentsProcessFailed() {
cannotManageShare()
}
private fun cannotManageShare() {
Toast.makeText(this, R.string.error_handling_incoming_share, Toast.LENGTH_LONG).show()
finish()
}
private fun handleTextShare(intent: Intent): Boolean {
if (intent.type == ClipDescription.MIMETYPE_TEXT_PLAIN) {
val sharedText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
return if (sharedText.isNullOrEmpty()) {
false
} else {
val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Text(sharedText))
roomListFragment = RoomListFragment.newInstance(roomListParams)
replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer)
true
}
}
return false
}
private fun startLoginActivity() {
val intent = LoginActivity.newIntent(this, null)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
finish()
}
}

View File

@ -0,0 +1,30 @@
/*
* 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.riotx.features.share
import android.os.Parcelable
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import kotlinx.android.parcel.Parcelize
sealed class SharedData: Parcelable {
@Parcelize
data class Text(val text: String): SharedData()
@Parcelize
data class Attachments(val attachmentData: List<ContentAttachmentData>): SharedData()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="?riotx_attachment_selector_border" />
<corners android:radius="16dp" />
</shape>
</item>
<item
android:bottom="1dp"
android:left="1dp"
android:right="1dp"
android:top="1dp">
<shape android:shape="rectangle">
<solid android:color="?riotx_attachment_selector_background" />
<corners android:radius="16dp" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFF" android:pathData="M12,1c-4.97,0 -9,4.03 -9,9v7c0,1.66 1.34,3 3,3h3v-8H5v-2c0,-3.87 3.13,-7 7,-7s7,3.13 7,7v2h-4v8h3c1.66,0 3,-1.34 3,-3v-7c0,-4.97 -4.03,-9 -9,-9z"/>
</vector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFF" android:pathData="M9.4,10.5l4.77,-8.26C13.47,2.09 12.75,2 12,2c-2.4,0 -4.6,0.85 -6.32,2.25l3.66,6.35 0.06,-0.1zM21.54,9c-0.92,-2.92 -3.15,-5.26 -6,-6.34L11.88,9h9.66zM21.8,10h-7.49l0.29,0.5 4.76,8.25C21,16.97 22,14.61 22,12c0,-0.69 -0.07,-1.35 -0.2,-2zM8.54,12l-3.9,-6.75C3.01,7.03 2,9.39 2,12c0,0.69 0.07,1.35 0.2,2h7.49l-1.15,-2zM2.46,15c0.92,2.92 3.15,5.26 6,6.34L12.12,15L2.46,15zM13.73,15l-3.9,6.76c0.7,0.15 1.42,0.24 2.17,0.24 2.4,0 4.6,-0.85 6.32,-2.25l-3.66,-6.35 -0.93,1.6z"/>
</vector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFF" android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
</vector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFF" android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z"/>
</vector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFF" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/incomingShareToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="0dp"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
app:contentInsetStart="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.SearchView
android:id="@+id/incomingShareSearchView"
style="@style/VectorSearchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:queryHint="@string/room_filtering_filter_hint"
app:searchIcon="@drawable/ic_filter" />
</androidx.appcompat.widget.Toolbar>
<FrameLayout
android:id="@+id/shareRoomListFragmentContainer"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/incomingShareToolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -143,7 +143,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/attachmentButton" /> app:layout_constraintStart_toEndOf="@id/attachmentButton" />
<EditText <im.vector.riotx.features.home.room.detail.composer.ComposerEditText
android:id="@+id/composerEditText" android:id="@+id/composerEditText"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -153,7 +153,7 @@
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier" app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
app:layout_constraintVertical_bias="1" /> app:layout_constraintVertical_bias="1" />
<EditText <im.vector.riotx.features.home.room.detail.composer.ComposerEditText
android:id="@+id/composerEditText" android:id="@+id/composerEditText"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -114,7 +114,7 @@
android:tint="?attr/colorAccent" android:tint="?attr/colorAccent"
tools:ignore="MissingConstraints" /> tools:ignore="MissingConstraints" />
<EditText <im.vector.riotx.features.home.room.detail.composer.ComposerEditText
android:id="@+id/composerEditText" android:id="@+id/composerEditText"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_attachment_type_selector"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:orientation="vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:baselineAligned="false"
android:orientation="horizontal"
android:weightSum="3">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton
android:id="@+id/attachmentCameraButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_camera_white_24dp"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:text="@string/attachment_type_camera" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton
android:id="@+id/attachmentGalleryButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_gallery_white_24dp"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:text="@string/attachment_type_gallery" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton
android:id="@+id/attachmentFileButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_file_white_24dp"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:text="@string/attachment_type_file" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:baselineAligned="false"
android:orientation="horizontal"
android:weightSum="3">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton
android:id="@+id/attachmentAudioButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_audio_white_24dp"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:text="@string/attachment_type_audio" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton
android:id="@+id/attachmentContactButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_contact_white_24dp"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:text="@string/attachment_type_contact" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton
android:id="@+id/attachmentStickersButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_stickers_white_24dp"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:text="@string/attachment_type_sticker" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -147,6 +147,16 @@
<color name="riotx_touch_guard_bg_dark">#BF000000</color> <color name="riotx_touch_guard_bg_dark">#BF000000</color>
<color name="riotx_touch_guard_bg_black">#BF000000</color> <color name="riotx_touch_guard_bg_black">#BF000000</color>
<attr name="riotx_attachment_selector_background" format="color" />
<color name="riotx_attachment_selector_background_light">#FFFFFFFF</color>
<color name="riotx_attachment_selector_background_dark">#FF22262E</color>
<color name="riotx_attachment_selector_background_black">#FF090A0C</color>
<attr name="riotx_attachment_selector_border" format="color" />
<color name="riotx_attachment_selector_border_light">#FFE9EDF1</color>
<color name="riotx_attachment_selector_border_dark">#FF22262E</color>
<color name="riotx_attachment_selector_border_black">#FF090A0C</color>
<!-- (color from RiotWeb) --> <!-- (color from RiotWeb) -->
<attr name="riotx_keys_backup_banner_accent_color" format="color" /> <attr name="riotx_keys_backup_banner_accent_color" format="color" />
<color name="riotx_keys_backup_banner_accent_color_light">#FFF8E3</color> <color name="riotx_keys_backup_banner_accent_color_light">#FFF8E3</color>

View File

@ -369,6 +369,9 @@
<string name="permissions_rationale_msg_contacts">Riot can check your address book to find other Matrix users based on their email and phone numbers. If you agree to share your address book for this purpose, please allow access on the next pop-up.</string> <string name="permissions_rationale_msg_contacts">Riot can check your address book to find other Matrix users based on their email and phone numbers. If you agree to share your address book for this purpose, please allow access on the next pop-up.</string>
<string name="permissions_msg_contacts_warning_other_androids">Riot can check your address book to find other Matrix users based on their email and phone numbers.\n\nDo you agree to share your address book for this purpose?</string> <string name="permissions_msg_contacts_warning_other_androids">Riot can check your address book to find other Matrix users based on their email and phone numbers.\n\nDo you agree to share your address book for this purpose?</string>
<string name="permissions_action_not_performed_missing_permissions">Sorry. Action not performed, due to missing permissions</string> <string name="permissions_action_not_performed_missing_permissions">Sorry. Action not performed, due to missing permissions</string>
<!-- medias slider string --> <!-- medias slider string -->

View File

@ -37,6 +37,14 @@
<string name="error_file_too_big">"The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."</string> <string name="error_file_too_big">"The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."</string>
<string name="error_attachment">"An error occurred while retrieving the attachment."</string>
<string name="attachment_type_file">"File"</string>
<string name="attachment_type_contact">"Contact"</string>
<string name="attachment_type_camera">"Camera"</string>
<string name="attachment_type_audio">"Audio"</string>
<string name="attachment_type_gallery">"Gallery"</string>
<string name="attachment_type_sticker">"Sticker"</string>
<string name="error_handling_incoming_share">Couldn\'t handle share data</string>
<string name="report_content_spam">"It's spam"</string> <string name="report_content_spam">"It's spam"</string>
<string name="report_content_inappropriate">"It's inappropriate"</string> <string name="report_content_inappropriate">"It's inappropriate"</string>
@ -52,4 +60,7 @@
<string name="content_reported_as_spam_content">"This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string> <string name="content_reported_as_spam_content">"This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string>
<string name="content_reported_as_inappropriate_title">"Reported as inappropriate"</string> <string name="content_reported_as_inappropriate_title">"Reported as inappropriate"</string>
<string name="content_reported_as_inappropriate_content">"This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string> <string name="content_reported_as_inappropriate_content">"This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string>
<string name="permissions_rationale_msg_keys_backup_export">Riot needs permission to save your E2E keys on disk.\n\nPlease allow access on the next pop-up to be able to export your keys manually.</string>
</resources> </resources>

View File

@ -324,4 +324,19 @@
<item name="android:textSize">12sp</item> <item name="android:textSize">12sp</item>
</style> </style>
<style name="AttachmentTypeSelectorButton">
<item name="android:layout_width">56dp</item>
<item name="android:layout_height">56dp</item>
<item name="android:scaleType">center</item>
</style>
<style name="AttachmentTypeSelectorLabel">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textColor">?riotx_text_primary</item>
<item name="android:textSize">14sp</item>
<item name="android:layout_marginTop">8dp</item>
</style>
</resources> </resources>

View File

@ -30,6 +30,8 @@
<item name="riotx_fab_label_bg">@color/riotx_fab_label_bg_black</item> <item name="riotx_fab_label_bg">@color/riotx_fab_label_bg_black</item>
<item name="riotx_fab_label_color">@color/riotx_fab_label_color_black</item> <item name="riotx_fab_label_color">@color/riotx_fab_label_color_black</item>
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_black</item> <item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_black</item>
<item name="riotx_attachment_selector_background">@color/riotx_attachment_selector_background_black</item>
<item name="riotx_attachment_selector_border">@color/riotx_attachment_selector_border_black</item>
<!-- Drawables --> <!-- Drawables -->
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_black</item> <item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_black</item>

View File

@ -28,6 +28,8 @@
<item name="riotx_fab_label_bg">@color/riotx_fab_label_bg_dark</item> <item name="riotx_fab_label_bg">@color/riotx_fab_label_bg_dark</item>
<item name="riotx_fab_label_color">@color/riotx_fab_label_color_dark</item> <item name="riotx_fab_label_color">@color/riotx_fab_label_color_dark</item>
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_dark</item> <item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_dark</item>
<item name="riotx_attachment_selector_background">@color/riotx_attachment_selector_background_dark</item>
<item name="riotx_attachment_selector_border">@color/riotx_attachment_selector_border_dark</item>
<item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_dark</item> <item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_dark</item>

View File

@ -29,6 +29,8 @@
<item name="riotx_fab_label_color">@color/riotx_fab_label_color_light</item> <item name="riotx_fab_label_color">@color/riotx_fab_label_color_light</item>
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_light</item> <item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_light</item>
<item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_light</item> <item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_light</item>
<item name="riotx_attachment_selector_background">@color/riotx_attachment_selector_background_light</item>
<item name="riotx_attachment_selector_border">@color/riotx_attachment_selector_border_light</item>
<!-- Drawables --> <!-- Drawables -->
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_light</item> <item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_light</item>