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:name="im.angry.openeuicc.ui.NotificationsActivity"
android:label="@string/profile_notifications" /> 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 <activity
android:name="com.journeyapps.barcodescanner.CaptureActivity" android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor" android:screenOrientation="fullSensor"

View file

@ -2,11 +2,21 @@ package im.angry.openeuicc.ui
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.Window import android.view.Window
import androidx.appcompat.view.ContextThemeWrapper
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.color.DynamicColors
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
abstract class BaseMaterialDialogFragment: DialogFragment() { 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 { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).also { return super.onCreateDialog(savedInstanceState).also {
it.window?.requestFeature(Window.FEATURE_NO_TITLE) 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 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() val instance = clazz.newInstance()
instance.arguments = Bundle().apply { instance.arguments = Bundle().apply {
putInt("slotId", slotId) putInt("slotId", slotId)
putInt("portId", portId) putInt("portId", portId)
addArguments()
} }
return instance return instance
} }

View file

@ -2,6 +2,7 @@ package im.angry.openeuicc.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.format.Formatter import android.text.format.Formatter
@ -30,8 +31,10 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), EuiccFragmentMarke
companion object { companion object {
const val TAG = "ProfileDownloadFragment" const val TAG = "ProfileDownloadFragment"
fun newInstance(slotId: Int, portId: Int): ProfileDownloadFragment = fun newInstance(slotId: Int, portId: Int, finishWhenDone: Boolean = false): ProfileDownloadFragment =
newInstanceEuicc(ProfileDownloadFragment::class.java, slotId, portId) newInstanceEuicc(ProfileDownloadFragment::class.java, slotId, portId) {
putBoolean("finishWhenDone", finishWhenDone)
}
} }
private lateinit var toolbar: Toolbar private lateinit var toolbar: Toolbar
@ -46,6 +49,10 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), EuiccFragmentMarke
private var downloading = false private var downloading = false
private val finishWhenDone by lazy {
requireArguments().getBoolean("finishWhenDone", false)
}
private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result -> private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result ->
result.contents?.let { content -> result.contents?.let { content ->
Log.d(TAG, content) Log.d(TAG, content)
@ -81,7 +88,9 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), EuiccFragmentMarke
toolbar.apply { toolbar.apply {
setTitle(R.string.profile_download) setTitle(R.string.profile_download)
setNavigationOnClickListener { setNavigationOnClickListener {
if (!downloading) dismiss() if (!downloading) {
dismiss()
}
} }
setOnMenuItemClickListener(this@ProfileDownloadFragment) setOnMenuItemClickListener(this@ProfileDownloadFragment)
} }
@ -195,4 +204,18 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), EuiccFragmentMarke
// Only send notifications if the user allowed us to // Only send notifications if the user allowed us to
preferenceRepository.notificationDownloadFlow.first() 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" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" 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 <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar" 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_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="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">New eSIM</string>
<string name="profile_download_server">Server (RSP / SM-DP+)</string> <string name="profile_download_server">Server (RSP / SM-DP+)</string>
<string name="profile_download_code">Activation Code</string> <string name="profile_download_code">Activation Code</string>

View file

@ -28,4 +28,14 @@
<style name="PositiveButtonStyle" parent="Widget.Material3.Button.TextButton.Dialog"> <style name="PositiveButtonStyle" parent="Widget.Material3.Button.TextButton.Dialog">
<item name="android:textColor">?attr/colorSecondary</item> <item name="android:textColor">?attr/colorSecondary</item>
</style> </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> </resources>

View file

@ -11,11 +11,10 @@ class LuiActivity : AppCompatActivity() {
setContentView(R.layout.activity_lui) setContentView(R.layout.activity_lui)
findViewById<View>(R.id.lui_skip).setOnClickListener { finish() } 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: Deactivate LuiActivity if there is no eSIM found.
// TODO: Support pre-filled download info (from carrier apps); UX // TODO: Support pre-filled download info (from carrier apps); UX
findViewById<View>(R.id.lui_download).setOnClickListener { 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:id="@+id/toolbar"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/Theme.OpenEUICC"
android:elevation="4dp" android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"