Compare commits
No commits in common. "master" and "master" have entirely different histories.
204 changed files with 1846 additions and 3076 deletions
|
|
@ -1,22 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
max_line_length = 120
|
||||
|
||||
[*.{kt,java}]
|
||||
indent_size = 4
|
||||
|
||||
[*.gradle.kts]
|
||||
indent_size = 4
|
||||
|
||||
[*.xml]
|
||||
indent_size = 4
|
||||
|
||||
[.idea/**/*.xml]
|
||||
indent_size = 2
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
name: Build Debug APKs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
|
@ -9,7 +7,7 @@ on:
|
|||
|
||||
jobs:
|
||||
build-debug:
|
||||
runs-on: [ docker, android-app-certs ]
|
||||
runs-on: [docker, android-app-certs]
|
||||
container:
|
||||
volumes:
|
||||
- android-app-keystore:/keystore
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
name: Build Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: '*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: [ docker, android-app-certs ]
|
||||
runs-on: [docker, android-app-certs]
|
||||
container:
|
||||
volumes:
|
||||
- android-app-keystore:/keystore
|
||||
|
|
|
|||
5
.idea/codeStyles/Project.xml
generated
5
.idea/codeStyles/Project.xml
generated
|
|
@ -1,11 +1,6 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="im.angry.openeuicc.util" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
|
|
|
|||
1
.idea/codeStyles/codeStyleConfig.xml
generated
1
.idea/codeStyles/codeStyleConfig.xml
generated
|
|
@ -1,5 +1,6 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
|
|
@ -3,4 +3,4 @@
|
|||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="1.7" />
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -3,4 +3,4 @@
|
|||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.24" />
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
2
.idea/migrations.xml
generated
2
.idea/migrations.xml
generated
|
|
@ -7,4 +7,4 @@
|
|||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
|
|
@ -4,4 +4,4 @@
|
|||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/libs/lpac-jni/src/main/jni/lpac" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
54
README.md
54
README.md
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
A fully free and open-source Local Profile Assistant implementation for Android devices.
|
||||
|
||||
There are two variants of this project, OpenEUICC and [EasyEUICC](https://easyeuicc.org):
|
||||
There are two variants of this project, OpenEUICC and EasyEUICC:
|
||||
|
||||
| | OpenEUICC | EasyEUICC |
|
||||
|:------------------------------|:-------------------------------:|:-------------------:|
|
||||
| :---------------------------- | :-----------------------------: | :-----------------: |
|
||||
| Privileged | Must be installed as system app | No |
|
||||
| Internal eSIM | Supported | Unsupported |
|
||||
| External eSIM [^1] | Supported | Supported |
|
||||
|
|
@ -22,31 +22,24 @@ Some side notes:
|
|||
1. When privileged, OpenEUICC supports any eUICC chip that implements the [SGP.22] standard, internal or external.
|
||||
However, there is **no guarantee** that external (removable) eSIMs actually follow the standard.
|
||||
Please **DO NOT** submit bug reports for non-functioning removable eSIMs.
|
||||
They are **NOT** officially supported unless they also support / are supported by EasyEUICC, the unprivileged
|
||||
variant.
|
||||
They are **NOT** officially supported unless they also support / are supported by EasyEUICC, the unprivileged variant.
|
||||
2. Both variants support accessing eUICC chips through USB CCID readers,
|
||||
regardless of whether the chip contains the correct ARA-M hash to allow for unprivileged access.
|
||||
However, only `T=0` readers that use the standard [USB CCID protocol][usb-ccid] are supported.
|
||||
3. Prebuilt release-mode EasyEUICC apks can be downloaded [here][releases].
|
||||
For OpenEUICC, no official release is currently provided and only debug mode APKs and Magisk modules can be found in
|
||||
the [CI page][actions].
|
||||
For OpenEUICC, no official release is currently provided and only debug mode APKs and Magisk modules can be found in the [CI page][actions].
|
||||
4. For removable eSIM chip vendors: to have your chip supported by official builds of EasyEUICC when inserted,
|
||||
include the ARA-M hash `2A2FA878BC7C3354C2CF82935A5945A3EDAE4AFA`.
|
||||
|
||||
[sgp.22]: https://www.gsma.com/solutions-and-impact/technologies/esim/gsma_resources/sgp-22-v2-2-2/ "SGP.22 v2.2.2"
|
||||
|
||||
[usb-ccid]: https://en.wikipedia.org/wiki/CCID_%28protocol%29 "USB CCID Protocol"
|
||||
|
||||
[releases]: https://gitea.angry.im/PeterCxy/OpenEUICC/releases "EasyEUICC Releases"
|
||||
|
||||
[actions]: https://gitea.angry.im/PeterCxy/OpenEUICC/actions "OpenEUICC Actions"
|
||||
|
||||
**This project is Free Software licensed under GNU GPL v3, WITHOUT the "or later" clause.**
|
||||
Any modification and derivative work **MUST** be released under the SAME license, which means, at the very least, that
|
||||
the source code **MUST** be available upon request.
|
||||
Any modification and derivative work **MUST** be released under the SAME license, which means, at the very least, that the source code **MUST** be available upon request.
|
||||
|
||||
**If you are releasing a modification of this app, you are kindly asked to make changes to at least the app name and
|
||||
package name.**
|
||||
**If you are releasing a modification of this app, you are kindly asked to make changes to at least the app name and package name.**
|
||||
|
||||
# Building (Gradle)
|
||||
|
||||
|
|
@ -85,46 +78,33 @@ For EasyEUICC:
|
|||
|
||||
There are two ways to include OpenEUICC in your AOSP-based system image:
|
||||
|
||||
1. Include this project and its [dependencies](https://gitea.angry.im/PeterCxy/android_prebuilts_openeuicc-deps) inside
|
||||
the AOSP tree.
|
||||
|
||||
- If inclusion in `manifest.xml` is required, remember to set the `sync-s` option to clone submodules.
|
||||
- The module name is `OpenEUICC`. You can include it in `PRODUCT_PACKAGES`, or simply build it standalone using `mm`.
|
||||
- Compilation of this project is **only** tested against the latest AOSP release version. The app itself should be
|
||||
compatible with older AOSP versions, but the source may not compile against an older AOSP source tree.
|
||||
|
||||
2. If compilation against AOSP source tree is not possible, consider [building with gradle](#building-gradle) and import
|
||||
the apk as a prebuilt.
|
||||
|
||||
- No official `Android.bp` is provided for this case but it should be straightforward to write.
|
||||
- You might want to include [`privapp_whitelist_im.angry.openeuicc.xml`] as well.
|
||||
1. Include this project and its [dependencies](https://gitea.angry.im/PeterCxy/android_prebuilts_openeuicc-deps) inside the AOSP tree.
|
||||
- If inclusion in `manifest.xml` is required, remember to set the `sync-s` option to clone submodules.
|
||||
- The module name is `OpenEUICC`. You can include it in `PRODUCT_PACKAGES`, or simply build it standalone using `mm`.
|
||||
- Compilation of this project is **only** tested against the latest AOSP release version. The app itself should be compatible with older AOSP versions, but the source may not compile against an older AOSP source tree.
|
||||
2. If compilation against AOSP source tree is not possible, consider [building with gradle](#building-gradle) and import the apk as a prebuilt.
|
||||
- No official `Android.bp` is provided for this case but it should be straightforward to write.
|
||||
- You might want to include [`privapp_whitelist_im.angry.openeuicc.xml`] as well.
|
||||
|
||||
[`privapp_whitelist_im.angry.openeuicc.xml`]: privapp_whitelist_im.angry.openeuicc.xml "OpenEUICC Privapp Whitelist"
|
||||
|
||||
# FAQs
|
||||
|
||||
- Q: Do you provide prebuilt binaries for OpenEUICC? \
|
||||
A: Debug-mode APKs and Magisk modules are available continuously as an artifact of the [Actions] CI used by this
|
||||
project. However, these debug-mode APKs are **not** intended for inclusion inside system images, nor are they
|
||||
supported by the developer in any sense. If you are a custom ROM developer, either include the entire OpenEUICC
|
||||
repository in your AOSP source tree, or generate an APK using `gradle` and import that as a prebuilt system app. Note
|
||||
that you might want [`privapp_whitelist_im.angry.openeuicc.xml`] as well.
|
||||
A: Debug-mode APKs and Magisk modules are available continuously as an artifact of the [Actions] CI used by this project. However, these debug-mode APKs are **not** intended for inclusion inside system images, nor are they supported by the developer in any sense. If you are a custom ROM developer, either include the entire OpenEUICC repository in your AOSP source tree, or generate an APK using `gradle` and import that as a prebuilt system app. Note that you might want [`privapp_whitelist_im.angry.openeuicc.xml`] as well.
|
||||
|
||||
- Q: Can EasyEUICC manage my phone's internal eSIM? \
|
||||
A: No. For EasyEUICC to work, the eSIM chip MUST proactively grant access via its ARA-M field.
|
||||
|
||||
- Q: Removable eSIMs? Are they a joke? \
|
||||
A: No, even though the name "removable embedded SIM" can sound like an oxymoron. In fact, there can be many advantages
|
||||
to these chips compared to fully embedded ones. For example, the ability to transfer eSIM profiles without carrier
|
||||
support or approval, or the ability to use eSIM on devices that do not and may never get the support, such as Wi-Fi
|
||||
hotspots.
|
||||
A: No, even though the name "removable embedded SIM" can sound like an oxymoron. In fact, there can be many advantages to these chips compared to fully embedded ones. For example, the ability to transfer eSIM profiles without carrier support or approval, or the ability to use eSIM on devices that do not and may never get the support, such as Wi-Fi hotspots.
|
||||
|
||||
# Copyright
|
||||
|
||||
Everything except `libs/lpac-jni` and `art/`:
|
||||
|
||||
```
|
||||
Copyright 2022-2026 OpenEUICC contributors
|
||||
Copyright 2022-2024 OpenEUICC contributors
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
|
|
@ -143,7 +123,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|||
`libs/lpac-jni`:
|
||||
|
||||
```
|
||||
Copyright (C) 2022-2026 OpenEUICC contributiors
|
||||
Copyright (C) 2022-2024 OpenEUICC contributiors
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package im.angry.openeuicc.common
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
|
@ -19,4 +21,4 @@ class ExampleInstrumentedTest {
|
|||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("im.angry.openeuicc.common.test", appContext.packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="im.angry.openeuicc.common">
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
|
@ -33,8 +33,8 @@
|
|||
android:label="@string/isdr_aid_list" />
|
||||
|
||||
<activity
|
||||
android:name="im.angry.openeuicc.ui.wizard.DownloadWizardActivity"
|
||||
android:exported="true"
|
||||
android:name="im.angry.openeuicc.ui.wizard.DownloadWizardActivity"
|
||||
android:label="@string/download_wizard">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
|
@ -46,16 +46,14 @@
|
|||
<!-- for example: "LPA:1$..." -->
|
||||
<!-- refs: https://www.iana.org/assignments/uri-schemes/prov/lpa -->
|
||||
<data android:scheme="lpa" />
|
||||
<data
|
||||
android:scheme="LPA"
|
||||
tools:ignore="AppLinkUrlError" />
|
||||
<data android:scheme="LPA" tools:ignore="AppLinkUrlError" />
|
||||
<data android:sspPrefix="1$" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name="im.angry.openeuicc.ui.DirectProfileDownloadActivity"
|
||||
android:exported="true"
|
||||
android:name="im.angry.openeuicc.ui.DirectProfileDownloadActivity"
|
||||
android:targetActivity="im.angry.openeuicc.ui.wizard.DownloadWizardActivity" />
|
||||
|
||||
<activity
|
||||
|
|
@ -65,7 +63,7 @@
|
|||
|
||||
<service
|
||||
android:name="im.angry.openeuicc.service.EuiccChannelManagerService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="shortService" />
|
||||
android:foregroundServiceType="shortService"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import im.angry.openeuicc.common.R
|
|||
import im.angry.openeuicc.core.usb.UsbApduInterface
|
||||
import im.angry.openeuicc.core.usb.UsbCcidContext
|
||||
import im.angry.openeuicc.util.*
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccChannelFactory {
|
||||
private var seService: SEService? = null
|
||||
|
|
@ -19,8 +20,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
|
||||
override suspend fun tryOpenEuiccChannel(
|
||||
port: UiccPortInfoCompat,
|
||||
isdrAid: ByteArray,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
isdrAid: ByteArray
|
||||
): EuiccChannel? = try {
|
||||
if (port.portIndex != 0) {
|
||||
Log.w(
|
||||
|
|
@ -45,7 +45,6 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
context.preferenceRepository.verboseLoggingFlow
|
||||
),
|
||||
isdrAid,
|
||||
seId,
|
||||
context.preferenceRepository.verboseLoggingFlow,
|
||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||
context.preferenceRepository.es10xMssFlow,
|
||||
|
|
@ -61,8 +60,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
|
||||
override fun tryOpenUsbEuiccChannel(
|
||||
ccidCtx: UsbCcidContext,
|
||||
isdrAid: ByteArray,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
isdrAid: ByteArray
|
||||
): EuiccChannel? = try {
|
||||
EuiccChannelImpl(
|
||||
context.getString(R.string.channel_type_usb),
|
||||
|
|
@ -72,7 +70,6 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
ccidCtx
|
||||
),
|
||||
isdrAid,
|
||||
seId,
|
||||
context.preferenceRepository.verboseLoggingFlow,
|
||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||
context.preferenceRepository.es10xMssFlow,
|
||||
|
|
@ -90,4 +87,4 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
seService?.shutdown()
|
||||
seService = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,8 @@ import android.hardware.usb.UsbManager
|
|||
import android.telephony.SubscriptionManager
|
||||
import android.util.Log
|
||||
import im.angry.openeuicc.core.usb.UsbCcidContext
|
||||
import im.angry.openeuicc.core.usb.interfaces
|
||||
import im.angry.openeuicc.core.usb.smartCard
|
||||
import im.angry.openeuicc.core.usb.interfaces
|
||||
import im.angry.openeuicc.di.AppContainer
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -32,7 +32,7 @@ open class DefaultEuiccChannelManager(
|
|||
|
||||
private val channelCache = mutableListOf<EuiccChannel>()
|
||||
|
||||
private var usbChannels = mutableListOf<EuiccChannel>()
|
||||
private var usbChannel: EuiccChannel? = null
|
||||
|
||||
private val lock = Mutex()
|
||||
|
||||
|
|
@ -51,96 +51,43 @@ open class DefaultEuiccChannelManager(
|
|||
protected open val uiccCards: Collection<UiccCardInfoCompat>
|
||||
get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
|
||||
|
||||
private suspend inline fun tryOpenChannelWithKnownAids(
|
||||
supportsMultiSE: Boolean,
|
||||
openFn: (ByteArray, EuiccChannel.SecureElementId) -> EuiccChannel?
|
||||
): List<EuiccChannel> {
|
||||
var isdrAidList =
|
||||
private suspend inline fun tryOpenChannelFirstValidAid(openFn: (ByteArray) -> EuiccChannel?): EuiccChannel? {
|
||||
val isdrAidList =
|
||||
parseIsdrAidList(appContainer.preferenceRepository.isdrAidListFlow.first())
|
||||
val ret = mutableListOf<EuiccChannel>()
|
||||
val openedAids = mutableListOf<ByteArray>()
|
||||
var hasReset = false
|
||||
var vendorDecider: VendorAidDecider? = null
|
||||
var seId = 0
|
||||
|
||||
outer@ while (true) {
|
||||
for (aid in isdrAidList) {
|
||||
if (vendorDecider != null && !vendorDecider.shouldOpenMore(openedAids, aid)) {
|
||||
break@outer
|
||||
}
|
||||
return isdrAidList.firstNotNullOfOrNull {
|
||||
Log.i(TAG, "Opening channel, trying ISDR AID ${it.encodeHex()}")
|
||||
|
||||
val channel =
|
||||
openFn(aid, EuiccChannel.SecureElementId.createFromInt(seId))?.let { channel ->
|
||||
if (channel.valid) {
|
||||
seId += 1
|
||||
channel
|
||||
} else {
|
||||
channel.close()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasReset) {
|
||||
val res = channel?.queryVendorAidListTransformation(isdrAidList)
|
||||
if (res != null) {
|
||||
// Reset the for loop since we needed to replace the AID list due to vendor-specific code
|
||||
Log.i(TAG, "AID list replaced, resetting open attempt")
|
||||
isdrAidList = res.first
|
||||
vendorDecider = res.second
|
||||
seId = 0
|
||||
ret.clear()
|
||||
openedAids.clear()
|
||||
channel.close()
|
||||
hasReset = true // Don't let anything reset again
|
||||
continue@outer
|
||||
}
|
||||
}
|
||||
|
||||
if (channel != null) {
|
||||
ret.add(channel)
|
||||
openedAids.add(aid)
|
||||
|
||||
// Don't try opening more than 1 channel unless we support multi SE or
|
||||
// there is a vendor implementation for deciding when we should stop
|
||||
// opening more channels
|
||||
if (!supportsMultiSE || vendorDecider == null) {
|
||||
break@outer
|
||||
}
|
||||
openFn(it)?.let { channel ->
|
||||
if (channel.valid) {
|
||||
channel
|
||||
} else {
|
||||
channel.close()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here we should exit, since the inner loop completed without resetting
|
||||
break
|
||||
}
|
||||
|
||||
// Set the hasMultipleSE field now since we only get to know that after we have iterated all AIDs
|
||||
// This also flips a flag in EuiccChannelImpl and prevents the field from being set again
|
||||
ret.forEach { it.hasMultipleSE = (seId > 1) }
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
private suspend fun tryOpenEuiccChannel(
|
||||
port: UiccPortInfoCompat,
|
||||
): List<EuiccChannel>? {
|
||||
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
|
||||
lock.withLock {
|
||||
if (port.card.physicalSlotIndex == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
return usbChannels
|
||||
return if (usbChannel != null && usbChannel!!.valid) {
|
||||
usbChannel
|
||||
} else {
|
||||
usbChannel = null
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// First get all channels for the requested port
|
||||
val existing =
|
||||
channelCache.filter { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex }
|
||||
if (existing.isNotEmpty()) {
|
||||
if (existing.all { it.valid && it.logicalSlotId == port.logicalSlotIndex }) {
|
||||
channelCache.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex }
|
||||
if (existing != null) {
|
||||
if (existing.valid && port.logicalSlotIndex == existing.logicalSlotId) {
|
||||
return existing
|
||||
} else {
|
||||
// If any channel shouldn't be considered valid anymore, close all existing for the same slot / port
|
||||
// and reopen
|
||||
existing.forEach {
|
||||
it.close()
|
||||
channelCache.remove(it)
|
||||
}
|
||||
existing.close()
|
||||
channelCache.remove(existing)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -149,19 +96,12 @@ open class DefaultEuiccChannelManager(
|
|||
return null
|
||||
}
|
||||
|
||||
// This function is not responsible for managing USB channels (see the initial check), so supportsMultiSE is true.
|
||||
val channels =
|
||||
tryOpenChannelWithKnownAids(supportsMultiSE = true) { isdrAid, seId ->
|
||||
euiccChannelFactory.tryOpenEuiccChannel(
|
||||
port,
|
||||
isdrAid,
|
||||
seId
|
||||
)
|
||||
}
|
||||
val channel =
|
||||
tryOpenChannelFirstValidAid { euiccChannelFactory.tryOpenEuiccChannel(port, it) }
|
||||
|
||||
if (channels.isNotEmpty()) {
|
||||
channelCache.addAll(channels)
|
||||
return channels
|
||||
if (channel != null) {
|
||||
channelCache.add(channel)
|
||||
return channel
|
||||
} else {
|
||||
Log.i(
|
||||
TAG,
|
||||
|
|
@ -172,19 +112,16 @@ open class DefaultEuiccChannelManager(
|
|||
}
|
||||
}
|
||||
|
||||
protected suspend fun findEuiccChannelByLogicalSlot(
|
||||
logicalSlotId: Int,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): EuiccChannel? =
|
||||
protected suspend fun findEuiccChannelByLogicalSlot(logicalSlotId: Int): EuiccChannel? =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
return@withContext usbChannels.find { it.seId == seId }
|
||||
return@withContext usbChannel
|
||||
}
|
||||
|
||||
for (card in uiccCards) {
|
||||
for (port in card.ports) {
|
||||
if (port.logicalSlotIndex == logicalSlotId) {
|
||||
return@withContext tryOpenEuiccChannel(port)?.find { it.seId == seId }
|
||||
return@withContext tryOpenEuiccChannel(port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -192,35 +129,23 @@ open class DefaultEuiccChannelManager(
|
|||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all EuiccChannels associated with a _physical_ slot, including all secure elements
|
||||
* on cards with multiple of them.
|
||||
*/
|
||||
private suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? {
|
||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
return usbChannels.ifEmpty { null }
|
||||
return usbChannel?.let { listOf(it) }
|
||||
}
|
||||
|
||||
for (card in uiccCards) {
|
||||
if (card.physicalSlotIndex != physicalSlotId) continue
|
||||
return card.ports.mapNotNull { tryOpenEuiccChannel(it) }
|
||||
.flatten()
|
||||
.ifEmpty { null }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all EuiccChannels associated with a physical slot + port. Note that this
|
||||
* may return multiple in case there are multiple SEs.
|
||||
*/
|
||||
private suspend fun findEuiccChannelsByPort(
|
||||
physicalSlotId: Int,
|
||||
portId: Int,
|
||||
): List<EuiccChannel>? =
|
||||
private suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
return@withContext usbChannels.ifEmpty { null }
|
||||
return@withContext usbChannel
|
||||
}
|
||||
|
||||
uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
|
||||
|
|
@ -243,17 +168,15 @@ open class DefaultEuiccChannelManager(
|
|||
return@withContext listOf(0)
|
||||
}
|
||||
|
||||
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId }?.toSet()?.toList()
|
||||
?: listOf()
|
||||
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId } ?: listOf()
|
||||
}
|
||||
|
||||
override suspend fun <R> withEuiccChannel(
|
||||
physicalSlotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
fn: suspend (EuiccChannel) -> R
|
||||
): R {
|
||||
val channel = findEuiccChannelsByPort(physicalSlotId, portId)?.find { it.seId == seId }
|
||||
val channel = findEuiccChannelByPort(physicalSlotId, portId)
|
||||
?: throw EuiccChannelManager.EuiccChannelNotFoundException()
|
||||
val wrapper = EuiccChannelWrapper(channel)
|
||||
try {
|
||||
|
|
@ -267,10 +190,9 @@ open class DefaultEuiccChannelManager(
|
|||
|
||||
override suspend fun <R> withEuiccChannel(
|
||||
logicalSlotId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
fn: suspend (EuiccChannel) -> R
|
||||
): R {
|
||||
val channel = findEuiccChannelByLogicalSlot(logicalSlotId, seId)
|
||||
val channel = findEuiccChannelByLogicalSlot(logicalSlotId)
|
||||
?: throw EuiccChannelManager.EuiccChannelNotFoundException()
|
||||
val wrapper = EuiccChannelWrapper(channel)
|
||||
try {
|
||||
|
|
@ -283,48 +205,37 @@ open class DefaultEuiccChannelManager(
|
|||
}
|
||||
|
||||
override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) {
|
||||
val numChannelsBefore = if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
usbChannels.size
|
||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
usbChannel?.close()
|
||||
usbChannel = null
|
||||
} else {
|
||||
// Don't use find* methods since they reopen channels if not found
|
||||
channelCache.filter { it.slotId == physicalSlotId && it.portId == portId }.size
|
||||
}
|
||||
|
||||
val resetChannels = {
|
||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
usbChannels.forEach { it.close() }
|
||||
usbChannels.clear()
|
||||
} else {
|
||||
// If there is already a valid channel, we close it proactively
|
||||
channelCache.filter { it.slotId == physicalSlotId && it.portId == portId }.forEach { it.close() }
|
||||
// If there is already a valid channel, we close it proactively
|
||||
// Sometimes the current channel can linger on for a bit even after it should have become invalid
|
||||
channelCache.find { it.slotId == physicalSlotId && it.portId == portId }?.apply {
|
||||
if (valid) close()
|
||||
}
|
||||
}
|
||||
|
||||
resetChannels()
|
||||
|
||||
withTimeout(timeoutMillis) {
|
||||
while (true) {
|
||||
try {
|
||||
val channels = if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
val channel = if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
// tryOpenUsbEuiccChannel() will always try to reopen the channel, even if
|
||||
// a USB channel already exists
|
||||
tryOpenUsbEuiccChannel()
|
||||
usbChannels
|
||||
usbChannel!!
|
||||
} else {
|
||||
// tryOpenEuiccChannel() will automatically dispose of invalid channels
|
||||
// and recreate when needed
|
||||
findEuiccChannelsByPort(physicalSlotId, portId)!!
|
||||
findEuiccChannelByPort(physicalSlotId, portId)!!
|
||||
}
|
||||
check(channels.isNotEmpty()) { "No channel" }
|
||||
check(channels.all { it.valid }) { "Invalid channel" }
|
||||
check(numChannelsBefore > 0 && channels.size >= numChannelsBefore) { "Less channels than before" }
|
||||
check(channel.valid) { "Invalid channel" }
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Slot $physicalSlotId port $portId reconnect failure, retrying in 1000 ms"
|
||||
)
|
||||
resetChannels()
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
|
|
@ -353,15 +264,6 @@ open class DefaultEuiccChannelManager(
|
|||
}
|
||||
})
|
||||
|
||||
override fun flowEuiccSecureElements(
|
||||
slotId: Int,
|
||||
portId: Int
|
||||
): Flow<EuiccChannel.SecureElementId> = flow {
|
||||
findEuiccChannelsByPort(slotId, portId)?.forEach {
|
||||
emit(it.seId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun tryOpenUsbEuiccChannel(): Pair<UsbDevice?, Boolean> =
|
||||
withContext(Dispatchers.IO) {
|
||||
usbManager.deviceList.values.forEach { device ->
|
||||
|
|
@ -375,18 +277,15 @@ open class DefaultEuiccChannelManager(
|
|||
"Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel"
|
||||
)
|
||||
|
||||
val ccidCtx =
|
||||
UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach
|
||||
val ccidCtx = UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach
|
||||
|
||||
try {
|
||||
// TODO: We should also support multiple SEs over USB readers (the code here already does, UI doesn't yet)
|
||||
val channels = tryOpenChannelWithKnownAids(supportsMultiSE = false) { isdrAid, seId ->
|
||||
euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, isdrAid, seId)
|
||||
val channel = tryOpenChannelFirstValidAid {
|
||||
euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, it)
|
||||
}
|
||||
if (channels.isNotEmpty() && channels[0].valid) {
|
||||
if (channel != null && channel.lpa.valid) {
|
||||
ccidCtx.allowDisconnect = true
|
||||
usbChannels.clear()
|
||||
usbChannels.addAll(channels)
|
||||
usbChannel = channel
|
||||
return@withContext Pair(device, true)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -410,9 +309,9 @@ open class DefaultEuiccChannelManager(
|
|||
channel.close()
|
||||
}
|
||||
|
||||
usbChannels.forEach { it.close() }
|
||||
usbChannels.clear()
|
||||
usbChannel?.close()
|
||||
usbChannel = null
|
||||
channelCache.clear()
|
||||
euiccChannelFactory.cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
package im.angry.openeuicc.core
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import im.angry.openeuicc.util.*
|
||||
import net.typeblog.lpac_jni.ApduInterface
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
|
|
@ -15,67 +13,6 @@ interface EuiccChannel {
|
|||
val logicalSlotId: Int
|
||||
val portId: Int
|
||||
|
||||
/**
|
||||
* A semi-obscure wrapper over the integer ID of a secure element on a card.
|
||||
*
|
||||
* Because the ID is arbitrary, this is intended to discourage the use of the
|
||||
* integer value directly. Additionally, it prevents accidentally calling the
|
||||
* wrong function in EuiccChannelManager with a ton of integer parameters.
|
||||
*/
|
||||
class SecureElementId private constructor(val id: Int) : Parcelable {
|
||||
companion object {
|
||||
val DEFAULT = SecureElementId(0)
|
||||
|
||||
/**
|
||||
* Create a SecureElementId from an integer ID. You should not call this directly
|
||||
* unless you know what you're doing.
|
||||
*
|
||||
* This is currently only ever used in the download flow.
|
||||
*/
|
||||
fun createFromInt(id: Int): SecureElementId =
|
||||
SecureElementId(id)
|
||||
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR = object : Parcelable.Creator<SecureElementId> {
|
||||
override fun createFromParcel(parcel: Parcel): SecureElementId =
|
||||
createFromInt(parcel.readInt())
|
||||
|
||||
override fun newArray(size: Int): Array<SecureElementId?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int =
|
||||
id.hashCode()
|
||||
|
||||
override fun equals(other: Any?): Boolean =
|
||||
if (other is SecureElementId) {
|
||||
this.id == other.id
|
||||
} else {
|
||||
super.equals(other)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int = id
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeInt(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Some chips support multiple SEs on one chip. The seId here is intended
|
||||
* to distinguish channels opened from these different SEs.
|
||||
*/
|
||||
val seId: SecureElementId
|
||||
|
||||
/**
|
||||
* Does this channel belong to a chip that supports multiple SEs?
|
||||
* Note that this is only made `var` to make initialization a bit less annoying --
|
||||
* this should never be set again after the channel is originally opened.
|
||||
* Attempting to do so will yield an exception.
|
||||
*/
|
||||
var hasMultipleSE: Boolean
|
||||
|
||||
val lpa: LocalProfileAssistant
|
||||
|
||||
val valid: Boolean
|
||||
|
|
@ -103,4 +40,4 @@ interface EuiccChannel {
|
|||
val isdrAid: ByteArray
|
||||
|
||||
fun close()
|
||||
}
|
||||
}
|
||||
|
|
@ -6,16 +6,11 @@ import im.angry.openeuicc.util.*
|
|||
// This class is here instead of inside DI because it contains a bit more logic than just
|
||||
// "dumb" dependency injection.
|
||||
interface EuiccChannelFactory {
|
||||
suspend fun tryOpenEuiccChannel(
|
||||
port: UiccPortInfoCompat,
|
||||
isdrAid: ByteArray,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): EuiccChannel?
|
||||
suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel?
|
||||
|
||||
fun tryOpenUsbEuiccChannel(
|
||||
ccidCtx: UsbCcidContext,
|
||||
isdrAid: ByteArray,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
isdrAid: ByteArray
|
||||
): EuiccChannel?
|
||||
|
||||
/**
|
||||
|
|
@ -24,4 +19,4 @@ interface EuiccChannelFactory {
|
|||
* re-acquired when this happens
|
||||
*/
|
||||
fun cleanup()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package im.angry.openeuicc.core
|
||||
|
||||
import im.angry.openeuicc.util.*
|
||||
import im.angry.openeuicc.util.UiccPortInfoCompat
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
|
@ -15,7 +15,6 @@ class EuiccChannelImpl(
|
|||
override val intrinsicChannelName: String?,
|
||||
override val apduInterface: ApduInterface,
|
||||
override val isdrAid: ByteArray,
|
||||
override val seId: EuiccChannel.SecureElementId,
|
||||
verboseLoggingFlow: Flow<Boolean>,
|
||||
ignoreTLSCertificateFlow: Flow<Boolean>,
|
||||
es10xMssFlow: Flow<Int>,
|
||||
|
|
@ -39,16 +38,5 @@ class EuiccChannelImpl(
|
|||
override val valid: Boolean
|
||||
get() = lpa.valid
|
||||
|
||||
private var hasMultipleSEInitialized = false
|
||||
|
||||
override var hasMultipleSE: Boolean = false
|
||||
set(value) {
|
||||
if (hasMultipleSEInitialized) {
|
||||
throw IllegalStateException("already initialized")
|
||||
}
|
||||
|
||||
field = value
|
||||
}
|
||||
|
||||
override fun close() = lpa.close()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
* or when this instance is destroyed.
|
||||
*
|
||||
* To precisely control the lifecycle of this object itself (and thus its cached channels),
|
||||
* all other components must access EuiccChannelManager objects through EuiccChannelManagerService.
|
||||
* all other compoents must access EuiccChannelManager objects through EuiccChannelManagerService.
|
||||
* Holding references independent of EuiccChannelManagerService is unsupported.
|
||||
*/
|
||||
interface EuiccChannelManager {
|
||||
|
|
@ -37,14 +37,6 @@ interface EuiccChannelManager {
|
|||
*/
|
||||
fun flowAllOpenEuiccPorts(): Flow<Pair<Int, Int>>
|
||||
|
||||
/**
|
||||
* Iterate over all the Secure Elements available on one eUICC.
|
||||
*
|
||||
* This is going to almost always return only 1 result, except in the case where
|
||||
* a card has multiple SEs.
|
||||
*/
|
||||
fun flowEuiccSecureElements(slotId: Int, portId: Int): Flow<EuiccChannel.SecureElementId>
|
||||
|
||||
/**
|
||||
* Scan all possible USB devices for CCID readers that may contain eUICC cards.
|
||||
* If found, try to open it for access, and add it to the internal EuiccChannel cache
|
||||
|
|
@ -75,7 +67,7 @@ interface EuiccChannelManager {
|
|||
*/
|
||||
suspend fun findAvailablePorts(physicalSlotId: Int): List<Int>
|
||||
|
||||
class EuiccChannelNotFoundException : Exception("EuiccChannel not found")
|
||||
class EuiccChannelNotFoundException: Exception("EuiccChannel not found")
|
||||
|
||||
/**
|
||||
* Find a EuiccChannel by its slot and port, then run a callback with a reference to it.
|
||||
|
|
@ -86,12 +78,19 @@ interface EuiccChannelManager {
|
|||
*
|
||||
* If a channel for that slot / port is not found, EuiccChannelNotFoundException is thrown
|
||||
*/
|
||||
suspend fun <R> withEuiccChannel(physicalSlotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, fn: suspend (EuiccChannel) -> R): R
|
||||
suspend fun <R> withEuiccChannel(
|
||||
physicalSlotId: Int,
|
||||
portId: Int,
|
||||
fn: suspend (EuiccChannel) -> R
|
||||
): R
|
||||
|
||||
/**
|
||||
* Same as withEuiccChannel(Int, Int, SecureElementId, (EuiccChannel) -> R) but instead uses logical slot ID
|
||||
* Same as withEuiccChannel(Int, Int, (EuiccChannel) -> R) but instead uses logical slot ID
|
||||
*/
|
||||
suspend fun <R> withEuiccChannel(logicalSlotId: Int, seId: EuiccChannel.SecureElementId, fn: suspend (EuiccChannel) -> R): R
|
||||
suspend fun <R> withEuiccChannel(
|
||||
logicalSlotId: Int,
|
||||
fn: suspend (EuiccChannel) -> R
|
||||
): R
|
||||
|
||||
/**
|
||||
* Invalidate all EuiccChannels previously cached by this Manager
|
||||
|
|
@ -106,4 +105,4 @@ interface EuiccChannelManager {
|
|||
suspend fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
|
||||
// no-op by default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,8 +26,6 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
|
|||
get() = channel.logicalSlotId
|
||||
override val portId: Int
|
||||
get() = channel.portId
|
||||
override val seId: EuiccChannel.SecureElementId
|
||||
get() = channel.seId
|
||||
private val lpaDelegate = lazy {
|
||||
LocalProfileAssistantWrapper(channel.lpa)
|
||||
}
|
||||
|
|
@ -42,11 +40,6 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
|
|||
get() = channel.atr
|
||||
override val isdrAid: ByteArray
|
||||
get() = channel.isdrAid
|
||||
override var hasMultipleSE: Boolean
|
||||
get() = channel.hasMultipleSE
|
||||
set(value) {
|
||||
channel.hasMultipleSE = value
|
||||
}
|
||||
|
||||
override fun close() = channel.close()
|
||||
|
||||
|
|
@ -57,4 +50,4 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
|
|||
(lpa as LocalProfileAssistantWrapper).invalidateWrapper()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ class OmapiApduInterface(
|
|||
private val service: SEService,
|
||||
private val port: UiccPortInfoCompat,
|
||||
private val verboseLoggingFlow: Flow<Boolean>
|
||||
) : ApduInterface, ApduInterfaceAtrProvider {
|
||||
): ApduInterface, ApduInterfaceAtrProvider {
|
||||
companion object {
|
||||
const val TAG = "OmapiApduInterface"
|
||||
}
|
||||
|
|
@ -83,4 +83,4 @@ class OmapiApduInterface(
|
|||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,7 @@ package im.angry.openeuicc.core.usb
|
|||
|
||||
import android.util.Log
|
||||
import im.angry.openeuicc.core.ApduInterfaceAtrProvider
|
||||
import im.angry.openeuicc.util.decodeHex
|
||||
import im.angry.openeuicc.util.encodeHex
|
||||
import im.angry.openeuicc.util.*
|
||||
import net.typeblog.lpac_jni.ApduInterface
|
||||
|
||||
class UsbApduInterface(
|
||||
|
|
@ -156,4 +155,4 @@ class UsbApduInterface(
|
|||
|
||||
return resp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import android.hardware.usb.UsbDeviceConnection
|
|||
import android.hardware.usb.UsbEndpoint
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import im.angry.openeuicc.util.encodeHex
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
|
@ -342,4 +342,4 @@ class UsbCcidTransceiver(
|
|||
)
|
||||
sendRaw(iccPowerCommand, 0, iccPowerCommand.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,4 +16,4 @@ interface AppContainer {
|
|||
val uiComponentFactory: UiComponentFactory
|
||||
val euiccChannelFactory: EuiccChannelFactory
|
||||
val customizableTextProvider: CustomizableTextProvider
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
package im.angry.openeuicc.di
|
||||
|
||||
import android.net.Uri
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
|
||||
interface CustomizableTextProvider {
|
||||
/**
|
||||
* Explanation string for when no eUICC is found on the device.
|
||||
|
|
@ -16,18 +13,8 @@ interface CustomizableTextProvider {
|
|||
val profileSwitchingTimeoutMessage: String
|
||||
|
||||
/**
|
||||
* Display the website link in settings; null if not available.
|
||||
* Format the name of a logical slot; internal only -- not intended for
|
||||
* other channels such as USB.
|
||||
*/
|
||||
val websiteUri: Uri?
|
||||
|
||||
/**
|
||||
* Format the name of a logical slot -- not for USB channels
|
||||
*/
|
||||
fun formatNonUsbChannelName(logicalSlotId: Int): String
|
||||
|
||||
/**
|
||||
* Format the name of a logical slot with a SE ID, in case of multi-SE chips; currently
|
||||
* this is used in the download flow to distinguish between them on the same chip.
|
||||
*/
|
||||
fun formatNonUsbChannelNameWithSeId(logicalSlotId: Int, seId: EuiccChannel.SecureElementId): String
|
||||
}
|
||||
fun formatInternalChannelName(logicalSlotId: Int): String
|
||||
}
|
||||
|
|
@ -42,4 +42,4 @@ open class DefaultAppContainer(context: Context) : AppContainer {
|
|||
override val customizableTextProvider by lazy {
|
||||
DefaultCustomizableTextProvider(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
package im.angry.openeuicc.di
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
|
||||
open class DefaultCustomizableTextProvider(private val context: Context) : CustomizableTextProvider {
|
||||
override val noEuiccExplanation: String
|
||||
|
|
@ -12,12 +10,6 @@ open class DefaultCustomizableTextProvider(private val context: Context) : Custo
|
|||
override val profileSwitchingTimeoutMessage: String
|
||||
get() = context.getString(R.string.profile_switch_timeout)
|
||||
|
||||
override val websiteUri: Uri?
|
||||
get() = null
|
||||
|
||||
override fun formatNonUsbChannelName(logicalSlotId: Int): String =
|
||||
override fun formatInternalChannelName(logicalSlotId: Int): String =
|
||||
context.getString(R.string.channel_name_format, logicalSlotId)
|
||||
|
||||
override fun formatNonUsbChannelNameWithSeId(logicalSlotId: Int, seId: EuiccChannel.SecureElementId): String =
|
||||
context.getString(R.string.channel_name_format_se, logicalSlotId, seId.id)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,16 @@
|
|||
package im.angry.openeuicc.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import im.angry.openeuicc.ui.EuiccManagementFragment
|
||||
import im.angry.openeuicc.ui.NoEuiccPlaceholderFragment
|
||||
import im.angry.openeuicc.ui.SettingsFragment
|
||||
|
||||
open class DefaultUiComponentFactory : UiComponentFactory {
|
||||
override fun createEuiccManagementFragment(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): EuiccManagementFragment =
|
||||
EuiccManagementFragment.newInstance(slotId, portId, seId)
|
||||
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||
EuiccManagementFragment.newInstance(slotId, portId)
|
||||
|
||||
override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment()
|
||||
|
||||
override fun createSettingsFragment(): Fragment = SettingsFragment()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,11 @@
|
|||
package im.angry.openeuicc.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import im.angry.openeuicc.ui.EuiccManagementFragment
|
||||
|
||||
interface UiComponentFactory {
|
||||
fun createEuiccManagementFragment(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): EuiccManagementFragment
|
||||
|
||||
fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment
|
||||
fun createNoEuiccPlaceholderFragment(): Fragment
|
||||
fun createSettingsFragment(): Fragment
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ import androidx.core.app.NotificationManagerCompat
|
|||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -381,7 +380,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
fun launchProfileDownloadTask(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
smdp: String,
|
||||
matchingId: String?,
|
||||
confirmationCode: String?,
|
||||
|
|
@ -392,8 +390,8 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
getString(R.string.task_profile_download_failure),
|
||||
R.drawable.ic_task_sim_card_download
|
||||
) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.lpa.downloadProfile(
|
||||
smdp,
|
||||
matchingId,
|
||||
|
|
@ -415,7 +413,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
fun launchProfileRenameTask(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
iccid: String,
|
||||
name: String
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
|
|
@ -424,7 +421,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
getString(R.string.task_profile_rename_failure),
|
||||
R.drawable.ic_task_rename
|
||||
) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.lpa.setNickname(
|
||||
iccid,
|
||||
name
|
||||
|
|
@ -435,7 +432,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
fun launchProfileDeleteTask(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
iccid: String
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
launchForegroundTask(
|
||||
|
|
@ -443,8 +439,8 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
getString(R.string.task_profile_delete_failure),
|
||||
R.drawable.ic_task_delete
|
||||
) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.lpa.deleteProfile(iccid)
|
||||
}
|
||||
|
||||
|
|
@ -457,7 +453,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
fun launchProfileSwitchTask(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
iccid: String,
|
||||
enable: Boolean, // Enable or disable the profile indicated in iccid
|
||||
reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect
|
||||
|
|
@ -467,9 +462,9 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
getString(R.string.task_profile_switch_failure),
|
||||
R.drawable.ic_task_switch
|
||||
) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
val (response, refreshed) =
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
val refresh = preferenceRepository.refreshAfterSwitchFlow.first()
|
||||
val response = channel.lpa.switchProfile(iccid, enable, refresh)
|
||||
if (response || !refresh) {
|
||||
|
|
@ -515,22 +510,18 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
}
|
||||
}
|
||||
|
||||
fun launchMemoryReset(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
fun launchMemoryReset(slotId: Int, portId: Int): ForegroundTaskSubscriberFlow =
|
||||
launchForegroundTask(
|
||||
getString(R.string.task_euicc_memory_reset),
|
||||
getString(R.string.task_euicc_memory_reset_failure),
|
||||
R.drawable.ic_euicc_memory_reset
|
||||
) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.lpa.euiccMemoryReset()
|
||||
}
|
||||
|
||||
preferenceRepository.notificationDeleteFlow.first()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
|
|
@ -35,7 +36,7 @@ abstract class BaseEuiccAccessActivity : AppCompatActivity() {
|
|||
bindService(
|
||||
Intent(this, EuiccChannelManagerService::class.java),
|
||||
euiccChannelManagerServiceConnection,
|
||||
BIND_AUTO_CREATE
|
||||
Context.BIND_AUTO_CREATE
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -48,4 +49,4 @@ abstract class BaseEuiccAccessActivity : AppCompatActivity() {
|
|||
* When called, euiccChannelManager is guaranteed to have been initialized
|
||||
*/
|
||||
abstract fun onInit()
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import androidx.fragment.app.DialogFragment
|
|||
import com.google.android.material.color.DynamicColors
|
||||
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)
|
||||
|
|
@ -23,4 +23,4 @@ abstract class BaseMaterialDialogFragment : DialogFragment() {
|
|||
it.window?.setBackgroundDrawableResource(R.drawable.dialog_background)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,9 +23,7 @@ import im.angry.openeuicc.common.R
|
|||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
|
||||
import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
|
||||
|
||||
|
|
@ -45,10 +43,9 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
private lateinit var infoList: RecyclerView
|
||||
|
||||
private var logicalSlotId: Int = -1
|
||||
private var seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT
|
||||
|
||||
data class Item(
|
||||
@get:StringRes
|
||||
@StringRes
|
||||
val titleResId: Int,
|
||||
val content: String?,
|
||||
val copiedToastResId: Int? = null,
|
||||
|
|
@ -59,6 +56,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_euicc_info)
|
||||
setSupportActionBar(requireViewById(R.id.toolbar))
|
||||
setupToolbarInsets()
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
swipeRefresh = requireViewById(R.id.swipe_refresh)
|
||||
|
|
@ -69,29 +67,18 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
}
|
||||
|
||||
logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
|
||||
seId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra("seId", EuiccChannel.SecureElementId::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra("seId")
|
||||
} ?: EuiccChannel.SecureElementId.DEFAULT
|
||||
|
||||
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
getString(R.string.channel_type_usb)
|
||||
} else {
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
|
||||
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId)
|
||||
}
|
||||
|
||||
title = getString(R.string.euicc_info_activity_title, channelTitle)
|
||||
|
||||
swipeRefresh.setOnRefreshListener { refresh() }
|
||||
|
||||
setupRootViewSystemBarInsets(
|
||||
window.decorView.rootView, arrayOf(
|
||||
this::activityToolbarInsetHandler,
|
||||
mainViewPaddingInsetHandler(infoList)
|
||||
)
|
||||
)
|
||||
setupRootViewInsets(infoList)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
|
|
@ -111,23 +98,8 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
swipeRefresh.isRefreshing = true
|
||||
|
||||
lifecycleScope.launch {
|
||||
euiccChannelManager.withEuiccChannel(logicalSlotId, seId) { channel ->
|
||||
// When the chip multi-SE, we need to include seId in the title (because we don't have access
|
||||
// to hasMultipleSE in the onCreate() function, we need to do it here).
|
||||
// TODO: Move channel formatting to somewhere centralized and remove this hack. (And also, of course, add support for USB)
|
||||
if (channel.hasMultipleSE && logicalSlotId != EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
withContext(Dispatchers.Main) {
|
||||
title =
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId(logicalSlotId, seId)
|
||||
}
|
||||
}
|
||||
|
||||
val items = buildEuiccInfoItems(channel)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
(infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems = items
|
||||
}
|
||||
}
|
||||
(infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems =
|
||||
euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildEuiccInfoItems)
|
||||
|
||||
swipeRefresh.isRefreshing = false
|
||||
}
|
||||
|
|
@ -142,11 +114,9 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
add(Item(R.string.euicc_info_isdr_aid, channel.isdrAid.encodeHex()))
|
||||
}
|
||||
channel.tryParseEuiccVendorInfo()?.let { vendorInfo ->
|
||||
// @formatter:off
|
||||
vendorInfo.skuName?.let { add(Item(R.string.euicc_info_sku, it)) }
|
||||
vendorInfo.serialNumber?.let { add(Item(R.string.euicc_info_sn, it, copiedToastResId = R.string.toast_sn_copied)) }
|
||||
vendorInfo.firmwareVersion?.let { add(Item(R.string.euicc_info_fw_ver, it)) }
|
||||
// @formatter:on
|
||||
}
|
||||
channel.lpa.euiccInfo2?.let { info ->
|
||||
add(Item(R.string.euicc_info_sgp22_version, info.sgp22Version.toString()))
|
||||
|
|
@ -230,4 +200,4 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
holder.bind(euiccInfoItems[position])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,8 @@ import android.widget.PopupMenu
|
|||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
|
|
@ -27,8 +29,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||
import im.angry.openeuicc.ui.wizard.DownloadWizardActivity
|
||||
|
|
@ -36,22 +38,19 @@ import im.angry.openeuicc.util.*
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
|
||||
open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||
EuiccChannelFragmentMarker {
|
||||
companion object {
|
||||
const val TAG = "EuiccManagementFragment"
|
||||
|
||||
fun newInstance(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId
|
||||
): EuiccManagementFragment =
|
||||
newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId, seId)
|
||||
fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||
newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId)
|
||||
}
|
||||
|
||||
private lateinit var swipeRefresh: SwipeRefreshLayout
|
||||
|
|
@ -59,7 +58,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
private lateinit var profileList: RecyclerView
|
||||
private var logicalSlotId: Int = -1
|
||||
private lateinit var eid: String
|
||||
private var enabledProfile: LocalProfileInfo? = null
|
||||
|
||||
private val adapter = EuiccProfileAdapter()
|
||||
|
||||
|
|
@ -91,22 +89,18 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
|
||||
val origFabMarginRight = (fab.layoutParams as ViewGroup.MarginLayoutParams).rightMargin
|
||||
val origFabMarginBottom = (fab.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin
|
||||
ViewCompat.setOnApplyWindowInsetsListener(fab) { v, insets ->
|
||||
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
|
||||
setupRootViewSystemBarInsets(
|
||||
view, arrayOf(
|
||||
mainViewPaddingInsetHandler(profileList),
|
||||
{ insets ->
|
||||
fab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
rightMargin = origFabMarginRight + insets.right
|
||||
bottomMargin = origFabMarginBottom + insets.bottom
|
||||
}
|
||||
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
rightMargin = origFabMarginRight + bars.right
|
||||
bottomMargin = origFabMarginBottom + bars.bottom
|
||||
}
|
||||
))
|
||||
|
||||
profileList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(view: RecyclerView, newState: Int) =
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE) fab.show() else fab.hide()
|
||||
})
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
setupRootViewInsets(profileList)
|
||||
|
||||
return view
|
||||
}
|
||||
|
|
@ -119,8 +113,10 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
|
||||
|
||||
fab.setOnClickListener {
|
||||
val intent = DownloadWizardActivity.newIntent(requireContext(), slotId, seId)
|
||||
startActivity(intent)
|
||||
Intent(requireContext(), DownloadWizardActivity::class.java).apply {
|
||||
putExtra("selectedLogicalSlot", logicalSlotId)
|
||||
startActivity(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,14 +141,13 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
menu.findItem(R.id.euicc_info).isVisible =
|
||||
logicalSlotId != -1
|
||||
menu.findItem(R.id.euicc_memory_reset).isVisible =
|
||||
enabledProfile == null
|
||||
runBlocking { preferenceRepository.euiccMemoryResetFlow.first() }
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.show_notifications -> {
|
||||
Intent(requireContext(), NotificationsActivity::class.java).apply {
|
||||
putExtra("logicalSlotId", logicalSlotId)
|
||||
putExtra("seId", seId)
|
||||
startActivity(this)
|
||||
}
|
||||
true
|
||||
|
|
@ -161,14 +156,13 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
R.id.euicc_info -> {
|
||||
Intent(requireContext(), EuiccInfoActivity::class.java).apply {
|
||||
putExtra("logicalSlotId", logicalSlotId)
|
||||
putExtra("seId", seId)
|
||||
startActivity(this)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.euicc_memory_reset -> {
|
||||
EuiccMemoryResetFragment.newInstance(slotId, portId, seId, eid)
|
||||
EuiccMemoryResetFragment.newInstance(slotId, portId, eid)
|
||||
.show(childFragmentManager, EuiccMemoryResetFragment.TAG)
|
||||
true
|
||||
}
|
||||
|
|
@ -213,7 +207,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
val profiles = withEuiccChannel { channel ->
|
||||
logicalSlotId = channel.logicalSlotId
|
||||
eid = channel.lpa.eID
|
||||
enabledProfile = channel.lpa.profiles.enabled
|
||||
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
||||
if (unfilteredProfileListFlow.value)
|
||||
channel.lpa.profiles
|
||||
|
|
@ -244,7 +237,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
|
||||
val err = euiccChannelManagerService
|
||||
.launchProfileSwitchTask(
|
||||
slotId, portId, seId, iccid, enable,
|
||||
slotId, portId, iccid, enable,
|
||||
reconnectTimeoutMillis = 30 * 1000
|
||||
)
|
||||
.waitDone()
|
||||
|
|
@ -297,17 +290,14 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
}
|
||||
}
|
||||
|
||||
protected open fun populatePopupWithProfileActions(
|
||||
popup: PopupMenu,
|
||||
profile: LocalProfileInfo
|
||||
) {
|
||||
protected open fun populatePopupWithProfileActions(popup: PopupMenu, profile: LocalProfileInfo) {
|
||||
popup.inflate(R.menu.profile_options)
|
||||
if (!profile.isEnabled) return
|
||||
popup.menu.findItem(R.id.enable).isVisible = false
|
||||
popup.menu.findItem(R.id.delete).isVisible = false
|
||||
|
||||
// We hide the disable option by default to avoid "bricking" some cards that won't get
|
||||
// recognized again by the phone's modem. However, we don't have that worry if we are
|
||||
// recognized again by the phone's modem. However we don't have that worry if we are
|
||||
// accessing it through a USB card reader, or when the user explicitly opted in
|
||||
if (!isUsb && !disableSafeguardFlow.value) return
|
||||
popup.menu.findItem(R.id.disable).isVisible = true
|
||||
|
|
@ -325,7 +315,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
}
|
||||
}
|
||||
|
||||
inner class FooterViewHolder : ViewHolder(FrameLayout(requireContext())) {
|
||||
inner class FooterViewHolder: ViewHolder(FrameLayout(requireContext())) {
|
||||
init {
|
||||
itemView.layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
|
|
@ -415,7 +405,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
// cannot cross profile class enable profile
|
||||
// e.g: testing -> operational or operational -> testing
|
||||
canEnable = enabledProfile == null ||
|
||||
enabledProfile.profileClass == profile.profileClass
|
||||
enabledProfile.profileClass == profile.profileClass
|
||||
}
|
||||
|
||||
private fun showOptionsMenu() {
|
||||
|
|
@ -441,36 +431,20 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.disable -> {
|
||||
enableOrDisableProfile(profile.iccid, false)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.rename -> {
|
||||
ProfileRenameFragment.newInstance(
|
||||
slotId,
|
||||
portId,
|
||||
seId,
|
||||
profile.iccid,
|
||||
profile.displayName
|
||||
)
|
||||
ProfileRenameFragment.newInstance(slotId, portId, profile.iccid, profile.displayName)
|
||||
.show(childFragmentManager, ProfileRenameFragment.TAG)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.delete -> {
|
||||
ProfileDeleteFragment.newInstance(
|
||||
slotId,
|
||||
portId,
|
||||
seId,
|
||||
profile.iccid,
|
||||
profile.displayName
|
||||
)
|
||||
ProfileDeleteFragment.newInstance(slotId, portId, profile.iccid, profile.displayName)
|
||||
.show(childFragmentManager, ProfileDeleteFragment.TAG)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
@ -482,11 +456,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
|
||||
when (ViewHolder.Type.fromInt(viewType)) {
|
||||
ViewHolder.Type.PROFILE -> {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.euicc_profile, parent, false)
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.euicc_profile, parent, false)
|
||||
ProfileViewHolder(view)
|
||||
}
|
||||
|
||||
ViewHolder.Type.FOOTER -> {
|
||||
FooterViewHolder()
|
||||
}
|
||||
|
|
@ -497,11 +469,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
position < profiles.size -> {
|
||||
ViewHolder.Type.PROFILE.value
|
||||
}
|
||||
|
||||
position >= profiles.size && position < profiles.size + footerViews.size -> {
|
||||
ViewHolder.Type.FOOTER.value
|
||||
}
|
||||
|
||||
else -> -1
|
||||
}
|
||||
|
||||
|
|
@ -512,7 +482,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
holder.setEnabledProfile(profiles.enabled)
|
||||
holder.setProfileSequenceNumber(position + 1)
|
||||
}
|
||||
|
||||
is FooterViewHolder -> {
|
||||
holder.attach(footerViews[position - profiles.size])
|
||||
}
|
||||
|
|
@ -527,4 +496,4 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
|
||||
override fun getItemCount(): Int = profiles.size + footerViews.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,11 +8,18 @@ import android.widget.EditText
|
|||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||
import im.angry.openeuicc.util.*
|
||||
import im.angry.openeuicc.util.EuiccChannelFragmentMarker
|
||||
import im.angry.openeuicc.util.EuiccProfilesChangedListener
|
||||
import im.angry.openeuicc.util.ensureEuiccChannelManager
|
||||
import im.angry.openeuicc.util.euiccChannelManagerService
|
||||
import im.angry.openeuicc.util.newInstanceEuicc
|
||||
import im.angry.openeuicc.util.notifyEuiccProfilesChanged
|
||||
import im.angry.openeuicc.util.portId
|
||||
import im.angry.openeuicc.util.slotId
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -22,8 +29,8 @@ class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
|
||||
private const val FIELD_EID = "eid"
|
||||
|
||||
fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, eid: String) =
|
||||
newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId, seId) {
|
||||
fun newInstance(slotId: Int, portId: Int, eid: String) =
|
||||
newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId) {
|
||||
putString(FIELD_EID, eid)
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +103,7 @@ class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
|
||||
euiccChannelManagerService.launchMemoryReset(slotId, portId, seId)
|
||||
euiccChannelManagerService.launchMemoryReset(slotId, portId)
|
||||
.onStart {
|
||||
parentFragment?.notifyEuiccProfilesChanged()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import androidx.activity.enableEdgeToEdge
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import im.angry.openeuicc.util.preferenceRepository
|
||||
import im.angry.openeuicc.util.setupToolbarInsets
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -23,17 +24,11 @@ class IsdrAidListActivity : AppCompatActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_isdr_aid_list)
|
||||
setSupportActionBar(requireViewById(R.id.toolbar))
|
||||
setupToolbarInsets()
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
isdrAidListEditor = requireViewById(R.id.isdr_aid_list_editor)
|
||||
|
||||
setupRootViewSystemBarInsets(
|
||||
window.decorView.rootView, arrayOf(
|
||||
this::activityToolbarInsetHandler,
|
||||
mainViewPaddingInsetHandler(isdrAidListEditor)
|
||||
)
|
||||
)
|
||||
|
||||
lifecycleScope.launch {
|
||||
preferenceRepository.isdrAidListFlow.onEach {
|
||||
isdrAidListEditor.text = Editable.Factory.getInstance().newEditable(it)
|
||||
|
|
@ -74,4 +69,4 @@ class IsdrAidListActivity : AppCompatActivity() {
|
|||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -51,18 +51,14 @@ class LogsActivity : AppCompatActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_logs)
|
||||
setSupportActionBar(requireViewById(R.id.toolbar))
|
||||
setupToolbarInsets()
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
swipeRefresh = requireViewById(R.id.swipe_refresh)
|
||||
scrollView = requireViewById(R.id.scroll_view)
|
||||
logText = requireViewById(R.id.log_text)
|
||||
|
||||
setupRootViewSystemBarInsets(
|
||||
window.decorView.rootView, arrayOf(
|
||||
this::activityToolbarInsetHandler,
|
||||
mainViewPaddingInsetHandler(scrollView)
|
||||
)
|
||||
)
|
||||
setupRootViewInsets(scrollView)
|
||||
|
||||
swipeRefresh.setOnRefreshListener {
|
||||
lifecycleScope.launch {
|
||||
|
|
@ -88,12 +84,10 @@ class LogsActivity : AppCompatActivity() {
|
|||
finish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.save -> {
|
||||
saveLogs()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
|
|
@ -113,4 +107,4 @@ class LogsActivity : AppCompatActivity() {
|
|||
scrollView.fullScroll(View.FOCUS_DOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,9 +16,6 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
|
|
@ -27,7 +24,6 @@ import com.google.android.material.tabs.TabLayout
|
|||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.ui.wizard.DownloadWizardActivity
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
|
|
@ -82,6 +78,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
setSupportActionBar(requireViewById(R.id.toolbar))
|
||||
setupToolbarInsets()
|
||||
loadingProgress = requireViewById(R.id.loading)
|
||||
tabs = requireViewById(R.id.main_tabs)
|
||||
viewPager = requireViewById(R.id.view_pager)
|
||||
|
|
@ -97,12 +94,6 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
|
||||
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
|
||||
})
|
||||
|
||||
setupRootViewSystemBarInsets(
|
||||
window.decorView.rootView, arrayOf(
|
||||
this::activityToolbarInsetHandler
|
||||
), consume = false
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
|
@ -121,12 +112,10 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
true
|
||||
}
|
||||
|
||||
R.id.reload -> {
|
||||
refresh()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
|
|
@ -137,10 +126,15 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
}
|
||||
|
||||
private fun ensureNotificationPermissions() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
|
||||
val permissions = arrayOf(android.Manifest.permission.POST_NOTIFICATIONS)
|
||||
if (permissions.all { checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED }) return
|
||||
requestPermissions(permissions, PERMISSION_REQUEST_CODE)
|
||||
val needsNotificationPerms = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
|
||||
val notificationPermsGranted =
|
||||
needsNotificationPerms && checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
|
||||
if (needsNotificationPerms && !notificationPermsGranted) {
|
||||
requestPermissions(
|
||||
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
|
||||
PERMISSION_REQUEST_CODE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun init(fromUsbEvent: Boolean = false) {
|
||||
|
|
@ -160,33 +154,21 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
euiccChannelManager.flowInternalEuiccPorts().onEach { (slotId, portId) ->
|
||||
Log.d(TAG, "slot $slotId port $portId")
|
||||
|
||||
euiccChannelManager.flowEuiccSecureElements(slotId, portId).onEach { seId ->
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
if (preferenceRepository.verboseLoggingFlow.first()) {
|
||||
Log.d(TAG, channel.lpa.eID)
|
||||
}
|
||||
// Request the system to refresh the list of profiles every time we start
|
||||
// Note that this is currently supposed to be no-op when unprivileged,
|
||||
// but it could change in the future
|
||||
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
||||
|
||||
val channelName = if (channel.hasMultipleSE) {
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId(
|
||||
channel.logicalSlotId,
|
||||
channel.seId
|
||||
)
|
||||
} else {
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelName(channel.logicalSlotId)
|
||||
}
|
||||
newPages.add(Page(channel.logicalSlotId, channelName) {
|
||||
appContainer.uiComponentFactory.createEuiccManagementFragment(
|
||||
slotId,
|
||||
portId,
|
||||
seId
|
||||
)
|
||||
})
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
if (preferenceRepository.verboseLoggingFlow.first()) {
|
||||
Log.d(TAG, channel.lpa.eID)
|
||||
}
|
||||
}.collect()
|
||||
// Request the system to refresh the list of profiles every time we start
|
||||
// Note that this is currently supposed to be no-op when unprivileged,
|
||||
// but it could change in the future
|
||||
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
||||
|
||||
val channelName =
|
||||
appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId)
|
||||
newPages.add(Page(channel.logicalSlotId, channelName) {
|
||||
appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId)
|
||||
})
|
||||
}
|
||||
}.collect()
|
||||
|
||||
// If USB readers exist, add them at the very last
|
||||
|
|
@ -227,12 +209,10 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
viewPager.currentItem = 0
|
||||
}
|
||||
|
||||
if (pages.isNotEmpty()) {
|
||||
if (pages.size > 0) {
|
||||
ensureNotificationPermissions()
|
||||
}
|
||||
|
||||
ShortcutManagerCompat.setDynamicShortcuts(this, buildShortcuts().take(4))
|
||||
|
||||
refreshing = false
|
||||
}
|
||||
|
||||
|
|
@ -251,13 +231,4 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
init(fromUsbEvent) // will set refreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun buildShortcuts(): List<ShortcutInfoCompat> {
|
||||
val downloadShortcut = ShortcutInfoCompat.Builder(this, "download")
|
||||
.setShortLabel(getString(R.string.profile_download))
|
||||
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_task_sim_card_download))
|
||||
.setIntent(DownloadWizardActivity.newIntent(this).apply { action = Intent.ACTION_VIEW })
|
||||
.build()
|
||||
return listOf(downloadShortcut)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import android.view.ViewGroup
|
|||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.OpenEuiccContextMarker
|
||||
import im.angry.openeuicc.util.*
|
||||
|
||||
class NoEuiccPlaceholderFragment : Fragment(), OpenEuiccContextMarker {
|
||||
override fun onCreateView(
|
||||
|
|
@ -20,4 +20,4 @@ class NoEuiccPlaceholderFragment : Fragment(), OpenEuiccContextMarker {
|
|||
textView.text = appContainer.customizableTextProvider.noEuiccExplanation
|
||||
return view
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.view.ContextMenu
|
||||
|
|
@ -21,7 +20,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -29,55 +27,42 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import net.typeblog.lpac_jni.LocalProfileNotification
|
||||
|
||||
class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||
class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||
private lateinit var swipeRefresh: SwipeRefreshLayout
|
||||
private lateinit var notificationList: RecyclerView
|
||||
private val notificationAdapter = NotificationAdapter()
|
||||
|
||||
private var logicalSlotId = -1
|
||||
private var seId = EuiccChannel.SecureElementId.DEFAULT
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_notifications)
|
||||
setSupportActionBar(requireViewById(R.id.toolbar))
|
||||
setupToolbarInsets()
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
swipeRefresh = requireViewById(R.id.swipe_refresh)
|
||||
notificationList = requireViewById(R.id.recycler_view)
|
||||
|
||||
setupRootViewSystemBarInsets(
|
||||
window.decorView.rootView, arrayOf(
|
||||
this::activityToolbarInsetHandler,
|
||||
mainViewPaddingInsetHandler(notificationList)
|
||||
)
|
||||
)
|
||||
setupRootViewInsets(notificationList)
|
||||
}
|
||||
|
||||
override fun onInit() {
|
||||
notificationList.apply {
|
||||
val context = this@NotificationsActivity
|
||||
adapter = notificationAdapter
|
||||
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||
addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
|
||||
registerForContextMenu(this)
|
||||
}
|
||||
notificationList.layoutManager =
|
||||
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
||||
notificationList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||
notificationList.adapter = notificationAdapter
|
||||
registerForContextMenu(notificationList)
|
||||
|
||||
logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
|
||||
seId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra("seId", EuiccChannel.SecureElementId::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra("seId")
|
||||
} ?: EuiccChannel.SecureElementId.DEFAULT
|
||||
|
||||
// This is slightly different from the MainActivity logic
|
||||
// due to the length (we don't want to display the full USB product name)
|
||||
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
getString(R.string.channel_type_usb)
|
||||
} else {
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
|
||||
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId)
|
||||
}
|
||||
|
||||
title = getString(R.string.profile_notifications_detailed_format, channelTitle)
|
||||
|
|
@ -101,7 +86,6 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
|
|||
finish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.help -> {
|
||||
AlertDialog.Builder(this, R.style.AlertDialogTheme).apply {
|
||||
setMessage(R.string.profile_notifications_help)
|
||||
|
|
@ -112,7 +96,6 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
|
|||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
|
|
@ -131,35 +114,29 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
|
|||
}
|
||||
|
||||
private fun refresh() {
|
||||
launchTask {
|
||||
notificationAdapter.notifications = withEuiccChannel { channel ->
|
||||
if (channel.hasMultipleSE && logicalSlotId != EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
withContext(Dispatchers.Main) {
|
||||
title =
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId(logicalSlotId, seId)
|
||||
}
|
||||
}
|
||||
launchTask {
|
||||
notificationAdapter.notifications =
|
||||
euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
|
||||
val nameMap = buildMap {
|
||||
for (profile in channel.lpa.profiles) {
|
||||
put(profile.iccid, profile.displayName)
|
||||
}
|
||||
}
|
||||
|
||||
val nameMap = channel.lpa.profiles
|
||||
.associate { Pair(it.iccid, it.displayName) }
|
||||
|
||||
channel.lpa.notifications.map {
|
||||
LocalProfileNotificationWrapper(it, nameMap[it.iccid] ?: "???")
|
||||
}
|
||||
}
|
||||
}
|
||||
channel.lpa.notifications.map {
|
||||
LocalProfileNotificationWrapper(it, nameMap[it.iccid] ?: "???")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <R> withEuiccChannel(fn: suspend (EuiccChannel) -> R) =
|
||||
euiccChannelManager.withEuiccChannel(logicalSlotId, seId, fn)
|
||||
|
||||
data class LocalProfileNotificationWrapper(
|
||||
val inner: LocalProfileNotification,
|
||||
val profileName: String
|
||||
)
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
inner class NotificationViewHolder(private val root: View) :
|
||||
inner class NotificationViewHolder(private val root: View):
|
||||
RecyclerView.ViewHolder(root), View.OnCreateContextMenuListener, OnMenuItemClickListener {
|
||||
private val address: TextView = root.requireViewById(R.id.notification_address)
|
||||
private val sequenceNumber: TextView =
|
||||
|
|
@ -193,8 +170,7 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
|
|||
LocalProfileNotification.Operation.Delete -> R.string.profile_notification_operation_delete
|
||||
LocalProfileNotification.Operation.Enable -> R.string.profile_notification_operation_enable
|
||||
LocalProfileNotification.Operation.Disable -> R.string.profile_notification_operation_disable
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
fun updateNotification(value: LocalProfileNotificationWrapper) {
|
||||
notification = value
|
||||
|
|
@ -205,13 +181,10 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
|
|||
value.inner.seqNumber
|
||||
)
|
||||
profileName.text = Html.fromHtml(
|
||||
root.context.getString(
|
||||
R.string.profile_notification_name_format,
|
||||
root.context.getString(R.string.profile_notification_name_format,
|
||||
operationToLocalizedText(value.inner.profileManagementOperation),
|
||||
value.profileName, value.inner.iccid
|
||||
),
|
||||
Html.FROM_HTML_MODE_COMPACT
|
||||
)
|
||||
value.profileName, value.inner.iccid),
|
||||
Html.FROM_HTML_MODE_COMPACT)
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(
|
||||
|
|
@ -231,7 +204,7 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
|
|||
R.id.notification_process -> {
|
||||
launchTask {
|
||||
withContext(Dispatchers.IO) {
|
||||
withEuiccChannel { channel ->
|
||||
euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
|
||||
channel.lpa.handleNotification(notification.inner.seqNumber)
|
||||
}
|
||||
}
|
||||
|
|
@ -240,11 +213,10 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
|
|||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.notification_delete -> {
|
||||
launchTask {
|
||||
withContext(Dispatchers.IO) {
|
||||
withEuiccChannel { channel ->
|
||||
euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
|
||||
channel.lpa.deleteNotification(notification.inner.seqNumber)
|
||||
}
|
||||
}
|
||||
|
|
@ -253,12 +225,11 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
|
|||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
inner class NotificationAdapter : RecyclerView.Adapter<NotificationViewHolder>() {
|
||||
inner class NotificationAdapter: RecyclerView.Adapter<NotificationViewHolder>() {
|
||||
var notifications: List<LocalProfileNotificationWrapper> = listOf()
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
set(value) {
|
||||
|
|
@ -278,4 +249,4 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
|
|||
holder.updateNotification(notifications[position])
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
|
|
@ -21,11 +20,11 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
private const val FIELD_ICCID = "iccid"
|
||||
private const val FIELD_NAME = "name"
|
||||
|
||||
fun newInstance(slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId, iccid: String, name: String) =
|
||||
newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId, seId) {
|
||||
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String) =
|
||||
newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) {
|
||||
putString(FIELD_ICCID, iccid)
|
||||
putString(FIELD_NAME, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val iccid by lazy {
|
||||
|
|
@ -89,7 +88,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
requireParentFragment().lifecycleScope.launch {
|
||||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, seId, iccid)
|
||||
euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid)
|
||||
.onStart {
|
||||
parentFragment?.notifyEuiccProfilesChanged()
|
||||
runCatching(::dismiss)
|
||||
|
|
@ -97,4 +96,4 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
.waitDone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ import androidx.appcompat.widget.Toolbar
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -25,13 +24,11 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
|||
|
||||
const val TAG = "ProfileRenameFragment"
|
||||
|
||||
fun newInstance(
|
||||
slotId: Int, portId: Int, seId: EuiccChannel.SecureElementId,
|
||||
iccid: String, currentName: String
|
||||
) = newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId, seId) {
|
||||
putString(FIELD_ICCID, iccid)
|
||||
putString(FIELD_CURRENT_NAME, currentName)
|
||||
}
|
||||
fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String) =
|
||||
newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) {
|
||||
putString(FIELD_ICCID, iccid)
|
||||
putString(FIELD_CURRENT_NAME, currentName)
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var toolbar: Toolbar
|
||||
|
|
@ -108,7 +105,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
|||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
val response = euiccChannelManagerService
|
||||
.launchProfileRenameTask(slotId, portId, seId, iccid, newName).waitDone()
|
||||
.launchProfileRenameTask(slotId, portId, iccid, newName).waitDone()
|
||||
|
||||
when (response) {
|
||||
is LocalProfileAssistant.ProfileNameTooLongException -> {
|
||||
|
|
@ -131,4 +128,4 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import im.angry.openeuicc.OpenEuiccApplication
|
|||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
class SettingsActivity: AppCompatActivity() {
|
||||
private val appContainer
|
||||
get() = (application as OpenEuiccApplication).appContainer
|
||||
|
||||
|
|
@ -17,14 +17,8 @@ class SettingsActivity : AppCompatActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
setSupportActionBar(requireViewById(R.id.toolbar))
|
||||
setupToolbarInsets()
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
setupRootViewSystemBarInsets(
|
||||
window.decorView.rootView, arrayOf(
|
||||
this::activityToolbarInsetHandler
|
||||
), consume = false
|
||||
)
|
||||
|
||||
val settingsFragment = appContainer.uiComponentFactory.createSettingsFragment()
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.settings_container, settingsFragment)
|
||||
|
|
@ -37,7 +31,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||
finish()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.collect
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
open class SettingsFragment : PreferenceFragmentCompat(), OpenEuiccContextMarker {
|
||||
open class SettingsFragment: PreferenceFragmentCompat() {
|
||||
private lateinit var developerPref: PreferenceCategory
|
||||
|
||||
// Hidden developer options switch
|
||||
|
|
@ -81,19 +81,15 @@ open class SettingsFragment : PreferenceFragmentCompat(), OpenEuiccContextMarker
|
|||
requirePreference<CheckBoxPreference>("pref_developer_refresh_after_switch")
|
||||
.bindBooleanFlow(preferenceRepository.refreshAfterSwitchFlow)
|
||||
|
||||
requirePreference<CheckBoxPreference>("pref_developer_euicc_memory_reset")
|
||||
.bindBooleanFlow(preferenceRepository.euiccMemoryResetFlow)
|
||||
|
||||
requirePreference<ListPreference>("pref_developer_es10x_mss")
|
||||
.bindIntFlow(preferenceRepository.es10xMssFlow, 63)
|
||||
|
||||
requirePreference<Preference>("pref_developer_isdr_aid_list").apply {
|
||||
intent = Intent(requireContext(), IsdrAidListActivity::class.java)
|
||||
}
|
||||
|
||||
requirePreference<Preference>("pref_info_website").apply {
|
||||
val uri = appContainer.customizableTextProvider.websiteUri ?: return@apply
|
||||
isVisible = true
|
||||
summary = uri.buildUpon().clearQuery().build().toString()
|
||||
intent = Intent(/* action = */ Intent.ACTION_VIEW, uri)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun <T : Preference> requirePreference(key: CharSequence) =
|
||||
|
|
@ -101,9 +97,7 @@ open class SettingsFragment : PreferenceFragmentCompat(), OpenEuiccContextMarker
|
|||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setupRootViewSystemBarInsets(requireView(), arrayOf(
|
||||
mainViewPaddingInsetHandler(requireView().requireViewById(R.id.recycler_view))
|
||||
))
|
||||
setupRootViewInsets(requireView().requireViewById(R.id.recycler_view))
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
|
|
@ -173,4 +167,4 @@ open class SettingsFragment : PreferenceFragmentCompat(), OpenEuiccContextMarker
|
|||
|
||||
overlayCat.parent?.removePreference(overlayCat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -157,9 +156,7 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
|||
R.id.child_container,
|
||||
appContainer.uiComponentFactory.createEuiccManagementFragment(
|
||||
slotId = EuiccChannelManager.USB_CHANNEL_ID,
|
||||
portId = 0,
|
||||
// TODO: What if a USB card has multiple SEs?
|
||||
seId = EuiccChannel.SecureElementId.DEFAULT
|
||||
portId = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -169,4 +166,4 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
|||
permissionButton.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,10 +7,10 @@ import androidx.preference.PreferenceCategory
|
|||
import androidx.preference.PreferenceViewHolder
|
||||
|
||||
@Suppress("unused")
|
||||
class LongSummaryPreferenceCategory : PreferenceCategory {
|
||||
constructor(ctx: Context) : super(ctx)
|
||||
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
|
||||
constructor(ctx: Context, attrs: AttributeSet, defStyle: Int) : super(ctx, attrs, defStyle)
|
||||
class LongSummaryPreferenceCategory: PreferenceCategory {
|
||||
constructor(ctx: Context): super(ctx)
|
||||
constructor(ctx: Context, attrs: AttributeSet): super(ctx, attrs)
|
||||
constructor(ctx: Context, attrs: AttributeSet, defStyle: Int): super(ctx, attrs, defStyle)
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package im.angry.openeuicc.ui.wizard
|
||||
|
||||
import android.app.assist.AssistContent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
|
|
@ -19,7 +17,6 @@ import androidx.core.view.updatePadding
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.ui.BaseEuiccAccessActivity
|
||||
import im.angry.openeuicc.util.*
|
||||
|
|
@ -27,26 +24,10 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.launch
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
|
||||
class DownloadWizardActivity : BaseEuiccAccessActivity() {
|
||||
companion object {
|
||||
const val TAG = "DownloadWizardActivity"
|
||||
|
||||
private const val FIELD_LOGICAL_SLOT_ID = "selectedLogicalSlot"
|
||||
|
||||
fun newIntent(
|
||||
context: Context,
|
||||
logicalSlotId: Int = 0,
|
||||
seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT
|
||||
) = Intent(context, DownloadWizardActivity::class.java).apply {
|
||||
val selectedSyntheticSlotId = DownloadWizardSlotSelectFragment
|
||||
.encodeSyntheticSlotId(logicalSlotId, seId)
|
||||
putExtra(FIELD_LOGICAL_SLOT_ID, selectedSyntheticSlotId)
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||
data class DownloadWizardState(
|
||||
var currentStepFragmentClassName: String?,
|
||||
var selectedSyntheticSlotId: Int,
|
||||
var selectedLogicalSlot: Int,
|
||||
var smdp: String,
|
||||
var matchingId: String?,
|
||||
var confirmationCode: String?,
|
||||
|
|
@ -85,7 +66,7 @@ class DownloadWizardActivity : BaseEuiccAccessActivity() {
|
|||
|
||||
state = DownloadWizardState(
|
||||
currentStepFragmentClassName = null,
|
||||
selectedSyntheticSlotId = intent.getIntExtra(FIELD_LOGICAL_SLOT_ID, 0),
|
||||
selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0),
|
||||
smdp = "",
|
||||
matchingId = null,
|
||||
confirmationCode = null,
|
||||
|
|
@ -113,20 +94,27 @@ class DownloadWizardActivity : BaseEuiccAccessActivity() {
|
|||
|
||||
val navigation = requireViewById<View>(R.id.download_wizard_navigation)
|
||||
val origHeight = navigation.layoutParams.height
|
||||
val fragmentRoot = requireViewById<View>(R.id.step_fragment_container)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView.rootView) { _, insets ->
|
||||
ViewCompat.setOnApplyWindowInsetsListener(navigation) { v, insets ->
|
||||
val bars = insets.getInsets(
|
||||
WindowInsetsCompat.Type.systemBars()
|
||||
or WindowInsetsCompat.Type.displayCutout()
|
||||
or WindowInsetsCompat.Type.ime()
|
||||
or WindowInsetsCompat.Type.displayCutout()
|
||||
or WindowInsetsCompat.Type.ime()
|
||||
)
|
||||
v.updatePadding(bars.left, 0, bars.right, bars.bottom)
|
||||
val newParams = navigation.layoutParams
|
||||
newParams.height = origHeight + bars.bottom
|
||||
navigation.layoutParams = newParams
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
fragmentRoot.updatePadding(bars.left, bars.top, bars.right, 0)
|
||||
navigation.updatePadding(bars.left, 0, bars.right, bars.bottom)
|
||||
val newNavParams = navigation.layoutParams
|
||||
newNavParams.height = origHeight + bars.bottom
|
||||
navigation.layoutParams = newNavParams
|
||||
val fragmentRoot = requireViewById<View>(R.id.step_fragment_container)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(fragmentRoot) { v, insets ->
|
||||
val bars = insets.getInsets(
|
||||
WindowInsetsCompat.Type.systemBars()
|
||||
or WindowInsetsCompat.Type.displayCutout()
|
||||
)
|
||||
v.updatePadding(bars.left, bars.top, bars.right, 0)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
}
|
||||
|
|
@ -163,7 +151,7 @@ class DownloadWizardActivity : BaseEuiccAccessActivity() {
|
|||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName)
|
||||
outState.putInt("selectedLogicalSlot", state.selectedSyntheticSlotId)
|
||||
outState.putInt("selectedLogicalSlot", state.selectedLogicalSlot)
|
||||
outState.putString("smdp", state.smdp)
|
||||
outState.putString("matchingId", state.matchingId)
|
||||
outState.putString("confirmationCode", state.confirmationCode)
|
||||
|
|
@ -179,20 +167,16 @@ class DownloadWizardActivity : BaseEuiccAccessActivity() {
|
|||
"currentStepFragmentClassName",
|
||||
state.currentStepFragmentClassName
|
||||
)
|
||||
state.selectedSyntheticSlotId =
|
||||
savedInstanceState.getInt("selectedSyntheticSlotId", state.selectedSyntheticSlotId)
|
||||
state.selectedLogicalSlot =
|
||||
savedInstanceState.getInt("selectedLogicalSlot", state.selectedLogicalSlot)
|
||||
state.smdp = savedInstanceState.getString("smdp", state.smdp)
|
||||
state.matchingId = savedInstanceState.getString("matchingId", state.matchingId)
|
||||
state.imei = savedInstanceState.getString("imei", state.imei)
|
||||
state.downloadStarted =
|
||||
savedInstanceState.getBoolean("downloadStarted", state.downloadStarted)
|
||||
state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID)
|
||||
state.confirmationCode =
|
||||
savedInstanceState.getString("confirmationCode", state.confirmationCode)
|
||||
state.confirmationCodeRequired = savedInstanceState.getBoolean(
|
||||
"confirmationCodeRequired",
|
||||
state.confirmationCodeRequired
|
||||
)
|
||||
state.confirmationCode = savedInstanceState.getString("confirmationCode", state.confirmationCode)
|
||||
state.confirmationCodeRequired = savedInstanceState.getBoolean("confirmationCodeRequired", state.confirmationCodeRequired)
|
||||
}
|
||||
|
||||
private fun onPrevPressed() {
|
||||
|
|
@ -216,13 +200,10 @@ class DownloadWizardActivity : BaseEuiccAccessActivity() {
|
|||
progressBar.isIndeterminate = true
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
if (state.selectedSyntheticSlotId >= 0) {
|
||||
if (state.selectedLogicalSlot >= 0) {
|
||||
try {
|
||||
val (slotId, seId) = DownloadWizardSlotSelectFragment.decodeSyntheticSlotId(
|
||||
state.selectedSyntheticSlotId
|
||||
)
|
||||
// This is run on IO by default
|
||||
euiccChannelManager.withEuiccChannel(slotId, seId) { channel ->
|
||||
euiccChannelManager.withEuiccChannel(state.selectedLogicalSlot) { channel ->
|
||||
// Be _very_ sure that the channel we got is valid
|
||||
if (!channel.valid) throw EuiccChannelManager.EuiccChannelNotFoundException()
|
||||
}
|
||||
|
|
@ -346,4 +327,4 @@ class DownloadWizardActivity : BaseEuiccAccessActivity() {
|
|||
|
||||
open fun beforeNext() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -138,4 +138,4 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS
|
|||
|
||||
ret.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -170,4 +170,4 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
|||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -153,12 +153,7 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
|||
} else {
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
|
||||
val (logicalSlotId, seId) = DownloadWizardSlotSelectFragment.decodeSyntheticSlotId(state.selectedSyntheticSlotId)
|
||||
|
||||
val (slotId, portId) = euiccChannelManager.withEuiccChannel(
|
||||
logicalSlotId,
|
||||
seId
|
||||
) { channel ->
|
||||
val (slotId, portId) = euiccChannelManager.withEuiccChannel(state.selectedLogicalSlot) { channel ->
|
||||
Pair(channel.slotId, channel.portId)
|
||||
}
|
||||
|
||||
|
|
@ -168,7 +163,6 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
|||
val ret = euiccChannelManagerService.launchProfileDownloadTask(
|
||||
slotId,
|
||||
portId,
|
||||
seId,
|
||||
state.smdp,
|
||||
state.matchingId,
|
||||
state.confirmationCode,
|
||||
|
|
@ -264,4 +258,4 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
|||
holder.bind(progressItems[position])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,46 +7,36 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
||||
companion object {
|
||||
internal fun encodeSyntheticSlotId(logicalSlotId: Int, seId: EuiccChannel.SecureElementId): Int =
|
||||
(logicalSlotId shl 16) + seId.id
|
||||
|
||||
internal fun decodeSyntheticSlotId(id: Int): Pair<Int, EuiccChannel.SecureElementId> =
|
||||
Pair(id shr 16, EuiccChannel.SecureElementId.createFromInt(id and 0xFF))
|
||||
const val LOW_NVRAM_THRESHOLD =
|
||||
30 * 1024 // < 30 KiB, alert about potential download failure
|
||||
}
|
||||
|
||||
private data class SlotInfo(
|
||||
val logicalSlotId: Int,
|
||||
val isRemovable: Boolean,
|
||||
val hasMultiplePorts: Boolean,
|
||||
val hasMultipleSEs: Boolean,
|
||||
val portId: Int,
|
||||
val seId: EuiccChannel.SecureElementId,
|
||||
val eID: String,
|
||||
val freeSpace: Int,
|
||||
val imei: String,
|
||||
val enabledProfileName: String?,
|
||||
val intrinsicChannelName: String?,
|
||||
) {
|
||||
// A synthetic slot ID used to uniquely identify this slot + SE chip in the download process
|
||||
// We assume we don't have anywhere near 2^16 ports...
|
||||
val syntheticSlotId: Int = (logicalSlotId shl 16) + seId.id
|
||||
}
|
||||
)
|
||||
|
||||
private var loaded = false
|
||||
|
||||
|
|
@ -66,6 +56,25 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
|
||||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
|
||||
|
||||
override fun beforeNext() {
|
||||
super.beforeNext()
|
||||
|
||||
if (adapter.selected.freeSpace < LOW_NVRAM_THRESHOLD) {
|
||||
val activity = requireActivity()
|
||||
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.profile_download_low_nvram_title)
|
||||
setMessage(R.string.profile_download_low_nvram_message)
|
||||
setCancelable(true)
|
||||
setPositiveButton(android.R.string.ok, null)
|
||||
setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
activity.finish()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
|
@ -76,12 +85,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
recyclerView.adapter = adapter
|
||||
recyclerView.layoutManager =
|
||||
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
|
||||
recyclerView.addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
requireContext(),
|
||||
LinearLayoutManager.VERTICAL
|
||||
)
|
||||
)
|
||||
recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL))
|
||||
return view
|
||||
}
|
||||
|
||||
|
|
@ -93,42 +97,37 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged", "MissingPermission")
|
||||
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||
private suspend fun init() {
|
||||
ensureEuiccChannelManager()
|
||||
showProgressBar(-1)
|
||||
val slots = euiccChannelManager.flowAllOpenEuiccPorts().flatMapConcat { (slotId, portId) ->
|
||||
euiccChannelManager.flowEuiccSecureElements(slotId, portId).map { seId ->
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
SlotInfo(
|
||||
channel.logicalSlotId,
|
||||
channel.port.card.isRemovable,
|
||||
channel.port.card.ports.size > 1,
|
||||
channel.hasMultipleSE,
|
||||
channel.portId,
|
||||
channel.seId,
|
||||
channel.lpa.eID,
|
||||
channel.lpa.euiccInfo2?.freeNvram ?: 0,
|
||||
try {
|
||||
telephonyManager.getImei(channel.logicalSlotId) ?: ""
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
},
|
||||
channel.lpa.profiles.enabled?.displayName,
|
||||
channel.intrinsicChannelName,
|
||||
)
|
||||
}
|
||||
val slots = euiccChannelManager.flowAllOpenEuiccPorts().map { (slotId, portId) ->
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
SlotInfo(
|
||||
channel.logicalSlotId,
|
||||
channel.port.card.isRemovable,
|
||||
channel.port.card.ports.size > 1,
|
||||
channel.portId,
|
||||
channel.lpa.eID,
|
||||
channel.lpa.euiccInfo2?.freeNvram ?: 0,
|
||||
try {
|
||||
telephonyManager.getImei(channel.logicalSlotId) ?: ""
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
},
|
||||
channel.lpa.profiles.enabled?.displayName,
|
||||
channel.intrinsicChannelName,
|
||||
)
|
||||
}
|
||||
}.toList().sortedBy { it.syntheticSlotId }
|
||||
}.toList().sortedBy { it.logicalSlotId }
|
||||
adapter.slots = slots
|
||||
|
||||
// Ensure we always have a selected slot by default
|
||||
val selectedIdx = slots.indexOfFirst { it.syntheticSlotId == state.selectedSyntheticSlotId }
|
||||
val selectedIdx = slots.indexOfFirst { it.logicalSlotId == state.selectedLogicalSlot }
|
||||
adapter.currentSelectedIdx = if (selectedIdx > 0) {
|
||||
selectedIdx
|
||||
} else {
|
||||
if (slots.isNotEmpty()) {
|
||||
state.selectedSyntheticSlotId = slots[0].syntheticSlotId
|
||||
state.selectedLogicalSlot = slots[0].logicalSlotId
|
||||
}
|
||||
0
|
||||
}
|
||||
|
|
@ -168,8 +167,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
adapter.notifyItemChanged(lastIdx)
|
||||
adapter.notifyItemChanged(curIdx)
|
||||
// Selected index isn't logical slot ID directly, needs a conversion
|
||||
state.selectedSyntheticSlotId =
|
||||
adapter.slots[adapter.currentSelectedIdx].syntheticSlotId
|
||||
state.selectedLogicalSlot = adapter.slots[adapter.currentSelectedIdx].logicalSlotId
|
||||
state.imei = adapter.slots[adapter.currentSelectedIdx].imei
|
||||
}
|
||||
|
||||
|
|
@ -189,17 +187,11 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
|
||||
title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
item.intrinsicChannelName ?: root.context.getString(R.string.channel_type_usb)
|
||||
} else if (item.hasMultipleSEs) {
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId(
|
||||
item.logicalSlotId,
|
||||
item.seId
|
||||
)
|
||||
} else {
|
||||
appContainer.customizableTextProvider.formatNonUsbChannelName(item.logicalSlotId)
|
||||
appContainer.customizableTextProvider.formatInternalChannelName(item.logicalSlotId)
|
||||
}
|
||||
eID.text = item.eID
|
||||
activeProfile.text = item.enabledProfileName
|
||||
?: root.context.getString(R.string.profile_no_enabled_profile)
|
||||
activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.profile_no_enabled_profile)
|
||||
freeSpace.text = formatFreeSpace(item.freeSpace)
|
||||
checkBox.isChecked = adapter.currentSelectedIdx == idx
|
||||
}
|
||||
|
|
@ -213,8 +205,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
get() = slots[currentSelectedIdx]
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SlotItemHolder {
|
||||
val root = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.download_slot_item, parent, false)
|
||||
val root = LayoutInflater.from(parent.context).inflate(R.layout.download_slot_item, parent, false)
|
||||
return SlotItemHolder(root)
|
||||
}
|
||||
|
||||
|
|
@ -224,4 +215,4 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
holder.bind(slots[position], position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -167,8 +167,8 @@ enum class SimplifiedErrorMessages(
|
|||
|
||||
private fun fromAPDUResponse(resp: ByteArray): SimplifiedErrorMessages? {
|
||||
val isSuccess = resp.size >= 2 &&
|
||||
resp[resp.size - 2] == 0x90.toByte() &&
|
||||
resp[resp.size - 1] == 0x00.toByte()
|
||||
resp[resp.size - 2] == 0x90.toByte() &&
|
||||
resp[resp.size - 1] == 0x00.toByte()
|
||||
if (isSuccess) return null
|
||||
return CardInternalError
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
|
|
@ -10,7 +9,6 @@ import im.angry.openeuicc.ui.BaseEuiccAccessActivity
|
|||
|
||||
private const val FIELD_SLOT_ID = "slotId"
|
||||
private const val FIELD_PORT_ID = "portId"
|
||||
private const val FIELD_SE_ID = "seId"
|
||||
|
||||
interface EuiccChannelFragmentMarker : OpenEuiccContextMarker
|
||||
|
||||
|
|
@ -19,19 +17,12 @@ private typealias BundleSetter = Bundle.() -> Unit
|
|||
// We must use extension functions because there is no way to add bounds to the type of "self"
|
||||
// in the definition of an interface, so the only way is to limit where the extension functions
|
||||
// can be applied.
|
||||
fun <T> newInstanceEuicc(
|
||||
clazz: Class<T>,
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
addArguments: BundleSetter = {}
|
||||
): T
|
||||
where T : Fragment, T : EuiccChannelFragmentMarker =
|
||||
fun <T> newInstanceEuicc(clazz: Class<T>, slotId: Int, portId: Int, addArguments: BundleSetter = {}): T
|
||||
where T : Fragment, T : EuiccChannelFragmentMarker =
|
||||
clazz.getDeclaredConstructor().newInstance().apply {
|
||||
arguments = Bundle()
|
||||
arguments!!.putInt(FIELD_SLOT_ID, slotId)
|
||||
arguments!!.putInt(FIELD_PORT_ID, portId)
|
||||
arguments!!.putParcelable(FIELD_SE_ID, seId)
|
||||
arguments!!.addArguments()
|
||||
}
|
||||
|
||||
|
|
@ -39,43 +30,31 @@ fun <T> newInstanceEuicc(
|
|||
// `channel` requires that the channel actually exists in EuiccChannelManager, which is
|
||||
// not always the case during operations such as switching
|
||||
val <T> T.slotId: Int
|
||||
where T : Fragment, T : EuiccChannelFragmentMarker
|
||||
where T : Fragment, T : EuiccChannelFragmentMarker
|
||||
get() = requireArguments().getInt(FIELD_SLOT_ID)
|
||||
val <T> T.portId: Int
|
||||
where T : Fragment, T : EuiccChannelFragmentMarker
|
||||
where T : Fragment, T : EuiccChannelFragmentMarker
|
||||
get() = requireArguments().getInt(FIELD_PORT_ID)
|
||||
val <T> T.seId: EuiccChannel.SecureElementId
|
||||
where T : Fragment, T : EuiccChannelFragmentMarker
|
||||
get() =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
requireArguments().getParcelable(
|
||||
FIELD_SE_ID,
|
||||
EuiccChannel.SecureElementId::class.java
|
||||
)!!
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
requireArguments().getParcelable(FIELD_SE_ID)!!
|
||||
}
|
||||
val <T> T.isUsb: Boolean
|
||||
where T : Fragment, T : EuiccChannelFragmentMarker
|
||||
where T : Fragment, T : EuiccChannelFragmentMarker
|
||||
get() = slotId == EuiccChannelManager.USB_CHANNEL_ID
|
||||
|
||||
private fun <T> T.requireEuiccActivity(): BaseEuiccAccessActivity
|
||||
where T : Fragment, T : OpenEuiccContextMarker =
|
||||
where T : Fragment, T : OpenEuiccContextMarker =
|
||||
requireActivity() as BaseEuiccAccessActivity
|
||||
|
||||
val <T> T.euiccChannelManager: EuiccChannelManager
|
||||
where T : Fragment, T : OpenEuiccContextMarker
|
||||
where T : Fragment, T : OpenEuiccContextMarker
|
||||
get() = requireEuiccActivity().euiccChannelManager
|
||||
|
||||
val <T> T.euiccChannelManagerService: EuiccChannelManagerService
|
||||
where T : Fragment, T : OpenEuiccContextMarker
|
||||
where T : Fragment, T : OpenEuiccContextMarker
|
||||
get() = requireEuiccActivity().euiccChannelManagerService
|
||||
|
||||
suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R
|
||||
where T : Fragment, T : EuiccChannelFragmentMarker {
|
||||
where T : Fragment, T : EuiccChannelFragmentMarker {
|
||||
ensureEuiccChannelManager()
|
||||
return euiccChannelManager.withEuiccChannel(slotId, portId, seId, fn)
|
||||
return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
|
||||
}
|
||||
|
||||
suspend fun <T> T.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker =
|
||||
|
|
@ -90,4 +69,4 @@ fun <T> T.notifyEuiccProfilesChanged() where T : Fragment {
|
|||
|
||||
interface EuiccProfilesChangedListener {
|
||||
fun onEuiccProfilesChanged()
|
||||
}
|
||||
}
|
||||
|
|
@ -79,10 +79,9 @@ fun LocalProfileAssistant.disableActiveProfileKeepIccId(refresh: Boolean): Strin
|
|||
suspend inline fun EuiccChannelManager.beginTrackedOperation(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
seId: EuiccChannel.SecureElementId,
|
||||
op: () -> Boolean
|
||||
) {
|
||||
val latestSeq = withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
val latestSeq = withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.lpa.notifications.firstOrNull()?.seqNumber
|
||||
?: 0
|
||||
}
|
||||
|
|
@ -92,7 +91,7 @@ suspend inline fun EuiccChannelManager.beginTrackedOperation(
|
|||
try {
|
||||
// Note that the exact instance of "channel" might have changed here if reconnected;
|
||||
// this is why we need to use two distinct calls to withEuiccChannel()
|
||||
withEuiccChannel(slotId, portId, seId) { channel ->
|
||||
withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.lpa.notifications.filter { it.seqNumber > latestSeq }.forEach {
|
||||
Log.d(TAG, "Handling notification $it")
|
||||
channel.lpa.handleNotification(it.seqNumber)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ internal object PreferenceKeys {
|
|||
val REFRESH_AFTER_SWITCH = booleanPreferencesKey("refresh_after_switch")
|
||||
val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list")
|
||||
val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
|
||||
val EUICC_MEMORY_RESET = booleanPreferencesKey("euicc_memory_reset")
|
||||
val ISDR_AID_LIST = stringPreferencesKey("isdr_aid_list")
|
||||
val ES10X_MSS = intPreferencesKey("es10x_mss")
|
||||
}
|
||||
|
|
@ -49,9 +50,14 @@ internal object PreferenceConstants {
|
|||
# Refs: <https://euicc-manual.osmocom.org/docs/lpa/applet-id-oem/>
|
||||
|
||||
# eUICC standard
|
||||
# Even if this AID is deleted here, it will still be attempted as the last resort.
|
||||
$EUICC_DEFAULT_ISDR_AID
|
||||
|
||||
# ESTKme AUX (deprecated, use SE0 instead)
|
||||
A06573746B6D65FFFFFFFF4953442D52
|
||||
|
||||
# ESTKme SE0
|
||||
A06573746B6D65FFFF4953442D522030
|
||||
|
||||
# eSIM.me
|
||||
A0000005591010000000008900000300
|
||||
|
||||
|
|
@ -60,20 +66,6 @@ internal object PreferenceConstants {
|
|||
|
||||
# Xesim
|
||||
A0000005591010FFFFFFFF8900000177
|
||||
|
||||
# LinksField
|
||||
A000000559104C696E6B736669656C64
|
||||
|
||||
# ESTKme SE0
|
||||
# For multi-SE eSTK.me products, this will always be attempted even if removed from the list
|
||||
${ESTKme.ESTK_SE0_AID.encodeHex()}
|
||||
|
||||
# ESTKme SE1
|
||||
# For multi-SE eSTK.me products, this will always be attempted even if removed from the list
|
||||
${ESTKme.ESTK_SE1_AID.encodeHex()}
|
||||
|
||||
# ESTKme AUX (deprecated, use SE0 instead)
|
||||
A06573746B6D65FFFFFFFF4953442D52
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
|
|
@ -93,6 +85,7 @@ open class PreferenceRepository(private val context: Context) {
|
|||
val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false)
|
||||
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
|
||||
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
|
||||
val euiccMemoryResetFlow = bindFlow(PreferenceKeys.EUICC_MEMORY_RESET, false)
|
||||
val isdrAidListFlow = bindFlow(
|
||||
PreferenceKeys.ISDR_AID_LIST,
|
||||
PreferenceConstants.DEFAULT_AID_LIST,
|
||||
|
|
|
|||
|
|
@ -31,23 +31,13 @@ fun formatFreeSpace(size: Int): String =
|
|||
|
||||
/**
|
||||
* Decode a list of potential ISDR AIDs, one per line. Lines starting with '#' are ignored.
|
||||
* If none is found, at least EUICC_DEFAULT_ISDR_AID is returned.
|
||||
* If EUICC_DEFAULT_ISDR_AID is not contained in the list, it is always appended as the last
|
||||
* element.
|
||||
* If none is found, at least EUICC_DEFAULT_ISDR_AID is returned
|
||||
*/
|
||||
fun parseIsdrAidList(s: String): List<ByteArray> {
|
||||
val ret = s.split('\n')
|
||||
.asSequence()
|
||||
fun parseIsdrAidList(s: String): List<ByteArray> =
|
||||
s.split('\n')
|
||||
.map(String::trim)
|
||||
.filter { !it.startsWith('#') }
|
||||
.map(String::trim)
|
||||
.filter(String::isNotEmpty)
|
||||
.mapNotNull { runCatching(it::decodeHex).getOrNull() }
|
||||
.toList()
|
||||
|
||||
return if (!ret.any { it.contentEquals(EUICC_DEFAULT_ISDR_AID.decodeHex()) }) {
|
||||
ret + EUICC_DEFAULT_ISDR_AID.decodeHex()
|
||||
} else {
|
||||
ret
|
||||
}
|
||||
}
|
||||
.ifEmpty { listOf(EUICC_DEFAULT_ISDR_AID.decodeHex()) }
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.se.omapi.Reader
|
||||
import android.se.omapi.SEService
|
||||
import android.telephony.TelephonyManager
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
val TelephonyManager.activeModemCountCompat: Int
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
|
|
@ -48,11 +55,16 @@ interface UiccPortInfoCompat {
|
|||
val logicalSlotIndex: Int
|
||||
}
|
||||
|
||||
data class FakeUiccCardInfoCompat(override val physicalSlotIndex: Int) : UiccCardInfoCompat {
|
||||
override val ports: Collection<UiccPortInfoCompat> = listOf(FakeUiccPortInfoCompat(this))
|
||||
data class FakeUiccCardInfoCompat(
|
||||
override val physicalSlotIndex: Int,
|
||||
): UiccCardInfoCompat {
|
||||
override val ports: Collection<UiccPortInfoCompat> =
|
||||
listOf(FakeUiccPortInfoCompat(this))
|
||||
}
|
||||
|
||||
data class FakeUiccPortInfoCompat(override val card: UiccCardInfoCompat) : UiccPortInfoCompat {
|
||||
data class FakeUiccPortInfoCompat(
|
||||
override val card: UiccCardInfoCompat
|
||||
): UiccPortInfoCompat {
|
||||
override val portIndex: Int = 0
|
||||
override val logicalSlotIndex: Int = card.physicalSlotIndex
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import androidx.activity.result.ActivityResultCaller
|
|||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
|
|
@ -36,71 +35,46 @@ fun DialogFragment.setWidthPercent(percentage: Int) {
|
|||
}
|
||||
|
||||
/**
|
||||
* A handler function for `setupRootViewSystemBarInsets`, which is intended to set up
|
||||
* insets for the top toolbar, in the case where the activity contains a toolbar with the default
|
||||
* ID `R.id.toolbar`, and a spacer `R.id.toolbar_spacer` for status bar background.
|
||||
* Call this method (in onActivityCreated or later)
|
||||
* to make the dialog near-full screen.
|
||||
*/
|
||||
fun AppCompatActivity.activityToolbarInsetHandler(insets: Insets) {
|
||||
val toolbar = requireViewById<View>(R.id.toolbar)
|
||||
toolbar.apply {
|
||||
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top
|
||||
}
|
||||
updatePadding(insets.left, paddingTop, insets.right, paddingBottom)
|
||||
}
|
||||
|
||||
requireViewById<View>(R.id.toolbar_spacer).updateLayoutParams {
|
||||
height = toolbar.top
|
||||
}
|
||||
fun DialogFragment.setFullScreen() {
|
||||
dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* A handler function for `setupRootViewSystemBarInsets`, which is intended to set up
|
||||
* left, right, and bottom padding for a "main view", usually a RecyclerView or a ScrollView.
|
||||
*
|
||||
* It ignores top paddings because that should be handled by the toolbar handler for the activity.
|
||||
* See above.
|
||||
*/
|
||||
fun mainViewPaddingInsetHandler(v: View): (Insets) -> Unit = { insets ->
|
||||
// Disable clipToPadding to make sure content actually display
|
||||
if (v is ViewGroup) {
|
||||
v.clipToPadding = false
|
||||
}
|
||||
v.updatePadding(insets.left, v.paddingTop, insets.right, insets.bottom)
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper for `ViewCompat.setOnApplyWindowInsetsListener`, which should only be called
|
||||
* on a root view of a certain component. For activities, this should usually be `window.decorView.rootView`,
|
||||
* and for Fragments this should be the outermost layer of view it inflated during creation.
|
||||
*
|
||||
* This function takes in an array of handler functions, and is expected to only ever be called
|
||||
* on views belonging to the same hierarchy. All sibling views should be handled from the array of
|
||||
* handler functions, rather than a separate call to this function OR `ViewCompat.setOnApplyWindowInsetsListener`.
|
||||
*
|
||||
* The reason this function exists is that on some versions of Android, the dispatch of window inset
|
||||
* events is completely broken. If an inset event is handled by a view, it will never be seen by any of
|
||||
* its siblings. By wrapping this function and restricting its use to only the "main" view hierarchy and
|
||||
* handling all sibling views using our own handler functions, we work around that issue.
|
||||
*
|
||||
* Note that this function by default returns `WindowInsetCompat.CONSUME`, which will prevent the event from
|
||||
* being dispatched further to child views. This may be a problem for activities that act as fragment hosts.
|
||||
* In that case, please set `consume = false` in order for the event to propagate.
|
||||
*/
|
||||
fun setupRootViewSystemBarInsets(rootView: View, handlers: Array<(Insets) -> Unit>, consume: Boolean = true) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets ->
|
||||
fun AppCompatActivity.setupToolbarInsets() {
|
||||
val spacer = requireViewById<View>(R.id.toolbar_spacer)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(requireViewById(R.id.toolbar)) { v, insets ->
|
||||
val bars = insets.getInsets(
|
||||
WindowInsetsCompat.Type.systemBars()
|
||||
or WindowInsetsCompat.Type.displayCutout()
|
||||
or WindowInsetsCompat.Type.displayCutout()
|
||||
)
|
||||
|
||||
handlers.forEach { it(bars) }
|
||||
|
||||
if (consume) {
|
||||
WindowInsetsCompat.CONSUMED
|
||||
} else {
|
||||
insets
|
||||
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = bars.top
|
||||
}
|
||||
v.updatePadding(bars.left, v.paddingTop, bars.right, v.paddingBottom)
|
||||
|
||||
spacer.updateLayoutParams {
|
||||
height = v.top
|
||||
}
|
||||
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
}
|
||||
|
||||
fun setupRootViewInsets(view: ViewGroup) {
|
||||
// Disable clipToPadding to make sure content actually display
|
||||
view.clipToPadding = false
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
|
||||
val bars = insets.getInsets(
|
||||
WindowInsetsCompat.Type.systemBars()
|
||||
or WindowInsetsCompat.Type.displayCutout()
|
||||
)
|
||||
|
||||
v.updatePadding(bars.left, v.paddingTop, bars.right, bars.bottom)
|
||||
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,4 +121,4 @@ fun <T : ActivityResultCaller> T.setupLogSaving(
|
|||
lastFileName = getLogFileName()
|
||||
launchSaveIntent.launch(lastFileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.se.omapi.SEService
|
||||
|
|
@ -18,18 +17,19 @@ import kotlinx.coroutines.runBlocking
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.RuntimeException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
val Context.packageInfo: PackageInfo
|
||||
get() = packageManager.getPackageInfo(packageName, /* flags = */ 0)!!
|
||||
|
||||
val Context.selfAppVersion: String
|
||||
get() = packageInfo.versionName!!
|
||||
|
||||
val Context.selfAppVersionCode: Long
|
||||
get() = packageInfo.longVersionCode
|
||||
get() =
|
||||
try {
|
||||
val pInfo = packageManager.getPackageInfo(packageName, 0)
|
||||
pInfo.versionName!!
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
|
||||
suspend fun readSelfLog(lines: Int = 2048): String = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
|
|
@ -96,12 +96,13 @@ inline fun <T> Bitmap.use(f: (Bitmap) -> T): T =
|
|||
recycle()
|
||||
}
|
||||
|
||||
fun decodeQrFromBitmap(bmp: Bitmap): String? = runCatching {
|
||||
val pixels = IntArray(bmp.width * bmp.height)
|
||||
bmp.getPixels(pixels, 0, bmp.width, 0, 0, bmp.width, bmp.height)
|
||||
fun decodeQrFromBitmap(bmp: Bitmap): String? =
|
||||
runCatching {
|
||||
val pixels = IntArray(bmp.width * bmp.height)
|
||||
bmp.getPixels(pixels, 0, bmp.width, 0, 0, bmp.width, bmp.height)
|
||||
|
||||
val luminanceSource = RGBLuminanceSource(bmp.width, bmp.height, pixels)
|
||||
val binaryBmp = BinaryBitmap(HybridBinarizer(luminanceSource))
|
||||
val luminanceSource = RGBLuminanceSource(bmp.width, bmp.height, pixels)
|
||||
val binaryBmp = BinaryBitmap(HybridBinarizer(luminanceSource))
|
||||
|
||||
QRCodeReader().decode(binaryBmp).text
|
||||
}.getOrNull()
|
||||
QRCodeReader().decode(binaryBmp).text
|
||||
}.getOrNull()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.util.Log
|
||||
import im.angry.openeuicc.core.ApduInterfaceAtrProvider
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import net.typeblog.lpac_jni.Version
|
||||
|
||||
|
|
@ -15,47 +16,20 @@ private val EUICC_VENDORS: Array<EuiccVendor> = arrayOf(ESTKme(), SIMLink())
|
|||
fun EuiccChannel.tryParseEuiccVendorInfo(): EuiccVendorInfo? =
|
||||
EUICC_VENDORS.firstNotNullOfOrNull { it.tryParseEuiccVendorInfo(this) }
|
||||
|
||||
fun EuiccChannel.queryVendorAidListTransformation(aidList: List<ByteArray>): Pair<List<ByteArray>, VendorAidDecider>? =
|
||||
EUICC_VENDORS.firstNotNullOfOrNull { it.transformAidListIfNeeded(this, aidList) }
|
||||
|
||||
fun interface VendorAidDecider {
|
||||
/**
|
||||
* Given a list of already opened AIDs, should we still attempt to open the next?
|
||||
*/
|
||||
fun shouldOpenMore(openedAids: List<ByteArray>, nextAid: ByteArray): Boolean
|
||||
}
|
||||
|
||||
interface EuiccVendor {
|
||||
fun tryParseEuiccVendorInfo(channel: EuiccChannel): EuiccVendorInfo?
|
||||
|
||||
/**
|
||||
* Removable eSIM products from some vendors may prefer a vendor-specific list of AIDs or
|
||||
* a specific ordering. For example, multi-SE products from eSTK.me might prefer us trying
|
||||
* SE0 and SE1 AIDs first instead of the generic GSMA ISD-R AID. This method is intended
|
||||
* to implement these vendor-specific cases.
|
||||
*
|
||||
* This method is called on an already opened `EuiccChannel`. If the method returns a non-null
|
||||
* value, the channel will be closed and the process that attempts to open all channels will
|
||||
* be restarted from the beginning. The method will not be called again for the same chip,
|
||||
* but it should still ensure idempotency when called with an already-transformed input.
|
||||
*
|
||||
* The second return value of this method is used to decide when we should stop attempting more
|
||||
* AIDs from the list.
|
||||
*/
|
||||
fun transformAidListIfNeeded(
|
||||
referenceChannel: EuiccChannel,
|
||||
aidList: List<ByteArray>
|
||||
): Pair<List<ByteArray>, VendorAidDecider>? = null
|
||||
}
|
||||
|
||||
class ESTKme : EuiccVendor {
|
||||
private class ESTKme : EuiccVendor {
|
||||
companion object {
|
||||
private val PRODUCT_AID = "A06573746B6D65FFFFFFFFFFFF6D6774".decodeHex()
|
||||
|
||||
val ESTK_SE0_AID = "A06573746B6D65FFFF4953442D522030".decodeHex()
|
||||
val ESTK_SE1_AID = "A06573746B6D65FFFF4953442D522031".decodeHex()
|
||||
}
|
||||
|
||||
private fun checkAtr(channel: EuiccChannel): Boolean =
|
||||
(channel.apduInterface as? ApduInterfaceAtrProvider)
|
||||
?.atr?.decodeToString()?.contains("estk.me")
|
||||
?: false
|
||||
|
||||
private fun decodeAsn1String(b: ByteArray): String? {
|
||||
if (b.size < 2) return null
|
||||
if (b[b.size - 2] != 0x90.toByte() || b[b.size - 1] != 0x00.toByte()) return null
|
||||
|
|
@ -63,6 +37,8 @@ class ESTKme : EuiccVendor {
|
|||
}
|
||||
|
||||
override fun tryParseEuiccVendorInfo(channel: EuiccChannel): EuiccVendorInfo? {
|
||||
if (!checkAtr(channel)) return null
|
||||
|
||||
val iface = channel.apduInterface
|
||||
return try {
|
||||
iface.withLogicalChannel(PRODUCT_AID) { transmit ->
|
||||
|
|
@ -83,38 +59,9 @@ class ESTKme : EuiccVendor {
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun transformAidListIfNeeded(
|
||||
referenceChannel: EuiccChannel,
|
||||
aidList: List<ByteArray>
|
||||
): Pair<List<ByteArray>, VendorAidDecider>? {
|
||||
try {
|
||||
referenceChannel.apduInterface.withLogicalChannel(PRODUCT_AID) {}
|
||||
} catch (_: Exception) {
|
||||
// Not eSTK!
|
||||
return null
|
||||
}
|
||||
|
||||
// If we get here, this is eSTK, and we need to rearrange aidList such that:
|
||||
// 1. SE0 and SE1 AIDs are _always_ included in the list
|
||||
// 2. SE0 and SE1 AIDs are always sorted at the beginning of the list
|
||||
val expected = listOf(ESTK_SE0_AID, ESTK_SE1_AID, *aidList.filter {
|
||||
!it.contentEquals(ESTK_SE0_AID) && !it.contentEquals(ESTK_SE1_AID)
|
||||
}.toTypedArray())
|
||||
|
||||
return if (expected == aidList) {
|
||||
null
|
||||
} else {
|
||||
Pair(expected, VendorAidDecider { openedAids, nextAid ->
|
||||
// Don't open any more channels if we have reached the GSMA default AID and at least 1
|
||||
// eSTK AID has been opened (note that above we re-sorted them to the top of the list)
|
||||
!(openedAids.isNotEmpty() && nextAid.contentEquals(EUICC_DEFAULT_ISDR_AID.decodeHex()))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SIMLink : EuiccVendor {
|
||||
private class SIMLink : EuiccVendor {
|
||||
companion object {
|
||||
private val EID_PATTERN = Regex("^89044045(84|21)67274948")
|
||||
}
|
||||
|
|
@ -144,4 +91,4 @@ class SIMLink : EuiccVendor {
|
|||
|
||||
return EuiccVendorInfo(skuName = skuName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_shortAnimTime"
|
||||
android:fromXDelta="-100%"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:fromXDelta="-100%"
|
||||
android:toXDelta="0%" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_shortAnimTime"
|
||||
android:fromXDelta="100%"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:fromXDelta="100%"
|
||||
android:toXDelta="0%" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!-- res/anim/slide_out.xml -->
|
||||
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_shortAnimTime"
|
||||
android:fromXDelta="0%"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:fromXDelta="0%"
|
||||
android:toXDelta="-100%" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!-- res/anim/slide_out.xml -->
|
||||
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_shortAnimTime"
|
||||
android:fromXDelta="0%"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:fromXDelta="0%"
|
||||
android:toXDelta="100%" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="?attr/colorSurface" />
|
||||
<corners android:radius="?attr/dialogCornerRadius" />
|
||||
</shape>
|
||||
<solid
|
||||
android:color="?attr/colorSurface"/>
|
||||
<corners
|
||||
android:radius="?attr/dialogCornerRadius" />
|
||||
</shape>
|
||||
|
|
@ -1,10 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16.59,7.58L10,14.17l-3.59,-3.58L5,12l5,5 8,-8zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
|
||||
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16.59,7.58L10,14.17l-3.59,-3.58L5,12l5,5 8,-8zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z" />
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
|
||||
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z" />
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
|
||||
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
|
||||
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M22,16L22,4c0,-1.1 -0.9,-2 -2,-2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zM11,12l2.03,2.71L16,11l4,5L8,16l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6L2,6z" />
|
||||
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M22,16L22,4c0,-1.1 -0.9,-2 -2,-2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zM11,12l2.03,2.71L16,11l4,5L8,16l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6L2,6z"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z" />
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,7 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M5,5h2v3h10V5h2v6h2V5c0,-1.1 -0.9,-2 -2,-2h-4.18C14.4,1.84 13.3,1 12,1S9.6,1.84 9.18,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h5v-2H5V5zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1s-1,-0.45 -1,-1S11.45,3 12,3z" />
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18.01,13l-1.42,1.41l1.58,1.58l-6.17,0l0,2l6.17,0l-1.58,1.59l1.42,1.41l3.99,-4z" />
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M5,5h2v3h10V5h2v6h2V5c0,-1.1 -0.9,-2 -2,-2h-4.18C14.4,1.84 13.3,1 12,1S9.6,1.84 9.18,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h5v-2H5V5zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1s-1,-0.45 -1,-1S11.45,3 12,3z"/>
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M18.01,13l-1.42,1.41l1.58,1.58l-6.17,0l0,2l6.17,0l-1.58,1.59l1.42,1.41l3.99,-4z"/>
|
||||
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
|
||||
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21,12.4V7l-4,-4H5C3.89,3 3,3.9 3,5v14c0,1.1 0.89,2 2,2h7.4L21,12.4zM15,15c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3s1.34,-3 3,-3S15,13.34 15,15zM6,6h9v4H6V6zM19.99,16.25l1.77,1.77L16.77,23H15v-1.77L19.99,16.25zM23.25,16.51l-0.85,0.85l-1.77,-1.77l0.85,-0.85c0.2,-0.2 0.51,-0.2 0.71,0l1.06,1.06C23.45,16 23.45,16.32 23.25,16.51z" />
|
||||
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M21,12.4V7l-4,-4H5C3.89,3 3,3.9 3,5v14c0,1.1 0.89,2 2,2h7.4L21,12.4zM15,15c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3s1.34,-3 3,-3S15,13.34 15,15zM6,6h9v4H6V6zM19.99,16.25l1.77,1.77L16.77,23H15v-1.77L19.99,16.25zM23.25,16.51l-0.85,0.85l-1.77,-1.77l0.85,-0.85c0.2,-0.2 0.51,-0.2 0.71,0l1.06,1.06C23.45,16 23.45,16.32 23.25,16.51z"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9.5,6.5v3h-3v-3H9.5M11,5H5v6h6V5L11,5zM9.5,14.5v3h-3v-3H9.5M11,13H5v6h6V13L11,13zM17.5,6.5v3h-3v-3H17.5M19,5h-6v6h6V5L19,5zM13,13h1.5v1.5H13V13zM14.5,14.5H16V16h-1.5V14.5zM16,13h1.5v1.5H16V13zM13,16h1.5v1.5H13V16zM14.5,17.5H16V19h-1.5V17.5zM16,16h1.5v1.5H16V16zM17.5,14.5H19V16h-1.5V14.5zM17.5,17.5H19V19h-1.5V17.5zM22,7h-2V4h-3V2h5V7zM22,22v-5h-2v3h-3v2H22zM2,22h5v-2H4v-3H2V22zM2,2v5h2V4h3V2H2z" />
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9.5,6.5v3h-3v-3H9.5M11,5H5v6h6V5L11,5zM9.5,14.5v3h-3v-3H9.5M11,13H5v6h6V13L11,13zM17.5,6.5v3h-3v-3H17.5M19,5h-6v6h6V5L19,5zM13,13h1.5v1.5H13V13zM14.5,14.5H16V16h-1.5V14.5zM16,13h1.5v1.5H16V13zM13,16h1.5v1.5H13V16zM14.5,17.5H16V19h-1.5V17.5zM16,16h1.5v1.5H16V16zM17.5,14.5H19V16h-1.5V14.5zM17.5,17.5H19V19h-1.5V17.5zM22,7h-2V4h-3V2h5V7zM22,22v-5h-2v3h-3v2H22zM2,22h5v-2H4v-3H2V22zM2,2v5h2V4h3V2H2z"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,2h-8L4,8v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4C20,2.9 19.1,2 18,2zM12,17l-4,-4h3V9.02L13,9v4h3L12,17z" />
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M18,2h-8L4,8v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4C20,2.9 19.1,2 18,2zM12,17l-4,-4h3V9.02L13,9v4h3L12,17z"/>
|
||||
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
|
||||
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<?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:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/step_fragment_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/download_wizard_navigation"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<View
|
||||
android:id="@+id/guideline"
|
||||
|
|
@ -25,14 +25,14 @@
|
|||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/download_wizard_navigation"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/guideline" />
|
||||
app:layout_constraintTop_toBottomOf="@id/guideline"
|
||||
app:layout_constraintBottom_toTopOf="@id/download_wizard_navigation"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/download_wizard_navigation"
|
||||
|
|
@ -40,16 +40,16 @@
|
|||
android:layout_height="48dp"
|
||||
android:background="?attr/colorSurfaceContainer"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/download_wizard_back"
|
||||
android:text="@string/download_wizard_back"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:text="@string/download_wizard_back"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconGravity="start"
|
||||
app:iconTint="?attr/colorPrimary"
|
||||
|
|
@ -58,11 +58,11 @@
|
|||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/download_wizard_next"
|
||||
android:text="@string/download_wizard_next"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:text="@string/download_wizard_next"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconGravity="end"
|
||||
app:iconTint="?attr/colorPrimary"
|
||||
|
|
@ -71,4 +71,4 @@
|
|||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<?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:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<include layout="@layout/toolbar_activity" />
|
||||
|
||||
|
|
@ -10,10 +10,10 @@
|
|||
android:id="@+id/swipe_refresh"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar">
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
|
|
@ -22,4 +22,4 @@
|
|||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
|
@ -12,13 +12,13 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:fontFamily="monospace"
|
||||
android:gravity="top|start"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textMultiLine"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:gravity="top|start"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<include layout="@layout/toolbar_activity" />
|
||||
|
||||
|
|
@ -11,10 +11,10 @@
|
|||
android:id="@+id/swipe_refresh"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar">
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scroll_view"
|
||||
|
|
@ -25,17 +25,17 @@
|
|||
android:id="@+id/log_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="10dp"
|
||||
android:textIsSelectable="true"
|
||||
android:focusable="true"
|
||||
android:textSize="10sp"
|
||||
android:fontFamily="monospace"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:longClickable="true"
|
||||
android:padding="10dp"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="10sp"
|
||||
tools:ignore="SmallSp" />
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
|
@ -8,24 +9,24 @@
|
|||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/main_tabs"
|
||||
android:background="?attr/colorSurfaceVariant"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurfaceVariant"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:tabTextColor="?attr/colorOnSurfaceVariant"
|
||||
app:tabSelectedTextColor="?attr/colorOnSurfaceVariant"
|
||||
app:tabTextColor="?attr/colorOnSurfaceVariant" />
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/main_tabs" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/main_tabs"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/view_pager"
|
||||
|
|
@ -35,6 +36,6 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/main_tabs" />
|
||||
app:layout_constraintTop_toBottomOf="@id/main_tabs"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<?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:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<include layout="@layout/toolbar_activity" />
|
||||
|
||||
|
|
@ -10,10 +10,10 @@
|
|||
android:id="@+id/swipe_refresh"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar">
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
|
|
@ -22,4 +22,4 @@
|
|||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<?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:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<include layout="@layout/toolbar_activity" />
|
||||
|
||||
|
|
@ -15,4 +15,4 @@
|
|||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
<?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="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:padding="20dp">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:padding="20dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/download_method_icon"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:tint="?attr/colorAccent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?attr/colorAccent" />
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_method_title"
|
||||
|
|
@ -21,24 +21,24 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:ellipsize="marquee"
|
||||
android:maxLines="1"
|
||||
android:textSize="15sp"
|
||||
app:layout_constrainedWidth="true"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="marquee"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/download_method_icon"
|
||||
app:layout_constraintEnd_toStartOf="@id/download_method_chevron"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@id/download_method_icon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constrainedWidth="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/download_method_chevron"
|
||||
android:src="@drawable/ic_chevron_right"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:src="@drawable/ic_chevron_right"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:tint="?attr/colorAccent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?attr/colorAccent" />
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -48,8 +48,8 @@
|
|||
android:id="@+id/download_progress_item_error_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:textColor="?attr/colorError"
|
||||
|
|
@ -78,4 +78,4 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/download_progress_item_title" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -3,11 +3,11 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingStart="20sp"
|
||||
android:paddingBottom="20sp"
|
||||
android:paddingTop="10sp"
|
||||
android:paddingStart="20sp"
|
||||
android:paddingEnd="20sp"
|
||||
android:paddingBottom="20sp">
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/slot_item_title"
|
||||
|
|
@ -83,6 +83,7 @@
|
|||
android:layout_marginTop="20sp"
|
||||
android:layout_marginEnd="10sp"
|
||||
app:constraint_referenced_ids="slot_item_type_label,slot_item_type,slot_item_eid_label,slot_item_eid,slot_item_active_profile_label,slot_item_active_profile,slot_item_free_space_label,slot_item_free_space"
|
||||
app:flow_wrapMode="aligned"
|
||||
app:flow_horizontalAlign="start"
|
||||
app:flow_horizontalBias="1"
|
||||
app:flow_horizontalGap="10sp"
|
||||
|
|
@ -91,7 +92,6 @@
|
|||
app:flow_verticalBias="0"
|
||||
app:flow_verticalGap="16sp"
|
||||
app:flow_verticalStyle="packed"
|
||||
app:flow_wrapMode="aligned"
|
||||
app:layout_constraintEnd_toStartOf="@id/slot_checkbox"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/slot_item_title" />
|
||||
|
|
@ -105,4 +105,4 @@
|
|||
app:layout_constraintStart_toEndOf="@id/flow1"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
<?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="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground">
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/euicc_info_title"
|
||||
|
|
@ -12,8 +12,8 @@
|
|||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginVertical="12dp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
|
|
@ -22,9 +22,9 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginVertical="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/euicc_info_title" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/euicc_info_title"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -20,16 +20,16 @@
|
|||
android:id="@+id/name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:singleLine="true"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/state"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
android:textSize="16sp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@+id/profile_menu"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/state"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constrainedWidth="true" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/profile_menu"
|
||||
|
|
@ -37,98 +37,98 @@
|
|||
android:layout_height="25dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:src="@drawable/ic_menu_black"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:includeFontPadding="true"
|
||||
android:singleLine="true"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="italic"
|
||||
app:layout_constraintBottom_toTopOf="@+id/provider_label"
|
||||
android:singleLine="true"
|
||||
android:includeFontPadding="true"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/name" />
|
||||
app:layout_constraintTop_toBottomOf="@id/name"
|
||||
app:layout_constraintBottom_toTopOf="@+id/provider_label"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/provider_label"
|
||||
android:text="@string/profile_provider"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:singleLine="true"
|
||||
android:text="@string/profile_provider"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@+id/profile_class_label"
|
||||
android:singleLine="true"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/state" />
|
||||
app:layout_constraintTop_toBottomOf="@id/state"
|
||||
app:layout_constraintBottom_toTopOf="@+id/profile_class_label"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/provider"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="7dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:singleLine="true"
|
||||
android:layout_marginStart="7dp"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/profile_class"
|
||||
android:singleLine="true"
|
||||
app:layout_constraintLeft_toRightOf="@id/provider_label"
|
||||
app:layout_constraintTop_toBottomOf="@id/state" />
|
||||
app:layout_constraintTop_toBottomOf="@id/state"
|
||||
app:layout_constraintBottom_toTopOf="@+id/profile_class"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/profile_class_label"
|
||||
android:text="@string/profile_class"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:singleLine="true"
|
||||
android:text="@string/profile_class"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:singleLine="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/iccid_label"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/provider_label" />
|
||||
app:layout_constraintTop_toBottomOf="@id/provider_label"
|
||||
app:layout_constraintBottom_toTopOf="@+id/iccid_label"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/profile_class"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="7dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:singleLine="true"
|
||||
android:layout_marginStart="7dp"
|
||||
android:textSize="14sp"
|
||||
android:singleLine="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/iccid"
|
||||
app:layout_constraintLeft_toRightOf="@id/profile_class_label"
|
||||
app:layout_constraintTop_toBottomOf="@id/provider" />
|
||||
app:layout_constraintTop_toBottomOf="@id/provider"
|
||||
app:layout_constraintBottom_toTopOf="@+id/iccid"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/iccid_label"
|
||||
android:text="@string/profile_iccid"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:singleLine="true"
|
||||
android:text="@string/profile_iccid"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:singleLine="true"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/profile_class_label" />
|
||||
app:layout_constraintTop_toBottomOf="@id/profile_class_label"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/iccid"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="7dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:singleLine="true"
|
||||
android:layout_marginStart="7dp"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:singleLine="true"
|
||||
app:layout_constraintLeft_toRightOf="@id/iccid_label"
|
||||
app:layout_constraintTop_toBottomOf="@id/profile_class" />
|
||||
app:layout_constraintTop_toBottomOf="@id/profile_class"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/profile_sequence_number"
|
||||
|
|
@ -142,4 +142,4 @@
|
|||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
|
@ -8,14 +8,14 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="40dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="40dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_profile"
|
||||
android:textStyle="italic"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -11,18 +11,18 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/download_wizard_details_title"
|
||||
android:text="@string/download_wizard_details"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="60dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginEnd="60dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/download_wizard_details"
|
||||
android:textSize="20sp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:layout_marginStart="60dp"
|
||||
android:layout_marginEnd="60dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
|
|
@ -32,10 +32,10 @@
|
|||
android:hint="@string/profile_download_server">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:maxLines="1"
|
||||
android:inputType="text"
|
||||
android:maxLines="1" />
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
|
@ -47,10 +47,10 @@
|
|||
app:passwordToggleEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:maxLines="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
|
@ -62,10 +62,10 @@
|
|||
app:passwordToggleEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:maxLines="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
|
@ -79,26 +79,26 @@
|
|||
app:passwordToggleEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:maxLines="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:inputType="numberPassword"
|
||||
android:maxLines="1" />
|
||||
android:inputType="numberPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
app:constraint_referenced_ids="profile_download_server,profile_download_code,profile_download_confirmation_code,profile_download_imei"
|
||||
app:flow_verticalGap="16dp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/download_wizard_details_title"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/download_wizard_details_title" />
|
||||
app:layout_constrainedWidth="true" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:fillViewport="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
|
@ -12,48 +12,48 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/download_wizard_diagnostics_title"
|
||||
android:text="@string/download_wizard_diagnostics"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="60dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginEnd="60dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/download_wizard_diagnostics"
|
||||
android:textSize="20sp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:layout_marginStart="60dp"
|
||||
android:layout_marginEnd="60dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/download_wizard_diagnostics_save"
|
||||
android:src="@drawable/ic_save_as_black"
|
||||
android:layout_margin="20dp"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_margin="20dp"
|
||||
android:contentDescription="@string/download_wizard_diagnostics_save"
|
||||
android:src="@drawable/ic_save_as_black"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:tint="?attr/colorAccent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?attr/colorAccent" />
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_wizard_diagnostics_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="10dp"
|
||||
android:textIsSelectable="true"
|
||||
android:focusable="true"
|
||||
android:textSize="10sp"
|
||||
android:fontFamily="monospace"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:longClickable="true"
|
||||
android:padding="10dp"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="10sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/download_wizard_diagnostics_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:ignore="SmallSp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
|
|
@ -1,33 +1,33 @@
|
|||
<?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:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_method_select_title"
|
||||
android:text="@string/download_wizard_method_select"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="60dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginEnd="60dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/download_wizard_method_select"
|
||||
android:textSize="20sp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:layout_marginStart="60dp"
|
||||
android:layout_marginEnd="60dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/download_method_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constrainedHeight="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/download_method_select_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/download_method_select_title" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constrainedHeight="true" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -1,33 +1,33 @@
|
|||
<?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:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_progress_title"
|
||||
android:text="@string/download_wizard_progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="60dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginEnd="60dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/download_wizard_progress"
|
||||
android:textSize="20sp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:layout_marginStart="60dp"
|
||||
android:layout_marginEnd="60dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/download_progress_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constrainedHeight="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/download_progress_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/download_progress_title" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constrainedHeight="true" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue