Android eSIM-LPA基于Android13的实现

Posted 柯基爱蹦跶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android eSIM-LPA基于Android13的实现相关的知识,希望对你有一定的参考价值。

eSIM-android-LPA基于 Android 13的实现

国际对ESIM相关所有规范定义在:GSMA Spec (SGP) - eSIM Consumer and IoT Specifications

国内对EID相关规定在:电信终端产业协会-EID管理实施规定

Android 官方文档在:eSIM实现

开发前知

什么是 eSIM

嵌入式SIM(又称 eSIMeUICC)技术,移动用户可以在没有实体 SIM 卡的情况下,下载运营商配置文件并激活运营商服务。该技术是由 GSMA 推动的全球规范,支持在任何移动设备上进行远程 SIM 配置 (RSP)。从 Android9 开始,Android 框架为访问 eSIM 和管理 eSIM 上的订阅配置文件提供了标准 API。借助这些 eUICC API,第三方可以在支持 eSIM 的 Android 设备上开发自己的运营商应用和 Local Profile Assistant (LPA)

  1. 为访问系统隐藏API,首先需编译相应 compileSdkVersionframework.jartelephony-common.jar,供 LPA 打包时编译检查使用

  2. 其次,应用需要作为系统应用存在,自签名后在使用系统应用签名,然后安装在 system/priv-app/ 下,且需要在 etc/permissions/privapp-permissions-platform.xml 文件里配置应用所必须的特殊权限,例如:

<privapp-permissions package="com.xxx.lpa">
    <permission name="android.permission.INTERNET"/>
    <permission name="android.permission.READ_PHONE_STATE"/>
    <permission name="android.permission.MODIFY_PHONE_STATE"/>
    <permission name="android.permission.BIND_EUICC_SERVICE"/>
    <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
    <permission name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
    <permission name="android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS"/>
</privapp-permissions>
  1. 同样,在系统使用到 Euicc 这种特殊硬件功能,需要在 etc/permissions/android.hardware.telephony.euicc.xml 文件里配置需要启用的硬件特性
<permissions>
    <feature name="android.hardware.telephony.euicc" />
    <feature name="android.hardware.telephony.radio.access" />
	<feature name="android.hardware.telephony.subscription" />
</permissions>
  1. 最后使用 EuiccManager.isEnabled 检查得到结果 true ,表明可以 正常使用 eSIM相关API进行开发

系统应用安装流程:

1. adb root
2. adb remount
3. adb push lpa.apk system/priv-app/lpa/lpa.apk 推送apk到系统目录
4. adb shell am restart/ adb shell reboot 重启am或者系统,让系统自动检查并安装

涉及名词

id类

  • AID 32位十六进制

    com.android.internal.telephony.uicc.euicc.EuiccCard

    样例:A0000005591010FFFFFFFF8900000100

  • eid 32位十进制

    样例:89033023001211360000000007226088

  • cardId 20位十六进制

    eSIM卡中是 eid,普通SIM卡中是 iccid

  • ICCID 20位十六进制

Integrate circuit card identity 集成电路卡识别码(固化在手机SIM卡中) ICCID为IC卡的唯一识别号码,共有20位数字组成

样例1:898602F30918A0009913
样例2:89860922780011058162
  • slotId/SlotIndex 卡槽ID

    0(卡槽1)、1(卡槽2)

其他类

  • APDU

    APDUISO/IEC 7816-4 规范中定义的,用于和SIM卡交互(发送/响应)的 协议,具体呈现为一串 16进制的编码指令

样例:

80E2910006BF3E035C015A(发送)、BF3E125A10890860302022000000220000153565489000(响应)

Android 13 中的 Euicc

从 Android 9 后就开始有 Euicc 的相关API,10、11、12、13新增了一些扩展或者标记了一些过时,比如新增了多卡功能、支持和运营商APP交互…

涉及关键入口类:

  • android.service.euicc.EuiccService -> EuiccServiceImpl 需要我们自己实现
  • android.telephony.euicc.EuiccManager -> EuiccController -> EuiccConnector

EuiccManager中保存的重要变量

