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 Jul 11, 2021
1 parent b2df00f commit f735cfd
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 13 deletions.
164 changes: 160 additions & 4 deletions atox/src/main/kotlin/ui/chat/ChatFragment.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
package ltd.evilcorp.atox.ui.chat

import android.Manifest.permission
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
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.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.activity.result.contract.ActivityResultContracts
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
Expand All @@ -29,9 +41,12 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.math.MathUtils.lerp
import java.io.File
import java.io.IOException
import java.net.URLConnection
import java.text.DateFormat
import java.util.Locale
import java.util.UUID
import java.util.concurrent.TimeUnit
import ltd.evilcorp.atox.BuildConfig
import ltd.evilcorp.atox.R
import ltd.evilcorp.atox.databinding.FragmentChatBinding
Expand All @@ -49,6 +64,7 @@ import ltd.evilcorp.core.vo.isComplete
import ltd.evilcorp.domain.tox.PublicKey

const val CONTACT_PUBLIC_KEY = "publicKey"
private const val REQUEST_RECORD_PERMISSION = 204
private const val MAX_CONFIRM_DELETE_STRING_LENGTH = 20

class OpenMultiplePersistableDocuments : ActivityResultContracts.OpenMultipleDocuments() {
Expand All @@ -65,6 +81,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 = ""

private val exportFtLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument()) { dest ->
if (dest == null) return@registerForActivityResult
Expand All @@ -80,6 +99,9 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl
}
}

// TODO(robinlinden): Don't do this, but what should onClick even do when something's being
// done while a button is held down?
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?): Unit = binding.run {
contactPubKey = requireStringArg(CONTACT_PUBLIC_KEY)
viewModel.setActiveChat(PublicKey(contactPubKey))
Expand Down Expand Up @@ -266,6 +288,46 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl
registerForContextMenu(send)
send.setOnClickListener { send(MessageType.Normal) }

mic.setOnTouchListener { _, motionEvent ->
if (motionEvent.action == MotionEvent.ACTION_DOWN) {
if (checkRecordPermission()) {
startRecording()
startTimer()
} else {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(permission.RECORD_AUDIO),
REQUEST_RECORD_PERMISSION
)
}
} 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 {
WindowInsetsControllerCompat(requireActivity().window, view).hide(WindowInsetsCompat.Type.ime())
attachFilesLauncher.launch(arrayOf("*/*"))
Expand All @@ -282,6 +344,14 @@ 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 Down Expand Up @@ -370,14 +440,100 @@ 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 = requireContext().filesDir.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 checkRecordPermission(): Boolean =
ContextCompat.checkSelfPermission(
requireContext(),
permission.RECORD_AUDIO
) == 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))
}
}

private 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))
}
}

private fun stopTimer() = binding.run {
timer.apply {
visibility = View.GONE
text = context.getString(R.string.default_timer)
}
mHandler.removeCallbacks(mRunnable)
}
}
19 changes: 18 additions & 1 deletion atox/src/main/kotlin/ui/chat/ChatViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ class ChatViewModel @Inject constructor(
fileTransferManager.deleteAll(publicKey)
}

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

fun setActiveChat(pk: PublicKey) {
if (pk.string().isEmpty()) {
Log.i(TAG, "Clearing active chat")
Expand Down Expand Up @@ -113,7 +117,11 @@ 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 @@ -145,6 +153,15 @@ 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>
19 changes: 19 additions & 0 deletions atox/src/main/res/drawable/ic_bas.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">

<path
android:strokeColor="#FF000000"
android:strokeWidth="1"
android:pathData="M2,11
A9,9 0 1,1 22,11
A9,9 0 1,1 2,11"
/>
<path
android:fillColor="#FF000000"
android:pathData="M12,14c1.6,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM10.8,4.9c0,-0.66 0.54,-1.2 1.2,-1.2 0.66,0 1.2,0.54 1.2,1.2l-0.01,6.2c0,0.66 -0.53,1.2 -1.19,1.2 -0.66,0 -1.2,-0.54 -1.2,-1.2L10.8,4.9zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>


</vector>
26 changes: 26 additions & 0 deletions atox/src/main/res/layout/fragment_chat.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar"/>

<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/bubble"
android:contentDescription="@string/send"
android:paddingLeft="20dp"
android:paddingTop="5dp"
android:id="@+id/timer"
android:paddingRight="20dp"
android:paddingBottom="5dp"
android:text="00:00"
android:textColor="@color/textWhiteColor"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar"/>

<LinearLayout android:id="@+id/bottomBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
Expand Down Expand Up @@ -88,5 +106,13 @@
android:background="@android:color/transparent"
android:contentDescription="@string/attach_file"
android:src="@drawable/attach_file"/>

<ImageButton android:id="@+id/mic"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="0"
android:background="@android:color/transparent"
android:contentDescription="@string/attach_file"
android:src="@drawable/ic_bas"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
3 changes: 2 additions & 1 deletion atox/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,5 @@
<string name="pref_confirm_password">Confirm new password</string>
<string name="password_updated">Password updated</string>
<string name="passwords_must_match">The passwords must match</string>
</resources>
<string name="default_timer">00:00</string>
</resources>
24 changes: 17 additions & 7 deletions domain/src/main/kotlin/feature/FileTransferManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,18 @@ class FileTransferManager @Inject constructor(

fun transfersFor(publicKey: PublicKey) = fileTransferRepository.get(publicKey.string())

fun create(pk: PublicKey, file: Uri) {
val (name, size) = context.contentResolver.query(file, null, null, null, null, null)?.use { cursor ->
cursor.moveToFirst()
val fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
val name = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
Pair(name, fileSize)
} ?: return
fun create(pk: PublicKey, file: Uri, fileO: File?) {
val (name, size) =

if (fileO == null) {
context.contentResolver.query(file, null, null, null, null, null)?.use { cursor ->
cursor.moveToFirst()
val fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
val name = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
Pair(name, fileSize)
} ?: return
} else
Pair(fileO.name, fileO.length())

val ft = FileTransfer(
pk.string(),
Expand Down Expand Up @@ -284,6 +289,11 @@ class FileTransferManager @Inject constructor(
}
}

fun deleteLocalFile(filePath: String) {
val file = File(filePath)
if (file.exists()) file.delete()
}

suspend fun delete(id: Int) {
fileTransfers.find { it.id == id }?.let {
if (it.isStarted() && !it.isComplete()) {
Expand Down

0 comments on commit f735cfd

Please sign in to comment.