1
0
mirror of https://gitlab.shinice.net/pixeldroid/PixelDroid synced 2025-02-02 00:26:44 +01:00

Improve upload flow performance & visual feedback (#224)

* Make less copies, detect if no changes are made for a fast path, give some feedback while processing the image

* avoid NPE on camera, use more generic inputstream so that file picking works again

* stop using resource in test

* stop using resource in test

* fix uri issue and add test

* Test dialog, stringify strings

* click error button, for fun

* test error button in post creation

* check retry of upload works

* Remove wrong button click in test

* add some tests for followers list

* test edit profile button

* test back button

* try to get all callbacks to be called

* Fix typo

* Make sure crop is not ignored
This commit is contained in:
Wv5twkFEKh54vo4tta9yu7dHa3 2020-06-03 16:32:02 +02:00 committed by GitHub
parent 9a758ee7bf
commit 0348696f3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 421 additions and 227 deletions

View File

@ -114,13 +114,29 @@ class DrawerMenuTest {
onView(withId(R.id.profilePictureImageView)).check(matches(isDisplayed()))
}
/*@Test
fun testDrawerAvatarClick() {
@Test
fun testDrawerOwnProfileFollowers() {
// Start the screen of your activity.
onView(withText(R.string.menu_account)).perform(click())
// Check that profile activity was opened.
onView(withId(R.id.profilePictureImageView)).check(matches(isDisplayed()))
}*/
onView(withId(R.id.editButton)).check(matches(isDisplayed()))
val followersText = context.getString(R.string.nb_followers)
.format(68)
onView(withText(followersText)).perform(click())
onView(withText("Dobios")).check(matches(isDisplayed()))
}
@Test
fun testDrawerOwnProfileFollowing() {
// Start the screen of your activity.
onView(withText(R.string.menu_account)).perform(click())
// Check that profile activity was opened.
onView(withId(R.id.editButton)).check(matches(isDisplayed()))
val followingText = context.getString(R.string.nb_following)
.format(27)
onView(withText(followingText)).perform(click())
onView(withText("Dobios")).check(matches(isDisplayed()))
}
/*@Test
fun testDrawerAccountNameClick() {

View File

@ -1,9 +1,16 @@
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
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso
import androidx.test.espresso.PerformException
@ -19,7 +26,9 @@ import androidx.test.rule.GrantPermissionRule
import com.google.android.material.tabs.TabLayout
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
@ -27,6 +36,8 @@ import org.junit.Rule
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 {
@ -40,12 +51,30 @@ class EditPhotoTest {
@get:Rule
var mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
private fun File.writeBitmap(bitmap: Bitmap) {
outputStream().use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 85, out)
out.flush()
}
}
@Before
fun before() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
// Launch PhotoEditActivity
val uri: Uri = Uri.parse("android.resource://com.h.pixeldroid/drawable/index")
var uri: Uri = "".toUri()
val scenario = ActivityScenario.launch(ProfileActivity::class.java)
scenario.onActivity {
val image = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888)
image.eraseColor(Color.GREEN)
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 intent = Intent(context, PhotoEditActivity::class.java).putExtra("picture_uri", uri)
activityScenario = ActivityScenario.launch<PhotoEditActivity>(intent).onActivity{a -> activity = a}
@ -138,6 +167,12 @@ class EditPhotoTest {
.check(matches(withText(R.string.save_image_success)))
}
@Test
fun backButton() {
Espresso.onView(withId(R.id.toolbar)).check(matches(isDisplayed()))
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())
@ -145,6 +180,23 @@ class EditPhotoTest {
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())
@ -152,4 +204,12 @@ class EditPhotoTest {
Espresso.onView(withId(R.id.menu_crop)).perform(click())
Espresso.onView(withId(R.id.image_preview)).check(matches(isDisplayed()))
}
@Test
fun alreadyUploadingDialog() {
activityScenario.onActivity { a -> a.saving = true }
Espresso.onView(withId(R.id.action_upload)).perform(click())
Thread.sleep(1000)
Espresso.onView(withText(R.string.busy_dialog_text)).check(matches(isDisplayed()))
}
}

View File

@ -12,6 +12,10 @@ import androidx.test.espresso.Espresso
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.contrib.DrawerActions
import androidx.test.espresso.contrib.DrawerMatchers
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
@ -164,29 +168,28 @@ class IntentTest {
}
}
/*@Test
fun launchesIntent() {
// Open Drawer to click on navigation.
ActivityScenario.launch(MainActivity::class.java)
Espresso.onView(ViewMatchers.withId(R.id.drawer_layout))
.check(ViewAssertions.matches(DrawerMatchers.isClosed(Gravity.LEFT))) // Left Drawer should be closed.
.perform(DrawerActions.open()) // Open Drawer
Espresso.onView(ViewMatchers.withId(R.id.drawer))
.perform(NavigationViewActions.navigateTo(R.id.nav_account))
@Test
fun clickEditProfileMakesIntent() {
ActivityScenario.launch(MainActivity::class.java)
Espresso.onView(ViewMatchers.withId(R.id.drawer_layout))
.check(ViewAssertions.matches(DrawerMatchers.isClosed())) // Left Drawer should be closed.
.perform(DrawerActions.open()) // Open Drawer
val expectedIntent: Matcher<Intent> = CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasDataString(CoreMatchers.containsString("settings/home"))
)
Thread.sleep(1000)
Espresso.onView(ViewMatchers.withId(R.id.editButton)).perform(ViewActions.click())
Thread.sleep(1000)
// Start the screen of your activity.
Espresso.onView(ViewMatchers.withText(R.string.menu_account)).perform(ViewActions.click())
// Check that profile activity was opened.
Espresso.onView(ViewMatchers.withId(R.id.editButton))
.perform(ViewActions.click())
intended(expectedIntent)
} */
}
@After
fun after() {

View File

@ -2,7 +2,12 @@ package com.h.pixeldroid
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Environment
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
@ -17,15 +22,19 @@ import com.h.pixeldroid.db.InstanceDatabaseEntity
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.utils.DBUtils
import kotlinx.android.synthetic.main.activity_post_creation.*
import org.hamcrest.Matchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.Timeout
import org.junit.runner.RunWith
import java.io.File
@RunWith(AndroidJUnit4::class)
class PostCreationActivityTest {
private var testScenario: ActivityScenario<PostCreationActivity>? = null
private val mockServer = MockServer()
private lateinit var db: AppDatabase
@ -33,6 +42,13 @@ class PostCreationActivityTest {
@get:Rule
val globalTimeout: Timeout = Timeout.seconds(30)
private fun File.writeBitmap(bitmap: Bitmap) {
outputStream().use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 85, out)
out.flush()
}
}
@Before
fun setup() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
@ -59,11 +75,23 @@ class PostCreationActivityTest {
)
)
db.close()
val uri: Uri = Uri.parse("android.resource://com.h.pixeldroid/drawable/index")
val intent = Intent(context, PostCreationActivity::class.java)
.putExtra("picture_uri", uri)
ActivityScenario.launch<PostCreationActivity>(intent)
var uri: Uri = "".toUri()
val scenario = ActivityScenario.launch(ProfileActivity::class.java)
scenario.onActivity {
val image = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888)
image.eraseColor(Color.GREEN)
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 intent = Intent(context, PostCreationActivity::class.java).putExtra("picture_uri", uri)
testScenario = ActivityScenario.launch(intent)
}
@Test
@ -72,4 +100,12 @@ class PostCreationActivityTest {
// should send on main activity
onView(withId(R.id.main_activity_main_linear_layout)).check(matches(isDisplayed()))
}
@Test
fun errorShown() {
testScenario!!.onActivity { a -> a.upload_error.visibility = VISIBLE }
onView(withId(R.id.retry_upload_button)).perform(click())
// should send on main activity
onView(withId(R.id.retry_upload_button)).check(matches(not(isDisplayed())))
}
}