private final EuiccConnector mConnector;
private final SubscriptionManager mSubscriptionManager;
private final TelephonyManager mTelephonyManager;
private final AppOpsManager mAppOpsManager;
private final PackageManager mPackageManager;
  • android.telephony.euicc.EuiccCardManager -> EuiccCardController -> EuiccCard/UiccController/SubscriptionController

  • android.telephony.TelephonyManager -> SubscriptionManager

  • android.telephony.SubscriptionManager -> SubscriptionController

涉及关键底层类:

com.android.internal.telephony.euicc.EuiccConnector(连接 EuiccManager 和 EuiccService)

com.android.internal.telephony.uicc包下:

UiccController(直接操作卡槽)
euicc.EuiccCard(EuiccController 中部分Card方法的实现)
euicc.apdu.ApduSender(EuiccCard 中进行 APDU 命令发送)
 ApduSender错误举例:android.telephony.IccOpenLogicalChannelResponse(Response to the TelephonyManager.iccOpenLogicalChannel command.)
euicc.Tags(EuiccCard 中使用于 APDU 命令发送的 ASN.1 tag 定义)
euicc.EuiccCardErrorException(Card相关方法产生的异常定义)
euicc.apdu.ApduException(APDU 命令产生的异常定义)

总结一下:

EuiccManager 类中的方法,一部分通过各种 xxxManager 实现(最后发送APDU命令),一部分通过 EuiccConnector 类调用到 EuiccService 中需要开发者自行实现。

大致调用栈:

xxxManager -> xxxController -> (EuiccConnector -> EuiccService/ EuiccCard -> ApduSender)

EuiccService的实现

从 Lui 层面来看,主要需要实现其中的5个方法:

  1. onDownloadSubcription(下载订阅文件)
  2. onGetEuiccProfileInfoList(获取所有 Profile 信息)
  3. onSwitchToSubscription(切换 Profile)
  4. onUpdateSubscriptionNickname(编辑某 Profile 的昵称字段)
  5. onDeleteSubscription(删除某 Profile)

但其实内部还需实现最基础的方法: onGetEid(获取 eid),这一个跟上面5个方法不一样,需区分开来,下面方法未按照顺序排列,除了 getEid 都是由易入难

前提:

入口类:EuiccCardManager
除了 getEid 每个方法均回调为:
  EuiccCardManager.ResultCallback(resultCode, result)
resultCode:响应码,0为成功(EuiccService.RESULT_OK),其他负数值为失败,失败需处理返回异常
result:APDU响应指令

onGetEid 实现:

入口函数:

public String EuiccManager.getEid() 
	return getIEuiccController().getEid(...)

EuiccController 层通过 EuiccConnector ,最终调用到 EuiccService.onGetEid 方法里实现,这里一不小心就会进到死循环的坑!

实现很简单,Android 10 及以上版本使用新增方法 getUiccCardsInfo ,以下版本使用旧 getUiccSlotsInfo,获取所有卡的信息列表,因为同时只能启用一个卡槽功能,所以直接找不为空的就行了。其实在 EuiccCard 类中有 getEid() 的公开实现,但系统没有提供上层入口方法。

override fun onGetEid(slotId: Int): String 
    val eid =
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
            tm.uiccCardsInfo.find 
                it.isEuicc && !it.eid.isNullOrEmpty()
            ?.eid ?: ""
         else 
            tm.uiccSlotsInfo.find 
                it.isEuicc && !it.cardId.isNullOrEmpty()
            ?.cardId ?: ""
        
XLog.d("Service onGetEid slotId: $slotId, eid:$eid")
return eid

双卡槽下插入单eSIM卡,对比一下两个Info

TelephonyManager.getUiccSlotInfo():

UiccSlotInfo (mIsActive=true, mIsEuicc=true, mCardId=89033023001211360000000007226088, cardState=2, phoneId=0, mIsExtendedApduSupported=false, mIsRemovable=true)

UiccSlotInfo (mIsActive=true, mIsEuicc=false, mCardId=, cardState=1, phoneId=1, mIsExtendedApduSupported=false, mIsRemovable=true)

TelephonyManager.getUiccCardInfo():

UiccCardInfo (mIsEuicc=true, mCardId=5, mEid=89033023001211360000000007226088, mIccId=89861000000000000022, mSlotIndex=0, mIsRemovable=true)

UiccCardInfo (mIsEuicc=false, mCardId=-2, mEid=null, mIccId=, mSlotIndex=1, mIsRemovable=true)

onGetEuiccProfileInfoList 实现

调用函数:

public void EuiccCardManager.requestAllProfiles(
	String cardId, 
    @CallbackExecutor Executor executor, 
	ResultCallback<EuiccProfileInfo> callback
) 
	getIEuiccCardController().getAllProfiles(...)
	...

ProfileInfoList样例: 最后需展示在 Lui 上供用户操作

[
  
    "iccid": "898602F30918A0009911",
    "nickname": "CM",
    "profileName": "",
    "providerName": "",
    "state": 1
  ,
  
    "iccid": "89860922780011058161",
    "nickname": "CU",
    "profileName": "",
    "providerName": "",
    "state": 0
  ,
  
    "iccid": "89861122215036305451",
    "nickname": "CT",
    "profileName": "",
    "providerName": "",
    "state": 0
  
]

onSwitchToSubscription 实现

调用函数:

public void EuiccCardManager.switchToProfile(
	String cardId, String iccid, boolean refresh,
    @CallbackExecutor Executor executor, 
	ResultCallback<EuiccProfileInfo> callback
) 
	getIEuiccCardController().switchToProfile(...)
	...

遇到的错误 1:

逻辑通道被占用
switchToProfile callback onException: 
  com.android.internal.telephony.uicc.euicc.EuiccCardException: Cannot send APDU.
  ApduException: The logical channel is in use. (apduStatus=0)

出现原因:

  1. 在下载订阅后立即进行切换操作
  2. 多线程并发调用了底层 EuiccCard 的同一个接口(一个逻辑通道启用并占用时需要释放后才能被重新使用)

如何排查或解决:

  1. 在下载后进行延时几秒,再调用切换
  2. 检查 Lui 内有没有并发调用同一接口

onUpdateSubscriptionNickname 实现

调用函数:

public void EuiccCardManager.setNickname(
	String cardId, String iccid, String nickname,
    @CallbackExecutor Executor executor, 
	ResultCallback<EuiccProfileInfo> callback
) 
	getIEuiccCardController().setNickname(...)
	...


onDeleteSubscription 实现

调用函数:

public void EuiccCardManager.deleteProfile(
	String cardId, String iccid,
    @CallbackExecutor Executor executor, 
	ResultCallback<EuiccProfileInfo> callback
) 
	getIEuiccCardController().deleteProfile(...)
	...


DownloadSubscription 实现:

一、下载流程

下载流程较为复杂

运营商APP 拉起 LPA 发起下载并携带 ActivationCode(激活码通常是扫描二维码得到),Lui 上的 EuiccManager.downloadSubscription 为入口函数,参数 PendingIntent 包含 action:ACTION_DOWNLOAD ,同时注册 BroadcastReceiver 接收下载结果。下载途中可能需要到运营商APP端进行下载码的确认。

最终需实现方法 EuiccService.onDownloadSubscription:

override fun onDownloadSubscription(
    slotId: Int,
    subscription: DownloadableSubscription?,
    switchAfterDownload: Boolean,
    forceDeactivateSim: Boolean,
    resolvedBundle: Bundle?,
): DownloadSubscriptionResult 
	return null

二、下载实现步骤

所有接口调用,header 里是状态字段,response 里是验证信息。

header样例:


  "header": 
    "functionExecutionStatus": 
      "status": "Executed-Success"
    
  ,
  "transactionId": "FD913D91F79702FBF27DE2B4ABBD468A"

response样例:

"pendingNotification":"vzeBrr8naIAQzS/mWM/RNgcHY2VAyc6yGr8vJoABB4ECB4AMEWRwcGx1cy5jY3NtZWMuY29tWgqYaAEAAAAAAAAzBgorBgEEAYOOA2UCoh+gHU8QoAAABVkQEP+JAAARAAQJMAegBTADgAEAXzdA0iN1N/yZEKdsbUub2PXOW9eT5NJi97ajNnu44rMTVimQnDzwvk4+hvsQFHH293xn7SUnfZM6gJswqaiF9gpONB=="

1. getEid 是前提,操作卡时使用

val cardId = onGetEid(slotId)

2. 解析并验证激活码

获取激活码,为空或非法需中断操作返回错误:

val activationCode = subscription?.encodedActivationCode
// 激活码样例:1$dpplus.xxx.com:8445/xxfreexx-dp-service$2TSU5-NKVPM-MIS64-9CLYD-KD411

val args = ac.split('$')
val smdpAddress = "https://$args[1]/"
val matchingId = args[2] // 服务端验证使用

3. 初始化、双向验证,使用激活码到 SM-DP+ 拉取订阅信息

接口调用流程:

1. initiateAuthentication
2. authenticateClient
3. getBoundProfilePackage
4. handleNotification

接口响应重要标记:

1. euiccChallenge 字段
2. authenticateServerResponse 字段
3. prepareDownloadResponse 字段
4. pendingNotification 字段

如果顺利且成功,上面的每一次调用和一次响应是对应的,表明下载流程完整。


下面是完整的、带前因后果的 API 流程:

  • API:EuiccCardManager.onGetEuiccChallenge
  • API:EuiccCardManager.onGetEuiccInfo1

1. initiateAuthentication 接口调用

euiccChallenge 响应,返回(euiccChallenge,euiccInfo1,SM-DP+服务器地址)

// SM-DP+服务连接,初始化验证
val initAuthResp = es9.initiateAuthentication(
    InitiateAuthentication.Request(
        euiccChallenge.toBase64(),
        euiccInfo1.toBase64(),
        smdpAddress
    )
).execute().body()
  • API:EuiccCardManager.onAuthenticateServer
val serverSigned1 = initAuthResp.serverSigned1.base64ToBytes()
val serverSignature1 = initAuthResp.serverSignature1.base64ToBytes()
val ciPkIdToBeUsed = initAuthResp.euiccCiPKIdToBeUsed.base64ToBytes()
val serverCertificate = initAuthResp.serverCertificate.base64ToBytes()

euiccCardManager.authenticateServer(cardId,
    matchingId,
    serverSigned1,
    serverSignature1,
    ciPkIdToBeUsed,
    serverCertificate,
    AsyncTask.THREAD_POOL_EXECUTOR
)  resultCode, result ->
	...

2. authenticateClient 接口调用

authenticateServerResponse 响应,返回 functionExecutionStatus.status=Executed-Success 继续

val authClientResp =
    es9.authenticateClient(AuthenticateClient.Request(initAuthResp.transactionId, authServerResp.toBase64()))
        .execute().body()
  • API:EuiccCardManager.onPrepareDownload
euiccCardManager.prepareDownload(
    cardId,
    if (subscription.getEncodedActivationCode().endsWith("\\$1")) 
        sha256(sha256(subscription.confirmationCode.toByteArray()) + authClientResp.transactionId.hexStringToBytes())
     else 
        null
    ,
    authClientResp.smdpSigned2.base64ToBytes(),
    authClientResp.smdpSignature2.base64ToBytes(),
    authClientResp.smdpCertificate.base64ToBytes(),
    AsyncTask.THREAD_POOL_EXECUTOR
)  resultCode, result ->
	...

3. getBoundProfilePackage 接口调用

prepareDownloadResponse 响应

 val getBoundProfilePackageResp = es9.getBoundProfilePackage(
     GetBoundProfilePackage.Request(
         authClientResp.transactionId,
         prepareDownloadResp.toBase64()
     )
 ).execute().body()
  • API:EuiccCardManager.onLoadBoundProfilePackage
euiccCardManager.loadBoundProfilePackage(cardId, bpp, AsyncTask.THREAD_POOL_EXECUTOR)  resultCode, result ->
	...

4. handleNotification 接口调用

pendingNotification 响应

es9.handleNotification(HandleNotification.Request(installationResult.toBase64()))
    .execute().body()

4. 判断是否需要下载后切换Profile,需要的话执行

遇到的错误1:多线程并发调用 或 下载后立即切换 Profile 产生,检查调用链,对下载后切换中间延时

