Unverified Commit 7981ae86 authored by Sanimys's avatar Sanimys Committed by GitHub

enable creating albums (#229)

* Moved the crop button so that it doesn't take space in the activity

* Semi transparent in the middle, same position than the image

* First draft of the album creation

* choose multiple images in gallery

* Added functionalities to Album creation

* merge with master

* Gallery of images selected for the album creation

* to merge with master

* Images editable individually

* Creation of album is now possible

* Added tests

* Added test to edit picture selected

* merge PostCreation and AlbumCreation

* Merged completely PostCreation and AlbumCreation

* removed albumCreation in Manifest

* Refactored slightly

* Don't re-upload all images at each edit, only re-upload one

* Make sure all images are uploaded, correctly calculate progress

* comment assert, sorry

* fix test

* fix merge
Co-authored-by: default avatarJoachim Dunant <joachim.dunant@epfl.ch>
Co-authored-by: default avatarMatthieu <61561059+Wv5twkFEKh54vo4tta9yu7dHa3@users.noreply.github.com>
parent a409d69c
Pipeline #31 canceled with stage
in 5 minutes and 10 seconds
......@@ -3,11 +3,9 @@ package com.h.pixeldroid
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Environment
import android.view.View
import android.webkit.MimeTypeMap
import android.widget.SeekBar
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
......@@ -28,7 +26,6 @@ import com.h.pixeldroid.adapters.ThumbnailAdapter
import com.h.pixeldroid.testUtility.CustomMatchers
import junit.framework.Assert.assertTrue
import kotlinx.android.synthetic.main.fragment_edit_image.*
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf
import org.junit.Assert
import org.junit.Before
......@@ -37,7 +34,6 @@ import org.junit.Test
import org.junit.rules.Timeout
import org.junit.runner.RunWith
import java.io.File
import java.net.URI
@RunWith(AndroidJUnit4::class)
class EditPhotoTest {
......@@ -170,33 +166,9 @@ class EditPhotoTest {
@Test
fun backButton() {
Espresso.onView(withId(R.id.toolbar)).check(matches(isDisplayed()))
Espresso.onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click());
Espresso.onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click())
assertTrue(activityScenario.state == Lifecycle.State.DESTROYED) }
@Test
fun buttonUploadLaunchNewPostActivity() {
Espresso.onView(withId(R.id.action_upload)).perform(click())
Thread.sleep(1000)
Espresso.onView(withId(R.id.post_creation_picture_frame)).check(matches(isDisplayed()))
}
@Test
fun modifiedUploadLaunchesNewPostActivity() {
Espresso.onView(withId(R.id.recycler_view))
.perform(actionOnItemAtPosition<ThumbnailAdapter.MyViewHolder>(2, CustomMatchers.clickChildViewWithId(R.id.thumbnail)))
Thread.sleep(1000)
Espresso.onView(withId(R.id.tabs)).perform(selectTabAtPosition(1))
Espresso.onView(withId(R.id.seekbar_brightness)).perform(setProgress(5))
Thread.sleep(1000)
Espresso.onView(withId(R.id.action_upload)).perform(click())
Thread.sleep(1000)
Espresso.onView(withId(R.id.post_creation_picture_frame)).check(matches(isDisplayed()))
}
@Test
fun croppingIsPossible() {
Espresso.onView(withId(R.id.cropImageButton)).perform(click())
......
package com.h.pixeldroid
import android.content.Context
import android.Manifest
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Environment
import android.util.Log
import android.view.View.VISIBLE
import androidx.core.net.toUri
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.h.pixeldroid.adapters.ThumbnailAdapter
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.InstanceDatabaseEntity
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.testUtility.CustomMatchers
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.utils.DBUtils
import kotlinx.android.synthetic.main.activity_post_creation.*
......@@ -42,6 +45,9 @@ class PostCreationActivityTest {
@get:Rule
val globalTimeout: Timeout = Timeout.seconds(30)
@get:Rule
val mRuntimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
private fun File.writeBitmap(bitmap: Bitmap) {
outputStream().use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 85, out)
......@@ -76,21 +82,28 @@ class PostCreationActivityTest {
)
db.close()
var uri: Uri = "".toUri()
val scenario = ActivityScenario.launch(ProfileActivity::class.java)
var uri1: String = ""
var uri2: String = ""
val scenario = ActivityScenario.launch(MainActivity::class.java)
scenario.onActivity {
val image = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888)
image.eraseColor(Color.GREEN)
val image1 = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888)
image1.eraseColor(Color.GREEN)
val image2 = Bitmap.createBitmap(270, 270, Bitmap.Config.ARGB_8888)
image2.eraseColor(Color.RED)
val folder =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
if (!folder.exists()) {
folder.mkdir()
}
val file = File.createTempFile("temp_img", ".png", folder)
file.writeBitmap(image)
uri = file.toUri()
val file1 = File.createTempFile("temp_img1", ".png", folder)
val file2 = File.createTempFile("temp_img2", ".png", folder)
file1.writeBitmap(image1)
uri1 = file1.toUri().toString()
file2.writeBitmap(image2)
uri2 = file2.toUri().toString()
Log.d("test", uri1+"\n"+uri2)
}
val intent = Intent(context, PostCreationActivity::class.java).putExtra("picture_uri", uri)
val intent = Intent(context, PostCreationActivity::class.java).putExtra("pictures_uri", arrayListOf(uri1, uri2))
testScenario = ActivityScenario.launch(intent)
}
......@@ -108,4 +121,38 @@ class PostCreationActivityTest {
// should send on main activity
onView(withId(R.id.retry_upload_button)).check(matches(not(isDisplayed())))
}
@Test
fun editImage() {
onView(withId(R.id.image_grid)).perform(
RecyclerViewActions.actionOnItemAtPosition<PostCreationActivity.PostCreationAdapter.ViewHolder>(
0,
CustomMatchers.clickChildViewWithId(R.id.galleryImage)
)
)
Thread.sleep(1000)
onView(withId(R.id.recycler_view))
.perform(
RecyclerViewActions.actionOnItemAtPosition<ThumbnailAdapter.MyViewHolder>(
2,
CustomMatchers.clickChildViewWithId(R.id.thumbnail)
)
)
Thread.sleep(1000)
onView(withId(R.id.action_upload)).perform(click())
Thread.sleep(1000)
onView(withId(R.id.image_grid)).check(matches(isDisplayed()))
}
@Test
fun cancelEdit() {
onView(withId(R.id.image_grid)).perform(
RecyclerViewActions.actionOnItemAtPosition<PostCreationActivity.PostCreationAdapter.ViewHolder>(
0,
CustomMatchers.clickChildViewWithId(R.id.galleryImage)
)
)
Thread.sleep(1000)
}
}
\ No newline at end of file
......@@ -27,7 +27,6 @@
<activity
android:name=".PhotoEditActivity"
android:theme="@style/AppTheme.NoActionBar"/>
<activity android:name=".PostCreationActivity"
android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity"
......
No preview for this file type
......@@ -88,7 +88,6 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
System.loadLibrary("NativeImageProcessor")
}
companion object{
private var executor: ExecutorService = newSingleThreadExecutor()
private var future: Future<*>? = null
......@@ -114,7 +113,7 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
initialUri = intent.getParcelableExtra("picture_uri")
imageUri = initialUri
// set on-click listener
cropButton.setOnClickListener {
startCrop()
......@@ -187,12 +186,12 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
}
}
return super.onOptionsItemSelected(item)
}
//</editor-fold>
//<editor-fold desc="FILTERS">
return super.onOptionsItemSelected(item)
}
//</editor-fold>
override fun onFilterSelected(filter: Filter) {
resetControls()
filteredImage = compressedOriginalImage!!.copy(BITMAP_CONFIG, true)
......@@ -346,11 +345,15 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
return finalImage
}
private fun uploadImage(file: String) {
val intent = Intent (applicationContext, PostCreationActivity::class.java)
intent.putExtra("picture_uri", Uri.parse(file))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
applicationContext!!.startActivity(intent)
private fun sendBackImage(file: String) {
val intent = Intent(this, PostCreationActivity::class.java)
.apply {
putExtra("result", file)
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
}
setResult(Activity.RESULT_OK, intent)
finish()
}
private fun saveImageToGallery(save: Boolean) {
......@@ -464,8 +467,8 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
}
if(saving) {
this.runOnUiThread {
if (!save) {
uploadImage(path)
if(!save) {
sendBackImage(path)
} else {
MediaScannerConnection.scanFile(
this,
......
package com.h.pixeldroid
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import android.util.Log
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.google.android.material.textfield.TextInputEditText
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.interfaces.PostCreationListener
import com.h.pixeldroid.objects.Attachment
import com.h.pixeldroid.objects.Instance
import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.utils.DBUtils
import com.mikepenz.iconics.Iconics
import io.reactivex.Observable
import com.h.pixeldroid.utils.ProgressRequestBody
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.PublishSubject
import kotlinx.android.synthetic.main.activity_post_creation.*
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import kotlinx.android.synthetic.main.image_album_creation.view.*
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okio.BufferedSink
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.*
class PostCreationActivity : AppCompatActivity(){
class PostCreationActivity : AppCompatActivity(), PostCreationListener {
private val TAG = "Post Creation Activity"
private lateinit var recycler : RecyclerView
private lateinit var adapter : PostCreationAdapter
private lateinit var accessToken: String
private lateinit var pixelfedAPI: PixelfedAPI
private lateinit var pictureFrame: ImageView
private lateinit var imageUri: Uri
private var muListOfIds: MutableList<String> = mutableListOf()
private var progressList: ArrayList<Int> = arrayListOf()
private var positionResult = 0
private var user: UserDatabaseEntity? = null
private var listOfIds: List<String> = emptyList()
private var posts: ArrayList<String> = ArrayList()
private var maxLength: Int = Instance.DEFAULT_MAX_TOOT_CHARS
......@@ -58,14 +61,13 @@ class PostCreationActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Iconics.init(this)
setContentView(R.layout.activity_post_creation)
imageUri = intent.getParcelableExtra("picture_uri")!!
pictureFrame = findViewById(R.id.post_creation_picture_frame)
Glide.with(this).load(imageUri).into(pictureFrame)
// load images
posts = intent.getStringArrayListExtra("pictures_uri")!!
progressList = posts.map { 0 } as ArrayList<Int>
muListOfIds = posts.map { "" }.toMutableList()
val db = DBUtils.initDB(applicationContext)
user = db.userDao().getActiveUser()
......@@ -86,34 +88,38 @@ class PostCreationActivity : AppCompatActivity(){
accessToken = user?.accessToken.orEmpty()
pixelfedAPI = PixelfedAPI.create(domain)
// check if the pictures are alright
// TODO
//upload the picture and display progress while doing so
upload()
adapter = PostCreationAdapter(posts)
adapter.listener = this
recycler = findViewById(R.id.image_grid)
recycler.layoutManager = GridLayoutManager(this, if (posts.size > 2) 2 else 1)
recycler.adapter = adapter
// get the description and send the post
findViewById<Button>(R.id.post_creation_send_button).setOnClickListener {
if (setDescription() && listOfIds.isNotEmpty()) post()
if (setDescription() && muListOfIds.isNotEmpty()) post()
}
// Button to retry image upload when it fails
findViewById<Button>(R.id.retry_upload_button).setOnClickListener {
upload_error.visibility = GONE
upload_error.visibility = View.GONE
muListOfIds = posts.map { "" }.toMutableList()
progressList = posts.map { 0 } as ArrayList<Int>
upload()
}
}
override fun onDestroy() {
super.onDestroy()
//delete the temporary image
//image.delete()
}
private fun setDescription(): Boolean {
val textField = findViewById<TextInputEditText>(R.id.new_post_description_input_field)
val content = textField.text.toString()
if (content.length > maxLength) {
// error, too many characters
textField.error = getString(R.string.description_max_characters).format(maxLength)
// error, too much characters
textField.error = "Description must contain $maxLength characters at most."
return false
}
// store the description
......@@ -122,57 +128,69 @@ class PostCreationActivity : AppCompatActivity(){
}
private fun upload() {
val imageInputStream = contentResolver.openInputStream(imageUri)!!
val size =
if(imageUri.toString().startsWith("content")) {
contentResolver.query(imageUri, null, null, null, null)
?.use { cursor ->
/*
* Get the column indexes of the data in the Cursor,
* move to the first row in the Cursor, get the data,
* and display it.
*/
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
cursor.getLong(sizeIndex)
} ?: 0
} else {
imageUri.toFile().length()
}
val imagePart = ProgressRequestBody(imageInputStream, size)
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", System.currentTimeMillis().toString(), imagePart)
.build()
for ((index, post) in posts.withIndex()) {
val imageUri = Uri.parse(post)
val imageInputStream = contentResolver.openInputStream(imageUri)!!
val size =
if (imageUri.toString().startsWith("content")) {
contentResolver.query(imageUri, null, null, null, null)
?.use { cursor ->
/* Get the column indexes of the data in the Cursor,
* move to the first row in the Cursor, get the data,
* and display it.
*/
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
cursor.getLong(sizeIndex)
} ?: 0
} else {
imageUri.toFile().length()
}
val sub = imagePart.progressSubject
.subscribeOn(Schedulers.io())
.subscribe { percentage ->
uploadProgressBar.progress = percentage.toInt()
}
val imagePart = ProgressRequestBody(imageInputStream, size)
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", System.currentTimeMillis().toString(), imagePart)
.build()
val sub = imagePart.progressSubject
.subscribeOn(Schedulers.io())
.subscribe { percentage ->
progressList[index] = percentage.toInt()
uploadProgressBar.progress =
progressList.sum() / progressList.size
}
var postSub : Disposable?= null
val inter = pixelfedAPI.mediaUpload("Bearer $accessToken", requestBody.parts[0])
postSub = inter
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ attachment ->
listOfIds = listOf(attachment.id!!)
},{e->
upload_error.visibility = VISIBLE
e.printStackTrace()
postSub?.dispose()
sub.dispose()
}, {
uploadProgressBar.visibility = GONE
upload_completed_textview.visibility = VISIBLE
enableButton(true)
postSub?.dispose()
sub.dispose()
})
var postSub: Disposable? = null
val inter = pixelfedAPI.mediaUpload("Bearer $accessToken", requestBody.parts[0])
postSub = inter
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ attachment: Attachment ->
progressList[index] = 0
muListOfIds[index] = attachment.id!!
},
{ e ->
upload_error.visibility = View.VISIBLE
e.printStackTrace()
postSub?.dispose()
sub.dispose()
},
{
progressList[index] = 100
if(progressList.all{it == 100}){
enableButton(true)
uploadProgressBar.visibility = View.GONE
upload_completed_textview.visibility = View.VISIBLE
}
postSub?.dispose()
sub.dispose()
}
)
}
}
private fun post() {
......@@ -180,7 +198,7 @@ class PostCreationActivity : AppCompatActivity(){
pixelfedAPI.postStatus(
authorization = "Bearer $accessToken",
statusText = description,
media_ids = listOfIds
media_ids = muListOfIds.toList()
).enqueue(object: Callback<Status> {
override fun onFailure(call: Call<Status>, t: Throwable) {
enableButton(true)
......@@ -205,67 +223,79 @@ class PostCreationActivity : AppCompatActivity(){
}
})
}
private fun enableButton(enable: Boolean = true){
post_creation_send_button.isEnabled = enable
if(enable){
posting_progress_bar.visibility = GONE
post_creation_send_button.visibility = VISIBLE
posting_progress_bar.visibility = View.GONE
post_creation_send_button.visibility = View.VISIBLE
} else{
posting_progress_bar.visibility = VISIBLE
post_creation_send_button.visibility = GONE
posting_progress_bar.visibility = View.VISIBLE
post_creation_send_button.visibility = View.GONE
}
}
}
override fun onClick(position: Int) {
positionResult = position
val intent = Intent(this, PhotoEditActivity::class.java)
.putExtra("picture_uri", Uri.parse(posts[position]))
.putExtra("no upload", false)
startActivityForResult(intent, positionResult)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == positionResult) {
if (resultCode == Activity.RESULT_OK && data != null) {
posts[positionResult] = data.getStringExtra("result")!!
adapter.notifyItemChanged(positionResult)
muListOfIds.clear()
upload()
}
else if(resultCode == Activity.RESULT_CANCELED){
Toast.makeText(applicationContext, "Edition cancelled", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show()
}
}
}
class ProgressRequestBody(private val mFile: InputStream, private val length: Long) : RequestBody() {
class PostCreationAdapter(private val posts: ArrayList<String>): RecyclerView.Adapter<PostCreationAdapter.ViewHolder>() {
private var context: Context? = null
var listener: PostCreationListener? = null
private val getProgressSubject: PublishSubject<Float> = PublishSubject.create()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {