Attachments/Sharing: refact a bit and handle more data.

This commit is contained in:
ganfra 2019-10-10 16:55:50 +02:00
parent 6e39164b20
commit ae5b6bd2b9
15 changed files with 535 additions and 210 deletions

View File

@ -65,6 +65,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])
@ -185,6 +186,8 @@ interface ScreenComponent {
fun inject(reactionButton: ReactionButton)
fun inject(incomingShareActivity: IncomingShareActivity)
@Component.Factory
interface Factory {
fun create(vectorComponent: VectorComponent,

View File

@ -19,50 +19,55 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import com.kbeanie.multipicker.api.CameraImagePicker
import com.kbeanie.multipicker.api.FilePicker
import com.kbeanie.multipicker.api.ImagePicker
import com.kbeanie.multipicker.api.Picker.*
import com.kbeanie.multipicker.api.callbacks.FilePickerCallback
import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback
import com.kbeanie.multipicker.api.entity.ChosenFile
import com.kbeanie.multipicker.api.entity.ChosenImage
import com.kbeanie.multipicker.api.entity.ChosenVideo
import com.kbeanie.multipicker.core.PickerManager
import com.kbeanie.multipicker.utils.IntentUtils
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.riotx.core.platform.Restorable
private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"
class AttachmentsHelper(private val fragment: Fragment, private val callback: Callback) : Restorable {
/**
* 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 pickerManagerFactory: PickerManagerFactory) : Restorable {
companion object {
fun create(fragment: Fragment, callback: Callback): AttachmentsHelper {
return AttachmentsHelper(FragmentPickerManagerFactory(fragment, callback))
}
fun create(activity: Activity, callback: Callback): AttachmentsHelper {
return AttachmentsHelper(ActivityPickerManagerFactory(activity, callback))
}
}
interface Callback {
fun onAttachmentsReady(attachments: List<ContentAttachmentData>)
fun onAttachmentsProcessFailed()
}
private val attachmentsPickerCallback = AttachmentsPickerCallback(callback)
private var capturePath: String? = null
private val imagePicker by lazy {
ImagePicker(fragment).also {
it.setImagePickerCallback(attachmentsPickerCallback)
it.allowMultiple()
}
pickerManagerFactory.createImagePicker()
}
private val videoPicker by lazy {
pickerManagerFactory.createVideoPicker()
}
private val cameraImagePicker by lazy {
CameraImagePicker(fragment).also {
it.setImagePickerCallback(attachmentsPickerCallback)
}
pickerManagerFactory.createCameraImagePicker()
}
private val filePicker by lazy {
FilePicker(fragment).also {
it.allowMultiple()
it.setFilePickerCallback(attachmentsPickerCallback)
}
pickerManagerFactory.createFilePicker()
}
// Restorable
override fun onSaveInstanceState(outState: Bundle) {
capturePath?.also {
outState.putString(CAPTURE_PATH_KEY, it)
@ -71,26 +76,42 @@ class AttachmentsHelper(private val fragment: Fragment, private val callback: Ca
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
capturePath = savedInstanceState?.getString(CAPTURE_PATH_KEY)
if (capturePath != null) {
cameraImagePicker.reinitialize(capturePath)
}
}
var capturePath: String? = null
private set
// 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 capture image picking
*/
fun openCamera() {
capturePath = cameraImagePicker.pickImage()
}
/**
* 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 = getPickerManager(requestCode)
val pickerManager = getPickerManagerForRequestCode(requestCode)
if (pickerManager != null) {
pickerManager.submit(data)
return true
@ -99,7 +120,26 @@ class AttachmentsHelper(private val fragment: Fragment, private val callback: Ca
return false
}
private fun getPickerManager(requestCode: Int): PickerManager? {
/**
* This methods aims to handle share intent.
*
* @return true if it can handle the intent data, false otherwise
*/
fun handleShare(intent: Intent): Boolean {
val type = intent.type ?: return false
if (type.startsWith("image")) {
imagePicker.submit(IntentUtils.getPickerIntentForSharing(intent))
} else if (type.startsWith("video")) {
videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent))
} else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) {
filePicker.submit(IntentUtils.getPickerIntentForSharing(intent))
} else {
return false
}
return true
}
private fun getPickerManagerForRequestCode(requestCode: Int): PickerManager? {
return when (requestCode) {
PICK_IMAGE_DEVICE -> imagePicker
PICK_IMAGE_CAMERA -> cameraImagePicker
@ -108,81 +148,4 @@ class AttachmentsHelper(private val fragment: Fragment, private val callback: Ca
}
}
private inner class AttachmentsPickerCallback(private val callback: Callback) : ImagePickerCallback, FilePickerCallback {
override fun onFilesChosen(files: MutableList<ChosenFile>?) {
if (files.isNullOrEmpty()) {
callback.onAttachmentsProcessFailed()
} else {
val attachments = files.map {
it.toContentAttachmentData()
}
callback.onAttachmentsReady(attachments)
}
}
override fun onImagesChosen(images: MutableList<ChosenImage>?) {
if (images.isNullOrEmpty()) {
callback.onAttachmentsProcessFailed()
} else {
val attachments = images.map {
it.toContentAttachmentData()
}
callback.onAttachmentsReady(attachments)
}
}
override fun onError(error: String?) {
callback.onAttachmentsProcessFailed()
}
}
private fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
type = mapType(),
size = size,
date = createdAt.time,
name = displayName
)
}
private 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
}
}
private fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
type = mapType(),
name = displayName,
size = size,
height = height.toLong(),
width = width.toLong(),
date = createdAt.time
)
}
private fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
type = ContentAttachmentData.Type.VIDEO,
size = size,
date = createdAt.time,
height = height.toLong(),
width = width.toLong(),
duration = duration,
name = displayName
)
}
}