E  getProfile in switchToProfile callback onException: 
 com.android.internal.telephony.uicc.euicc.EuiccCardException: Cannot parse response: BF2D03810101
 at com.android.internal.telephony.uicc.euicc.EuiccCard$2.onResult(EuiccCard.java:1251)
 at com.android.internal.telephony.uicc.euicc.EuiccCard$2.onResult(EuiccCard.java:1242)
 at com.android.internal.telephony.uicc.euicc.apdu.ApduSender$4.onResult(ApduSender.java:275)
 at com.android.internal.telephony.uicc.euicc.apdu.ApduSender$4.onResult(ApduSender.java:266)
 at com.android.internal.telephony.uicc.euicc.async.AsyncMessageInvocation.handleMessage(AsyncMessageInvocation.java:57)
 at android.os.Handler.dispatchMessage(Handler.java:102)
 at android.os.Looper.loop(Looper.java:223)
 at android.app.ActivityThread.main(ActivityThread.java:7822)
 at java.lang.reflect.Method.invoke(Native Method)
 at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
 at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:952)

Caused by: com.android.internal.telephony.uicc.asn1.TagNotFoundException: null (tag=160)
 at com.android.internal.telephony.uicc.asn1.Asn1Node.getChild(Asn1Node.java:330)
 at com.android.internal.telephony.uicc.euicc.EuiccCard.lambda$getProfile$5(EuiccCard.java:288)
 at com.android.internal.telephony.uicc.euicc.-$$Lambda$EuiccCard$TTvsStUIyUFrPpvGTlsjBCy3NyM.handleResult(Unknown Source:0)
 at com.android.internal.telephony.uicc.euicc.EuiccCard$2.onResult(EuiccCard.java:1246)
 at com.android.internal.telephony.uicc.euicc.EuiccCard$2.onResult(EuiccCard.java:1242) 
 at com.android.internal.telephony.uicc.euicc.apdu.ApduSender$4.onResult(ApduSender.java:275) 
 at com.android.internal.telephony.uicc.euicc.apdu.ApduSender$4.onResult(ApduSender.java:266) 
 at com.android.internal.telephony.uicc.euicc.async.AsyncMessageInvocation.handleMessage(AsyncMessageInvocation.java:57) 
 ...
 at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:952) 

遇到的错误 2:
由于 eid 过长,在 onDownloadSubscription 返回实体类中传入 cardId: Int 字段过长(这个字段可不传,内部未使用)

java.lang.NumberFormatException: For input string: "89033023001211360000000007226077"
 at java.lang.Integer.parseInt(Integer.java:618)
 at java.lang.Integer.parseInt(Integer.java:650)
 at com.xxx.lpa.service.EuiccService.onDownloadSubscription(EuiccService.kt:193)
 at android.service.euicc.EuiccService$IEuiccServiceWrapper$1.run(EuiccService.java:669)
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
 at java.lang.Thread.run(Thread.java:923)

下载流程结束

基于android的简单计算器

一、计算器布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<EditText
android:layout_width="fill_parent"
android:layout_height="60dp"
android:editable="false"
android:gravity="bottom|right"
android:background="@drawable/white_bg"
android:id="@+id/et_input"
/>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="horizontal"
android:gravity="center_horizontal"
>

<Button
android:id="@+id/btn_clear"
android:layout_width="60dp"
android:layout_height="60dp"
android:text="@string/btn_clear"
android:background="@drawable/white_selector"
android:textSize="20sp"
/>
<Button
android:id="@+id/btn_del"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:background="@drawable/white_selector"
android:text="@string/btn_del"
android:textSize="20sp"
/>
<Button
android:id="@+id/btn_divide"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:text="@string/btn_divide"
android:background="@drawable/white_selector"
android:textSize="20sp"
/>
<Button
android:id="@+id/btn_multiply"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:background="@drawable/white_selector"
android:text="@string/btn_multiply"
android:textSize="20sp"
/>
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:gravity="center_horizontal"
>

