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 May 18, 2021
1 parent 5b94690 commit d83b6c0
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 13 deletions.
2 changes: 2 additions & 0 deletions atox/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<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 @@ -16,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
186 changes: 182 additions & 4 deletions atox/src/main/kotlin/ui/chat/ChatFragment.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
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.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
Expand All @@ -26,9 +36,12 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.asLiveData
import androidx.navigation.fragment.findNavController
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 @@ -48,6 +61,7 @@ import ltd.evilcorp.domain.tox.PublicKey
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 @@ -57,6 +71,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 @@ -185,6 +202,45 @@ 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 @@ -206,6 +262,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 All @@ -215,6 +279,19 @@ 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 @@ -327,14 +404,115 @@ 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)
}
}
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 @@ -57,6 +57,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 @@ -87,7 +91,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 @@ -119,6 +127,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("")

Expand Down
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 @@ -149,4 +149,5 @@
<string name="pref_nospam_description">Changing your nospam means that your Tox ID changes, and that your current Tox ID can no longer be used to send you friend requests</string>
<string name="saved">Saved</string>
<string name="delete_message">Delete message</string>
</resources>
<string name="default_timer">00:00</string>
</resources>
Loading

0 comments on commit d83b6c0

Please sign in to comment.