diff --git a/.gitignore b/.gitignore index e54a3c380e..76cd170215 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ /tmp ktlint +.idea/copyright/New_vector.xml +.idea/copyright/profiles_settings.xml diff --git a/CHANGES.md b/CHANGES.md index 1c0e8798de..8d536dbd97 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,8 @@ Improvements: - Persist active tab between sessions (#503) - Do not upload file too big for the homeserver (#587) - Handle read markers (#84) + - Attachments: start using system pickers (#52) + - Attachments: start handling incoming share (#58) - Mark all messages as read (#396) - Add ability to report content (#515) diff --git a/vector/build.gradle b/vector/build.gradle index 78018a6107..3ef125d331 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -316,7 +316,7 @@ dependencies { implementation 'me.leolin:ShortcutBadger:1.1.22@aar' // File picker - implementation 'com.github.jaiselrahman:FilePicker:1.2.2' + implementation 'com.kbeanie:multipicker:1.6@aar' // DI implementation "com.google.dagger:dagger:$daggerVersion" diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 01d8db467e..0c9bac61a1 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ package="im.vector.riotx"> + + + + + + + + + + + + + + + + + Copyright 2014 Leo Lin -
  • - FilePicker -
    - Copyright (c) 2018, Jaisel Rahman -
  • diff-match-patch
    @@ -359,6 +354,11 @@ SOFTWARE.
    Copyright 2017 Gabriel Ittner.
  • +
  • + Android-multipicker-library +
    + Copyright 2018 Kumar Bibek +
  •  Apache License
    diff --git a/vector/src/main/java/im/vector/riotx/ActiveSessionObservableStore.kt b/vector/src/main/java/im/vector/riotx/ActiveSessionObservableStore.kt
    new file mode 100644
    index 0000000000..fd6a92e820
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/ActiveSessionObservableStore.kt
    @@ -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>()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
    similarity index 59%
    rename from vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt
    rename to vector/src/main/java/im/vector/riotx/AppStateHandler.kt
    index 16d3f42824..d0301e2c9f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
    @@ -14,56 +14,59 @@
      * 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 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.room.model.RoomSummary
     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.SelectedGroupStore
     import io.reactivex.Observable
    +import io.reactivex.android.schedulers.AndroidSchedulers
    +import io.reactivex.disposables.CompositeDisposable
     import io.reactivex.functions.BiFunction
    +import io.reactivex.rxkotlin.addTo
     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 session: Session,
    -                                                        private val selectedGroupStore: SelectedGroupStore,
    -                                                        private val homeRoomListStore: HomeRoomListObservableStore
    -) : VectorViewModel(initialState) {
    +    private val compositeDisposable = CompositeDisposable()
     
    -    @AssistedInject.Factory
    -    interface Factory {
    -        fun create(initialState: EmptyState): HomeActivityViewModel
    +    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    +    fun entersForeground() {
    +        observeRoomsAndGroup()
         }
     
    -    companion object : MvRxViewModelFactory {
    -
    -        @JvmStatic
    -        override fun create(viewModelContext: ViewModelContext, state: EmptyState): HomeActivityViewModel? {
    -            val homeActivity: HomeActivity = (viewModelContext as ActivityViewModelContext).activity()
    -            return homeActivity.homeActivityViewModelFactory.create(state)
    -        }
    +    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    +    fun entersBackground() {
    +        compositeDisposable.clear()
         }
     
    -    init {
    -        observeRoomAndGroup()
    -    }
    -
    -    private fun observeRoomAndGroup() {
    +    private fun observeRoomsAndGroup() {
             Observable
                     .combineLatest, Option, List>(
    -                        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(),
                             BiFunction { rooms, selectedGroupOption ->
                                 val selectedGroup = selectedGroupOption.orNull()
    @@ -83,7 +86,7 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState:
                                         .filter { !it.isDirect }
                                         .filter {
                                             selectedGroup?.groupId == ALL_COMMUNITIES_GROUP_ID
    -                                                || selectedGroup?.roomIds?.contains(it.roomId) ?: true
    +                                        || selectedGroup?.roomIds?.contains(it.roomId) ?: true
                                         }
                                 filteredDirectRooms + filteredGroupRooms
                             }
    @@ -91,6 +94,6 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState:
                     .subscribe {
                         homeRoomListStore.post(it)
                     }
    -                .disposeOnClear()
    +                .addTo(compositeDisposable)
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    index 081d1c69aa..b1fd6a8485 100644
    --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    @@ -74,6 +74,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
         @Inject lateinit var vectorPreferences: VectorPreferences
         @Inject lateinit var versionProvider: VersionProvider
         @Inject lateinit var notificationUtils: NotificationUtils
    +    @Inject lateinit var appStateHandler: AppStateHandler
         lateinit var vectorComponent: VectorComponent
         private var fontThreadHandler: Handler? = null
     
    @@ -134,6 +135,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
                     FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder)
                 }
             })
    +        ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
             // This should be done as early as possible
             initKnownEmojiHashSet(appContext)
         }
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    index 60255dbbdd..da3c041a1c 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    @@ -16,8 +16,10 @@
     
     package im.vector.riotx.core.di
     
    +import arrow.core.Option
     import im.vector.matrix.android.api.auth.Authenticator
     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.verification.IncomingVerificationRequestHandler
     import java.util.concurrent.atomic.AtomicReference
    @@ -26,6 +28,7 @@ import javax.inject.Singleton
     
     @Singleton
     class ActiveSessionHolder @Inject constructor(private val authenticator: Authenticator,
    +                                              private val sessionObservableStore: ActiveSessionObservableStore,
                                                   private val keyRequestHandler: KeyRequestHandler,
                                                   private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler
     ) {
    @@ -34,12 +37,14 @@ class ActiveSessionHolder @Inject constructor(private val authenticator: Authent
     
         fun setActiveSession(session: Session) {
             activeSession.set(session)
    +        sessionObservableStore.post(Option.fromNullable(session))
             keyRequestHandler.start(session)
             incomingVerificationRequestHandler.start(session)
         }
     
         fun clearActiveSession() {
             activeSession.set(null)
    +        sessionObservableStore.post(Option.empty())
             keyRequestHandler.stop()
             incomingVerificationRequestHandler.stop()
         }
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    index 2cbc3d2a8b..da9daac68d 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    @@ -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.settings.*
     import im.vector.riotx.features.settings.push.PushGatewaysFragment
    +import im.vector.riotx.features.share.IncomingShareActivity
     import im.vector.riotx.features.ui.UiStateRepository
     
     @Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class])
    @@ -183,6 +184,8 @@ interface ScreenComponent {
     
         fun inject(reactionButton: ReactionButton)
     
    +    fun inject(incomingShareActivity: IncomingShareActivity)
    +
         @Component.Factory
         interface Factory {
             fun create(vectorComponent: VectorComponent,
    diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/ViewExtensions.kt b/vector/src/main/java/im/vector/riotx/core/extensions/ViewExtensions.kt
    index c4f718e574..e7bd2122b6 100644
    --- a/vector/src/main/java/im/vector/riotx/core/extensions/ViewExtensions.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/extensions/ViewExtensions.kt
    @@ -48,3 +48,10 @@ fun EditText.showPassword(visible: Boolean, updateCursor: Boolean = true) {
         }
         if (updateCursor) setSelection(text?.length ?: 0)
     }
    +
    +fun View.getMeasurements(): Pair {
    +    measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
    +    val width = measuredWidth
    +    val height = measuredHeight
    +    return width to height
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt
    new file mode 100644
    index 0000000000..c525d588ae
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/KeyboardStateUtils.kt
    @@ -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(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
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    index 346a4b07e9..8f97ef0247 100644
    --- a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    @@ -22,6 +22,7 @@ import android.content.Context
     import android.content.pm.PackageManager
     import android.os.Build
     import android.widget.Toast
    +import androidx.annotation.StringRes
     import androidx.appcompat.app.AlertDialog
     import androidx.core.app.ActivityCompat
     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_VIDEO_RECORDING = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO
     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)
     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_CHANGE_AVATAR = 574
     const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
    +const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
     
     /**
      * Log the used permissions statuses.
    @@ -98,8 +101,9 @@ fun logPermissionStatuses(context: Context) {
      */
     fun checkPermissions(permissionsToBeGrantedBitMap: Int,
                          activity: Activity,
    -                     requestCode: Int = PERMISSION_REQUEST_CODE): Boolean {
    -    return checkPermissions(permissionsToBeGrantedBitMap, activity, null, requestCode)
    +                     requestCode: Int,
    +                     @StringRes rationaleMessage: Int = 0): Boolean {
    +    return checkPermissions(permissionsToBeGrantedBitMap, activity, null, requestCode, rationaleMessage)
     }
     
     /**
    @@ -111,8 +115,9 @@ fun checkPermissions(permissionsToBeGrantedBitMap: Int,
      */
     fun checkPermissions(permissionsToBeGrantedBitMap: Int,
                          fragment: Fragment,
    -                     requestCode: Int = PERMISSION_REQUEST_CODE): Boolean {
    -    return checkPermissions(permissionsToBeGrantedBitMap, fragment.activity, fragment, requestCode)
    +                     requestCode: Int,
    +                     @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,
                                  activity: Activity?,
                                  fragment: Fragment?,
    -                             requestCode: Int): Boolean {
    +                             requestCode: Int,
    +                             @StringRes rationaleMessage: Int
    +                             ): Boolean {
         var isPermissionGranted = false
     
         // sanity check
    @@ -159,7 +166,6 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
             val permissionListAlreadyDenied = ArrayList()
             val permissionsListToBeGranted = ArrayList()
             var isRequestPermissionRequired = false
    -        var explanationMessage = ""
     
             // retrieve the permissions to be granted according to the request code bit map
             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 (!permissionListAlreadyDenied.isEmpty()) {
    -            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")
    -                    }
    -                }
    -            }
    -
    +        if (permissionListAlreadyDenied.isNotEmpty() && rationaleMessage != 0) {
                 // display the dialog with the info text
                 AlertDialog.Builder(activity)
                         .setTitle(R.string.permissions_rationale_popup_title)
    -                    .setMessage(explanationMessage)
    +                    .setMessage(rationaleMessage)
                         .setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() }
                         .setPositiveButton(R.string.ok) { _, _ ->
                             if (permissionsListToBeGranted.isNotEmpty()) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt
    new file mode 100644
    index 0000000000..e27fa70aed
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentTypeSelectorView.kt
    @@ -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(R.id.attachmentGalleryButton).configure(Type.GALLERY)
    +        cameraButton = layout.findViewById(R.id.attachmentCameraButton).configure(Type.CAMERA)
    +        fileButton = layout.findViewById(R.id.attachmentFileButton).configure(Type.FILE)
    +        stickersButton = layout.findViewById(R.id.attachmentStickersButton).configure(Type.STICKER)
    +        audioButton = layout.findViewById(R.id.attachmentAudioButton).configure(Type.AUDIO)
    +        contactButton = layout.findViewById(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 {
    +        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)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt
    new file mode 100644
    index 0000000000..8a4a0d9309
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsHelper.kt
    @@ -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)
    +        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
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt
    new file mode 100644
    index 0000000000..5e843fcdfd
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt
    @@ -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
    +    )
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt
    new file mode 100644
    index 0000000000..dc7b028aba
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsPickerCallback.kt
    @@ -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?) {
    +        if (audios.isNullOrEmpty()) {
    +            callback.onAttachmentsProcessFailed()
    +        } else {
    +            val attachments = audios.map {
    +                it.toContentAttachmentData()
    +            }
    +            callback.onContentAttachmentsReady(attachments)
    +        }
    +    }
    +
    +    override fun onFilesChosen(files: MutableList?) {
    +        if (files.isNullOrEmpty()) {
    +            callback.onAttachmentsProcessFailed()
    +        } else {
    +            val attachments = files.map {
    +                it.toContentAttachmentData()
    +            }
    +            callback.onContentAttachmentsReady(attachments)
    +        }
    +    }
    +
    +    override fun onImagesChosen(images: MutableList?) {
    +        if (images.isNullOrEmpty()) {
    +            callback.onAttachmentsProcessFailed()
    +        } else {
    +            val attachments = images.map {
    +                it.toContentAttachmentData()
    +            }
    +            callback.onContentAttachmentsReady(attachments)
    +        }
    +    }
    +
    +    override fun onVideosChosen(videos: MutableList?) {
    +        if (videos.isNullOrEmpty()) {
    +            callback.onAttachmentsProcessFailed()
    +        } else {
    +            val attachments = videos.map {
    +                it.toContentAttachmentData()
    +            }
    +            callback.onContentAttachmentsReady(attachments)
    +        }
    +    }
    +
    +    override fun onError(error: String?) {
    +        callback.onAttachmentsProcessFailed()
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt
    new file mode 100644
    index 0000000000..80acefdcf2
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/ContactAttachment.kt
    @@ -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 = emptyList(),
    +        val emails: List = emptyList()
    +) {
    +
    +    fun toHumanReadable(): String {
    +        return buildString {
    +            append(displayName)
    +            phones.concatIn(this)
    +            emails.concatIn(this)
    +        }
    +    }
    +
    +    private fun List.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())
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt b/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt
    new file mode 100644
    index 0000000000..6c03f21ab3
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/attachments/PickerManagerFactory.kt
    @@ -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)
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
    index 291da66362..6868fb84bb 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
    @@ -130,7 +130,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
         }
     
         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 {
                     override fun onPassphrase(passphrase: String) {
                         showWaitingView()
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    index 4a7514416d..7b61ca2c0f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    @@ -135,7 +135,13 @@ class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
             }
     
             dialog.findViewById(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)
                 }
                 dialog.dismiss()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
    index 1e0121a500..af367164fc 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
    @@ -27,7 +27,6 @@ import androidx.core.view.isVisible
     import androidx.drawerlayout.widget.DrawerLayout
     import androidx.lifecycle.Observer
     import androidx.lifecycle.ViewModelProviders
    -import com.airbnb.mvrx.viewModel
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ActiveSessionHolder
     import im.vector.riotx.core.di.ScreenComponent
    @@ -55,11 +54,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
             object OpenGroup : Navigation()
         }
     
    -    private val homeActivityViewModel: HomeActivityViewModel by viewModel()
         private lateinit var navigationViewModel: HomeNavigationViewModel
     
         @Inject lateinit var activeSessionHolder: ActiveSessionHolder
    -    @Inject lateinit var homeActivityViewModelFactory: HomeActivityViewModel.Factory
         @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
         @Inject lateinit var pushManager: PushersManager
         @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    index 25b526fb8a..a219d25c09 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    @@ -16,7 +16,7 @@
     
     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.room.model.message.MessageFileContent
     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 SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
    -    data class SendMedia(val mediaFiles: List) : RoomDetailActions()
    +    data class SendMedia(val attachments: List) : RoomDetailActions()
         data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
         data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
         data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index 9aa2f3cccd..13d9ac4a3d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -52,14 +52,12 @@ import com.github.piasy.biv.BigImageViewer
     import com.github.piasy.biv.loader.ImageLoader
     import com.google.android.material.snackbar.Snackbar
     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.AutocompleteCallback
     import com.otaliastudios.autocomplete.CharPolicy
     import im.vector.matrix.android.api.permalinks.PermalinkFactory
     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.room.model.Membership
     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.NotificationAreaView
     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.CommandAutocompletePolicy
     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.reactions.EmojiReactionPickerActivity
     import im.vector.riotx.features.settings.VectorPreferences
    +import im.vector.riotx.features.share.SharedData
     import im.vector.riotx.features.themes.ThemeUtils
     import kotlinx.android.parcel.Parcelize
     import kotlinx.android.synthetic.main.fragment_room_detail.*
    @@ -125,20 +129,20 @@ import javax.inject.Inject
     @Parcelize
     data class RoomDetailArgs(
             val roomId: String,
    -        val eventId: String? = null
    +        val eventId: String? = null,
    +        val sharedData: SharedData? = null
     ) : Parcelable
     
    -private const val CAMERA_VALUE_TITLE = "attachment"
    -private const val REQUEST_FILES_REQUEST_CODE = 0
    -private const val TAKE_IMAGE_REQUEST_CODE = 1
    -private const val REACTION_SELECT_REQUEST_CODE = 2
    +private const val REACTION_SELECT_REQUEST_CODE = 0
     
     class RoomDetailFragment :
             VectorBaseFragment(),
             TimelineEventController.Callback,
             AutocompleteUserPresenter.Callback,
             VectorInviteView.Callback,
    -        JumpToReadMarkerView.Callback {
    +        JumpToReadMarkerView.Callback,
    +        AttachmentTypeSelectorView.Callback,
    +        AttachmentsHelper.Callback {
     
         companion object {
     
    @@ -199,9 +203,12 @@ class RoomDetailFragment :
     
         private lateinit var actionViewModel: ActionsHandler
         private lateinit var layoutManager: LinearLayoutManager
    +    private lateinit var attachmentsHelper: AttachmentsHelper
    +    private lateinit var keyboardStateUtils: KeyboardStateUtils
     
         @BindView(R.id.composerLayout)
         lateinit var composerLayout: TextComposerView
    +    private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
     
         private var lockSendButton = false
     
    @@ -212,6 +219,8 @@ class RoomDetailFragment :
         override fun onActivityCreated(savedInstanceState: Bundle?) {
             super.onActivityCreated(savedInstanceState)
             actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
    +        attachmentsHelper = AttachmentsHelper.create(this, this).register()
    +        keyboardStateUtils = KeyboardStateUtils(requireActivity())
             setupToolbar(roomToolbar)
             setupRecyclerView()
             setupComposer()
    @@ -275,6 +284,14 @@ class RoomDetailFragment :
             roomDetailViewModel.requestLiveData.observeEvent(this) {
                 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() {
    @@ -404,7 +421,7 @@ class RoomDetailFragment :
             if (text != composerLayout.composerEditText.text.toString()) {
                 // Ignore update to avoid saving a draft
                 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?) {
    -        super.onActivityResult(requestCode, resultCode, data)
    -        if (resultCode == RESULT_OK && data != null) {
    +        val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data)
    +        if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
                 when (requestCode) {
    -                REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
    -                REACTION_SELECT_REQUEST_CODE                        -> {
    -                    val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) ?: return
    -                    val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) ?: return
    +                REACTION_SELECT_REQUEST_CODE -> {
    +                    val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
    +                            ?: return
    +                    val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
    +                            ?: return
                         // TODO check if already reacted with that?
                         roomDetailViewModel.process(RoomDetailActions.SendReaction(eventId, reaction))
                     }
    @@ -598,47 +616,27 @@ class RoomDetailFragment :
             composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
                 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() {
             composerLayout.attachmentButton.setOnClickListener {
    -            val intent = Intent(requireContext(), FilePickerActivity::class.java)
    -            intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder()
    -                    .setCheckPermission(true)
    -                    .setShowFiles(true)
    -                    .setShowAudios(true)
    -                    .setSkipZeroSizeFiles(true)
    -                    .build())
    -            startActivityForResult(intent, REQUEST_FILES_REQUEST_CODE)
    -            /*
    -            val items = ArrayList()
    -            // Send file
    -            items.add(DialogListItem.SendFile)
    -            // Send voice
    -
    -            if (vectorPreferences.isSendVoiceFeatureEnabled()) {
    -                items.add(DialogListItem.SendVoice.INSTANCE)
    +            if (!::attachmentTypeSelector.isInitialized) {
    +                attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this)
                 }
    -
    -
    -            // 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()
    -                    */
    +            attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing)
             }
         }
     
    @@ -646,38 +644,6 @@ class RoomDetailFragment :
             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 = data.getParcelableArrayListExtra(FilePickerActivity.MEDIA_FILES)
    -        roomDetailViewModel.process(RoomDetailActions.SendMedia(files))
    -    }
    -
         private fun renderState(state: RoomDetailViewState) {
             readMarkerHelper.updateWith(state)
             renderRoomSummary(state)
    @@ -941,11 +907,16 @@ class RoomDetailFragment :
             if (allGranted(grantResults)) {
                 if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) {
                     val action = roomDetailViewModel.pendingAction
    -
                     if (action != null) {
                         roomDetailViewModel.pendingAction = null
                         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
                 if (myDisplayName == text) {
                     // current user
    -                if (composerLayout.composerEditText.text.isBlank()) {
    +                if (composerLayout.composerEditText.text.isNullOrBlank()) {
                         composerLayout.composerEditText.append(Command.EMOTE.command + " ")
    -                    composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
    +                    composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
     //                    vibrate = true
                     }
                 } else {
                     // another user
    -                if (composerLayout.composerEditText.text.isBlank()) {
    +                if (composerLayout.composerEditText.text.isNullOrBlank()) {
                         // Ensure displayName will not be interpreted as a Slash command
                         if (text.startsWith("/")) {
                             composerLayout.composerEditText.append("\\")
                         }
                         composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ")
                     } else {
    -                    composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
    +                    composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
                     }
     
     //                vibrate = true
    @@ -1227,4 +1198,41 @@ class RoomDetailFragment :
         override fun onClearReadMarkerClicked() {
             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) {
    +        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))
    +    }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    index 4d93c8a16c..f3934f618c 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    @@ -16,7 +16,6 @@
     
     package im.vector.riotx.features.home.room.detail
     
    -import android.net.Uri
     import androidx.annotation.IdRes
     import androidx.lifecycle.LiveData
     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.MatrixPatterns
     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.isImageMessage
     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.R
     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.resources.UserPreferencesProvider
     import im.vector.riotx.core.utils.LiveEvent
    @@ -69,7 +65,6 @@ import java.util.concurrent.TimeUnit
     class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState,
                                                           userPreferencesProvider: UserPreferencesProvider,
                                                           private val vectorPreferences: VectorPreferences,
    -                                                      private val imageTools: ImageTools,
                                                           private val session: Session
     ) : VectorViewModel(initialState) {
     
    @@ -471,32 +466,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         }
     
         private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
    -        val attachments = action.mediaFiles.map {
    -            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 attachments = action.attachments
             val homeServerCapabilities = session.getHomeServerCapabilities()
    -
             val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize
     
             if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {
    @@ -505,7 +476,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
             } else {
                 when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
                     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)))
                 }
             }
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt
    new file mode 100644
    index 0000000000..273aeecbfa
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt
    @@ -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)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt
    index 62df5d5e95..0a6d3dde08 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt
    @@ -17,9 +17,9 @@
     package im.vector.riotx.features.home.room.detail.composer
     
     import android.content.Context
    +import android.net.Uri
     import android.util.AttributeSet
     import android.view.ViewGroup
    -import android.widget.EditText
     import android.widget.ImageButton
     import android.widget.ImageView
     import android.widget.TextView
    @@ -39,6 +39,10 @@ import im.vector.riotx.R
     class TextComposerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
                                                      defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
     
    +    interface Callback : ComposerEditText.Callback
    +
    +    var callback: Callback? = null
    +
         @BindView(R.id.composer_related_message_sender)
         lateinit var composerRelatedMessageTitle: TextView
         @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)
         lateinit var composerRelatedMessageCloseButton: ImageButton
         @BindView(R.id.composerEditText)
    -    lateinit var composerEditText: EditText
    +    lateinit var composerEditText: ComposerEditText
         @BindView(R.id.composer_avatar_view)
         lateinit var composerAvatarImageView: ImageView
     
    -    var currentConstraintSetId: Int = -1
    +    private var currentConstraintSetId: Int = -1
     
         private val animationDuration = 100L
     
    @@ -62,6 +66,11 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
             inflate(context, R.layout.merge_composer_layout, this)
             ButterKnife.bind(this)
             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) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/filtered/FilteredRoomsActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/filtered/FilteredRoomsActivity.kt
    index 98fd3fa2c2..82fd203b87 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/filtered/FilteredRoomsActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/filtered/FilteredRoomsActivity.kt
    @@ -42,9 +42,7 @@ class FilteredRoomsActivity : VectorBaseActivity() {
     
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
    -
             configureToolbar(filteredRoomsToolbar)
    -
             if (isFirstCreation()) {
                 roomListFragment = RoomListFragment.newInstance(RoomListParams(RoomListFragment.DisplayMode.FILTERED))
                 replaceFragment(roomListFragment, R.id.filteredRoomsFragmentContainer, FRAGMENT_TAG)
    @@ -58,12 +56,10 @@ class FilteredRoomsActivity : VectorBaseActivity() {
                 }
     
                 override fun onQueryTextChange(newText: String): Boolean {
    -                // TODO Create a viewModel and remove this public fun
                     roomListFragment.filterRoomsWith(newText)
                     return true
                 }
             })
    -
             // Open the keyboard immediately
             filteredRoomsSearchView.requestFocus()
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt
    index 5e95497015..3ea6745a94 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt
    @@ -32,6 +32,7 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListFragment.Displa
                 RoomListFragment.DisplayMode.PEOPLE   -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
                 RoomListFragment.DisplayMode.ROOMS    -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN
                 RoomListFragment.DisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN
    +            RoomListFragment.DisplayMode.SHARE    -> roomSummary.membership == Membership.JOIN
             }
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    index 6665500676..a705c91a9e 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    @@ -41,13 +41,15 @@ import im.vector.riotx.core.platform.StateView
     import im.vector.riotx.core.platform.VectorBaseFragment
     import im.vector.riotx.features.home.room.list.widget.FabMenuView
     import im.vector.riotx.features.notifications.NotificationDrawerManager
    +import im.vector.riotx.features.share.SharedData
     import kotlinx.android.parcel.Parcelize
     import kotlinx.android.synthetic.main.fragment_room_list.*
     import javax.inject.Inject
     
     @Parcelize
     data class RoomListParams(
    -        val displayMode: RoomListFragment.DisplayMode
    +        val displayMode: RoomListFragment.DisplayMode,
    +        val sharedData: SharedData? = null
     ) : Parcelable
     
     class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
    @@ -56,7 +58,8 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
             HOME(R.string.bottom_action_home),
             PEOPLE(R.string.bottom_action_people_x),
             ROOMS(R.string.bottom_action_rooms),
    -        FILTERED(/* Not used */ R.string.bottom_action_rooms)
    +        FILTERED(/* Not used */ 0),
    +        SHARE(/* Not used */ 0)
         }
     
         companion object {
    @@ -106,7 +109,12 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
             setupRecyclerView()
             roomListViewModel.subscribe { renderState(it) }
             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
    @@ -121,10 +129,10 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
     
         private fun setupCreateRoomButton() {
             when (roomListParams.displayMode) {
    -            DisplayMode.HOME     -> createChatFabMenu.isVisible = true
    -            DisplayMode.PEOPLE   -> createChatRoomButton.isVisible = true
    -            DisplayMode.ROOMS    -> createGroupRoomButton.isVisible = true
    -            DisplayMode.FILTERED -> Unit // No button in this mode
    +            DisplayMode.HOME   -> createChatFabMenu.isVisible = true
    +            DisplayMode.PEOPLE -> createChatRoomButton.isVisible = true
    +            DisplayMode.ROOMS  -> createGroupRoomButton.isVisible = true
    +            else               -> Unit // No button in this mode
             }
     
             createChatRoomButton.setOnClickListener {
    @@ -147,10 +155,10 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
                                 RecyclerView.SCROLL_STATE_DRAGGING,
                                 RecyclerView.SCROLL_STATE_SETTLING -> {
                                     when (roomListParams.displayMode) {
    -                                    DisplayMode.HOME     -> createChatFabMenu.hide()
    -                                    DisplayMode.PEOPLE   -> createChatRoomButton.hide()
    -                                    DisplayMode.ROOMS    -> createGroupRoomButton.hide()
    -                                    DisplayMode.FILTERED -> Unit
    +                                    DisplayMode.HOME   -> createChatFabMenu.hide()
    +                                    DisplayMode.PEOPLE -> createChatRoomButton.hide()
    +                                    DisplayMode.ROOMS  -> createGroupRoomButton.hide()
    +                                    else               -> Unit
                                     }
                                 }
                             }
    @@ -187,10 +195,10 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
         private val showFabRunnable = Runnable {
             if (isAdded) {
                 when (roomListParams.displayMode) {
    -                DisplayMode.HOME     -> createChatFabMenu.show()
    -                DisplayMode.PEOPLE   -> createChatRoomButton.show()
    -                DisplayMode.ROOMS    -> createGroupRoomButton.show()
    -                DisplayMode.FILTERED -> Unit
    +                DisplayMode.HOME   -> createChatFabMenu.show()
    +                DisplayMode.PEOPLE -> createChatRoomButton.show()
    +                DisplayMode.ROOMS  -> createGroupRoomButton.show()
    +                else               -> Unit
                 }
             }
         }
    @@ -235,7 +243,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
                     }
                     .isNullOrEmpty()
             val emptyState = when (roomListParams.displayMode) {
    -            DisplayMode.HOME     -> {
    +            DisplayMode.HOME   -> {
                     if (hasNoRoom) {
                         StateView.State.Empty(
                                 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))
                     }
                 }
    -            DisplayMode.PEOPLE   ->
    +            DisplayMode.PEOPLE ->
                     StateView.State.Empty(
                             getString(R.string.room_list_people_empty_title),
                             ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_chat),
                             getString(R.string.room_list_people_empty_body)
                     )
    -            DisplayMode.ROOMS    ->
    +            DisplayMode.ROOMS  ->
                     StateView.State.Empty(
                             getString(R.string.room_list_rooms_empty_title),
                             ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_group),
                             getString(R.string.room_list_rooms_empty_body)
                     )
    -            DisplayMode.FILTERED ->
    +            else               ->
                     // Always display the content in this mode, because if the footer
                     StateView.State.Content
             }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    index cb74b1144d..b7a10edd49 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    @@ -230,6 +230,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
                 RoomListFragment.DisplayMode.PEOPLE   -> chronologicalRoomComparator
                 RoomListFragment.DisplayMode.ROOMS    -> chronologicalRoomComparator
                 RoomListFragment.DisplayMode.FILTERED -> chronologicalRoomComparator
    +            RoomListFragment.DisplayMode.SHARE    -> chronologicalRoomComparator
             }
     
             return RoomSummaries().apply {
    diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
    index 744b7fdfc1..a3f9c009ed 100644
    --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
    @@ -16,6 +16,7 @@
     
     package im.vector.riotx.features.navigation
     
    +import android.app.Activity
     import android.content.Context
     import android.content.Intent
     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.roompreview.RoomPreviewActivity
     import im.vector.riotx.features.settings.VectorSettingsActivity
    +import im.vector.riotx.features.share.SharedData
     import timber.log.Timber
     import javax.inject.Inject
     import javax.inject.Singleton
    @@ -46,6 +48,13 @@ class DefaultNavigator @Inject constructor() : Navigator {
             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?) {
             if (context is VectorBaseActivity) {
                 context.notImplemented("Open not joined room")
    diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    index 7d018f24ca..4112dbbfc8 100644
    --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    @@ -16,13 +16,17 @@
     
     package im.vector.riotx.features.navigation
     
    +import android.app.Activity
     import android.content.Context
     import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
    +import im.vector.riotx.features.share.SharedData
     
     interface Navigator {
     
         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 openRoomPreview(publicRoom: PublicRoom, context: Context)
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    index c89b8435f9..2f52cdef13 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    @@ -211,7 +211,7 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() {
          */
         private fun exportKeys() {
             // 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 ->
                     ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
                         override fun onPassphrase(passphrase: String) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt
    new file mode 100644
    index 0000000000..0d2f9ee040
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt
    @@ -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) {
    +        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()
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/share/SharedData.kt b/vector/src/main/java/im/vector/riotx/features/share/SharedData.kt
    new file mode 100644
    index 0000000000..741d7b0eb3
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/share/SharedData.kt
    @@ -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): SharedData()
    +}
    diff --git a/vector/src/main/res/drawable-hdpi/ic_attachment_stickers_white_24dp.png b/vector/src/main/res/drawable-hdpi/ic_attachment_stickers_white_24dp.png
    new file mode 100644
    index 0000000000..bf765cfc6d
    Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ic_attachment_stickers_white_24dp.png differ
    diff --git a/vector/src/main/res/drawable-mdpi/ic_attachment_stickers_white_24dp.png b/vector/src/main/res/drawable-mdpi/ic_attachment_stickers_white_24dp.png
    new file mode 100644
    index 0000000000..3f4b3e50a5
    Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/ic_attachment_stickers_white_24dp.png differ
    diff --git a/vector/src/main/res/drawable-xhdpi/ic_attachment_stickers_white_24dp.png b/vector/src/main/res/drawable-xhdpi/ic_attachment_stickers_white_24dp.png
    new file mode 100644
    index 0000000000..0a96b2bc5d
    Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ic_attachment_stickers_white_24dp.png differ
    diff --git a/vector/src/main/res/drawable-xxhdpi/ic_attachment_stickers_white_24dp.png b/vector/src/main/res/drawable-xxhdpi/ic_attachment_stickers_white_24dp.png
    new file mode 100644
    index 0000000000..6cd7ac653d
    Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ic_attachment_stickers_white_24dp.png differ
    diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_attachment_stickers_white_24dp.png b/vector/src/main/res/drawable-xxxhdpi/ic_attachment_stickers_white_24dp.png
    new file mode 100644
    index 0000000000..eab49d19f6
    Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ic_attachment_stickers_white_24dp.png differ
    diff --git a/vector/src/main/res/drawable/bg_attachment_type_selector.xml b/vector/src/main/res/drawable/bg_attachment_type_selector.xml
    new file mode 100644
    index 0000000000..12787f911a
    --- /dev/null
    +++ b/vector/src/main/res/drawable/bg_attachment_type_selector.xml
    @@ -0,0 +1,24 @@
    +
    +
    +    
    +        
    +            
    +            
    +        
    +    
    +
    +    
    +        
    +            
    +            
    +
    +        
    +
    +    
    +
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/drawable/ic_attachment_audio_white_24dp.xml b/vector/src/main/res/drawable/ic_attachment_audio_white_24dp.xml
    new file mode 100644
    index 0000000000..3de9a237e8
    --- /dev/null
    +++ b/vector/src/main/res/drawable/ic_attachment_audio_white_24dp.xml
    @@ -0,0 +1,4 @@
    +
    +
    +    
    +
    diff --git a/vector/src/main/res/drawable/ic_attachment_camera_white_24dp.xml b/vector/src/main/res/drawable/ic_attachment_camera_white_24dp.xml
    new file mode 100644
    index 0000000000..5c2920d252
    --- /dev/null
    +++ b/vector/src/main/res/drawable/ic_attachment_camera_white_24dp.xml
    @@ -0,0 +1,4 @@
    +
    +
    +    
    +
    diff --git a/vector/src/main/res/drawable/ic_attachment_contact_white_24dp.xml b/vector/src/main/res/drawable/ic_attachment_contact_white_24dp.xml
    new file mode 100644
    index 0000000000..20f05d692e
    --- /dev/null
    +++ b/vector/src/main/res/drawable/ic_attachment_contact_white_24dp.xml
    @@ -0,0 +1,4 @@
    +
    +
    +    
    +
    diff --git a/vector/src/main/res/drawable/ic_attachment_file_white_24dp.xml b/vector/src/main/res/drawable/ic_attachment_file_white_24dp.xml
    new file mode 100644
    index 0000000000..4e6b9458f8
    --- /dev/null
    +++ b/vector/src/main/res/drawable/ic_attachment_file_white_24dp.xml
    @@ -0,0 +1,4 @@
    +
    +
    +    
    +
    diff --git a/vector/src/main/res/drawable/ic_attachment_gallery_white_24dp.xml b/vector/src/main/res/drawable/ic_attachment_gallery_white_24dp.xml
    new file mode 100644
    index 0000000000..d4e68f125b
    --- /dev/null
    +++ b/vector/src/main/res/drawable/ic_attachment_gallery_white_24dp.xml
    @@ -0,0 +1,4 @@
    +
    +
    +    
    +
    diff --git a/vector/src/main/res/layout/activity_incoming_share.xml b/vector/src/main/res/layout/activity_incoming_share.xml
    new file mode 100644
    index 0000000000..9ab2fdc06e
    --- /dev/null
    +++ b/vector/src/main/res/layout/activity_incoming_share.xml
    @@ -0,0 +1,43 @@
    +
    +
    +
    +    
    +
    +        
    +
    +            
    +
    +        
    +
    +        
    +
    +    
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml b/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml
    index ffc77da3fb..ac04dfe3ec 100644
    --- a/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml
    +++ b/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml
    @@ -143,7 +143,7 @@
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toEndOf="@id/attachmentButton" />
     
    -    
     
    -    
     
    -    
    +
    +
    +
    +    
    +
    +        
    +
    +            
    +
    +            
    +
    +        
    +
    +
    +        
    +
    +            
    +
    +            
    +
    +        
    +
    +
    +        
    +
    +            
    +
    +            
    +
    +        
    +
    +    
    +
    +    
    +
    +        
    +
    +            
    +
    +            
    +
    +        
    +
    +
    +        
    +
    +            
    +
    +            
    +
    +        
    +
    +
    +        
    +
    +            
    +
    +            
    +
    +        
    +    
    +
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors_riotx.xml
    index 0e51d46cc2..aec2a4c535 100644
    --- a/vector/src/main/res/values/colors_riotx.xml
    +++ b/vector/src/main/res/values/colors_riotx.xml
    @@ -147,6 +147,16 @@
         #BF000000
         #BF000000
     
    +    
    +    #FFFFFFFF
    +    #FF22262E
    +    #FF090A0C
    +
    +    
    +    #FFE9EDF1
    +    #FF22262E
    +    #FF090A0C
    +
         
         
         #FFF8E3
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 8b83175784..a4b0a61104 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -369,6 +369,9 @@
         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.
         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?
     
    +
    +
    +
         Sorry. Action not performed, due to missing permissions
     
         
    diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml
    index 30fae49f13..2c72e3031a 100644
    --- a/vector/src/main/res/values/strings_riotX.xml
    +++ b/vector/src/main/res/values/strings_riotX.xml
    @@ -37,6 +37,14 @@
     
         "The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."
     
    +    "An error occurred while retrieving the attachment."
    +    "File"
    +    "Contact"
    +    "Camera"
    +    "Audio"
    +    "Gallery"
    +    "Sticker"
    +    Couldn\'t handle share data
     
         "It's spam"
         "It's inappropriate"
    @@ -52,4 +60,7 @@
         "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"
         "Reported as inappropriate"
         "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"
    +
    +    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.
    +
     
    diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml
    index 1b19c87e08..c5b04de730 100644
    --- a/vector/src/main/res/values/styles_riot.xml
    +++ b/vector/src/main/res/values/styles_riot.xml
    @@ -324,4 +324,19 @@
             12sp
         
     
    +
    +    
    +
    +    
    +
     
    \ No newline at end of file
    diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml
    index 129a2e9b1c..7398a4bcb7 100644
    --- a/vector/src/main/res/values/theme_black.xml
    +++ b/vector/src/main/res/values/theme_black.xml
    @@ -30,6 +30,8 @@
             @color/riotx_fab_label_bg_black
             @color/riotx_fab_label_color_black
             @color/riotx_touch_guard_bg_black
    +        @color/riotx_attachment_selector_background_black
    +        @color/riotx_attachment_selector_border_black
     
             
             @drawable/highlighted_message_background_black
    diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml
    index 4143229293..f09cb0c874 100644
    --- a/vector/src/main/res/values/theme_dark.xml
    +++ b/vector/src/main/res/values/theme_dark.xml
    @@ -28,6 +28,8 @@
             @color/riotx_fab_label_bg_dark
             @color/riotx_fab_label_color_dark
             @color/riotx_touch_guard_bg_dark
    +        @color/riotx_attachment_selector_background_dark
    +        @color/riotx_attachment_selector_border_dark
     
             @color/riotx_keys_backup_banner_accent_color_dark
     
    diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml
    index 8d51486e76..1da010b8ff 100644
    --- a/vector/src/main/res/values/theme_light.xml
    +++ b/vector/src/main/res/values/theme_light.xml
    @@ -29,6 +29,8 @@
             @color/riotx_fab_label_color_light
             @color/riotx_touch_guard_bg_light
             @color/riotx_keys_backup_banner_accent_color_light
    +        @color/riotx_attachment_selector_background_light
    +        @color/riotx_attachment_selector_border_light
     
             
             @drawable/highlighted_message_background_light