<Button
android:id="@+id/btn_7"
android:layout_width="60dp"
android:layout_height="60dp"
android:text="@string/btn_7"
android:background="@drawable/white_selector"
android:textSize="20sp"
/>
<Button
android:id="@+id/btn_8"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:text="@string/btn_8"
android:background="@drawable/white_selector"
android:textSize="20sp"
/>
<Button
android:id="@+id/btn_9"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:background="@drawable/white_selector"
android:text="@string/btn_9"
android:textSize="20sp"
/>
<Button
android:id="@+id/btn_minus"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:background="@drawable/white_selector"
android:text="@string/btn_minus"
android:textSize="20sp"
/>
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:gravity="center_horizontal"
>

<Button
android:id="@+id/btn_4"
android:layout_width="60dp"
android:layout_height="60dp"
android:background="@drawable/white_selector"
android:text="@string/btn_4"
android:textSize="20sp"
/>
<Button
android:id="@+id/btn_5"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:background="@drawable/white_selector"
android:text="@string/btn_5"
android:textSize="20sp"
/>
<Button
android:id="@+id/btn_6"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:background="@drawable/white_selector"
android:text="@string/btn_6"
android:textSize="20sp"
/>
<Button
android:id="@+id/btn_plus"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:text="@string/btn_plus"
android:background="@drawable/white_selector"
android:textSize="20sp"
/>
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:gravity="center_horizontal"

>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
>


<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<Button
android:id="@+id/btn_1"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:background="@drawable/white_selector"
android:text="@string/btn_1"
android:textSize="20sp"
/>
<Button
android:id="@+id/btn_2"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:background="@drawable/white_selector"
android:text="@string/btn_2"
android:textSize="20sp"
/>
<Button
android:id="@+id/btn_3"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:background="@drawable/white_selector"
android:text="@string/btn_3"
android:textSize="20sp"
/>
</LinearLayout>

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp"
>
<Button
android:id="@+id/btn_0"
android:layout_width="130dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:background="@drawable/white_selector"
android:text="@string/btn_0"
android:textSize="20sp"
/>
<Button
android:id="@+id/btn_point"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="10dp"
android:background="@drawable/white_selector"
android:text="@string/btn_point"
android:textSize="20sp"
/>
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/btn_equal"
android:layout_width="60dp"
android:layout_height="130dp"
android:layout_marginLeft="10dp"
android:background="@drawable/orange_selector"
android:text="@string/btn_equal"
android:textSize="20sp"
/>
</LinearLayout>

</LinearLayout>

 

二、实现按钮的 点击事件

package com.example.calculatordemo;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;

public class mainActivity extends Activity implements OnClickListener {
Button btn_0;
Button btn_1;
Button btn_2;
Button btn_3;
Button btn_4;
Button btn_5;
Button btn_6;
Button btn_7;
Button btn_8;
Button btn_9;
Button btn_point;
Button btn_clear;
Button btn_del;
Button btn_plus;
Button btn_minus;
Button btn_multiply;
Button btn_divide;
Button btn_equal;
EditText et_input;
boolean clear_flag;//清空标示
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.main_layout);
btn_0=(Button) findViewById(R.id.btn_0);
btn_1=(Button) findViewById(R.id.btn_1);
btn_2=(Button) findViewById(R.id.btn_2);
btn_3=(Button) findViewById(R.id.btn_3);
btn_4=(Button) findViewById(R.id.btn_4);
btn_5=(Button) findViewById(R.id.btn_5);
btn_6=(Button) findViewById(R.id.btn_6);
btn_7=(Button) findViewById(R.id.btn_7);
btn_8=(Button) findViewById(R.id.btn_8);
btn_9=(Button) findViewById(R.id.btn_9);
btn_point=(Button) findViewById(R.id.btn_point);
btn_clear=(Button) findViewById(R.id.btn_clear);
btn_del=(Button) findViewById(R.id.btn_del);
btn_plus=(Button) findViewById(R.id.btn_plus);
btn_minus=(Button) findViewById(R.id.btn_minus);
btn_multiply=(Button) findViewById(R.id.btn_multiply);
btn_divide=(Button) findViewById(R.id.btn_divide);
btn_equal=(Button) findViewById(R.id.btn_equal);
//以上是实例化的按钮
et_input=(EditText) findViewById(R.id.et_input);//实例化显示频
btn_0.setOnClickListener(this);
btn_1.setOnClickListener(this);
btn_2.setOnClickListener(this);
btn_3.setOnClickListener(this);
btn_4.setOnClickListener(this);
btn_5.setOnClickListener(this);
btn_6.setOnClickListener(this);
btn_7.setOnClickListener(this);
btn_8.setOnClickListener(this);
btn_9.setOnClickListener(this);
btn_clear.setOnClickListener(this);
btn_del.setOnClickListener(this);
btn_plus.setOnClickListener(this);
btn_minus.setOnClickListener(this);
btn_multiply.setOnClickListener(this);
btn_divide.setOnClickListener(this);
btn_equal.setOnClickListener(this);
btn_point.setOnClickListener(this);

