Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/attachments #619

Merged
merged 23 commits into from
Oct 23, 2019
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3073470
Attachments: start working on new UI (using system file picker) [WIP]
ganfra Oct 8, 2019
0a9ebb6
Attachments: use a lib which handles for us all the intent stuff.
ganfra Oct 9, 2019
6e39164
Sharing: start handling incoming share [WIP]
ganfra Oct 9, 2019
ae5b6bd
Attachments/Sharing: refact a bit and handle more data.
ganfra Oct 10, 2019
2069635
Attachments: try to improve a bit the UI and adding options [WIP]
ganfra Oct 10, 2019
ee5ebb4
Attachments: get better layout
ganfra Oct 11, 2019
0ca8696
Attachments/Share: cleaning code and add contact picking
ganfra Oct 11, 2019
13a5f78
Merge branch 'develop' into feature/attachments
ganfra Oct 11, 2019
679b0ff
Use klint and update CHANGES
ganfra Oct 11, 2019
c28be6a
Fix code quality check
ganfra Oct 11, 2019
3c3c6ae
Removes the RoomList handling from a viewmodel as it doesn't have a V…
ganfra Oct 15, 2019
6cd0452
Clean after Benoit's review
ganfra Oct 15, 2019
a1a71e2
App state: fix session
ganfra Oct 16, 2019
05a069b
Attachments: fix themes for selection view
ganfra Oct 16, 2019
9e43648
Use klint
ganfra Oct 16, 2019
8e3234d
Clean some code
ganfra Oct 21, 2019
2c8cd89
Handle rich content from app (WIP not compiling)
ganfra Oct 21, 2019
c7a4d34
Attachments: handle rich content from keyboard
ganfra Oct 22, 2019
7388a40
Permissions: allow to provide the rationale message as it requires "c…
ganfra Oct 22, 2019
2974f8b
Merge branch 'develop' into feature/attachments
ganfra Oct 22, 2019
6d55c15
Fix lint issue
ganfra Oct 22, 2019
dbc17ae
Use AppCompatEditText instead of EditText
bmarty Oct 22, 2019
cac5fb7
Code cleanup
bmarty Oct 22, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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

Other changes:
- Accessibility improvements to read receipts in the room timeline and reactions emoji chooser
Expand Down
1 change: 1 addition & 0 deletions vector/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ dependencies {

// File picker
implementation 'com.github.jaiselrahman:FilePicker:1.2.2'
implementation 'com.kbeanie:multipicker:1.6@aar'

// DI
implementation "com.google.dagger:dagger:$daggerVersion"
Expand Down
21 changes: 21 additions & 0 deletions vector/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package="im.vector.riotx">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" />

<application
android:name=".VectorApplication"
Expand Down Expand Up @@ -79,6 +80,26 @@
</intent-filter>
</activity>

<activity
android:name=".features.share.IncomingShareActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<!-- Configure specific file types that you want to handle -->
<!-- <data android:mimeType="video/*" -->
<data android:mimeType="*/*"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE"/>
<!-- Configure specific file types that you want to handle -->
<!-- <data android:mimeType="image/*" -->
<data android:mimeType="*/*"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
</intent-filter>
</activity>
<!-- Services -->

<service
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -185,6 +186,8 @@ interface ScreenComponent {

fun inject(reactionButton: ReactionButton)

fun inject(incomingShareActivity: IncomingShareActivity)

@Component.Factory
interface Factory {
fun create(vectorComponent: VectorComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,10 @@ fun EditText.showPassword(visible: Boolean, updateCursor: Boolean = true) {
}
if (updateCursor) setSelection(text?.length ?: 0)
}

fun View.getMeasurements(): Pair<Int, Int> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a data class to avoid using generic pair? (give sense to first and second)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a utility method, I don't think it's worth it

measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
val width = measuredWidth
val height = measuredHeight
return width to height
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package im.vector.riotx.core.utils

import android.app.Activity
import android.graphics.Rect
import android.view.View
import android.view.ViewTreeObserver

class KeyboardStateUtils(activity: Activity) : ViewTreeObserver.OnGlobalLayoutListener {

private val contentView: View = activity.findViewById<View>(android.R.id.content).also {
it.viewTreeObserver.addOnGlobalLayoutListener(this)
}
var isKeyboardShowing: Boolean = false

override fun onGlobalLayout() {
val rect = Rect()
contentView.getWindowVisibleDisplayFrame(rect)
val screenHeight = contentView.rootView.height

val keypadHeight = screenHeight - rect.bottom
isKeyboardShowing = keypadHeight > screenHeight * 0.15
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,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
Expand All @@ -66,6 +67,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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.riotx.features.attachments

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.TargetApi
import android.content.Context
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.util.Pair
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewAnimationUtils
import android.view.animation.Animation
import android.view.animation.AnimationSet
import android.view.animation.OvershootInterpolator
import android.view.animation.ScaleAnimation
import android.view.animation.TranslateAnimation
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.core.view.doOnNextLayout
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import im.vector.riotx.R
import im.vector.riotx.core.extensions.getMeasurements
import im.vector.riotx.core.utils.PERMISSIONS_EMPTY
import im.vector.riotx.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import kotlin.math.max

private const val ANIMATION_DURATION = 250

/**
* This class is the view presenting choices for picking attachments.
* It will return result through [Callback].
*/
class AttachmentTypeSelectorView(context: Context,
inflater: LayoutInflater,
var callback: Callback?)
: PopupWindow(context) {

interface Callback {
fun onTypeSelected(type: Type)
}

private val iconColorGenerator = ColorGenerator.MATERIAL

private var galleryButton: ImageButton
private var cameraButton: ImageButton
private var fileButton: ImageButton
private var stickersButton: ImageButton
private var audioButton: ImageButton
private var contactButton: ImageButton

private var anchor: View? = null

init {
val root = FrameLayout(context)
val layout = inflater.inflate(R.layout.view_attachment_type_selector, root, true)
galleryButton = layout.findViewById<ImageButton>(R.id.attachmentGalleryButton).configure(Type.GALLERY)
cameraButton = layout.findViewById<ImageButton>(R.id.attachmentCameraButton).configure(Type.CAMERA)
fileButton = layout.findViewById<ImageButton>(R.id.attachmentFileButton).configure(Type.FILE)
stickersButton = layout.findViewById<ImageButton>(R.id.attachmentStickersButton).configure(Type.STICKER)
audioButton = layout.findViewById<ImageButton>(R.id.attachmentAudioButton).configure(Type.AUDIO)
contactButton = layout.findViewById<ImageButton>(R.id.attachmentContactButton).configure(Type.CONTACT)
contentView = root
width = LinearLayout.LayoutParams.MATCH_PARENT
height = LinearLayout.LayoutParams.WRAP_CONTENT
animationStyle = 0
@Suppress("DEPRECATION")
setBackgroundDrawable(BitmapDrawable())
inputMethodMode = INPUT_METHOD_NOT_NEEDED
isFocusable = true
isTouchable = true
}

fun show(anchor: View, isKeyboardOpen: Boolean) {
this.anchor = anchor
val anchorCoordinates = IntArray(2)
anchor.getLocationOnScreen(anchorCoordinates)
if (isKeyboardOpen) {
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] + anchor.height)
} else {
val contentViewHeight = if (contentView.height == 0) {
contentView.getMeasurements().second
} else {
contentView.height
}
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] - contentViewHeight)
}
contentView.doOnNextLayout {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
animateWindowInCircular(anchor, contentView)
} else {
animateWindowInTranslate(contentView)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
animateButtonIn(galleryButton, ANIMATION_DURATION / 2)
animateButtonIn(cameraButton, ANIMATION_DURATION / 2)
animateButtonIn(fileButton, ANIMATION_DURATION / 4)
animateButtonIn(audioButton, ANIMATION_DURATION / 2)
animateButtonIn(contactButton, ANIMATION_DURATION / 4)
animateButtonIn(stickersButton, 0)
}
}

override fun dismiss() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
animateWindowOutCircular(anchor, contentView)
} else {
animateWindowOutTranslate(contentView)
}
}

