Skip to content
This repository has been archived by the owner on Dec 24, 2023. It is now read-only.

Commit

Permalink
Merge pull request #4 from flex3r/kotlin_and_mvvm
Browse files Browse the repository at this point in the history
Rewrote app in kotlin
ThePridestalker authored Oct 9, 2019

Verified

This commit was signed with the committer’s verified signature.
Moritzoni Moritz
2 parents 7997645 + 764d2c6 commit 3efcb95
Showing 15 changed files with 446 additions and 400 deletions.
13 changes: 13 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: "kotlin-android"
apply plugin: "kotlin-android-extensions"
apply plugin: "kotlin-kapt"

android {
compileSdkVersion 29
dataBinding.enabled = true
defaultConfig {
applicationId "com.nuuls.axel.nuulsimageuploader"
minSdkVersion 19
@@ -20,6 +24,7 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions.jvmTarget = "1.8"
}

dependencies {
@@ -28,4 +33,12 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.squareup.okhttp3:okhttp:4.2.1'
implementation 'androidx.exifinterface:exifinterface:1.0.0'
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0-alpha05"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0-alpha05"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha05"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha05"

implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.50'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.nuuls.axel.nuulsimageuploader

import android.net.Uri
import android.widget.Button
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.databinding.BindingAdapter

@BindingAdapter("preview")
fun ImageView.setPreview(uri: Uri?) {
if (uri == null) {
setImageResource(R.drawable.nuulslogo)
} else {
setImageURI(uri)
}
}

@BindingAdapter("state")
fun Button.setState(uploading: Boolean) {
text = if (uploading) {
setBackgroundColor(ContextCompat.getColor(context, R.color.upload_progress_background))
context.getString(R.string.uploading)
} else {
setBackgroundColor(ContextCompat.getColor(context, R.color.upload_background))
context.getString(R.string.upload)
}
}

This file was deleted.

265 changes: 0 additions & 265 deletions app/src/main/java/com/nuuls/axel/nuulsimageuploader/MainActivity.java

This file was deleted.

156 changes: 156 additions & 0 deletions app/src/main/java/com/nuuls/axel/nuulsimageuploader/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package com.nuuls.axel.nuulsimageuploader

import android.Manifest
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.FileProvider
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import com.nuuls.axel.nuulsimageuploader.databinding.MainActivityBinding
import java.io.File
import java.io.IOException

class MainActivity : AppCompatActivity() {
private lateinit var binding: MainActivityBinding
private lateinit var viewModel: MainViewModel

private var currentImagePath: String = ""

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
binding = DataBindingUtil.setContentView<MainActivityBinding>(this, R.layout.main_activity).apply {
lifecycleOwner = this@MainActivity
vm = viewModel
buttonCamera.setOnClickListener {
if (ActivityCompat.checkSelfPermission(this@MainActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
startCamera()
} else {
requestPermission(PERMISSION_CAPTURE_REQUEST_CODE)
}
}
}

viewModel.event.observe(this) {
when (it) {
is UploadEvent.Result -> {
generateToast("Copied: ${it.url}")

val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("nuuls url", it.url))
}
is UploadEvent.Error -> generateToast(it.message)
}
}

// Handling the intent coming from other app
val type = intent.type
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
// if im receiving an image from other app
if (Intent.ACTION_SEND == intent.action && type != null) {
handleSendImage(intent)
}
} else {
requestPermission(PERMISSION_SHARE_REQUEST_CODE)
}
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (grantResults.getOrNull(0) == PackageManager.PERMISSION_GRANTED) {
when (requestCode) {
PERMISSION_SHARE_REQUEST_CODE -> {
handleSendImage(intent)
}
PERMISSION_CAPTURE_REQUEST_CODE -> {
startCamera()
}
}
}
}

private fun requestPermission(code: Int) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), code)
}

private fun startCamera() {
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { captureIntent ->
captureIntent.resolveActivity(packageManager)?.also {
try {
MediaUtils.createImageFile(this).apply { currentImagePath = absolutePath }
} catch (e: IOException) {
null
}?.also {
val uri = FileProvider.getUriForFile(this, "$packageName.fileprovider", it)
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
startActivityForResult(captureIntent, CAMERA_REQUEST_CODE)
}
}
}
}

private fun handleSendImage(intent: Intent) {
if (intent.type?.startsWith("image/") == false) {
return
}

// copy the shared image to a new file so we don't remove exif data form the original
val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri ?: return
val mimeType = contentResolver?.getType(uri)
val mimeTypeMap = MimeTypeMap.getSingleton()
val extension = mimeTypeMap.getExtensionFromMimeType(mimeType) ?: return
val copy = MediaUtils.createImageFile(this, extension)

try {
contentResolver.openInputStream(uri)?.run { copy.outputStream().use { copyTo(it) } }
if (copy.extension == "jpg" || copy.extension == "jpeg") {
MediaUtils.removeExifAttributes(copy.absolutePath)
}
val copyUri = FileProvider.getUriForFile(this, "$packageName.fileprovider", copy)
viewModel.setPath(copy.absolutePath, copyUri)
} catch (e: IOException) {
Log.e(TAG, Log.getStackTraceString(e))
copy.delete()
}

}

// after getting the pic taken from the camera activity
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK && requestCode == CAMERA_REQUEST_CODE) {
val imageFile = File(currentImagePath)
val uri = FileProvider.getUriForFile(this, "$packageName.fileprovider", imageFile)
try {
MediaUtils.removeExifAttributes(currentImagePath)
viewModel.setPath(currentImagePath, uri)
} catch (e: IOException) {
Log.e(TAG, Log.getStackTraceString(e))
imageFile.delete()
}
}
}

private fun generateToast(text: String) {
Toast.makeText(this, text, Toast.LENGTH_LONG).show()
}