View File

@ -0,0 +1,69 @@
/*
* 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.ChosenFile
import com.kbeanie.multipicker.api.entity.ChosenImage
import com.kbeanie.multipicker.api.entity.ChosenVideo
import im.vector.matrix.android.api.session.content.ContentAttachmentData
fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
type = mapType(),
size = size,
date = createdAt.time,
name = displayName
)
}
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(),
date = createdAt.time
)
}
fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
type = ContentAttachmentData.Type.VIDEO,
size = size,
date = createdAt.time,
height = height.toLong(),
width = width.toLong(),
duration = duration,
name = displayName
)
}

View File

@ -0,0 +1,68 @@
/*
* 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.FilePickerCallback
import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback
import com.kbeanie.multipicker.api.callbacks.VideoPickerCallback
import com.kbeanie.multipicker.api.entity.ChosenFile
import com.kbeanie.multipicker.api.entity.ChosenImage
import com.kbeanie.multipicker.api.entity.ChosenVideo
/**
* This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback]
*/
class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback) : ImagePickerCallback, FilePickerCallback, VideoPickerCallback {
override fun onFilesChosen(files: MutableList<ChosenFile>?) {
if (files.isNullOrEmpty()) {
callback.onAttachmentsProcessFailed()
} else {
val attachments = files.map {
it.toContentAttachmentData()
}
callback.onAttachmentsReady(attachments)
}
}
override fun onImagesChosen(images: MutableList<ChosenImage>?) {
if (images.isNullOrEmpty()) {
callback.onAttachmentsProcessFailed()
} else {
val attachments = images.map {
it.toContentAttachmentData()
}
callback.onAttachmentsReady(attachments)
}
}
override fun onVideosChosen(videos: MutableList<ChosenVideo>?) {
if (videos.isNullOrEmpty()) {
callback.onAttachmentsProcessFailed()
} else {
val attachments = videos.map {
it.toContentAttachmentData()
}
callback.onAttachmentsReady(attachments)
}
}
override fun onError(error: String?) {
callback.onAttachmentsProcessFailed()
}
}

View File