private fun animateButtonIn(button: View, delay: Int) {
val animation = AnimationSet(true)
val scale = ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f)
animation.addAnimation(scale)
animation.interpolator = OvershootInterpolator(1f)
animation.duration = ANIMATION_DURATION.toLong()
animation.startOffset = delay.toLong()
button.startAnimation(animation)
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private fun animateWindowInCircular(anchor: View?, contentView: View) {
val coordinates = getClickCoordinates(anchor, contentView)
val animator = ViewAnimationUtils.createCircularReveal(contentView,
coordinates.first,
coordinates.second,
0f,
max(contentView.width, contentView.height).toFloat())
animator.duration = ANIMATION_DURATION.toLong()
animator.start()
}

private fun animateWindowInTranslate(contentView: View) {
val animation = TranslateAnimation(0f, 0f, contentView.height.toFloat(), 0f)
animation.duration = ANIMATION_DURATION.toLong()
getContentView().startAnimation(animation)
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private fun animateWindowOutCircular(anchor: View?, contentView: View) {
val coordinates = getClickCoordinates(anchor, contentView)
val animator = ViewAnimationUtils.createCircularReveal(getContentView(),
coordinates.first,
coordinates.second,
max(getContentView().width, getContentView().height).toFloat(),
0f)

animator.duration = ANIMATION_DURATION.toLong()
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super@AttachmentTypeSelectorView.dismiss()
}
})
animator.start()
}

private fun animateWindowOutTranslate(contentView: View) {
val animation = TranslateAnimation(0f, 0f, 0f, (contentView.top + contentView.height).toFloat())
animation.duration = ANIMATION_DURATION.toLong()
animation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {}

override fun onAnimationEnd(animation: Animation) {
super@AttachmentTypeSelectorView.dismiss()
}

override fun onAnimationRepeat(animation: Animation) {}
})

getContentView().startAnimation(animation)
}

private fun getClickCoordinates(anchor: View?, contentView: View): Pair<Int, Int> {
val anchorCoordinates = IntArray(2)
anchor?.getLocationOnScreen(anchorCoordinates)
val contentCoordinates = IntArray(2)
contentView.getLocationOnScreen(contentCoordinates)
val x = anchorCoordinates[0] - contentCoordinates[0]
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)
}
}
Loading