Compare commits

...

4 commits

Author SHA1 Message Date
efeaea2567 ProfileDownloadFragment: support finishing activity when done 2024-02-01 22:02:35 -05:00
fd332bcabe SlotSelectFragment: do not use constructor parameters 2024-02-01 21:57:14 -05:00
a55fbd2767 ui: Fi styling for all DialogFragments 2024-02-01 21:54:49 -05:00
c807588521 feat: Direct profile download interface with slot selection
This can be used by both the system LuiActivity and potentially in the
unprivileged version as a carrier interface as well.
2024-02-01 21:43:31 -05:00
13 changed files with 203 additions and 8 deletions

View file

@ -16,6 +16,11 @@
android:name="im.angry.openeuicc.ui.NotificationsActivity"
android:label="@string/profile_notifications" />
<activity
android:name="im.angry.openeuicc.ui.DirectProfileDownloadActivity"
android:label="@string/profile_download"
android:theme="@style/Theme.AppCompat.Translucent" />
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"

View file

@ -2,11 +2,21 @@ package im.angry.openeuicc.ui
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Window
import androidx.appcompat.view.ContextThemeWrapper
import androidx.fragment.app.DialogFragment
import com.google.android.material.color.DynamicColors
import im.angry.openeuicc.common.R
abstract class BaseMaterialDialogFragment: DialogFragment() {
override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater {
val inflater = super.onGetLayoutInflater(savedInstanceState)
val wrappedContext = ContextThemeWrapper(requireContext(), R.style.Theme_OpenEUICC)
val dynamicWrappedContext = DynamicColors.wrapContextIfAvailable(wrappedContext)
return inflater.cloneInContext(dynamicWrappedContext)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).also {
it.window?.requestFeature(Window.FEATURE_NO_TITLE)

View file

@ -0,0 +1,30 @@
package im.angry.openeuicc.ui
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.util.openEuiccApplication
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class DirectProfileDownloadActivity : AppCompatActivity(), SlotSelectFragment.SlotSelectedListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
withContext(Dispatchers.IO) {
openEuiccApplication.euiccChannelManager.enumerateEuiccChannels()
}
SlotSelectFragment.newInstance()
.show(supportFragmentManager, SlotSelectFragment.TAG)
}
}
override fun onSlotSelected(slotId: Int, portId: Int) {
ProfileDownloadFragment.newInstance(slotId, portId, finishWhenDone = true)
.show(supportFragmentManager, ProfileDownloadFragment.TAG)
}
override fun onSlotSelectCancelled() = finish()
}

View file

@ -8,11 +8,12 @@ import im.angry.openeuicc.util.openEuiccApplication
interface EuiccFragmentMarker
fun <T> newInstanceEuicc(clazz: Class<T>, slotId: Int, portId: Int): T where T: Fragment, T: EuiccFragmentMarker {
fun <T> newInstanceEuicc(clazz: Class<T>, slotId: Int, portId: Int, addArguments: Bundle.() -> Unit = {}): T where T: Fragment, T: EuiccFragmentMarker {
val instance = clazz.newInstance()
instance.arguments = Bundle().apply {
putInt("slotId", slotId)
putInt("portId", portId)
addArguments()
}
return instance
}

View file

@ -2,6 +2,7 @@ package im.angry.openeuicc.ui
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.text.Editable
import android.text.format.Formatter
@ -30,8 +31,10 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), EuiccFragmentMarke
companion object {
const val TAG = "ProfileDownloadFragment"
fun newInstance(slotId: Int, portId: Int): ProfileDownloadFragment =
newInstanceEuicc(ProfileDownloadFragment::class.java, slotId, portId)
fun newInstance(slotId: Int, portId: Int, finishWhenDone: Boolean = false): ProfileDownloadFragment =
newInstanceEuicc(ProfileDownloadFragment::class.java, slotId, portId) {
putBoolean("finishWhenDone", finishWhenDone)
}
}
private lateinit var toolbar: Toolbar
@ -46,6 +49,10 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), EuiccFragmentMarke
private var downloading = false
private val finishWhenDone by lazy {
requireArguments().getBoolean("finishWhenDone", false)
}
private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result ->
result.contents?.let { content ->
Log.d(TAG, content)
@ -81,7 +88,9 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), EuiccFragmentMarke
toolbar.apply {
setTitle(R.string.profile_download)
setNavigationOnClickListener {
if (!downloading) dismiss()
if (!downloading) {
dismiss()
}
}
setOnMenuItemClickListener(this@ProfileDownloadFragment)
}
@ -195,4 +204,18 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), EuiccFragmentMarke
// Only send notifications if the user allowed us to
preferenceRepository.notificationDownloadFlow.first()
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
if (finishWhenDone) {
activity?.finish()
}
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
if (finishWhenDone) {
activity?.finish()
}
}
}

