ProfileDownloadFragment.kt 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. package im.angry.openeuicc.ui
  2. import android.annotation.SuppressLint
  3. import android.app.Dialog
  4. import android.content.DialogInterface
  5. import android.graphics.BitmapFactory
  6. import android.os.Bundle
  7. import android.text.Editable
  8. import android.util.Log
  9. import android.view.*
  10. import android.widget.ProgressBar
  11. import android.widget.TextView
  12. import android.widget.Toast
  13. import androidx.activity.result.contract.ActivityResultContracts
  14. import androidx.appcompat.app.AlertDialog
  15. import androidx.appcompat.widget.Toolbar
  16. import androidx.lifecycle.lifecycleScope
  17. import com.google.android.material.textfield.TextInputLayout
  18. import com.journeyapps.barcodescanner.ScanContract
  19. import com.journeyapps.barcodescanner.ScanOptions
  20. import im.angry.openeuicc.common.R
  21. import im.angry.openeuicc.service.EuiccChannelManagerService
  22. import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
  23. import im.angry.openeuicc.util.*
  24. import kotlinx.coroutines.Dispatchers
  25. import kotlinx.coroutines.flow.onEach
  26. import kotlinx.coroutines.launch
  27. import kotlinx.coroutines.withContext
  28. class ProfileDownloadFragment : BaseMaterialDialogFragment(),
  29. Toolbar.OnMenuItemClickListener, EuiccChannelFragmentMarker {
  30. companion object {
  31. const val TAG = "ProfileDownloadFragment"
  32. const val LOW_NVRAM_THRESHOLD = 30 * 1024 // < 30 KiB, the alert may fail
  33. fun newInstance(slotId: Int, portId: Int, finishWhenDone: Boolean = false): ProfileDownloadFragment =
  34. newInstanceEuicc(ProfileDownloadFragment::class.java, slotId, portId) {
  35. putBoolean("finishWhenDone", finishWhenDone)
  36. }
  37. }
  38. private lateinit var toolbar: Toolbar
  39. private lateinit var profileDownloadServer: TextInputLayout
  40. private lateinit var profileDownloadCode: TextInputLayout
  41. private lateinit var profileDownloadConfirmationCode: TextInputLayout
  42. private lateinit var profileDownloadIMEI: TextInputLayout
  43. private lateinit var profileDownloadFreeSpace: TextView
  44. private lateinit var progress: ProgressBar
  45. private var freeNvram: Int = -1
  46. private var downloading = false
  47. private val finishWhenDone by lazy {
  48. requireArguments().getBoolean("finishWhenDone", false)
  49. }
  50. private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result ->
  51. result.contents?.let { content ->
  52. onScanResult(content)
  53. }
  54. }
  55. private val gallerySelectorLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { result ->
  56. if (result == null) return@registerForActivityResult
  57. lifecycleScope.launch(Dispatchers.IO) {
  58. runCatching {
  59. requireContext().contentResolver.openInputStream(result)?.let { input ->
  60. val bmp = BitmapFactory.decodeStream(input)
  61. input.close()
  62. decodeQrFromBitmap(bmp)?.let {
  63. withContext(Dispatchers.Main) {
  64. onScanResult(it)
  65. }
  66. }
  67. bmp.recycle()
  68. }
  69. }
  70. }
  71. }
  72. private fun onScanResult(result: String) {
  73. val components = result.split("$")
  74. if (components.size < 3 || components[0] != "LPA:1") return
  75. profileDownloadServer.editText?.setText(components[1])
  76. profileDownloadCode.editText?.setText(components[2])
  77. }
  78. override fun onCreateView(
  79. inflater: LayoutInflater,
  80. container: ViewGroup?,
  81. savedInstanceState: Bundle?
  82. ): View {
  83. val view = inflater.inflate(R.layout.fragment_profile_download, container, false)
  84. toolbar = view.requireViewById(R.id.toolbar)
  85. profileDownloadServer = view.requireViewById(R.id.profile_download_server)
  86. profileDownloadCode = view.requireViewById(R.id.profile_download_code)
  87. profileDownloadConfirmationCode = view.requireViewById(R.id.profile_download_confirmation_code)
  88. profileDownloadIMEI = view.requireViewById(R.id.profile_download_imei)
  89. profileDownloadFreeSpace = view.requireViewById(R.id.profile_download_free_space)
  90. progress = view.requireViewById(R.id.progress)
  91. toolbar.inflateMenu(R.menu.fragment_profile_download)
  92. return view
  93. }
  94. override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  95. super.onViewCreated(view, savedInstanceState)
  96. toolbar.apply {
  97. setTitle(R.string.profile_download)
  98. setNavigationOnClickListener {
  99. if (!downloading) {
  100. dismiss()
  101. }
  102. }
  103. setOnMenuItemClickListener(this@ProfileDownloadFragment)
  104. }
  105. }
  106. override fun onMenuItemClick(item: MenuItem): Boolean = downloading ||
  107. when (item.itemId) {
  108. R.id.scan -> {
  109. barcodeScannerLauncher.launch(ScanOptions().apply {
  110. setDesiredBarcodeFormats(ScanOptions.QR_CODE)
  111. setOrientationLocked(false)
  112. })
  113. true
  114. }
  115. R.id.scan_from_gallery -> {
  116. gallerySelectorLauncher.launch("image/*")
  117. true
  118. }
  119. R.id.ok -> {
  120. if (freeNvram > LOW_NVRAM_THRESHOLD) {
  121. startDownloadProfile()
  122. } else {
  123. AlertDialog.Builder(requireContext()).apply {
  124. setTitle(R.string.profile_download_low_nvram_title)
  125. setMessage(R.string.profile_download_low_nvram_message)
  126. setIcon(android.R.drawable.ic_dialog_alert)
  127. setCancelable(true)
  128. setPositiveButton(android.R.string.ok) { _, _ ->
  129. startDownloadProfile()
  130. }
  131. setNegativeButton(android.R.string.cancel, null)
  132. show()
  133. }
  134. }
  135. true
  136. }
  137. else -> false
  138. }
  139. override fun onResume() {
  140. super.onResume()
  141. setWidthPercent(95)
  142. }
  143. @SuppressLint("MissingPermission")
  144. override fun onStart() {
  145. super.onStart()
  146. lifecycleScope.launch(Dispatchers.IO) {
  147. ensureEuiccChannelManager()
  148. if (euiccChannelManagerService.isForegroundTaskRunning) {
  149. withContext(Dispatchers.Main) {
  150. dismiss()
  151. }
  152. return@launch
  153. }
  154. withEuiccChannel { channel ->
  155. val imei = try {
  156. telephonyManager.getImei(channel.logicalSlotId) ?: ""
  157. } catch (e: Exception) {
  158. ""
  159. }
  160. // Fetch remaining NVRAM
  161. val str = channel.lpa.euiccInfo2?.freeNvram?.also {
  162. freeNvram = it
  163. }?.let { formatFreeSpace(it) }
  164. withContext(Dispatchers.Main) {
  165. profileDownloadFreeSpace.text = getString(
  166. R.string.profile_download_free_space,
  167. str ?: getText(R.string.unknown)
  168. )
  169. profileDownloadIMEI.editText!!.text =
  170. Editable.Factory.getInstance().newEditable(imei)
  171. }
  172. }
  173. }
  174. }
  175. override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
  176. return super.onCreateDialog(savedInstanceState).also {
  177. it.setCanceledOnTouchOutside(false)
  178. }
  179. }
  180. private fun startDownloadProfile() {
  181. val server = profileDownloadServer.editText!!.let {
  182. it.text.toString().trim().apply {
  183. if (isEmpty()) {
  184. it.requestFocus()
  185. return@startDownloadProfile
  186. }
  187. }
  188. }
  189. val code = profileDownloadCode.editText!!.text.toString().trim()
  190. .ifBlank { null }
  191. val confirmationCode = profileDownloadConfirmationCode.editText!!.text.toString().trim()
  192. .ifBlank { null }
  193. val imei = profileDownloadIMEI.editText!!.text.toString().trim()
  194. .ifBlank { null }
  195. downloading = true
  196. profileDownloadServer.editText!!.isEnabled = false
  197. profileDownloadCode.editText!!.isEnabled = false
  198. profileDownloadConfirmationCode.editText!!.isEnabled = false
  199. profileDownloadIMEI.editText!!.isEnabled = false
  200. progress.isIndeterminate = true
  201. progress.visibility = View.VISIBLE
  202. lifecycleScope.launch {
  203. ensureEuiccChannelManager()
  204. euiccChannelManagerService.waitForForegroundTask()
  205. val err = doDownloadProfile(server, code, confirmationCode, imei)
  206. if (err != null) {
  207. Log.d(TAG, "Error downloading profile")
  208. Log.d(TAG, Log.getStackTraceString(err))
  209. Toast.makeText(requireContext(), R.string.profile_download_failed, Toast.LENGTH_LONG).show()
  210. }
  211. if (parentFragment is EuiccProfilesChangedListener) {
  212. (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
  213. }
  214. try {
  215. dismiss()
  216. } catch (e: IllegalStateException) {
  217. // Ignored
  218. }
  219. }
  220. }
  221. private suspend fun doDownloadProfile(
  222. server: String,
  223. code: String?,
  224. confirmationCode: String?,
  225. imei: String?
  226. ) = withContext(Dispatchers.Main) {
  227. // The service is responsible for launching the actual blocking part on the IO context
  228. // On our side, we need the Main context because of the UI updates
  229. euiccChannelManagerService.launchProfileDownloadTask(
  230. slotId,
  231. portId,
  232. server,
  233. code,
  234. confirmationCode,
  235. imei
  236. ).onEach {
  237. if (it is EuiccChannelManagerService.ForegroundTaskState.InProgress) {
  238. progress.progress = it.progress
  239. progress.isIndeterminate = it.progress == 0
  240. } else {
  241. progress.progress = 100
  242. progress.isIndeterminate = false
  243. }
  244. }.waitDone()
  245. }
  246. override fun onDismiss(dialog: DialogInterface) {
  247. super.onDismiss(dialog)
  248. if (finishWhenDone) {
  249. activity?.finish()
  250. }
  251. }
  252. override fun onCancel(dialog: DialogInterface) {
  253. super.onCancel(dialog)
  254. if (finishWhenDone) {
  255. activity?.finish()
  256. }
  257. }
  258. }