companion object {
private val TAG = MainActivity::class.java.simpleName
private const val PERMISSION_CAPTURE_REQUEST_CODE = 1
private const val PERMISSION_SHARE_REQUEST_CODE = 2
private const val CAMERA_REQUEST_CODE = 2
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.nuuls.axel.nuulsimageuploader

import android.net.Uri
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import java.io.File

class MainViewModel : ViewModel() {
private var path = ""

val event = SingleLiveEvent<UploadEvent>()
val urlLiveData = MutableLiveData("")
val uriLiveData = MutableLiveData<Uri>()
val uploadLiveData = MutableLiveData(false)

fun upload() {
if (path.isEmpty() || uploadLiveData.value == true) {
event.postValue(UploadEvent.Error("Take a picture first :D"))
return
}
uploadLiveData.postValue(true)
viewModelScope.launch {
val result = NuulsUploader.upload(File(path))
if (result != null) {
urlLiveData.postValue(result)
event.postValue(UploadEvent.Result(result))
} else {
event.postValue(UploadEvent.Error("ERROR"))
}
path = ""
uriLiveData.postValue(null)
uploadLiveData.postValue(false)
}
}

fun setPath(path: String, uri: Uri) {
this.path = path
this.urlLiveData.value = ""
this.uriLiveData.value = uri
}
}
39 changes: 39 additions & 0 deletions app/src/main/java/com/nuuls/axel/nuulsimageuploader/MediaUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.nuuls.axel.nuulsimageuploader

import android.content.Context
import android.os.Environment
import androidx.exifinterface.media.ExifInterface
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*

object MediaUtils {
private val GPS_ATTRIBUTES = listOf(
ExifInterface.TAG_GPS_VERSION_ID,
ExifInterface.TAG_GPS_AREA_INFORMATION,
ExifInterface.TAG_GPS_LATITUDE,
ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE,
ExifInterface.TAG_GPS_LONGITUDE_REF,
ExifInterface.TAG_GPS_ALTITUDE,
ExifInterface.TAG_GPS_ALTITUDE_REF,
ExifInterface.TAG_GPS_TIMESTAMP,
ExifInterface.TAG_GPS_DATESTAMP
)

@Throws(IOException::class)
fun createImageFile(context: Context, suffix: String = "jpg"): File {
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(timeStamp, ".$suffix", storageDir)
}

@Throws(IOException::class)
fun removeExifAttributes(path: String) {
ExifInterface(path).run {
GPS_ATTRIBUTES.forEach { if (getAttribute(it) != null) setAttribute(it, null) }
saveAttributes()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.nuuls.axel.nuulsimageuploader

import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File

object NuulsUploader {
private val client = OkHttpClient()
private val MEDIA_TYPE = "image/png".toMediaType()
private const val URL = "https://i.nuuls.com/upload"
private val TAG = NuulsUploader::class.java.simpleName

suspend fun upload(file: File): String? = withContext(Dispatchers.IO) {
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("xd", "justNameItXD.png", file.asRequestBody(MEDIA_TYPE))
.build()
val request = Request.Builder()
.url(URL)
.post(requestBody)
.build()

try {
val response = client.newCall(request).execute()
if (response.isSuccessful) {
return@withContext response.body?.string()
}
} catch (t: Throwable) {
Log.e(TAG, Log.getStackTraceString(t))
}

return@withContext null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.nuuls.axel.nuulsimageuploader

import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean

class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)

@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
observe(owner, observer::onChanged)
}

@MainThread
fun observe(owner: LifecycleOwner, observer: (T) -> Unit) {
super.observe(owner, PendingObserver(observer))
}

override fun setValue(value: T) {
pending.set(true)
super.setValue(value)
}

private inner class PendingObserver<T>(private val observer: (T) -> Unit) : Observer<T> {
override fun onChanged(t: T) {
if (pending.compareAndSet(true, false)) {
observer(t)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.nuuls.axel.nuulsimageuploader

sealed class UploadEvent {
data class Error(val message: String) : UploadEvent()
data class Result(val url: String) : UploadEvent()
}
67 changes: 0 additions & 67 deletions app/src/main/res/layout/activity_main.xml

This file was deleted.

80 changes: 80 additions & 0 deletions app/src/main/res/layout/main_activity.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<layout 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">

<data>

<variable
name="vm"
type="com.nuuls.axel.nuulsimageuploader.MainViewModel" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/main_background"
tools:context=".MainActivity">

<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginBottom="5dp"
android:contentDescription="@string/image_preview"
android:scaleType="centerInside"
app:layout_constraintBottom_toTopOf="@id/imageUrl"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:preview="@{vm.uriLiveData}" />

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/imageUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@{vm.urlLiveData}"
android:textColor="@android:color/white"
app:layout_constraintBottom_toTopOf="@id/buttonCamera"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/imageView" />

<Button
android:id="@+id/buttonCamera"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="5dp"
android:background="@color/camera_background"
android:enabled="@{!vm.uploadLiveData}"
android:text="@string/open_camera"
android:textColor="@android:color/white"
app:layout_constraintBottom_toTopOf="@id/buttonUpload"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/imageUrl" />

<Button
android:id="@+id/buttonUpload"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="5dp"
android:enabled="@{!vm.uploadLiveData}"
android:onClick="@{() -> vm.upload()}"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/buttonCamera"
app:state="@{vm.uploadLiveData}" />


</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
4 changes: 4 additions & 0 deletions app/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
@@ -3,4 +3,8 @@
<color name="colorPrimary">#181818</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="main_background">#181818</color>
<color name="camera_background">#2196f3</color>
<color name="upload_background">#4caf50</color>
<color name="upload_progress_background">#de2761</color>
</resources>
4 changes: 3 additions & 1 deletion app/src/main/res/xml/filepaths.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<paths>
<external-path name="Pictures" path="Pictures"/>
<external-path
name="Pictures"
path="Android/data/com.nuuls.axel.nuulsimageuploader/files/Pictures" />
</paths>
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.1'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.50'


// NOTE: Do not place your application dependencies here; they belong

0 comments on commit 3efcb95

Please sign in to comment.