android之Sim Tool Kit流程分析
2016-04-18 19:41
411 查看
一.简介
STK 或者 UTK 就是 Sim Tool Kit (sim卡工具包),定制了一系列与运营商相关的应用(查询天气,话费,彩铃等),可以理解为安装在SIM卡上的应用。运营商将相关应用信息保存在SIM卡中,STK应用需要从SIM卡中读取相关应用信息,SIM卡也会向STK应用主动上报应用信息。
我们知道SIM卡是插在Modem中的,要读取SIM卡的内容,就必须要经过Modem层,而与Modem层进行交互离不开AT指令。在framework中,发送AT指令主要是在RIL中完成的。所以读取SIM卡的内容的流程可以归结为:STK应用---->RILJ---->RILC---->Modem---->运营商的基站。而这里只需要跟踪一下STK应用到RIL的RILJ层就可以了,以后有空可以研究一下RILC层的代码。
既然最后到达的是RIL层,那么我们从RIL层往STK应用层反推它的流程。看看从SIM卡中读取的信息是怎样到达STK应用的。在我们插卡开机的时候,Modem检测到有卡插入,这时候它会读取SIM中的相关信息,并把消息上报给RIL层。Modem上报消息的过程这里先不管。我们只研究RIL,要研究RIL层,我们需要先了解一下RIL层的消息分类。
二.RIL中的消息类型
RIL中的消息类型主要分为两种:
solicited response message: 比如拨号,接听,挂断等这些做主动请求的操作的消息
unsolicited response message:GSM/GPRS Modem硬件模块主动上报的,例如来电,接收短信,基站信息等消息。是从硬件来的消息,RIL层被动接收。
STK应用在RIL中的消息有:
solicited:
RIL_REQUEST_STK_GET_PROFILE
RIL_REQUEST_STK_SET_PROFILE
RIL_REQUEST_STK_SEND_ENVELOPE_COMMAND // 打开子菜单
RIL_REQUEST_STK_SEND_TERMINAL_RESPONSE //获取子菜单信息
RIL_REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM
RIL_REQUEST_REPORT_STK_SERVICE_IS_RUNNING
RIL_REQUEST_STK_SEND_ENVELOPE_WITH_STATUS
UnSolicited:
RIL_UNSOL_STK_SESSION_END
RIL_UNSOL_STK_PROACTIVE_COMMAND // 显示菜单
RIL_UNSOL_STK_EVENT_NOTIFY
RIL_UNSOL_STK_CALL_SETUP
无论是哪一种消息,从Modem处上传上来的信息,它的处理过程大体一致。我们只需要沿着具体一个消息的进行追踪,就可以弄清楚它的流程走向。这里我们研究一下UnSolicited的这个消息RIL_UNSOL_STK_PROACTIVE_COMMAND。
三.流程分析
在插卡开机的时候,Modem读取此SIM卡中STK显示的菜单,并向RIL层发送RIL_UNSOL_STK_PROACTIVE_COMMAND消息。这个消息被RILReceiver捕获并处理,从这里开始。流程分三个部分,分别是:1.RILJ部分。2.Framework部分。3.STK部分(应用层)
1.RILJ部分
RILReceiver是RIL的内部类,负责监听Socket消息,从Socket中接收并处理RILC上报的消息。(RILJ与RILC的通信主要通过Socket)其逻辑是:
一. 维护Socket的连接。
二. 把从Socket中读取到的消息交给RIL中processResponse()方法处理。
这个方法主要是根据消息的两种类型,做分别的处理。Parcel存储着从Socket中读取的信息。
Parcel:Parcel就是一个存放读取数据的容器, Android系统中的binder进程间通信(IPC)就使用了Parcel类来进行客户端与服务端数据的交互,而且AIDL的数据也是通过Parcel来交互的。在Java空间和C++都实现了Parcel,由于它在C/C++中,直接使用了内存来读取数据,因此,它更有效率。
主要研究UnSolicited消息,所以来看看processUnsolicited做了什么。
2.Framework部分
这个notifyRegistrant()方法是调用的哪个类里边的呢。我们来看看mCatProCmdRegistrant这个变量:protected Registrant mCatProCmdRegistrant;
很明显,这个Registrant.java是Android里的消息处理机制,用一个类把Handler发送消息的操作封装起来,并做了异步的处理。当需要此Handler发送消息时,只需要调用这个消息通知的方法notifyRegistrant()即可。
那么,mCatProCmdRegistrant这个变量是怎么初始化的呢?由于RIL.java继承了BaseCommands.java,在这个方法中,定义了mCatProCmdRegistrant变量并提供了set方法。
而调用这个setOnCatProactiveCmd()方法的,在CatService.java中,如下:CatService.java的构造方法不仅仅设置了MSG_ID_PROACTIVE_COMMAND这个消息的Handler,还设置了其他消息的Handler。
CatService.java
构造方法:
mCmdIf.setOnCatSessionEnd(this, MSG_ID_SESSION_END, null);
mCmdIf.setOnCatProactiveCmd(this, MSG_ID_PROACTIVE_COMMAND, null);//Handler的what标示是MSG_ID_PROACTIVE_COMMAND
mCmdIf.setOnCatEvent(this, MSG_ID_EVENT_NOTIFY, null);
mCmdIf.setOnCatCallSetUp(this, MSG_ID_CALL_SETUP, null);
好了,到这里我们先整理一下:
插卡开机,Medem读取SIM卡信息,上报RIL_UNSOL_STK_PROACTIVE_COMMAND消息。RIlJ中的RILReceiver类进行接收并交给processUnsolicited()方法处理。processUnsolicited()方法根据消息类型通知对应的Handler处理()。而这些Handler的处理在CatService.java的handleMessage()方法中,如下:
查看HandleMessage()方法中的MSG_ID_PROACTIVE_COMMAND消息处理。最后会调用mMsgDecoder.sendStartDecodingMessageParams(new RilMessage(msg.what, data))这个方法:
这个方法最后也是通过发送CMD_START消息,交给StateStart.java处理,decodeMessageParams()这个方法主要是解码,具体的解码过程需要查看Bertlv.java里的代码。从CMD_START消息的发送到解码结束这个过程,流程有点复杂,这里就不做具体的分析了,解码结束后,会发送MSG_ID_RIL_MSG_DECODED消息,查看CatService.java的handleMessage()方法,是的,它又回到了CatService.java中。在这个类中,频繁的使用了Handler的消息机制,我们只要关注好Handler的what标识就可以了。接下来会执行handleRilMsg()方法:
查看handleProactiveCommand()最后的一段代码,发现它发送一个广播,那这个广播是谁接收的呢?查看AppInterface.CAT_CMD_ACTION这个变量存储的包名是:android.intent.action.stk.command。
通过搜索这个包名,我们可以追溯到了STK应用中的AndroidManifest.xml中,看看这里,定义了一个receiver来接收来自framework中的广播。至此,流程跟踪到了STk应用层。
3.STK部分(应用层)
AndroidManifest.xml:
StkCmdReceiver主要负责从接收来自framework中的广播,并启动StkAppService服务。
这个StkAppService是STK中的核心服务,它主要负责调度各个显示层显示相应的数据。
onStart()方法经过简单的处理,然后交给内部类ServiceHandler进行处理,根据类型的不同,交由不同的Activity显示。比如,这里交由handleCmd()方法:
启动某个Activity显示:
至此,开机显示STK信息的流程就追踪结束了。当然STK应用层面的东西还是很多的,在这一章就不打算再分析了,如果后面有时间,再继续看看。
StkLauncherActivity.java:入口类
StkMenuActivity.java 主页面(信息列表)
StkInputActivity.java 输入页面
StkDialogActivity.java 对话框页面
StkAppInstaller.java 这个类主要用来设置是否在桌面上显示这个STK图标
StkCmdReceiver.java 接收广播
STK 或者 UTK 就是 Sim Tool Kit (sim卡工具包),定制了一系列与运营商相关的应用(查询天气,话费,彩铃等),可以理解为安装在SIM卡上的应用。运营商将相关应用信息保存在SIM卡中,STK应用需要从SIM卡中读取相关应用信息,SIM卡也会向STK应用主动上报应用信息。
我们知道SIM卡是插在Modem中的,要读取SIM卡的内容,就必须要经过Modem层,而与Modem层进行交互离不开AT指令。在framework中,发送AT指令主要是在RIL中完成的。所以读取SIM卡的内容的流程可以归结为:STK应用---->RILJ---->RILC---->Modem---->运营商的基站。而这里只需要跟踪一下STK应用到RIL的RILJ层就可以了,以后有空可以研究一下RILC层的代码。
既然最后到达的是RIL层,那么我们从RIL层往STK应用层反推它的流程。看看从SIM卡中读取的信息是怎样到达STK应用的。在我们插卡开机的时候,Modem检测到有卡插入,这时候它会读取SIM中的相关信息,并把消息上报给RIL层。Modem上报消息的过程这里先不管。我们只研究RIL,要研究RIL层,我们需要先了解一下RIL层的消息分类。
二.RIL中的消息类型
RIL中的消息类型主要分为两种:
solicited response message: 比如拨号,接听,挂断等这些做主动请求的操作的消息
unsolicited response message:GSM/GPRS Modem硬件模块主动上报的,例如来电,接收短信,基站信息等消息。是从硬件来的消息,RIL层被动接收。
STK应用在RIL中的消息有:
solicited:
RIL_REQUEST_STK_GET_PROFILE
RIL_REQUEST_STK_SET_PROFILE
RIL_REQUEST_STK_SEND_ENVELOPE_COMMAND // 打开子菜单
RIL_REQUEST_STK_SEND_TERMINAL_RESPONSE //获取子菜单信息
RIL_REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM
RIL_REQUEST_REPORT_STK_SERVICE_IS_RUNNING
RIL_REQUEST_STK_SEND_ENVELOPE_WITH_STATUS
UnSolicited:
RIL_UNSOL_STK_SESSION_END
RIL_UNSOL_STK_PROACTIVE_COMMAND // 显示菜单
RIL_UNSOL_STK_EVENT_NOTIFY
RIL_UNSOL_STK_CALL_SETUP
无论是哪一种消息,从Modem处上传上来的信息,它的处理过程大体一致。我们只需要沿着具体一个消息的进行追踪,就可以弄清楚它的流程走向。这里我们研究一下UnSolicited的这个消息RIL_UNSOL_STK_PROACTIVE_COMMAND。
三.流程分析
在插卡开机的时候,Modem读取此SIM卡中STK显示的菜单,并向RIL层发送RIL_UNSOL_STK_PROACTIVE_COMMAND消息。这个消息被RILReceiver捕获并处理,从这里开始。流程分三个部分,分别是:1.RILJ部分。2.Framework部分。3.STK部分(应用层)
1.RILJ部分
RILReceiver是RIL的内部类,负责监听Socket消息,从Socket中接收并处理RILC上报的消息。(RILJ与RILC的通信主要通过Socket)其逻辑是:
一. 维护Socket的连接。
二. 把从Socket中读取到的消息交给RIL中processResponse()方法处理。
这个方法主要是根据消息的两种类型,做分别的处理。Parcel存储着从Socket中读取的信息。
Parcel:Parcel就是一个存放读取数据的容器, Android系统中的binder进程间通信(IPC)就使用了Parcel类来进行客户端与服务端数据的交互,而且AIDL的数据也是通过Parcel来交互的。在Java空间和C++都实现了Parcel,由于它在C/C++中,直接使用了内存来读取数据,因此,它更有效率。
private void processResponse (Parcel p) { int type; type = p.readInt(); if (type == RESPONSE_UNSOLICITED) { processUnsolicited (p); } else if (type == RESPONSE_SOLICITED) { RILRequest rr = processSolicited (p); if (rr != null) { rr.release(); decrementWakeLock(); } } }
主要研究UnSolicited消息,所以来看看processUnsolicited做了什么。
protected void processUnsolicited (Parcel p) { int response; Object ret; response = p.readInt(); //获取UnSolicited Response消息类型 try {switch(response) { //根据UnSolicited Response消息类型获取不同的ret对象 .... case RIL_UNSOL_STK_SESSION_END: ret = responseVoid(p); break; case RIL_UNSOL_STK_PROACTIVE_COMMAND: ret = responseString(p); break; case RIL_UNSOL_STK_EVENT_NOTIFY: ret = responseString(p); break; case RIL_UNSOL_STK_CALL_SETUP: ret = responseInts(p); break; ... } switch(response) { //根据UnSolicited Response消息类型发出不同的消息通知 ... case RIL_UNSOL_STK_PROACTIVE_COMMAND: if (RILJ_LOGD) unsljLogRet(response, ret); if (mCatProCmdRegistrant != null) { mCatProCmdRegistrant.notifyRegistrant( new AsyncResult (null, ret, null)); } break; ... } }
2.Framework部分
这个notifyRegistrant()方法是调用的哪个类里边的呢。我们来看看mCatProCmdRegistrant这个变量:protected Registrant mCatProCmdRegistrant;
Registrant.java public void notifyRegistrant(AsyncResult ar) { internalNotifyRegistrant (ar.result, ar.exception); } void internalNotifyRegistrant (Object result, Throwable exception) { Handler h = getHandler(); if (h == null) { clear(); } else { Message msg = Message.obtain(); msg.what = what; msg.obj = new AsyncResult(userObj, result, exception); h.sendMessage(msg); } }
很明显,这个Registrant.java是Android里的消息处理机制,用一个类把Handler发送消息的操作封装起来,并做了异步的处理。当需要此Handler发送消息时,只需要调用这个消息通知的方法notifyRegistrant()即可。
那么,mCatProCmdRegistrant这个变量是怎么初始化的呢?由于RIL.java继承了BaseCommands.java,在这个方法中,定义了mCatProCmdRegistrant变量并提供了set方法。
BaseCommands.java protected Registrant mCatProCmdRegistrant; @Override public void setOnCatProactiveCmd(Handler h, int what, Object obj) { mCatProCmdRegistrant = new Registrant (h, what, obj); }
而调用这个setOnCatProactiveCmd()方法的,在CatService.java中,如下:CatService.java的构造方法不仅仅设置了MSG_ID_PROACTIVE_COMMAND这个消息的Handler,还设置了其他消息的Handler。
CatService.java
构造方法:
mCmdIf.setOnCatSessionEnd(this, MSG_ID_SESSION_END, null);
mCmdIf.setOnCatProactiveCmd(this, MSG_ID_PROACTIVE_COMMAND, null);//Handler的what标示是MSG_ID_PROACTIVE_COMMAND
mCmdIf.setOnCatEvent(this, MSG_ID_EVENT_NOTIFY, null);
mCmdIf.setOnCatCallSetUp(this, MSG_ID_CALL_SETUP, null);
好了,到这里我们先整理一下:
插卡开机,Medem读取SIM卡信息,上报RIL_UNSOL_STK_PROACTIVE_COMMAND消息。RIlJ中的RILReceiver类进行接收并交给processUnsolicited()方法处理。processUnsolicited()方法根据消息类型通知对应的Handler处理()。而这些Handler的处理在CatService.java的handleMessage()方法中,如下:
@Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_ID_PROACTIVE_COMMAND: String data = null; if (msg.obj != null) { AsyncResult ar = (AsyncResult) msg.obj; if (ar != null && ar.result != null) { try { data = (String) ar.result; } catch (ClassCastException e) { break; } } } if (lastCmd != null && data != null && lastCmd.equals(data)) { CatLog.d(this, "<" + mPhoneId + ">" + "duplicate command, ignored !!"); break; } if (data != null) { lastCmd = new String(data); } else { lastCmd = null; } if(MSG_ID_PROACTIVE_COMMAND == msg.what && null != data) { if((mSetupMenuFlag&(1<<mPhoneId))==0 && isSetupMenuCMD(data)) { saveSetupMenuData(data); } } mMsgDecoder.sendStartDecodingMessageParams(new RilMessage(msg.what, data)); break; case MSG_ID_RIL_MSG_DECODED: handleRilMsg((RilMessage) msg.obj); break; }
查看HandleMessage()方法中的MSG_ID_PROACTIVE_COMMAND消息处理。最后会调用mMsgDecoder.sendStartDecodingMessageParams(new RilMessage(msg.what, data))这个方法:
RilMessageDecoder.java public void sendStartDecodingMessageParams(RilMessage rilMsg) { Message msg = obtainMessage(CMD_START); msg.obj = rilMsg; sendMessage(msg); } private class StateStart extends State { @Override public boolean processMessage(Message msg) { if (msg.what == CMD_START) { if (decodeMessageParams((RilMessage)msg.obj)) { transitionTo(mStateCmdParamsReady); } } else { CatLog.d(this, "StateStart unexpected expecting START=" + CMD_START + " got " + msg.what); } return true; } }
这个方法最后也是通过发送CMD_START消息,交给StateStart.java处理,decodeMessageParams()这个方法主要是解码,具体的解码过程需要查看Bertlv.java里的代码。从CMD_START消息的发送到解码结束这个过程,流程有点复杂,这里就不做具体的分析了,解码结束后,会发送MSG_ID_RIL_MSG_DECODED消息,查看CatService.java的handleMessage()方法,是的,它又回到了CatService.java中。在这个类中,频繁的使用了Handler的消息机制,我们只要关注好Handler的what标识就可以了。接下来会执行handleRilMsg()方法:
private void handleRilMsg(RilMessage rilMsg) { case MSG_ID_PROACTIVE_COMMAND: try { cmdParams = (CommandParams) rilMsg.mData; } catch (ClassCastException e) { // for error handling : cast exception CatLog.d(this, "Fail to parse proactive command"); sendTerminalResponse(mCurrntCmd.mCmdDet, ResultCode.CMD_DATA_NOT_UNDERSTOOD, false, 0x00, null); break; } if (cmdParams != null) { if (rilMsg.mResCode == ResultCode.OK || // ignore icon problem rilMsg.mResCode == ResultCode.PRFRMD_ICON_NOT_DISPLAYED) { handleProactiveCommand(cmdParams); } else { // for proactive commands that couldn't be decoded // successfully respond with the code generated by the // message decoder. sendTerminalResponse(cmdParams.mCmdDet, rilMsg.mResCode, false, 0, null); } } break; }
查看handleProactiveCommand()最后的一段代码,发现它发送一个广播,那这个广播是谁接收的呢?查看AppInterface.CAT_CMD_ACTION这个变量存储的包名是:android.intent.action.stk.command。
private void handleProactiveCommand(CommandParams cmdParams) { Intent intent = new Intent(AppInterface.CAT_CMD_ACTION); intent.putExtra("STK CMD", cmdMsg); intent.putExtra("phone_id", mPhoneId); intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); mContext.sendBroadcast(intent); }
通过搜索这个包名,我们可以追溯到了STK应用中的AndroidManifest.xml中,看看这里,定义了一个receiver来接收来自framework中的广播。至此,流程跟踪到了STk应用层。
3.STK部分(应用层)
AndroidManifest.xml:
<receiver android:name="com.android.stk.StkCmdReceiver" > <intent-filter> <action android:name="android.intent.action.stk.command" /> <action android:name="android.intent.action.stk.session_end" /> <action android:name="android.intent.action.stk.event" /> <action android:name="android.intent.action.LOCALE_CHANGED" /> </intent-filter> </receiver>
StkCmdReceiver主要负责从接收来自framework中的广播,并启动StkAppService服务。
public class StkCmdReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); int phoneId = intent.getIntExtra(StkAppService.PHONE_ID, 0); if(action.equals(AppInterface.CAT_CMD_EVENT) || Intent.ACTION_LOCALE_CHANGED.equals(action)) { handleEventDownload(context, intent); return; } if (phoneId == StkAppService.PHONE_ID_NUM) { if (action.equals(AppInterface.CAT_CMD_ACTION)) { handleCommandMessage(context, intent); } else if (action.equals(AppInterface.CAT_SESSION_END_ACTION)) { handleSessionEnd(context, intent); } } } private void handleCommandMessage(Context context, Intent intent) { Bundle args = new Bundle(); args.putInt(StkAppService.OPCODE, StkAppService.OP_CMD); args.putParcelable(StkAppService.CMD_MSG, intent .getParcelableExtra("STK CMD")); context.startService(new Intent(context, StkAppService.class) .putExtras(args)); } }
这个StkAppService是STK中的核心服务,它主要负责调度各个显示层显示相应的数据。
StkAppService.java public void onStart(Intent intent, int startId) { Bundle args = intent.getExtras(); if (args == null) { return; } Message msg = mServiceHandler.obtainMessage(); msg.arg1 = args.getInt(OPCODE); switch(msg.arg1) { case OP_CMD: msg.obj = args.getParcelable(CMD_MSG); break; } ... mServiceHandler.sendMessage(msg); } private final class ServiceHandler extends Handler { @Override public void handleMessage(Message msg) { int opcode = msg.arg1; switch (opcode) { case OP_CMD: CatCmdMessage cmdMsg = (CatCmdMessage) msg.obj; if (!isCmdInteractive(cmdMsg)) { handleCmd(cmdMsg); } else { if (!mCmdInProgress) { mCmdInProgress = true; handleCmd((CatCmdMessage) msg.obj); } else { mCmdsQ.addLast(new DelayedCmd(OP_CMD, (CatCmdMessage) msg.obj)); } } break; } } }
onStart()方法经过简单的处理,然后交给内部类ServiceHandler进行处理,根据类型的不同,交由不同的Activity显示。比如,这里交由handleCmd()方法:
private void handleCmd(CatCmdMessage cmdMsg) { if (cmdMsg == null) { return; } // save local reference for state tracking. mCurrentCmd = cmdMsg; CommandDetails cmddet = cmdMsg.getCmdDet(); boolean waitForUsersResponse = true; CatLog.d(this, cmdMsg.getCmdType().name()); switch (cmdMsg.getCmdType()) { case DISPLAY_TEXT: …. case SELECT_ITEM: mCurrentMenu = cmdMsg.getMenu(); isSendSS = false; //launchMenuActivity(cmdMsg.getMenu()); if (phoneIsIdle()) { launchMenuActivity(cmdMsg.getMenu()); CatLog.d(this,"SELECT_ITEM start StkMenuActivity"); } else { CatLog.d(this, "SELECT_ITEM is on call, mMenuIsVisibile = " + mMenuIsVisibile); if (mMenuIsVisibile) { launchMenuActivity(cmdMsg.getMenu()); CatLog.d(this,"mMenuIsVisibile start StkMenuActivity"); } return; } break; } }
启动某个Activity显示:
private void launchMenuActivity(Menu menu) { Intent newIntent = new Intent(Intent.ACTION_VIEW); newIntent.setClassName(PACKAGE_NAME, MENU_ACTIVITY_NAME); int intentFlags = Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP; if (menu == null) { // We assume this was initiated by the user pressing the tool kit icon intentFlags |= getFlagActivityNoUserAction(InitiatedByUserAction.yes); newIntent.putExtra("STATE", StkMenuActivity.STATE_MAIN); } else { // We don't know and we'll let getFlagActivityNoUserAction decide. intentFlags |= getFlagActivityNoUserAction(InitiatedByUserAction.unknown); newIntent.putExtra("STATE", StkMenuActivity.STATE_SECONDARY); } newIntent.setFlags(intentFlags); CatLog.d(this, "launchMenuActivity: intent=" + newIntent + ", mCurrentMenu="+ mCurrentMenu); mContext.startActivity(newIntent); mStkMenuRef = true; }
至此,开机显示STK信息的流程就追踪结束了。当然STK应用层面的东西还是很多的,在这一章就不打算再分析了,如果后面有时间,再继续看看。
StkLauncherActivity.java:入口类
StkMenuActivity.java 主页面(信息列表)
StkInputActivity.java 输入页面
StkDialogActivity.java 对话框页面
StkAppInstaller.java 这个类主要用来设置是否在桌面上显示这个STK图标
StkCmdReceiver.java 接收广播
相关文章推荐
- 【Android】布局——图片紧随文字
- 将第三方apk编译进Android系统文件system.img
- PRODUCT_COPY_FILES的深入理解,为何不能在Android.mk使用
- Android和IOS系统对比
- 《疯狂的android讲义第3版》读书笔记
- Android Studio 快捷键、插件、基本设置、注意事项
- Android应用程序插件化研究之DexClassLoader
- android中自定义radiobutton的背景色选择默认图标
- android回调的一些总结
- Android资源文件-Shape
- 存储卡路径(接口路径)
- Android Studio 教你在debug调试模式下使用正式签名
- Android Studio开发工具学习篇章一
- Android 判断横屏还是竖屏以及设置方式
- android常用快捷方式
- mac下配置android开发环境
- 关于Android调用invalidate()之后不能及时调用ondraw()的解决办法
- Android开发之DialogFragment
- H5和Android交互
- android apk文件反编译(Mac)