//意思啊哈那个是设置按钮的点击事件
}

@Override
public void onClick(View v) {
// TODO Auto-generated method stub
String str =et_input.getText().toString();
switch (v.getId()) {
case R.id.btn_0:
case R.id.btn_1:
case R.id.btn_2:
case R.id.btn_3:
case R.id.btn_4:
case R.id.btn_5:
case R.id.btn_6:
case R.id.btn_7:
case R.id.btn_8:
case R.id.btn_9:
case R.id.btn_point:
if (clear_flag) {
clear_flag=false;
str="";
et_input.setText("");

}
et_input.setText(str+((Button)v).getText());
break;
case R.id.btn_plus:
case R.id.btn_minus:
case R.id.btn_multiply:
case R.id.btn_divide:
if (clear_flag) {
clear_flag=false;
str="";
et_input.setText("");

}
et_input.setText(str+" "+((Button)v).getText()+" ");
break;
case R.id.btn_clear:
if(clear_flag){
clear_flag=false;
str="";
et_input.setText("");
}
break;
case R.id.btn_del:
if (clear_flag) {
clear_flag=false;
str="";
et_input.setText("");

}else if (str!=null&&!str.equals("")) {
et_input.setText(str.substring(0, str.length()-1));

}
break;
case R.id.btn_equal:
getResult();
break;

}
}
private void getResult(){
String exp=et_input.getText().toString();
if(exp==null||exp.equals("")){
return;
}
if (!exp.contains(" ")) {
return;

}
if (clear_flag) {
clear_flag=false;
return;
}
clear_flag=true;
double result=0;
String s1=exp.substring(0,exp.indexOf(" "));//运算符前面的字符串
String op=exp.substring(exp.indexOf(" ")+1,exp.indexOf(" ")+2);
String s2=exp.substring(exp.indexOf(" ")+3);//运算符后面的字符串
if(!s1.equals("")&&!s2.equals("")){
double d1=Double.parseDouble(s1);
double d2=Double.parseDouble(s2);
if(op.equals("+")){
result=d1+d2;
}else if(op.equals("-")){
result=d1-d2;
}else if(op.equals("×")){
result=d1*d2;
}else if(op.equals("÷")){
if (d2==0) {
result=0;
}else {
result=d1/d2;
}
}
if (!s1.contains(".")&&!s2.contains(".")&&!op.equals("÷")) {
int r=(int)result;
et_input.setText(r+"");
}else {
et_input.setText(result+"");
}

}else if (!s1.equals("")&&s2.equals("")) {
et_input.setText(exp);
}else if (s1.equals("")&&!s2.equals("")) {

double d2=Double.parseDouble(s1);
if(op.equals("+")){
result=0+d2;
}else if(op.equals("-")){
result=0-d2;
}else if(op.equals("×")){
result=0;
}else if(op.equals("÷")){

result=0;
}
if (!s2.contains(".")) {
int r=(int)result;
et_input.setText(r+"");
}else {
et_input.setText(result+"");
}
}else {
et_input.setText("");
}
}
}

 

以上是关于Android eSIM-LPA基于Android13的实现的主要内容,如果未能解决你的问题,请参考以下文章

基于android-async-http的android服务

Android日历——基于RecyclerView的日历

Android手机是基于linux的,可以当做一般的linux电脑用吗

基于android的简单计算器

求教,基于webview的android应用开发。

Android 基于UDP的Socket通信