diff --git a/.idea/misc.xml b/.idea/misc.xml index 9c6b75e..67135cd 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,10 +3,13 @@ diff --git a/app/build.gradle b/app/build.gradle index e6264f9..996e927 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,6 +44,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.cardview:cardview:1.0.0" + implementation 'com.journeyapps:zxing-android-embedded:4.3.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7e0fdf2..cc80baf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ + + \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/OpenEUICCApplication.kt b/app/src/main/java/im/angry/openeuicc/OpenEUICCApplication.kt new file mode 100644 index 0000000..bdd6520 --- /dev/null +++ b/app/src/main/java/im/angry/openeuicc/OpenEUICCApplication.kt @@ -0,0 +1,8 @@ +package im.angry.openeuicc + +import android.app.Application +import im.angry.openeuicc.core.EuiccChannelRepositoryProxy + +class OpenEUICCApplication : Application() { + val euiccChannelRepo = EuiccChannelRepositoryProxy(this) +} \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/ui/EuiccChannelFragmentUtils.kt b/app/src/main/java/im/angry/openeuicc/ui/EuiccChannelFragmentUtils.kt new file mode 100644 index 0000000..4aaa49a --- /dev/null +++ b/app/src/main/java/im/angry/openeuicc/ui/EuiccChannelFragmentUtils.kt @@ -0,0 +1,23 @@ +package im.angry.openeuicc.ui + +import android.os.Bundle +import androidx.fragment.app.Fragment +import im.angry.openeuicc.OpenEUICCApplication +import im.angry.openeuicc.core.EuiccChannel + +interface EuiccFragmentMarker + +fun newInstanceEuicc(clazz: Class, slotId: Int): T where T: Fragment, T: EuiccFragmentMarker { + val instance = clazz.newInstance() + instance.arguments = Bundle().apply { + putInt("slotId", slotId) + } + return instance +} + +val T.slotId: Int where T: Fragment, T: EuiccFragmentMarker + get() = requireArguments().getInt("slotId") + +val T.channel: EuiccChannel where T: Fragment, T: EuiccFragmentMarker + get() = + (requireActivity().application as OpenEUICCApplication).euiccChannelRepo.availableChannels[slotId] \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index 4b17565..7b0a6b9 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -11,14 +11,18 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import im.angry.openeuicc.R -import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.databinding.EuiccProfileBinding import im.angry.openeuicc.databinding.FragmentEuiccBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class EuiccManagementFragment(private val channel: EuiccChannel) : Fragment() { +class EuiccManagementFragment : Fragment(), EuiccFragmentMarker { + companion object { + fun newInstance(slotId: Int): EuiccManagementFragment = + newInstanceEuicc(EuiccManagementFragment::class.java, slotId) + } + private var _binding: FragmentEuiccBinding? = null private val binding get() = _binding!! @@ -39,6 +43,11 @@ class EuiccManagementFragment(private val channel: EuiccChannel) : Fragment() { binding.profileList.adapter = adapter binding.profileList.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false) + + binding.fab.setOnClickListener { + ProfileDownloadFragment.newInstance(slotId) + .show(childFragmentManager, ProfileDownloadFragment.TAG) + } } override fun onStart() { diff --git a/app/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 8c9a305..5551219 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -9,8 +9,9 @@ import android.widget.ArrayAdapter import android.widget.Spinner import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope +import im.angry.openeuicc.OpenEUICCApplication import im.angry.openeuicc.R -import im.angry.openeuicc.core.EuiccChannelRepositoryProxy +import im.angry.openeuicc.core.EuiccChannelRepository import im.angry.openeuicc.databinding.ActivityMainBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -21,7 +22,7 @@ class MainActivity : AppCompatActivity() { const val TAG = "MainActivity" } - private val repo = EuiccChannelRepositoryProxy(this) + private lateinit var repo: EuiccChannelRepository private lateinit var spinnerAdapter: ArrayAdapter private lateinit var spinner: Spinner @@ -35,6 +36,8 @@ class MainActivity : AppCompatActivity() { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + repo = (application as OpenEUICCApplication).euiccChannelRepo + spinnerAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item) lifecycleScope.launch { @@ -75,9 +78,9 @@ class MainActivity : AppCompatActivity() { } withContext(Dispatchers.Main) { - repo.availableChannels.forEach { - spinnerAdapter.add(it.name) - fragments.add(EuiccManagementFragment(it)) + repo.availableChannels.forEachIndexed { idx, channel -> + spinnerAdapter.add(channel.name) + fragments.add(EuiccManagementFragment.newInstance(idx)) } if (fragments.isNotEmpty()) { diff --git a/app/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt new file mode 100644 index 0000000..bdd5fd2 --- /dev/null +++ b/app/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -0,0 +1,78 @@ +package im.angry.openeuicc.ui + +import android.app.Dialog +import android.os.Bundle +import android.view.* +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.DialogFragment +import com.journeyapps.barcodescanner.ScanContract +import com.journeyapps.barcodescanner.ScanOptions +import im.angry.openeuicc.R +import im.angry.openeuicc.databinding.FragmentProfileDownloadBinding +import im.angry.openeuicc.util.setWidthPercent + +class ProfileDownloadFragment : DialogFragment(), EuiccFragmentMarker, Toolbar.OnMenuItemClickListener { + companion object { + const val TAG = "ProfileDownloadFragment" + + fun newInstance(slotId: Int): ProfileDownloadFragment = + newInstanceEuicc(ProfileDownloadFragment::class.java, slotId) + } + + private var _binding: FragmentProfileDownloadBinding? = null + private val binding get() = _binding!! + + private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result -> + result.contents?.let { content -> + val components = content.split("$") + if (components.size != 3 || components[0] != "LPA:1") return@registerForActivityResult + binding.profileDownloadServer.editText?.setText(components[1]) + binding.profileDownloadCode.editText?.setText(components[2]) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentProfileDownloadBinding.inflate(inflater, container, false) + binding.toolbar.inflateMenu(R.menu.fragment_profile_download) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.toolbar.apply { + setTitle(R.string.profile_download) + setNavigationOnClickListener { + dismiss() + } + setOnMenuItemClickListener(this@ProfileDownloadFragment) + } + } + + override fun onMenuItemClick(item: MenuItem): Boolean = + when (item.itemId) { + R.id.scan -> { + barcodeScannerLauncher.launch(ScanOptions().apply { + setDesiredBarcodeFormats(ScanOptions.QR_CODE) + setOrientationLocked(false) + }) + true + } + else -> false + } + + override fun onResume() { + super.onResume() + setWidthPercent(95) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).also { + it.window?.requestFeature(Window.FEATURE_NO_TITLE) + it.setCanceledOnTouchOutside(false) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/util/UiUtils.kt b/app/src/main/java/im/angry/openeuicc/util/UiUtils.kt new file mode 100644 index 0000000..24c1ad8 --- /dev/null +++ b/app/src/main/java/im/angry/openeuicc/util/UiUtils.kt @@ -0,0 +1,28 @@ +package im.angry.openeuicc.util + +import android.content.res.Resources +import android.graphics.Rect +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment + +// Source: +/** + * Call this method (in onActivityCreated or later) to set + * the width of the dialog to a percentage of the current + * screen width. + */ +fun DialogFragment.setWidthPercent(percentage: Int) { + val percent = percentage.toFloat() / 100 + val dm = Resources.getSystem().displayMetrics + val rect = dm.run { Rect(0, 0, widthPixels, heightPixels) } + val percentWidth = rect.width() * percent + dialog?.window?.setLayout(percentWidth.toInt(), ViewGroup.LayoutParams.WRAP_CONTENT) +} + +/** + * Call this method (in onActivityCreated or later) + * to make the dialog near-full screen. + */ +fun DialogFragment.setFullScreen() { + dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) +} diff --git a/app/src/main/res/drawable/ic_scan_black.xml b/app/src/main/res/drawable/ic_scan_black.xml new file mode 100644 index 0000000..597e8d7 --- /dev/null +++ b/app/src/main/res/drawable/ic_scan_black.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_profile_download.xml b/app/src/main/res/layout/fragment_profile_download.xml new file mode 100644 index 0000000..dab6655 --- /dev/null +++ b/app/src/main/res/layout/fragment_profile_download.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/fragment_profile_download.xml b/app/src/main/res/menu/fragment_profile_download.xml new file mode 100644 index 0000000..6f52963 --- /dev/null +++ b/app/src/main/res/menu/fragment_profile_download.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dcc7169..0c6c866 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,4 +7,9 @@ Disabled Provider: ICCID: + + New eSIM + Server (RSP / SM-DP+) + Activation Code + Scan QR Code \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 7e1d6b0..747a861 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -9,8 +9,21 @@ @color/pink_600 @color/pink_800 @color/white + ?attr/colorSecondary ?attr/colorPrimaryVariant + + + + + + \ No newline at end of file