MainActivity.kt 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. package im.angry.openeuicc.ui
  2. import android.annotation.SuppressLint
  3. import android.content.BroadcastReceiver
  4. import android.content.Context
  5. import android.content.Intent
  6. import android.content.IntentFilter
  7. import android.content.pm.PackageManager
  8. import android.hardware.usb.UsbManager
  9. import android.os.Build
  10. import android.os.Bundle
  11. import android.telephony.TelephonyManager
  12. import android.util.Log
  13. import android.view.Menu
  14. import android.view.MenuItem
  15. import android.view.View
  16. import android.widget.ProgressBar
  17. import androidx.activity.enableEdgeToEdge
  18. import androidx.fragment.app.Fragment
  19. import androidx.lifecycle.lifecycleScope
  20. import androidx.viewpager2.adapter.FragmentStateAdapter
  21. import androidx.viewpager2.widget.ViewPager2
  22. import com.google.android.material.tabs.TabLayout
  23. import com.google.android.material.tabs.TabLayoutMediator
  24. import im.angry.openeuicc.common.R
  25. import im.angry.openeuicc.core.EuiccChannelManager
  26. import im.angry.openeuicc.util.*
  27. import kotlinx.coroutines.Dispatchers
  28. import kotlinx.coroutines.flow.collect
  29. import kotlinx.coroutines.flow.first
  30. import kotlinx.coroutines.flow.onEach
  31. import kotlinx.coroutines.launch
  32. import kotlinx.coroutines.withContext
  33. @SuppressLint("NotifyDataSetChanged")
  34. open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
  35. companion object {
  36. const val TAG = "MainActivity"
  37. const val PERMISSION_REQUEST_CODE = 1000
  38. }
  39. private lateinit var loadingProgress: ProgressBar
  40. private lateinit var tabs: TabLayout
  41. private lateinit var viewPager: ViewPager2
  42. private var refreshing = false
  43. private data class Page(
  44. val logicalSlotId: Int,
  45. val title: String,
  46. val createFragment: () -> Fragment
  47. )
  48. private val pages: MutableList<Page> = mutableListOf()
  49. private val pagerAdapter by lazy {
  50. object : FragmentStateAdapter(this) {
  51. override fun getItemCount() = pages.size
  52. override fun createFragment(position: Int): Fragment = pages[position].createFragment()
  53. }
  54. }
  55. protected lateinit var tm: TelephonyManager
  56. private val usbReceiver = object : BroadcastReceiver() {
  57. override fun onReceive(context: Context?, intent: Intent?) {
  58. if (intent?.action == UsbManager.ACTION_USB_DEVICE_ATTACHED || intent?.action == UsbManager.ACTION_USB_DEVICE_DETACHED) {
  59. refresh(true)
  60. }
  61. }
  62. }
  63. @SuppressLint("WrongConstant", "UnspecifiedRegisterReceiverFlag")
  64. override fun onCreate(savedInstanceState: Bundle?) {
  65. enableEdgeToEdge()
  66. super.onCreate(savedInstanceState)
  67. setContentView(R.layout.activity_main)
  68. setSupportActionBar(requireViewById(R.id.toolbar))
  69. setupToolbarInsets()
  70. loadingProgress = requireViewById(R.id.loading)
  71. tabs = requireViewById(R.id.main_tabs)
  72. viewPager = requireViewById(R.id.view_pager)
  73. viewPager.adapter = pagerAdapter
  74. TabLayoutMediator(tabs, viewPager) { tab, pos ->
  75. tab.text = pages[pos].title
  76. }.attach()
  77. tm = telephonyManager
  78. registerReceiver(usbReceiver, IntentFilter().apply {
  79. addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
  80. addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
  81. })
  82. }
  83. override fun onDestroy() {
  84. super.onDestroy()
  85. unregisterReceiver(usbReceiver)
  86. }
  87. override fun onCreateOptionsMenu(menu: Menu): Boolean {
  88. menuInflater.inflate(R.menu.activity_main, menu)
  89. return true
  90. }
  91. override fun onOptionsItemSelected(item: MenuItem): Boolean =
  92. when (item.itemId) {
  93. R.id.settings -> {
  94. startActivity(Intent(this, SettingsActivity::class.java))
  95. true
  96. }
  97. R.id.reload -> {
  98. refresh()
  99. true
  100. }
  101. else -> super.onOptionsItemSelected(item)
  102. }
  103. override fun onInit() {
  104. lifecycleScope.launch {
  105. init()
  106. }
  107. }
  108. private fun ensureNotificationPermissions() {
  109. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
  110. val permissions = arrayOf(android.Manifest.permission.POST_NOTIFICATIONS)
  111. if (permissions.all { checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED }) return
  112. requestPermissions(permissions, PERMISSION_REQUEST_CODE)
  113. }
  114. private suspend fun init(fromUsbEvent: Boolean = false) {
  115. refreshing = true // We don't check this here -- the check happens in refresh()
  116. loadingProgress.visibility = View.VISIBLE
  117. viewPager.visibility = View.GONE
  118. tabs.visibility = View.GONE
  119. // Prevent concurrent access with any running foreground task
  120. euiccChannelManagerService.waitForForegroundTask()
  121. val (usbDevice, _) = withContext(Dispatchers.IO) {
  122. euiccChannelManager.tryOpenUsbEuiccChannel()
  123. }
  124. val newPages: MutableList<Page> = mutableListOf()
  125. euiccChannelManager.flowInternalEuiccPorts().onEach { (slotId, portId) ->
  126. Log.d(TAG, "slot $slotId port $portId")
  127. euiccChannelManager.flowEuiccSecureElements(slotId, portId).onEach { seId ->
  128. euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
  129. if (preferenceRepository.verboseLoggingFlow.first()) {
  130. Log.d(TAG, channel.lpa.eID)
  131. }
  132. // Request the system to refresh the list of profiles every time we start
  133. // Note that this is currently supposed to be no-op when unprivileged,
  134. // but it could change in the future
  135. euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
  136. val channelName =
  137. appContainer.customizableTextProvider.formatNonUsbChannelName(channel.logicalSlotId)
  138. newPages.add(Page(channel.logicalSlotId, channelName) {
  139. appContainer.uiComponentFactory.createEuiccManagementFragment(
  140. slotId,
  141. portId,
  142. seId
  143. )
  144. })
  145. }
  146. }.collect()
  147. }.collect()
  148. // If USB readers exist, add them at the very last
  149. // We use a wrapper fragment to handle logic specific to USB readers
  150. usbDevice?.let {
  151. val productName = it.productName ?: getString(R.string.channel_type_usb)
  152. newPages.add(Page(EuiccChannelManager.USB_CHANNEL_ID, productName) {
  153. UsbCcidReaderFragment()
  154. })
  155. }
  156. viewPager.visibility = View.VISIBLE
  157. if (newPages.size > 1) {
  158. tabs.visibility = View.VISIBLE
  159. } else if (newPages.isEmpty()) {
  160. newPages.add(Page(-1, "") {
  161. appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment()
  162. })
  163. }
  164. newPages.sortBy { it.logicalSlotId }
  165. pages.clear()
  166. pages.addAll(newPages)
  167. loadingProgress.visibility = View.GONE
  168. pagerAdapter.notifyDataSetChanged()
  169. // Reset the adapter so that the current view actually gets cleared
  170. // notifyDataSetChanged() doesn't cause the current view to be removed.
  171. viewPager.adapter = pagerAdapter
  172. if (fromUsbEvent && usbDevice != null) {
  173. // If this refresh was triggered by a USB insertion while active, scroll to that page
  174. viewPager.post {
  175. viewPager.setCurrentItem(pages.size - 1, true)
  176. }
  177. } else {
  178. viewPager.currentItem = 0
  179. }
  180. if (pages.isNotEmpty()) {
  181. ensureNotificationPermissions()
  182. }
  183. refreshing = false
  184. }
  185. private fun refresh(fromUsbEvent: Boolean = false) {
  186. if (refreshing) return
  187. lifecycleScope.launch {
  188. refreshing = true
  189. loadingProgress.visibility = View.VISIBLE
  190. viewPager.visibility = View.GONE
  191. tabs.visibility = View.GONE
  192. pages.clear()
  193. pagerAdapter.notifyDataSetChanged()
  194. viewPager.adapter = pagerAdapter
  195. init(fromUsbEvent) // will set refreshing = false
  196. }
  197. }
  198. }