Skip to content

Commit

Permalink
Add push to record voice and review, then send or delete
Browse files Browse the repository at this point in the history
  • Loading branch information
helmutsreinis authored and robinlinden committed Mar 26, 2021
1 parent 0e0b185 commit bcc961c
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 29 deletions.
3 changes: 3 additions & 0 deletions atox/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<uses-feature android:name="android.hardware.camera" android:required="false"/>

Expand All @@ -15,6 +17,7 @@
android:allowBackup="false"
android:icon="@mipmap/launcher_icon"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/launcher_icon_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
Expand Down
213 changes: 200 additions & 13 deletions atox/src/main/kotlin/ui/chat/ChatFragment.kt
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
package ltd.evilcorp.atox.ui.chat

import android.Manifest.permission
import android.app.Activity
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.content.pm.PackageManager
import android.media.MediaRecorder
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.renderscript.ScriptGroup
import android.util.Log
import android.view.ContextMenu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.widget.AdapterView
import android.widget.ImageButton
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import androidx.core.content.res.ResourcesCompat
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import java.io.File
import java.net.URLConnection
import java.text.DateFormat
import java.util.Locale
import ltd.evilcorp.atox.BuildConfig
import ltd.evilcorp.atox.R
import ltd.evilcorp.atox.databinding.FragmentChatBinding
Expand All @@ -37,16 +45,19 @@ import ltd.evilcorp.atox.ui.BaseFragment
import ltd.evilcorp.atox.ui.colorByStatus
import ltd.evilcorp.atox.ui.setAvatarFromContact
import ltd.evilcorp.atox.vmFactory
import ltd.evilcorp.core.vo.ConnectionStatus
import ltd.evilcorp.core.vo.FileTransfer
import ltd.evilcorp.core.vo.Message
import ltd.evilcorp.core.vo.MessageType
import ltd.evilcorp.core.vo.isComplete
import ltd.evilcorp.core.vo.*
import ltd.evilcorp.domain.tox.PublicKey
import java.io.File
import java.io.IOException
import java.net.URLConnection
import java.text.DateFormat
import java.util.*
import java.util.concurrent.TimeUnit

const val CONTACT_PUBLIC_KEY = "publicKey"
private const val REQUEST_CODE_FT_EXPORT = 1234
private const val REQUEST_CODE_ATTACH = 5678
private const val REQUEST_RECORD_PERMISSION = 204
private const val MAX_CONFIRM_DELETE_STRING_LENGTH = 20

