package com.dmc.callcrm.ui import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.media.MediaPlayer import android.net.Uri import android.os.Bundle import android.provider.CallLog import android.text.Editable import android.text.TextWatcher import android.view.LayoutInflater import android.view.PopupMenu import android.view.View import android.view.ViewGroup import android.widget.EditText import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.dmc.callcrm.ApiConfig import com.dmc.callcrm.R import com.dmc.callcrm.data.CallStats import com.dmc.callcrm.data.Prefs import com.dmc.callcrm.network.ApiClient import com.dmc.callcrm.network.CallNoteRequest import com.dmc.callcrm.network.MessageTemplate import com.dmc.callcrm.network.NoteTemplate import com.google.android.material.chip.Chip import com.google.android.material.tabs.TabLayout import kotlinx.coroutines.launch class CallHistoryFragment : Fragment() { private lateinit var adapter: CallAdapter private lateinit var prefs: Prefs private var all: List = emptyList() private var typeFilter: Int? = null private var query = "" private var mediaPlayer: MediaPlayer? = null override fun onCreateView(i: LayoutInflater, c: ViewGroup?, s: Bundle?): View = i.inflate(R.layout.fragment_list, c, false) override fun onViewCreated(v: View, s: Bundle?) { prefs = Prefs(requireContext()) v.findViewById(R.id.title).text = "Call History" adapter = CallAdapter( emptyList(), noteFor = { item -> prefs.noteFor(prefs.noteKey(item.number, item.dateMs, item.durationSec)) }, onCall = { dial(it.number) }, onWhatsapp = { openWhatsappPicker(it) }, onPlay = { playRecording(it) }, onCopy = { copyNumber(it) }, onMore = { item, anchor -> showMoreMenu(item, anchor) }, onNote = { openNoteDialog(it) } ) v.findViewById(R.id.list).apply { layoutManager = LinearLayoutManager(context); adapter = this@CallHistoryFragment.adapter } val tabs = v.findViewById(R.id.tabs) listOf("All Calls", "Incoming", "Outgoing", "Missed").forEach { tabs.addTab(tabs.newTab().setText(it)) } tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(t: TabLayout.Tab) { typeFilter = when (t.position) { 1 -> CallLog.Calls.INCOMING_TYPE 2 -> CallLog.Calls.OUTGOING_TYPE 3 -> CallLog.Calls.MISSED_TYPE else -> null } reload() } override fun onTabUnselected(t: TabLayout.Tab) {} override fun onTabReselected(t: TabLayout.Tab) {} }) v.findViewById(R.id.search).addTextChangedListener(object : TextWatcher { override fun afterTextChanged(e: Editable?) { query = e?.toString()?.trim().orEmpty(); render() } override fun beforeTextChanged(s: CharSequence?, a: Int, b: Int, c: Int) {} override fun onTextChanged(s: CharSequence?, a: Int, b: Int, c: Int) {} }) reload() } private fun reload() { all = CallStats(requireContext()).recent(300, typeFilter) render() } private fun render() { val list = if (query.isEmpty()) all else all.filter { (it.name ?: "").contains(query, true) || it.number.contains(query) } adapter.submit(list) } private fun dial(number: String) { try { startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:$number"))) } catch (_: Exception) {} } private fun copyNumber(item: CallStats.CallItem) { val cm = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(ClipData.newPlainText("Phone number", item.number)) Toast.makeText(context, "Number copied", Toast.LENGTH_SHORT).show() } private fun showMoreMenu(item: CallStats.CallItem, anchor: View) { val menu = PopupMenu(requireContext(), anchor) menu.menu.add("View call details") val hasNote = !prefs.noteFor(prefs.noteKey(item.number, item.dateMs, item.durationSec)).isNullOrBlank() if (hasNote) menu.menu.add("Remove note") menu.setOnMenuItemClickListener { mi -> when (mi.title) { "View call details" -> { val df = java.text.SimpleDateFormat("dd MMM yyyy, hh:mm a", java.util.Locale.getDefault()) AlertDialog.Builder(requireContext()) .setTitle(item.name ?: "Unknown") .setMessage( "Number: ${item.number}\n" + "Time: ${df.format(java.util.Date(item.dateMs))}\n" + "Duration: ${item.durationSec}s" ) .setPositiveButton("Close", null) .show() } "Remove note" -> { val key = prefs.noteKey(item.number, item.dateMs, item.durationSec) prefs.saveNoteLocally(key, "") adapter.refreshNotes() saveNoteToServer(item, "") } } true } menu.show() } // ---- Notes ---- private fun openNoteDialog(item: CallStats.CallItem) { val key = prefs.noteKey(item.number, item.dateMs, item.durationSec) val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_note, null) val noteText = view.findViewById(R.id.noteText) val chipGroup = view.findViewById(R.id.chipGroup) noteText.setText(prefs.noteFor(key).orEmpty()) val dialog = AlertDialog.Builder(requireContext()) .setTitle("Call note") .setView(view) .setPositiveButton("Save", null) .setNegativeButton("Cancel", null) .create() dialog.setOnShowListener { dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { val note = noteText.text.toString() prefs.saveNoteLocally(key, note) adapter.refreshNotes() saveNoteToServer(item, note) dialog.dismiss() } } dialog.show() if (!prefs.isRegistered) return viewLifecycleOwner.lifecycleScope.launch { try { val resp = ApiClient.service.noteTemplates(prefs.bearer()) val templates = resp.body()?.templates.orEmpty() chipGroup.removeAllViews() templates.forEach { t: NoteTemplate -> val chip = Chip(requireContext()) chip.text = t.title chip.isClickable = true chip.setOnClickListener { noteText.setText(t.body ?: t.title) } chipGroup.addView(chip) } } catch (_: Exception) { /* templates are a convenience; ignore failures */ } } } private fun saveNoteToServer(item: CallStats.CallItem, note: String) { if (!prefs.isRegistered) return viewLifecycleOwner.lifecycleScope.launch { try { ApiClient.service.saveCallNote( prefs.bearer(), CallNoteRequest( number = item.number, name = item.name, type = item.type, date = item.dateMs, duration = item.durationSec, note = note, deviceCallId = item.id ) ) } catch (_: Exception) { Toast.makeText(context, "Note saved on device; will sync when online", Toast.LENGTH_SHORT).show() } } } // ---- WhatsApp ---- private fun openWhatsappPicker(item: CallStats.CallItem) { if (!prefs.isRegistered) { sendWhatsapp(item.number, null); return } viewLifecycleOwner.lifecycleScope.launch { val templates = try { ApiClient.service.messageTemplates(prefs.bearer()).body()?.templates.orEmpty() } catch (_: Exception) { emptyList() } if (templates.isEmpty()) { sendWhatsapp(item.number, null); return@launch } val labels = (listOf("No template — open chat") + templates.map { it.title }).toTypedArray() AlertDialog.Builder(requireContext()) .setTitle("Send WhatsApp message") .setItems(labels) { _, which -> val body = if (which == 0) null else templates[which - 1].body sendWhatsapp(item.number, body) } .show() } } private fun sendWhatsapp(number: String, message: String?) { val digits = number.filter { it.isDigit() } val text = message?.let { Uri.encode(it) } val uri = if (text != null) Uri.parse("https://wa.me/$digits?text=$text") else Uri.parse("https://wa.me/$digits") try { startActivity(Intent(Intent.ACTION_VIEW, uri)) } catch (_: Exception) { Toast.makeText(context, "WhatsApp is not installed", Toast.LENGTH_SHORT).show() } } // ---- Recording playback ---- private fun playRecording(item: CallStats.CallItem) { if (!prefs.isRegistered) { Toast.makeText(context, "Register the app first to fetch recordings", Toast.LENGTH_SHORT).show() return } viewLifecycleOwner.lifecycleScope.launch { val resp = try { ApiClient.service.findRecording(prefs.bearer(), item.number, item.dateMs, item.durationSec) } catch (_: Exception) { null } val body = resp?.body() if (body?.ok != true || body.found != true || body.recordingId == null) { Toast.makeText(context, "No recording found for this call", Toast.LENGTH_SHORT).show() return@launch } showPlayerDialog(body.recordingId, prefs.apiToken.orEmpty()) } } private fun showPlayerDialog(recordingId: Long, token: String) { mediaPlayer?.release() val url = ApiConfig.recordingStreamUrl(recordingId, token) val dialog = AlertDialog.Builder(requireContext()) .setTitle("Playing recording…") .setMessage("Buffering…") .setNegativeButton("Stop") { d, _ -> mediaPlayer?.release(); mediaPlayer = null; d.dismiss() } .setCancelable(false) .create() dialog.show() mediaPlayer = MediaPlayer().apply { setOnPreparedListener { dialog.setMessage("Playing…") start() } setOnCompletionListener { dialog.setMessage("Finished") release(); mediaPlayer = null } setOnErrorListener { _, _, _ -> dialog.dismiss() Toast.makeText(context, "Couldn't play the recording", Toast.LENGTH_SHORT).show() true } try { setDataSource(url) prepareAsync() } catch (_: Exception) { dialog.dismiss() Toast.makeText(context, "Couldn't play the recording", Toast.LENGTH_SHORT).show() } } } override fun onDestroyView() { mediaPlayer?.release(); mediaPlayer = null super.onDestroyView() } }