View file

@ -0,0 +1,78 @@
package im.angry.openeuicc.ui
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Spinner
import androidx.appcompat.widget.Toolbar
import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.util.openEuiccApplication
import im.angry.openeuicc.util.setWidthPercent
class SlotSelectFragment : BaseMaterialDialogFragment() {
companion object {
const val TAG = "SlotSelectFragment"
fun newInstance(): SlotSelectFragment {
return SlotSelectFragment()
}
}
interface SlotSelectedListener {
fun onSlotSelected(slotId: Int, portId: Int)
fun onSlotSelectCancelled()
}
private lateinit var toolbar: Toolbar
private lateinit var spinner: Spinner
private val channels: List<EuiccChannel> by lazy {
openEuiccApplication.euiccChannelManager.knownChannels.sortedBy { it.logicalSlotId }
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_slot_select, container, false)
toolbar = view.findViewById(R.id.toolbar)
toolbar.setTitle(R.string.slot_select)
toolbar.inflateMenu(R.menu.fragment_slot_select)
val adapter = ArrayAdapter<String>(inflater.context, R.layout.spinner_item)
spinner = view.findViewById(R.id.spinner)
spinner.adapter = adapter
channels.forEach { channel ->
adapter.add(getString(R.string.channel_name_format, channel.logicalSlotId))
}
toolbar.setNavigationOnClickListener {
(requireActivity() as SlotSelectedListener).onSlotSelectCancelled()
}
toolbar.setOnMenuItemClickListener {
val channel = channels[spinner.selectedItemPosition]
(requireActivity() as SlotSelectedListener).onSlotSelected(channel.slotId, channel.portId)
dismiss()
true
}
return view
}
override fun onResume() {
super.onResume()
setWidthPercent(75)
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
(requireActivity() as SlotSelectedListener).onSlotSelectCancelled()
}
}

View file

@ -2,7 +2,8 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintWidth_percent="1"
app:navigationIcon="?homeAsUpIndicator" />
<Spinner
android:id="@+id/spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="48dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/ok"
android:icon="@drawable/ic_check_black"
android:title="@string/slot_select_select"
app:showAsAction="ifRoom"/>
</menu>

View file

@ -19,6 +19,9 @@
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
<string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string>
<string name="slot_select">Select Slot</string>
<string name="slot_select_select">Select</string>
<string name="profile_download">New eSIM</string>
<string name="profile_download_server">Server (RSP / SM-DP+)</string>
<string name="profile_download_code">Activation Code</string>

View file

@ -28,4 +28,14 @@
<style name="PositiveButtonStyle" parent="Widget.Material3.Button.TextButton.Dialog">
<item name="android:textColor">?attr/colorSecondary</item>
</style>
<style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowAnimationStyle">@android:style/Animation</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>

View file

@ -11,11 +11,10 @@ class LuiActivity : AppCompatActivity() {
setContentView(R.layout.activity_lui)
findViewById<View>(R.id.lui_skip).setOnClickListener { finish() }
// TODO: Allow users to select slots, and then hand over directly to ProfileDownloadFragment
// TODO: Deactivate LuiActivity if there is no eSIM found.
// TODO: Support pre-filled download info (from carrier apps); UX
findViewById<View>(R.id.lui_download).setOnClickListener {
startActivity(Intent(this, PrivilegedMainActivity::class.java))
startActivity(Intent(this, DirectProfileDownloadActivity::class.java))
}
}
}

View file

@ -8,7 +8,6 @@
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:theme="@style/Theme.OpenEUICC"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"