/* * Copyright (c) 2023 Samson Achiaga * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.certified.audionote.ui import android.Manifest import android.app.DatePickerDialog import android.app.TimePickerDialog import android.content.pm.PackageManager import android.media.MediaRecorder import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.widget.DatePicker import android.widget.TimePicker import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.NavController import androidx.navigation.Navigation import androidx.navigation.fragment.navArgs import com.certified.audionote.R import com.certified.audionote.databinding.DialogEditReminderBinding import com.certified.audionote.databinding.FragmentAddNoteBinding import com.certified.audionote.model.Note import com.certified.audionote.utils.Extensions.safeNavigate import com.certified.audionote.utils.Extensions.showKeyboardFor import com.certified.audionote.utils.Extensions.showToast import com.certified.audionote.utils.ReminderAvailableState import com.certified.audionote.utils.cancelAlarm import com.certified.audionote.utils.currentDate import com.certified.audionote.utils.filePath import com.certified.audionote.utils.formatReminderDate import com.certified.audionote.utils.roundOffDecimal import com.certified.audionote.utils.startAlarm import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import timerx.* import java.io.File import java.io.IOException import java.util.Calendar import java.util.concurrent.TimeUnit @AndroidEntryPoint class AddNoteFragment : Fragment(), DatePickerDialog.OnDateSetListener, TimePickerDialog.OnTimeSetListener { private var _binding: FragmentAddNoteBinding? = null private val binding get() = _binding!! private val viewModel: NotesViewModel by viewModels() private lateinit var navController: NavController private val args: EditNoteFragmentArgs by navArgs() private lateinit var _note: Note private var isRecording = false private var pickedDateTime: Calendar? = null private val currentDateTime by lazy { currentDate() } private var mediaRecorder: MediaRecorder? = null private var file: File? = null private var stopWatch: Stopwatch? = null private var timer: Timer? = null private val requestAudioRecordingPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> if (!isGranted) MaterialAlertDialogBuilder(requireContext()).apply { setTitle(getString(R.string.audio_record_permission)) setMessage(getString(R.string.permission_required)) setPositiveButton(getString(R.string.ok)) { dialog, _ -> dialog.dismiss() } show() } else startRecording() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { // Inflate the layout for this fragment _binding = FragmentAddNoteBinding.inflate(layoutInflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) navController = Navigation.findNavController(view) binding.lifecycleOwner = viewLifecycleOwner binding.viewModel = viewModel args.note.let { _note = it binding.note = it } binding.btnBack.setOnClickListener { if (binding.etNoteTitle.text.toString().isNotBlank()) saveNote() else try { file?.delete() } catch (e: Exception) { e.printStackTrace() } navController.safeNavigate(AddNoteFragmentDirections.actionAddNoteFragmentToHomeFragment()) } binding.cardAddReminder.setOnClickListener { if (viewModel.reminderAvailableState.value == ReminderAvailableState.NO_REMINDER) pickDate() else openEditReminderDialog() } binding.btnRecord.setOnClickListener { recordAudio() } binding.fabSaveNote.setOnClickListener { if (binding.etNoteTitle.text.toString().isNotBlank()) saveNote() else { showToast(requireContext().getString(R.string.title_required)) binding.etNoteTitle.requestFocus() } } } override fun onResume() { super.onResume() binding.etNoteTitle.apply { requestFocus() showKeyboardFor(requireContext()) } updateStatusBarColor(binding.note!!.color) } override fun onDestroyView() { super.onDestroyView() mediaRecorder = null timer?.apply { stop() reset() } timer = null stopWatch?.apply { stop() reset() } stopWatch = null _binding = null } private fun recordAudio() { binding.apply { if (!isRecording) { if (ContextCompat.checkSelfPermission( requireContext(), Manifest.permission.RECORD_AUDIO ) == PackageManager.PERMISSION_GRANTED ) { btnRecord.setImageDrawable( ResourcesCompat.getDrawable( resources, R.drawable.ic_mic_recording, null ) ).run { try { file?.delete() } catch (e: Exception) { e.printStackTrace() } isRecording = true startRecording() } } else if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) MaterialAlertDialogBuilder(requireContext()).apply { setTitle(getString(R.string.audio_record_permission)) setMessage(getString(R.string.permission_required)) setPositiveButton(getString(R.string.ok)) { dialog, _ -> dialog.dismiss() } show() } else requestAudioRecordingPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO) } else { btnRecord.setImageDrawable( ResourcesCompat.getDrawable( resources, R.drawable.ic_mic_not_recording, null ) ) .run { isRecording = false stopRecording() } } } } override fun onDateSet(p0: DatePicker?, p1: Int, p2: Int, p3: Int) { pickedDateTime = currentDate() pickedDateTime!!.set(p1, p2, p3) val hourOfDay = currentDateTime.get(Calendar.HOUR_OF_DAY) val minuteOfDay = currentDateTime.get(Calendar.MINUTE) val timePickerDialog = TimePickerDialog(requireContext(), this, hourOfDay, minuteOfDay, false) timePickerDialog.setOnDismissListener { viewModel._reminderAvailableState.value = ReminderAvailableState.HAS_REMINDER _note.reminder = pickedDateTime!!.timeInMillis binding.tvReminderDate.text = formatReminderDate(pickedDateTime!!.timeInMillis) } timePickerDialog.show() } override fun onTimeSet(p0: TimePicker?, p1: Int, p2: Int) { pickedDateTime!!.set(Calendar.HOUR_OF_DAY, p1) pickedDateTime!!.set(Calendar.MINUTE, p2) if (pickedDateTime!!.timeInMillis <= currentDate().timeInMillis) { pickedDateTime!!.run { set(Calendar.DAY_OF_MONTH, currentDateTime.get(Calendar.DAY_OF_MONTH) + 1) set(Calendar.YEAR, currentDateTime.get(Calendar.YEAR)) set(Calendar.MONTH, currentDateTime.get(Calendar.MONTH)) } } } private fun pickDate() { val startYear = currentDateTime.get(Calendar.YEAR) val startMonth = currentDateTime.get(Calendar.MONTH) val startDay = currentDateTime.get(Calendar.DAY_OF_MONTH) val datePickerDialog = DatePickerDialog(requireContext(), this, startYear, startMonth, startDay) datePickerDialog.show() } private fun openEditReminderDialog() { val view = DialogEditReminderBinding.inflate(layoutInflater) val bottomSheetDialog = BottomSheetDialog(requireContext()) view.apply { note = _note btnDeleteReminder.setOnClickListener { viewModel._reminderAvailableState.value = ReminderAvailableState.NO_REMINDER _note.reminder = null bottomSheetDialog.dismiss() } btnModifyReminder.setOnClickListener { bottomSheetDialog.dismiss() pickDate() } } bottomSheetDialog.edgeToEdgeEnabled bottomSheetDialog.setContentView(view.root) bottomSheetDialog.show() } private fun saveNote() { binding.apply { stopRecording() if (_note.audioLength <= 0) { showToast(requireContext().getString(R.string.record_note_before_saving)) return } val note = _note.copy( title = etNoteTitle.text.toString().trim(), description = etNoteDescription.text.toString().trim() ) this@AddNoteFragment.viewModel.insertNote(note) showToast(requireContext().getString(R.string.note_saved)) if (pickedDateTime?.timeInMillis != null && pickedDateTime!!.timeInMillis <= currentDateTime.timeInMillis) startAlarm(requireContext(), pickedDateTime!!.timeInMillis, note) navController.safeNavigate(AddNoteFragmentDirections.actionAddNoteFragmentToHomeFragment()) } } private fun startRecording() { val filePath = filePath(requireActivity()) val fileName = "${System.currentTimeMillis()}.3gp" _note.filePath = "$filePath/$fileName" stopWatch = buildStopwatch { startFormat("MM:SS") onTick { millis: Long, time: CharSequence-> binding.tvTimer.text = time } changeFormatWhen(1, TimeUnit.HOURS, "HH:MM:SS") } mediaRecorder = MediaRecorder().apply { setAudioSource(MediaRecorder.AudioSource.MIC) setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) setOutputFile("$filePath/$fileName") setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) try { prepare() start() stopWatch!!.start() file = File("$filePath/$fileName") } catch (e: IOException) { showToast(requireContext().getString(R.string.error_occurred)) } } } private fun stopRecording() { mediaRecorder?.apply { stop() release() } mediaRecorder = null stopWatch?.apply { stop() _note.audioLength = stopWatch!!.currentTimeInMillis / 1000 reset() } stopWatch = null if (_note.audioLength <= 0) return file = File(_note.filePath) val fileByte = (file!!.readBytes().size.toDouble() / 1048576.00) val fileSize = roundOffDecimal(fileByte) _note.size = fileSize } private fun updateStatusBarColor(color: Int) { val window = requireActivity().window window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) // window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) window.statusBarColor = color } }