@ -0,0 +1,104 @@
/*
* 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.CameraImagePicker
import com.kbeanie.multipicker.api.FilePicker
import com.kbeanie.multipicker.api.ImagePicker
import com.kbeanie.multipicker.api.VideoPicker
interface PickerManagerFactory {
fun createImagePicker(): ImagePicker
fun createCameraImagePicker(): CameraImagePicker
fun createVideoPicker(): VideoPicker
fun createFilePicker(): FilePicker
}
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)
}
}
}
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)
}
}
}

View File

@ -106,6 +106,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.*
@ -120,7 +121,8 @@ import javax.inject.Inject
@Parcelize
data class RoomDetailArgs(
val roomId: String,
val eventId: String? = null
val eventId: String? = null,
val sharedData: SharedData? = null
) : Parcelable
@ -195,15 +197,7 @@ class RoomDetailFragment :
private lateinit var actionViewModel: ActionsHandler
private lateinit var layoutManager: LinearLayoutManager
private lateinit var _attachmentsHelper: AttachmentsHelper
private val attachmentsHelper: AttachmentsHelper
get() {
if (::_attachmentsHelper.isInitialized.not()) {
_attachmentsHelper = AttachmentsHelper(this, this).register()
}
return _attachmentsHelper
}
private lateinit var attachmentsHelper: AttachmentsHelper
@BindView(R.id.composerLayout)
lateinit var composerLayout: TextComposerView
@ -218,6 +212,7 @@ class RoomDetailFragment :
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
attachmentsHelper = AttachmentsHelper.create(this, this).register()
setupToolbar(roomToolbar)
setupRecyclerView()
setupComposer()
@ -277,6 +272,15 @@ class RoomDetailFragment :
roomDetailViewModel.selectSubscribe(RoomDetailViewState::syncState) { syncState ->
syncStateView.render(syncState)
}
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() {
@ -306,9 +310,9 @@ class RoomDetailFragment :
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(getString(R.string.error_file_too_big,
error.filename,
TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes),
TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes)
error.filename,
TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes),
TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes)
))
.setPositiveButton(R.string.ok, null)
.show()
@ -385,11 +389,11 @@ class RoomDetailFragment :
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
val parser = Parser.builder().build()
val document = parser.parse(messageContent.formattedBody
?: messageContent.body)
?: messageContent.body)
formattedBody = eventHtmlRenderer.render(document)
}
composerLayout.composerRelatedMessageContent.text = formattedBody
?: nonFormattedBody
?: nonFormattedBody
updateComposerText(defaultContent)
@ -398,11 +402,11 @@ class RoomDetailFragment :
avatarRenderer.render(event.senderAvatar, event.root.senderId
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
avatarRenderer.render(event.senderAvatar,
event.root.senderId ?: "",
event.senderName,
composerLayout.composerRelatedMessageAvatar)
event.root.senderId ?: "",
event.senderName,
composerLayout.composerRelatedMessageAvatar)
composerLayout.expand {
//need to do it here also when not using quick reply
focusComposerAndShowKeyboard()
@ -439,9 +443,9 @@ class RoomDetailFragment :
when (requestCode) {
REACTION_SELECT_REQUEST_CODE -> {
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
?: return
?: return
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
?: return
?: return
//TODO check if already reacted with that?
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
}

View File

@ -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()
}

View File

@ -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
}
}
}

View File

@ -39,14 +39,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 timber.log.Timber
import javax.inject.Inject
@Parcelize
data class RoomListParams(
val displayMode: RoomListFragment.DisplayMode
val displayMode: RoomListFragment.DisplayMode,
val sharedData: SharedData? = null
) : Parcelable
@ -56,7 +57,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 {
@ -86,7 +88,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
@ -101,10 +108,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 {
@ -127,10 +134,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
}
}
}
@ -167,10 +174,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
}
}
}
@ -201,7 +208,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),
@ -215,19 +222,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
}

View File

@ -218,6 +218,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 {

View File

@ -16,8 +16,10 @@
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.content.ContentAttachmentData
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseActivity
@ -33,6 +35,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 +49,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")

View File

@ -16,13 +16,18 @@
package im.vector.riotx.features.navigation
import android.app.Activity
import android.content.Context
import im.vector.matrix.android.api.session.content.ContentAttachmentData
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)

View File

@ -1,85 +1,105 @@
package im.vector.riotx.features.share
import android.content.ClipDescription
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import com.kbeanie.multipicker.api.AudioPicker
import com.kbeanie.multipicker.api.FilePicker
import com.kbeanie.multipicker.api.ImagePicker
import com.kbeanie.multipicker.api.VideoPicker
import com.kbeanie.multipicker.api.callbacks.AudioPickerCallback
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.ChosenAudio
import com.kbeanie.multipicker.api.entity.ChosenFile
import com.kbeanie.multipicker.api.entity.ChosenImage
import com.kbeanie.multipicker.api.entity.ChosenVideo
import com.kbeanie.multipicker.utils.IntentUtils
import android.widget.Toast
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 timber.log.Timber
import im.vector.riotx.features.attachments.AttachmentsHelper
import im.vector.riotx.features.home.HomeActivity
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 im.vector.riotx.features.login.LoginConfig
import kotlinx.android.synthetic.main.activity_incoming_share.*
import javax.inject.Inject
class IncomingShareActivity :
VectorBaseActivity(),
ImagePickerCallback,
VideoPickerCallback,
FilePickerCallback,
AudioPickerCallback {
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 (!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) {
val handled = handleShare(intent)
if (!handled) {
finish()
var isShareManaged = attachmentsHelper.handleShare(intent)
if (!isShareManaged) {
isShareManaged = handleTextShare(intent)
}
if (!isShareManaged) {
cantManageShare()
}
} else {
finish()
cantManageShare()
}
}
private fun handleShare(intent: Intent): Boolean {
val type = intent.type ?: return false
if (type.startsWith("image")) {
val picker = ImagePicker(this)
picker.setImagePickerCallback(this)
picker.submit(IntentUtils.getPickerIntentForSharing(intent))
} else if (type.startsWith("video")) {
val picker = VideoPicker(this)
picker.setVideoPickerCallback(this)
picker.submit(IntentUtils.getPickerIntentForSharing(intent))
} else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) {
val picker = FilePicker(this)
picker.setFilePickerCallback(this)
picker.submit(IntentUtils.getPickerIntentForSharing(intent))
} else if (type.startsWith("audio")) {
val picker = AudioPicker(this)
picker.setAudioPickerCallback(this)
picker.submit(IntentUtils.getPickerIntentForSharing(intent))
} else {
return false
override fun onAttachmentsReady(attachments: List<ContentAttachmentData>) {
val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Attachments(attachments))
roomListFragment = RoomListFragment.newInstance(roomListParams)
replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer)
}
override fun onAttachmentsProcessFailed() {
cantManageShare()
}
private fun cantManageShare() {
Toast.makeText(this, "Couldn't handle share data", 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 true
return false
}
override fun onAudiosChosen(p0: MutableList<ChosenAudio>?) {
Timber.v("On audios chosen $p0")
/**
* Start the login screen with identity server and home server pre-filled
*/
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()
}
override fun onFilesChosen(p0: MutableList<ChosenFile>?) {
Timber.v("On files chosen $p0")
}
override fun onImagesChosen(p0: MutableList<ChosenImage>?) {
Timber.v("On images chosen $p0")
}
override fun onError(p0: String?) {
Timber.v("On error")
}
override fun onVideosChosen(p0: MutableList<ChosenVideo>?) {
Timber.v("On videos chosen $p0")
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.share
import android.os.Parcelable
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import kotlinx.android.parcel.Parcelize
sealed class SharedData: Parcelable {
@Parcelize
data class Text(val text: String): SharedData()
@Parcelize
data class Attachments(val attachmentData: List<ContentAttachmentData>): SharedData()
}

View File

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