View File

@ -1,20 +1,30 @@
package com.h.pixeldroid
import android.app.Activity
import android.app.AlertDialog
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.drawable.BitmapDrawable
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.view.Menu
import android.view.MenuItem
import android.view.View.GONE
import android.view.View.VISIBLE
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.bumptech.glide.Glide
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
@ -30,9 +40,9 @@ import com.zomato.photofilters.imageprocessors.subfilters.BrightnessSubFilter
import com.zomato.photofilters.imageprocessors.subfilters.ContrastSubFilter
import com.zomato.photofilters.imageprocessors.subfilters.SaturationSubfilter
import kotlinx.android.synthetic.main.activity_photo_edit.*
import kotlinx.android.synthetic.main.content_photo_edit.*
import java.io.File
import java.io.IOException
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
@ -49,6 +59,7 @@ private val REQUIRED_PERMISSIONS = arrayOf(android.Manifest.permission.READ_EXTE
class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditImageFragmentListener {
internal var saving: Boolean = false
private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888
private val BRIGHTNESS_START = 0
private val SATURATION_START = 1.0f
@ -58,15 +69,12 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
private var compressedImage: Bitmap? = null
private var compressedOriginalImage: Bitmap? = null
private lateinit var filteredImage: Bitmap
private lateinit var finalImage: Bitmap
private var actualFilter: Filter? = null
private lateinit var filterListFragment: FilterListFragment
private lateinit var editImageFragment: EditImageFragment
private lateinit var outputDirectory: File
lateinit var viewPager: NonSwipeableViewPager
lateinit var tabLayout: TabLayout
@ -74,11 +82,6 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
private var saturationFinal = SATURATION_START
private var contrastFinal = CONTRAST_START
private var imageUri: Uri? = null
private var cropUri: Uri? = null
object URI {var picture_uri: Uri? = null}
init {
System.loadLibrary("NativeImageProcessor")
}
@ -87,6 +90,12 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
companion object{
private var executor: ExecutorService = newSingleThreadExecutor()
private var future: Future<*>? = null
private var saveExecutor: ExecutorService = newSingleThreadExecutor()
private var saveFuture: Future<*>? = null
private var initialUri: Uri? = null
internal var imageUri: Uri? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -101,7 +110,8 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
val cropButton: FloatingActionButton = findViewById(R.id.cropImageButton)
cropUri = intent.getParcelableExtra("picture_uri")
initialUri = intent.getParcelableExtra("picture_uri")
imageUri = initialUri
// set on-click listener
cropButton.setOnClickListener {
@ -109,25 +119,21 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
}
loadImage()
val file = File.createTempFile("temp_compressed_img", ".png", cacheDir)
file.writeBitmap(compressedImage!!)
URI.picture_uri = Uri.fromFile(file)
viewPager = findViewById(R.id.viewPager)
tabLayout = findViewById(R.id.tabs)
setupViewPager(viewPager)
tabLayout.setupWithViewPager(viewPager)
outputDirectory = getOutputDirectory()
}
//<editor-fold desc="ON LAUNCH">
private fun loadImage() {
originalImage = MediaStore.Images.Media.getBitmap(contentResolver, cropUri)
originalImage = MediaStore.Images.Media.getBitmap(contentResolver, imageUri)
compressedImage = resizeImage(originalImage!!.copy(BITMAP_CONFIG, true))
compressedOriginalImage = compressedImage!!.copy(BITMAP_CONFIG, true)
filteredImage = compressedImage!!.copy(BITMAP_CONFIG, true)
image_preview.setImageBitmap(compressedImage)
Glide.with(this).load(compressedImage).into(image_preview)
}
private fun resizeImage(image: Bitmap): Bitmap {
@ -159,11 +165,16 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
return true
}
override fun onStop() {
super.onStop()
saving = false
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
android.R.id.home -> {
super.onBackPressed()
onBackPressed()
}
R.id.action_upload -> {
saveImageToGallery(false)
@ -253,19 +264,15 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
//<editor-fold desc="CROPPING">
private fun startCrop() {
applyFinalFilters(MediaStore.Images.Media.getBitmap(contentResolver, cropUri))
val file = File.createTempFile("temp_crop_img", ".png", cacheDir)
file.writeBitmap(finalImage)
val uCrop: UCrop = UCrop.of(Uri.fromFile(file), URI.picture_uri!!)
val uCrop: UCrop = UCrop.of(initialUri!!, Uri.fromFile(file))
uCrop.start(this)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(resultCode == Activity.RESULT_OK) {
imageUri = data!!.data
if (requestCode == UCrop.RESULT_ERROR) {
handleCropError(data)
} else {
@ -286,8 +293,8 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
private fun handleCropResult(data: Intent?) {
val resultCrop: Uri? = UCrop.getOutput(data!!)
if(resultCrop != null) {
imageUri = resultCrop
image_preview.setImageURI(resultCrop)
val bitmap = (image_preview.drawable as BitmapDrawable).bitmap
originalImage = bitmap.copy(Bitmap.Config.ARGB_8888, true)
compressedImage = resizeImage(originalImage!!.copy(BITMAP_CONFIG, true))
@ -295,7 +302,7 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
filteredImage = compressedImage!!.copy(BITMAP_CONFIG, true)
resetFilteredImage()
} else {
Toast.makeText(this, "Cannot retrieve image", Toast.LENGTH_SHORT).show()
Toast.makeText(this, R.string.crop_result_error, Toast.LENGTH_SHORT).show()
}
}
@ -304,7 +311,7 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
if(resultError != null) {
Toast.makeText(this, "" + resultError, Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Unexpected Error", Toast.LENGTH_SHORT).show()
Toast.makeText(this, R.string.crop_result_error, Toast.LENGTH_SHORT).show()
}
}
@ -329,16 +336,17 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
}
}
private fun applyFinalFilters(image: Bitmap?) {
private fun applyFinalFilters(image: Bitmap?): Bitmap {
val editFilter = Filter().addEditFilters(brightnessFinal, saturationFinal, contrastFinal)
finalImage = editFilter.processFilter(image!!.copy(BITMAP_CONFIG, true))
var finalImage = editFilter.processFilter(image!!.copy(BITMAP_CONFIG, true))
if (actualFilter!=null) finalImage = actualFilter!!.processFilter(finalImage)
return finalImage
}
private fun uploadImage(file: File) {
private fun uploadImage(file: String) {
val intent = Intent (applicationContext, PostCreationActivity::class.java)
intent.putExtra("picture_uri", Uri.fromFile(file))
intent.putExtra("picture_uri", Uri.parse(file))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
applicationContext!!.startActivity(intent)
}
@ -364,45 +372,115 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
applicationContext, it) == PackageManager.PERMISSION_GRANTED
}
/** Use external media if it is available, our app's file directory otherwise */
private fun getOutputDirectory(): File {
val appContext = applicationContext
val mediaDir = externalMediaDirs.firstOrNull()?.let {
File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() } }
return if (mediaDir != null && mediaDir.exists())
mediaDir else appContext.filesDir
private fun getOutputFile(name: String): Pair<OutputStream, String> {
val outputStream: OutputStream
val path: String
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver: ContentResolver = contentResolver
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
contentValues.put(
MediaStore.MediaColumns.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES
)
val imageUri: Uri =
resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)!!
path = imageUri.toString()
outputStream = resolver.openOutputStream(Objects.requireNonNull(imageUri))!!
} else {
val imagesDir =
Environment.getExternalStoragePublicDirectory(getString(R.string.app_name))
imagesDir.mkdir()
val file = File(imagesDir, name)
path = Uri.fromFile(file).toString()
outputStream = file.outputStream()
}
return Pair(outputStream, path)
}
private fun File.writeBitmap(bitmap: Bitmap) {
outputStream().use { out ->
private fun OutputStream.writeBitmap(bitmap: Bitmap) {
use { out ->
//(quality is ignored for PNG)
bitmap.compress(Bitmap.CompressFormat.PNG, 85, out)
out.flush()
}
}
private fun permissionsGrantedToSave(save: Boolean) {
val file =
if(!save){
//put picture in cache
File.createTempFile("temp_edit_img", ".png", cacheDir)
} else{
// Save the picture (quality is ignored for PNG)
File(outputDirectory, SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
.format(System.currentTimeMillis()) + ".png")
if (saving) {
val builder = AlertDialog.Builder(this)
builder.apply {
setMessage(R.string.busy_dialog_text)
setNegativeButton(R.string.busy_dialog_ok_button) { _, _ -> }
}
try {
applyFinalFilters(originalImage)
file.writeBitmap(finalImage)
} catch (e: IOException) {
Snackbar.make(coordinator_edit, getString(R.string.save_image_failed),
Snackbar.LENGTH_LONG).show()
// Create the AlertDialog
builder.show()
return
}
saving = true
progressBarSaveFile.visibility = VISIBLE
saveFuture = saveExecutor.submit {
val outputStream: OutputStream
var path: String
if (!save) {
//put picture in cache
val tempFile = File.createTempFile("temp_edit_img", ".png", cacheDir)
path = Uri.fromFile(tempFile).toString()
outputStream = tempFile.outputStream()
} else {
// Save the picture to gallery
val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
.format(System.currentTimeMillis()) + ".png"
val pair = getOutputFile(name)
outputStream = pair.first
path = pair.second
}
try {
if(brightnessFinal != BRIGHTNESS_START || contrastFinal != CONTRAST_START
|| saturationFinal != SATURATION_START
|| (actualFilter != null && actualFilter!!.name != getString(R.string.normal_filter))) {
outputStream.writeBitmap(applyFinalFilters(originalImage))
}
else {
if(save) {
contentResolver.openInputStream(imageUri!!)!!.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
}
else path = imageUri.toString()
}
} catch (e: IOException) {
this.runOnUiThread {
Snackbar.make(
coordinator_edit, getString(R.string.save_image_failed),
Snackbar.LENGTH_LONG
).show()
}
}
if(saving) {
this.runOnUiThread {
if (!save) {
uploadImage(path)
} else {
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension("png")
MediaScannerConnection.scanFile(
this,
arrayOf(path),
arrayOf(mimeType), null)
if (!save) {
uploadImage(file)
} else {
Snackbar.make(coordinator_edit, getString(R.string.save_image_success),
Snackbar.LENGTH_LONG).show()
Snackbar.make(
coordinator_edit, getString(R.string.save_image_success),
Snackbar.LENGTH_LONG
).show()
}
progressBarSaveFile.visibility = GONE
saving = false
}
}
}
}
//</editor-fold>

View File

@ -5,6 +5,7 @@ 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
@ -12,13 +13,16 @@ 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 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.objects.Instance
import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.utils.DBUtils
import com.mikepenz.iconics.Iconics
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
@ -43,7 +47,7 @@ class PostCreationActivity : AppCompatActivity(){
private lateinit var accessToken: String
private lateinit var pixelfedAPI: PixelfedAPI
private lateinit var pictureFrame: ImageView
private lateinit var image: File
private lateinit var imageUri: Uri
private var user: UserDatabaseEntity? = null
private var listOfIds: List<String> = emptyList()
@ -54,14 +58,14 @@ class PostCreationActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Iconics.init(this)
setContentView(R.layout.activity_post_creation)
val imageUri: Uri = intent.getParcelableExtra("picture_uri")!!
saveImage(imageUri)
imageUri = intent.getParcelableExtra("picture_uri")!!
pictureFrame = findViewById(R.id.post_creation_picture_frame)
pictureFrame.setImageURI(image.toUri())
Glide.with(this).load(imageUri).into(pictureFrame)
val db = DBUtils.initDB(applicationContext)
user = db.userDao().getActiveUser()
@ -100,27 +104,7 @@ class PostCreationActivity : AppCompatActivity(){
override fun onDestroy() {
super.onDestroy()
//delete the temporary image
image.delete()
}
private fun saveImage(imageUri: Uri) {
try {
val stream = applicationContext.contentResolver
.openAssetFileDescriptor(imageUri, "r")!!
.createInputStream()
val bm = BitmapFactory.decodeStream(stream)
val bos = ByteArrayOutputStream()
bm.compress(Bitmap.CompressFormat.PNG, 0, bos)
image = File.createTempFile("temp_compressed_img", ".png", cacheDir)
val fos = FileOutputStream(image)
fos.write(bos.toByteArray())
fos.flush()
fos.close()
} catch (error: IOException) {
error.printStackTrace()
throw error
}
//image.delete()
}
private fun setDescription(): Boolean {
@ -137,11 +121,30 @@ class PostCreationActivity : AppCompatActivity(){
return true
}
private fun upload(){
val imagePart = ProgressRequestBody(image)
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", image.name, imagePart)
.addFormDataPart("file", System.currentTimeMillis().toString(), imagePart)
.build()
val sub = imagePart.progressSubject
@ -216,7 +219,7 @@ class PostCreationActivity : AppCompatActivity(){
}
class ProgressRequestBody(private val mFile: File) : RequestBody() {
class ProgressRequestBody(private val mFile: InputStream, private val length: Long) : RequestBody() {
private val getProgressSubject: PublishSubject<Float> = PublishSubject.create()
@ -232,17 +235,16 @@ class ProgressRequestBody(private val mFile: File) : RequestBody() {
@Throws(IOException::class)
override fun contentLength(): Long {
return mFile.length()
return length
}
@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {
val fileLength = contentLength()
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
val `in` = FileInputStream(mFile)
var uploaded: Long = 0
`in`.use {
mFile.use {
var read: Int
var lastProgressPercentUpdate = 0.0f
read = it.read(buffer)

View File

@ -54,8 +54,6 @@ class CameraFragment : Fragment() {
private lateinit var container: ConstraintLayout
private lateinit var viewFinder: PreviewView
private lateinit var outputDirectory: File
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE)
private val PICK_IMAGE_REQUEST = 1
private val CAPTURE_IMAGE_REQUEST = 2
@ -138,9 +136,6 @@ class CameraFragment : Fragment() {
// Every time the orientation of device changes, update rotation for use cases
// Determine the output directory
outputDirectory = getGalleryDirectory(requireContext())
// Wait for the views to be properly laid out
viewFinder.post {
@ -166,13 +161,13 @@ class CameraFragment : Fragment() {
private fun bindCameraUseCases() {
// Get screen metrics used to setup camera for full screen resolution
val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) }
val metrics = DisplayMetrics().also { viewFinder.display?.getRealMetrics(it) }
Log.d(TAG, "Screen metrics: ${metrics.widthPixels} x ${metrics.heightPixels}")
val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels)
Log.d(TAG, "Preview aspect ratio: $screenAspectRatio")
val rotation = viewFinder.display.rotation
val rotation = viewFinder.display?.rotation ?: 0
// Bind the CameraProvider to the LifeCycleOwner
val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
@ -324,7 +319,7 @@ class CameraFragment : Fragment() {
// Create output file to hold the image
val photoFile = File.createTempFile(
"${System.currentTimeMillis()}.jpg", null, context?.cacheDir
"cachedPhoto", ".png", context?.cacheDir
)
// Setup image capture metadata
@ -385,15 +380,5 @@ class CameraFragment : Fragment() {
private const val RATIO_4_3_VALUE = 4.0 / 3.0
private const val RATIO_16_9_VALUE = 16.0 / 9.0
/** Use external media if it is available, our app's file directory otherwise */
private fun getGalleryDirectory(context: Context): File {
val appContext = context.applicationContext
val mediaDir = context.externalMediaDirs.firstOrNull()?.let {
File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() } }
return if (mediaDir != null && mediaDir.exists())
mediaDir else appContext.filesDir
}
}
}

View File

@ -64,7 +64,7 @@ class EditImageFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
}
R.id.seekbar_contrast -> {
val tempProgress = .10f * prog
listener!!.onSaturationChange(tempProgress)
listener!!.onContrastChange(tempProgress)
}
}
}

View File

@ -1,13 +1,17 @@
package com.h.pixeldroid.fragments
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.util.TypedValue
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.decodeBitmap
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -51,10 +55,20 @@ class FilterListFragment : Fragment(), FilterListFragmentListener {
return view
}
fun displayImage(bitmap: Bitmap?) {
private fun displayImage(bitmap: Bitmap?) {
val r = Runnable {
val tbImage: Bitmap = (if (bitmap == null) {
MediaStore.Images.Media.getBitmap(requireActivity().contentResolver, PhotoEditActivity.URI.picture_uri)
// TODO: Shouldn't use deprecated API on newer versions of Android,
// but the proper way to do it seems to crash for OpenGL reasons
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// ImageDecoder.decodeBitmap(
// ImageDecoder.createSource(requireActivity().contentResolver, PhotoEditActivity.imageUri!!))
//} else {
MediaStore.Images.Media.getBitmap(
requireActivity().contentResolver,
PhotoEditActivity.imageUri
)
//}
} else {
Bitmap.createScaledBitmap(bitmap, 100, 100, false)
})
@ -75,7 +89,8 @@ class FilterListFragment : Fragment(), FilterListFragmentListener {
val tbItem = ThumbnailItem()
tbItem.image = tbImage
tbItem.filterName = getString(R.string.normal_filter)
tbItem.filter.name = getString(R.string.normal_filter)
tbItem.filterName = tbItem.filter.name
ThumbnailsManager.addThumb(tbItem)
val filters = FilterPack.getFilterPack(requireActivity())

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_edit"
@ -7,19 +7,91 @@
android:layout_height="match_parent"
tools:context=".PhotoEditActivity">
<com.google.android.material.appbar.AppBarLayout
<LinearLayout
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".PhotoEditActivity"
tools:showIn="@layout/activity_photo_edit"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
>
<ImageView
android:id="@+id/image_preview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight=".70"
android:scaleType="centerInside"/>
<com.h.pixeldroid.utils.NonSwipeableViewPager
android:id="@+id/viewPager"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight=".22" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
app:tabGravity="fill"
app:tabMode="fixed"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight=".08"/>
</LinearLayout>
<ProgressBar
android:id="@+id/progressBarSaveFile"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
</com.google.android.material.appbar.AppBarLayout>
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/left_guideline"
app:layout_constraintGuide_percent=".15"
android:orientation="vertical"/>
<include layout="@layout/content_photo_edit" />
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/right_guideline"
app:layout_constraintGuide_percent=".85"
android:orientation="vertical"/>
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/bottom_guideline"
app:layout_constraintGuide_percent=".7"
android:orientation="horizontal"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/cropImageButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="411dp"
android:backgroundTint="#FFFFFFFF"
android:src="@drawable/ic_crop_black_24dp"
app:layout_constraintLeft_toLeftOf="@+id/left_guideline"
app:layout_constraintRight_toRightOf="@+id/right_guideline"
app:layout_constraintBottom_toBottomOf="@+id/bottom_guideline" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_height="wrap_content"
android:background="@color/colorPrimaryActionBar"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,76 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".PhotoEditActivity"
tools:showIn="@layout/activity_photo_edit"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
>
<ImageView
android:id="@+id/image_preview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight=".70"
android:scaleType="centerInside" />
<com.h.pixeldroid.utils.NonSwipeableViewPager
android:id="@+id/viewPager"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight=".22" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
app:tabGravity="fill"
app:tabMode="fixed"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight=".08"/>
</LinearLayout>
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/left_guideline"
app:layout_constraintGuide_percent=".15"
android:orientation="vertical"/>
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/right_guideline"
app:layout_constraintGuide_percent=".85"
android:orientation="vertical"/>
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/bottom_guideline"
app:layout_constraintGuide_percent=".7"
android:orientation="horizontal"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/cropImageButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="411dp"
android:backgroundTint="#FFFFFFFF"
android:src="@drawable/ic_crop_black_24dp"
app:layout_constraintLeft_toLeftOf="@+id/left_guideline"
app:layout_constraintRight_toRightOf="@+id/right_guideline"
app:layout_constraintBottom_toBottomOf="@+id/bottom_guideline" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -9,12 +9,12 @@
android:orderInCategory="100"
android:title="CREATE POST"
android:icon="@drawable/ic_file_upload_24dp"
app:showAsAction="always"/>
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_save"
android:orderInCategory="101"
android:title="SAVE"
android:icon="@drawable/ic_save_24dp"
app:showAsAction="always"/>
app:showAsAction="ifRoom"/>
</menu>

View File

@ -49,6 +49,9 @@
<string name="tab_filters">FILTERS</string>
<string name="tab_edit">EDIT</string>
<string name="normal_filter">Normal</string>
<string name="busy_dialog_text">Still processing image, wait for that to finish first!</string>
<string name="busy_dialog_ok_button">OK, wait for that.</string>
<string name="crop_result_error">"Couldn't retrieve image after crop"</string>
<!-- Camera -->
<string name="capture_button_alt">Capture</string>
<string name="switch_camera_button_alt">Switch camera</string>

View File

@ -7,7 +7,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
classpath 'com.android.tools.build:gradle:4.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong

View File

@ -1,6 +1,6 @@
#Fri Feb 28 15:51:25 CET 2020
#Mon Jun 01 23:59:23 CEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip