NotificationsActivity.kt 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. package im.angry.openeuicc.ui
  2. import android.annotation.SuppressLint
  3. import android.os.Bundle
  4. import android.text.Html
  5. import android.view.ContextMenu
  6. import android.view.LayoutInflater
  7. import android.view.Menu
  8. import android.view.MenuItem
  9. import android.view.MenuItem.OnMenuItemClickListener
  10. import android.view.View
  11. import android.view.ViewGroup
  12. import android.widget.TextView
  13. import androidx.activity.enableEdgeToEdge
  14. import androidx.appcompat.app.AlertDialog
  15. import androidx.core.view.forEach
  16. import androidx.lifecycle.lifecycleScope
  17. import androidx.recyclerview.widget.DividerItemDecoration
  18. import androidx.recyclerview.widget.LinearLayoutManager
  19. import androidx.recyclerview.widget.RecyclerView
  20. import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
  21. import im.angry.openeuicc.common.R
  22. import im.angry.openeuicc.core.EuiccChannelManager
  23. import im.angry.openeuicc.util.*
  24. import kotlinx.coroutines.Dispatchers
  25. import kotlinx.coroutines.launch
  26. import kotlinx.coroutines.withContext
  27. import net.typeblog.lpac_jni.LocalProfileNotification
  28. class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
  29. private lateinit var swipeRefresh: SwipeRefreshLayout
  30. private lateinit var notificationList: RecyclerView
  31. private val notificationAdapter = NotificationAdapter()
  32. private var logicalSlotId = -1
  33. override fun onCreate(savedInstanceState: Bundle?) {
  34. enableEdgeToEdge()
  35. super.onCreate(savedInstanceState)
  36. setContentView(R.layout.activity_notifications)
  37. setSupportActionBar(requireViewById(R.id.toolbar))
  38. setupToolbarInsets()
  39. supportActionBar!!.setDisplayHomeAsUpEnabled(true)
  40. swipeRefresh = requireViewById(R.id.swipe_refresh)
  41. notificationList = requireViewById(R.id.recycler_view)
  42. setupRootViewInsets(notificationList)
  43. }
  44. override fun onInit() {
  45. notificationList.layoutManager =
  46. LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
  47. notificationList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
  48. notificationList.adapter = notificationAdapter
  49. registerForContextMenu(notificationList)
  50. logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
  51. // This is slightly different from the MainActivity logic
  52. // due to the length (we don't want to display the full USB product name)
  53. val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
  54. getString(R.string.usb)
  55. } else {
  56. getString(R.string.channel_name_format, logicalSlotId)
  57. }
  58. title = getString(R.string.profile_notifications_detailed_format, channelTitle)
  59. swipeRefresh.setOnRefreshListener {
  60. refresh()
  61. }
  62. refresh()
  63. }
  64. override fun onCreateOptionsMenu(menu: Menu?): Boolean {
  65. super.onCreateOptionsMenu(menu)
  66. menuInflater.inflate(R.menu.activity_notifications, menu)
  67. return true
  68. }
  69. override fun onOptionsItemSelected(item: MenuItem): Boolean =
  70. when (item.itemId) {
  71. android.R.id.home -> {
  72. finish()
  73. true
  74. }
  75. R.id.help -> {
  76. AlertDialog.Builder(this, R.style.AlertDialogTheme).apply {
  77. setMessage(R.string.profile_notifications_help)
  78. setPositiveButton(android.R.string.ok) { dialog, _ ->
  79. dialog.dismiss()
  80. }
  81. show()
  82. }
  83. true
  84. }
  85. else -> super.onOptionsItemSelected(item)
  86. }
  87. private fun launchTask(task: suspend () -> Unit) {
  88. swipeRefresh.isRefreshing = true
  89. lifecycleScope.launch {
  90. withContext(Dispatchers.IO) {
  91. euiccChannelManagerLoaded.await()
  92. }
  93. task()
  94. swipeRefresh.isRefreshing = false
  95. }
  96. }
  97. private fun refresh() {
  98. launchTask {
  99. notificationAdapter.notifications =
  100. euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
  101. val profiles = channel.lpa.profiles
  102. channel.lpa.notifications.map {
  103. val profile = profiles.find { p -> p.iccid == it.iccid }
  104. LocalProfileNotificationWrapper(it, profile?.displayName ?: "???")
  105. }
  106. }
  107. }
  108. }
  109. data class LocalProfileNotificationWrapper(
  110. val inner: LocalProfileNotification,
  111. val profileName: String
  112. )
  113. @SuppressLint("ClickableViewAccessibility")
  114. inner class NotificationViewHolder(private val root: View):
  115. RecyclerView.ViewHolder(root), View.OnCreateContextMenuListener, OnMenuItemClickListener {
  116. private val address: TextView = root.requireViewById(R.id.notification_address)
  117. private val profileName: TextView = root.requireViewById(R.id.notification_profile_name)
  118. private lateinit var notification: LocalProfileNotificationWrapper
  119. private var lastTouchX = 0f
  120. private var lastTouchY = 0f
  121. init {
  122. root.isClickable = true
  123. root.setOnCreateContextMenuListener(this)
  124. root.setOnTouchListener { _, event ->
  125. lastTouchX = event.x
  126. lastTouchY = event.y
  127. false
  128. }
  129. root.setOnLongClickListener {
  130. root.showContextMenu(lastTouchX, lastTouchY)
  131. true
  132. }
  133. }
  134. private fun operationToLocalizedText(operation: LocalProfileNotification.Operation) =
  135. root.context.getText(
  136. when (operation) {
  137. LocalProfileNotification.Operation.Install -> R.string.profile_notification_operation_download
  138. LocalProfileNotification.Operation.Delete -> R.string.profile_notification_operation_delete
  139. LocalProfileNotification.Operation.Enable -> R.string.profile_notification_operation_enable
  140. LocalProfileNotification.Operation.Disable -> R.string.profile_notification_operation_disable
  141. })
  142. fun updateNotification(value: LocalProfileNotificationWrapper) {
  143. notification = value
  144. address.text = value.inner.notificationAddress
  145. profileName.text = Html.fromHtml(
  146. root.context.getString(R.string.profile_notification_name_format,
  147. operationToLocalizedText(value.inner.profileManagementOperation),
  148. value.profileName, value.inner.iccid),
  149. Html.FROM_HTML_MODE_COMPACT)
  150. }
  151. override fun onCreateContextMenu(
  152. menu: ContextMenu?,
  153. v: View?,
  154. menuInfo: ContextMenu.ContextMenuInfo?
  155. ) {
  156. menuInflater.inflate(R.menu.notification_options, menu)
  157. menu!!.forEach {
  158. it.setOnMenuItemClickListener(this)
  159. }
  160. }
  161. override fun onMenuItemClick(item: MenuItem): Boolean =
  162. when (item.itemId) {
  163. R.id.notification_process -> {
  164. launchTask {
  165. withContext(Dispatchers.IO) {
  166. euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
  167. channel.lpa.handleNotification(notification.inner.seqNumber)
  168. }
  169. }
  170. refresh()
  171. }
  172. true
  173. }
  174. R.id.notification_delete -> {
  175. launchTask {
  176. withContext(Dispatchers.IO) {
  177. euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
  178. channel.lpa.deleteNotification(notification.inner.seqNumber)
  179. }
  180. }
  181. refresh()
  182. }
  183. true
  184. }
  185. else -> false
  186. }
  187. }
  188. inner class NotificationAdapter: RecyclerView.Adapter<NotificationViewHolder>() {
  189. var notifications: List<LocalProfileNotificationWrapper> = listOf()
  190. @SuppressLint("NotifyDataSetChanged")
  191. set(value) {
  192. field = value
  193. notifyDataSetChanged()
  194. }
  195. override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationViewHolder {
  196. val root = LayoutInflater.from(parent.context)
  197. .inflate(R.layout.notification_item, parent, false)
  198. return NotificationViewHolder(root)
  199. }
  200. override fun getItemCount(): Int = notifications.size
  201. override fun onBindViewHolder(holder: NotificationViewHolder, position: Int) =
  202. holder.updateNotification(notifications[position])
  203. }
  204. }