class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::inflate) {
Expand All @@ -56,6 +67,9 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl
private var contactName = ""
private var selectedFt: Int = Int.MIN_VALUE
private var fts: List<FileTransfer> = listOf()
private lateinit var mRecorder: MediaRecorder
private var mRecordedFilePath = ""
private var mRecordedFileName = ""

override fun onViewCreated(view: View, savedInstanceState: Bundle?): Unit = binding.run {
contactPubKey = requireStringArg(CONTACT_PUBLIC_KEY)
Expand Down Expand Up @@ -171,6 +185,44 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl
registerForContextMenu(send)
send.setOnClickListener { send(MessageType.Normal) }

mic.setOnTouchListener { view, motionEvent ->
if (motionEvent.action == MotionEvent.ACTION_DOWN) {
if (checkRecordPermission()) {
startRecording()
startTimer()
} else {
requestRecordPermission()
}
} else if (motionEvent.action == MotionEvent.ACTION_UP) {

if (checkRecordPermission()) {
stopRecording()
stopTimer()

try {
if (File(mRecordedFilePath).length() > 0)
AlertDialog.Builder(requireContext())
.setTitle(R.string.confirm)
.setPositiveButton(R.string.send) { _, _ ->
viewModel.createFt(Uri.fromFile(File(mRecordedFilePath)), File(mRecordedFilePath))

}
.setNegativeButton(R.string.delete, { _, _ ->
if (mRecordedFilePath.isNotEmpty())
viewModel.deleteFile(mRecordedFilePath)
}).show()
else
viewModel.deleteFile(mRecordedFilePath)
} catch (e: Exception) {
e.printStackTrace()
}
}
}

return@setOnTouchListener true

}

attach.setOnClickListener {
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
Expand All @@ -192,6 +244,17 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl
override fun onPause() {
viewModel.setDraft(binding.outgoingMessage.text.toString())
viewModel.setActiveChat(PublicKey(""))


if (binding.timer.visibility == View.VISIBLE) {
stopRecording()
stopTimer()
if (mRecordedFilePath.isNotEmpty())
viewModel.deleteFile(mRecordedFilePath)
}



super.onPause()
}

Expand All @@ -201,6 +264,22 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl
super.onResume()
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when (requestCode) {
REQUEST_RECORD_PERMISSION -> {
if (grantResults.size > 0) {
val storagePermission = grantResults[0] ==
PackageManager.PERMISSION_GRANTED;
val recordPermission = grantResults[1] ==
PackageManager.PERMISSION_GRANTED;

}
}

}
}


override fun onCreateContextMenu(
menu: ContextMenu,
v: View,
Expand Down Expand Up @@ -313,14 +392,122 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl

private fun updateActions() = binding.run {
send.visibility = if (outgoingMessage.text.isEmpty()) View.GONE else View.VISIBLE
attach.visibility = if (send.visibility == View.VISIBLE) View.GONE else View.VISIBLE
attach.isEnabled = viewModel.contactOnline
attach.setColorFilter(
adjustAttachmentButtons(attach)
adjustAttachmentButtons(mic)
}

private fun adjustAttachmentButtons(view: ImageButton) = binding.run {
view.visibility = if (send.visibility == View.VISIBLE) View.GONE else View.VISIBLE
view.isEnabled = viewModel.contactOnline
view.setColorFilter(
ResourcesCompat.getColor(
resources,
if (attach.isEnabled) R.color.colorPrimary else android.R.color.darker_gray,
if (view.isEnabled) R.color.colorPrimary else android.R.color.darker_gray,
null
)
)
}


private fun startRecording() {
try {
mRecorder = MediaRecorder()
mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC)
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
mRecorder.setAudioEncoder(MediaRecorder.OutputFormat.AMR_NB)
val rootPath = Environment.getExternalStorageDirectory().absolutePath
val file = File("$rootPath/ATox")
if (!file.exists()) {
file.mkdirs()
}
val fileName = "REC_" + UUID.randomUUID().toString().substring(30, 35) + ".m4a"

mRecordedFileName = fileName
mRecordedFilePath = "$rootPath/ATox/$fileName"
mRecorder.setOutputFile(mRecordedFilePath)

mRecorder.prepare()
mRecorder.start()
} catch (e: java.lang.IllegalStateException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
}


private fun stopRecording() {
try {
try {
mRecorder.stop()
} catch (e: IllegalStateException) {
e.printStackTrace()
}
mRecorder.reset() // set state to idle
mRecorder.release() // release resources back to the system

} catch (stopexception: RuntimeException) {
Log.d("recview", "rec error")
if (mRecordedFilePath.isNotEmpty())
viewModel.deleteFile(mRecordedFilePath)
}
}


private fun requestRecordPermission() {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(permission.WRITE_EXTERNAL_STORAGE, permission.RECORD_AUDIO),
REQUEST_RECORD_PERMISSION
)
}

fun checkRecordPermission(): Boolean {
val result = ContextCompat.checkSelfPermission(
requireContext(),
permission.WRITE_EXTERNAL_STORAGE
)
val result1 = ContextCompat.checkSelfPermission(
requireContext(),
permission.RECORD_AUDIO
)
return result == PackageManager.PERMISSION_GRANTED &&
result1 == PackageManager.PERMISSION_GRANTED
}


private val mHandler: Handler = Handler(Looper.getMainLooper())

private var mStartTime = 0L

private val mRunnable: Runnable by lazy {
Runnable {
viewModel.getTimerTime(mStartTime).let {
if (it.isNotEmpty())
binding.timer.text = it
}
mHandler.postDelayed(mRunnable, TimeUnit.SECONDS.toMillis(1))
}
}

fun startTimer() = binding.run {
timer.apply {
visibility = View.VISIBLE
text = context.getString(R.string.default_timer)

mStartTime = System.currentTimeMillis()

mHandler.postDelayed(mRunnable, TimeUnit.SECONDS.toMillis(1))
}
}


fun stopTimer() = binding.run {
timer.apply {
visibility = View.GONE
text = context.getString(R.string.default_timer)
}
mHandler.removeCallbacks(mRunnable)
}

}
34 changes: 25 additions & 9 deletions atox/src/main/kotlin/ui/chat/ChatViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,9 @@ import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import java.io.File
import java.io.FileInputStream
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ltd.evilcorp.atox.R
import ltd.evilcorp.atox.ui.NotificationHelper
import ltd.evilcorp.core.vo.Contact
Expand All @@ -28,6 +21,10 @@ import ltd.evilcorp.domain.feature.ChatManager
import ltd.evilcorp.domain.feature.ContactManager
import ltd.evilcorp.domain.feature.FileTransferManager
import ltd.evilcorp.domain.tox.PublicKey
import java.io.File
import java.io.FileInputStream
import java.text.SimpleDateFormat
import javax.inject.Inject

private const val TAG = "ChatViewModel"

Expand Down Expand Up @@ -55,6 +52,10 @@ class ChatViewModel @Inject constructor(
fileTransferManager.deleteAll(publicKey)
}

fun deleteFile(path: String) {
fileTransferManager.deleteLocalFile(path)
}

fun setActiveChat(pubKey: PublicKey) {
if (pubKey.string().isEmpty()) {
Log.i(TAG, "Clearing active chat")
Expand Down Expand Up @@ -85,7 +86,12 @@ class ChatViewModel @Inject constructor(
}

fun createFt(file: Uri) = launch {
fileTransferManager.create(publicKey, file)
fileTransferManager.create(publicKey, file, null)
}


fun createFt(file: Uri, fileO: File) = launch {
fileTransferManager.create(publicKey, file, fileO)
}

fun delete(msg: Message) = launch {
Expand Down Expand Up @@ -117,6 +123,16 @@ class ChatViewModel @Inject constructor(
}
}


fun getTimerTime(startTime: Long): String {
val diff = System.currentTimeMillis() - startTime
var seconds = (diff / 1000).toInt()
val minutes = seconds / 60
seconds = seconds % 60

return String.format("%02d:%02d", minutes, seconds)
}

fun setDraft(draft: String) = contactManager.setDraft(publicKey, draft)
fun clearDraft() = setDraft("")
}
8 changes: 8 additions & 0 deletions atox/src/main/res/drawable/bubble.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">

<solid android:color="@color/colorPrimary"></solid>


<corners android:radius="60dp"></corners>
</shape>
Loading

0 comments on commit bcc961c

Please sign in to comment.