| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- package im.angry.openeuicc.ui
- import android.annotation.SuppressLint
- import android.app.Dialog
- import android.content.DialogInterface
- import android.graphics.BitmapFactory
- import android.os.Bundle
- import android.text.Editable
- import android.util.Log
- import android.view.*
- import android.widget.ProgressBar
- import android.widget.TextView
- import android.widget.Toast
- import androidx.activity.result.contract.ActivityResultContracts
- import androidx.appcompat.app.AlertDialog
- import androidx.appcompat.widget.Toolbar
- import androidx.lifecycle.lifecycleScope
- import com.google.android.material.textfield.TextInputLayout
- import com.journeyapps.barcodescanner.ScanContract
- import com.journeyapps.barcodescanner.ScanOptions
- import im.angry.openeuicc.common.R
- import im.angry.openeuicc.service.EuiccChannelManagerService
- import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
- import im.angry.openeuicc.util.*
- import kotlinx.coroutines.Dispatchers
- import kotlinx.coroutines.flow.onEach
- import kotlinx.coroutines.launch
- import kotlinx.coroutines.withContext
- class ProfileDownloadFragment : BaseMaterialDialogFragment(),
- Toolbar.OnMenuItemClickListener, EuiccChannelFragmentMarker {
- companion object {
- const val TAG = "ProfileDownloadFragment"
- const val LOW_NVRAM_THRESHOLD = 30 * 1024 // < 30 KiB, the alert may fail
- fun newInstance(slotId: Int, portId: Int, finishWhenDone: Boolean = false): ProfileDownloadFragment =
- newInstanceEuicc(ProfileDownloadFragment::class.java, slotId, portId) {
- putBoolean("finishWhenDone", finishWhenDone)
- }
- }
- private lateinit var toolbar: Toolbar
- private lateinit var profileDownloadServer: TextInputLayout
- private lateinit var profileDownloadCode: TextInputLayout
- private lateinit var profileDownloadConfirmationCode: TextInputLayout
- private lateinit var profileDownloadIMEI: TextInputLayout
- private lateinit var profileDownloadFreeSpace: TextView
- private lateinit var progress: ProgressBar
- private var freeNvram: Int = -1
- private var downloading = false
- private val finishWhenDone by lazy {
- requireArguments().getBoolean("finishWhenDone", false)
- }
- private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result ->
- result.contents?.let { content ->
- onScanResult(content)
- }
- }
- private val gallerySelectorLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { result ->
- if (result == null) return@registerForActivityResult
- lifecycleScope.launch(Dispatchers.IO) {
- runCatching {
- requireContext().contentResolver.openInputStream(result)?.let { input ->
- val bmp = BitmapFactory.decodeStream(input)
- input.close()
- decodeQrFromBitmap(bmp)?.let {
- withContext(Dispatchers.Main) {
- onScanResult(it)
- }
- }
- bmp.recycle()
- }
- }
- }
- }
- private fun onScanResult(result: String) {
- val components = result.split("$")
- if (components.size < 3 || components[0] != "LPA:1") return
- profileDownloadServer.editText?.setText(components[1])
- profileDownloadCode.editText?.setText(components[2])
- }
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- val view = inflater.inflate(R.layout.fragment_profile_download, container, false)
- toolbar = view.requireViewById(R.id.toolbar)
- profileDownloadServer = view.requireViewById(R.id.profile_download_server)
- profileDownloadCode = view.requireViewById(R.id.profile_download_code)
- profileDownloadConfirmationCode = view.requireViewById(R.id.profile_download_confirmation_code)
- profileDownloadIMEI = view.requireViewById(R.id.profile_download_imei)
- profileDownloadFreeSpace = view.requireViewById(R.id.profile_download_free_space)
- progress = view.requireViewById(R.id.progress)
- toolbar.inflateMenu(R.menu.fragment_profile_download)
- return view
- }
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- toolbar.apply {
- setTitle(R.string.profile_download)
- setNavigationOnClickListener {
- if (!downloading) {
- dismiss()
- }
- }
- setOnMenuItemClickListener(this@ProfileDownloadFragment)
- }
- }
- override fun onMenuItemClick(item: MenuItem): Boolean = downloading ||
- when (item.itemId) {
- R.id.scan -> {
- barcodeScannerLauncher.launch(ScanOptions().apply {
- setDesiredBarcodeFormats(ScanOptions.QR_CODE)
- setOrientationLocked(false)
- })
- true
- }
- R.id.scan_from_gallery -> {
- gallerySelectorLauncher.launch("image/*")
- true
- }
- R.id.ok -> {
- if (freeNvram > LOW_NVRAM_THRESHOLD) {
- startDownloadProfile()
- } else {
- AlertDialog.Builder(requireContext()).apply {
- setTitle(R.string.profile_download_low_nvram_title)
- setMessage(R.string.profile_download_low_nvram_message)
- setIcon(android.R.drawable.ic_dialog_alert)
- setCancelable(true)
- setPositiveButton(android.R.string.ok) { _, _ ->
- startDownloadProfile()
- }
- setNegativeButton(android.R.string.cancel, null)
- show()
- }
- }
- true
- }
- else -> false
- }
- override fun onResume() {
- super.onResume()
- setWidthPercent(95)
- }
- @SuppressLint("MissingPermission")
- override fun onStart() {
- super.onStart()
- lifecycleScope.launch(Dispatchers.IO) {
- ensureEuiccChannelManager()
- if (euiccChannelManagerService.isForegroundTaskRunning) {
- withContext(Dispatchers.Main) {
- dismiss()
- }
- return@launch
- }
- withEuiccChannel { channel ->
- val imei = try {
- telephonyManager.getImei(channel.logicalSlotId) ?: ""
- } catch (e: Exception) {
- ""
- }
- // Fetch remaining NVRAM
- val str = channel.lpa.euiccInfo2?.freeNvram?.also {
- freeNvram = it
- }?.let { formatFreeSpace(it) }
- withContext(Dispatchers.Main) {
- profileDownloadFreeSpace.text = getString(
- R.string.profile_download_free_space,
- str ?: getText(R.string.unknown)
- )
- profileDownloadIMEI.editText!!.text =
- Editable.Factory.getInstance().newEditable(imei)
- }
- }
- }
- }
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- return super.onCreateDialog(savedInstanceState).also {
- it.setCanceledOnTouchOutside(false)
- }
- }
- private fun startDownloadProfile() {
- val server = profileDownloadServer.editText!!.let {
- it.text.toString().trim().apply {
- if (isEmpty()) {
- it.requestFocus()
- return@startDownloadProfile
- }
- }
- }
- val code = profileDownloadCode.editText!!.text.toString().trim()
- .ifBlank { null }
- val confirmationCode = profileDownloadConfirmationCode.editText!!.text.toString().trim()
- .ifBlank { null }
- val imei = profileDownloadIMEI.editText!!.text.toString().trim()
- .ifBlank { null }
- downloading = true
- profileDownloadServer.editText!!.isEnabled = false
- profileDownloadCode.editText!!.isEnabled = false
- profileDownloadConfirmationCode.editText!!.isEnabled = false
- profileDownloadIMEI.editText!!.isEnabled = false
- progress.isIndeterminate = true
- progress.visibility = View.VISIBLE
- lifecycleScope.launch {
- ensureEuiccChannelManager()
- euiccChannelManagerService.waitForForegroundTask()
- val err = doDownloadProfile(server, code, confirmationCode, imei)
- if (err != null) {
- Log.d(TAG, "Error downloading profile")
- Log.d(TAG, Log.getStackTraceString(err))
- Toast.makeText(requireContext(), R.string.profile_download_failed, Toast.LENGTH_LONG).show()
- }
- if (parentFragment is EuiccProfilesChangedListener) {
- (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
- }
- try {
- dismiss()
- } catch (e: IllegalStateException) {
- // Ignored
- }
- }
- }
- private suspend fun doDownloadProfile(
- server: String,
- code: String?,
- confirmationCode: String?,
- imei: String?
- ) = withContext(Dispatchers.Main) {
- // The service is responsible for launching the actual blocking part on the IO context
- // On our side, we need the Main context because of the UI updates
- euiccChannelManagerService.launchProfileDownloadTask(
- slotId,
- portId,
- server,
- code,
- confirmationCode,
- imei
- ).onEach {
- if (it is EuiccChannelManagerService.ForegroundTaskState.InProgress) {
- progress.progress = it.progress
- progress.isIndeterminate = it.progress == 0
- } else {
- progress.progress = 100
- progress.isIndeterminate = false
- }
- }.waitDone()
- }
- override fun onDismiss(dialog: DialogInterface) {
- super.onDismiss(dialog)
- if (finishWhenDone) {
- activity?.finish()
- }
- }
- override fun onCancel(dialog: DialogInterface) {
- super.onCancel(dialog)
- if (finishWhenDone) {
- activity?.finish()
- }
- }
- }
|