| /* |
| * Copyright (C) 2007 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.internal.telephony.gsm.stk; |
| |
| import android.content.Context; |
| import android.content.Intent; |
| import android.os.AsyncResult; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Message; |
| |
| import com.android.internal.telephony.IccUtils; |
| import com.android.internal.telephony.CommandsInterface; |
| import com.android.internal.telephony.gsm.SimCard; |
| import com.android.internal.telephony.gsm.SIMFileHandler; |
| import com.android.internal.telephony.gsm.SIMRecords; |
| |
| import android.util.Config; |
| |
| import java.io.ByteArrayOutputStream; |
| |
| /** |
| * Enumeration for representing the tag value of COMPREHENSION-TLV objects. If |
| * you want to get the actual value, call {@link #value() value} method. |
| * |
| * {@hide} |
| */ |
| enum ComprehensionTlvTag { |
| COMMAND_DETAILS(0x01), |
| DEVICE_IDENTITIES(0x02), |
| RESULT(0x03), |
| DURATION(0x04), |
| ALPHA_ID(0x05), |
| USSD_STRING(0x0a), |
| TEXT_STRING(0x0d), |
| TONE(0x0e), |
| ITEM(0x0f), |
| ITEM_ID(0x10), |
| RESPONSE_LENGTH(0x11), |
| FILE_LIST(0x12), |
| HELP_REQUEST(0x15), |
| DEFAULT_TEXT(0x17), |
| EVENT_LIST(0x19), |
| ICON_ID(0x1e), |
| ITEM_ICON_ID_LIST(0x1f), |
| IMMEDIATE_RESPONSE(0x2b), |
| LANGUAGE(0x2d), |
| URL(0x31), |
| BROWSER_TERMINATION_CAUSE(0x34), |
| TEXT_ATTRIBUTE(0x50); |
| |
| private int mValue; |
| |
| ComprehensionTlvTag(int value) { |
| mValue = value; |
| } |
| |
| /** |
| * Returns the actual value of this COMPREHENSION-TLV object. |
| * |
| * @return Actual tag value of this object |
| */ |
| public int value() { |
| return mValue; |
| } |
| |
| public static ComprehensionTlvTag fromInt(int value) { |
| for (ComprehensionTlvTag e : ComprehensionTlvTag.values()) { |
| if (e.mValue == value) { |
| return e; |
| } |
| } |
| return null; |
| } |
| } |
| |
| class RilMessage { |
| int mId; |
| Object mData; |
| ResultCode mResCode; |
| |
| RilMessage(int msgId, String rawData) { |
| mId = msgId; |
| mData = rawData; |
| } |
| |
| RilMessage(RilMessage other) { |
| this.mId = other.mId; |
| this.mData = other.mData; |
| this.mResCode = other.mResCode; |
| } |
| } |
| |
| /** |
| * Class that implements SIM Toolkit Telephony Service. Interacts with the RIL |
| * and application. |
| * |
| * {@hide} |
| */ |
| public class StkService extends Handler implements AppInterface { |
| |
| // Class members |
| private static SIMRecords mSimRecords; |
| |
| // Service members. |
| private static StkService sInstance; |
| private CommandsInterface mCmdIf; |
| private Context mContext; |
| private StkCmdMessage mCurrntCmd = null; |
| private StkCmdMessage mMenuCmd = null; |
| |
| private RilMessageDecoder mMsgDecoder = null; |
| |
| // Service constants. |
| static final int MSG_ID_SESSION_END = 1; |
| static final int MSG_ID_PROACTIVE_COMMAND = 2; |
| static final int MSG_ID_EVENT_NOTIFY = 3; |
| static final int MSG_ID_CALL_SETUP = 4; |
| static final int MSG_ID_REFRESH = 5; |
| static final int MSG_ID_RESPONSE = 6; |
| |
| static final int MSG_ID_RIL_MSG_DECODED = 10; |
| |
| // Events to signal SIM presence or absent in the device. |
| private static final int MSG_ID_SIM_LOADED = 20; |
| |
| private static final int DEV_ID_KEYPAD = 0x01; |
| private static final int DEV_ID_DISPLAY = 0x02; |
| private static final int DEV_ID_EARPIECE = 0x03; |
| private static final int DEV_ID_UICC = 0x81; |
| private static final int DEV_ID_TERMINAL = 0x82; |
| private static final int DEV_ID_NETWORK = 0x83; |
| |
| /* Intentionally private for singleton */ |
| private StkService(CommandsInterface ci, SIMRecords sr, Context context, |
| SIMFileHandler fh, SimCard sc) { |
| if (ci == null || sr == null || context == null || fh == null |
| || sc == null) { |
| throw new NullPointerException( |
| "Service: Input parameters must not be null"); |
| } |
| mCmdIf = ci; |
| mContext = context; |
| |
| // Get the RilMessagesDecoder for decoding the messages. |
| mMsgDecoder = RilMessageDecoder.getInstance(this, fh); |
| |
| // Register ril events handling. |
| mCmdIf.setOnStkSessionEnd(this, MSG_ID_SESSION_END, null); |
| mCmdIf.setOnStkProactiveCmd(this, MSG_ID_PROACTIVE_COMMAND, null); |
| mCmdIf.setOnStkEvent(this, MSG_ID_EVENT_NOTIFY, null); |
| mCmdIf.setOnStkCallSetUp(this, MSG_ID_CALL_SETUP, null); |
| //mCmdIf.setOnSimRefresh(this, MSG_ID_REFRESH, null); |
| |
| mSimRecords = sr; |
| |
| // Register for SIM ready event. |
| mSimRecords.registerForRecordsLoaded(this, MSG_ID_SIM_LOADED, null); |
| |
| mCmdIf.reportStkServiceIsRunning(null); |
| StkLog.d(this, "StkService: is running"); |
| } |
| |
| public void dispose() { |
| mSimRecords.unregisterForRecordsLoaded(this); |
| mCmdIf.unSetOnStkSessionEnd(this); |
| mCmdIf.unSetOnStkProactiveCmd(this); |
| mCmdIf.unSetOnStkEvent(this); |
| mCmdIf.unSetOnStkCallSetUp(this); |
| |
| this.removeCallbacksAndMessages(null); |
| } |
| |
| protected void finalize() { |
| StkLog.d(this, "Service finalized"); |
| } |
| |
| private void handleRilMsg(RilMessage rilMsg) { |
| if (rilMsg == null) { |
| return; |
| } |
| |
| // dispatch messages |
| CommandParams cmdParams = null; |
| switch (rilMsg.mId) { |
| case MSG_ID_EVENT_NOTIFY: |
| if (rilMsg.mResCode == ResultCode.OK) { |
| cmdParams = (CommandParams) rilMsg.mData; |
| if (cmdParams != null) { |
| handleProactiveCommand(cmdParams); |
| } |
| } |
| break; |
| case MSG_ID_PROACTIVE_COMMAND: |
| cmdParams = (CommandParams) rilMsg.mData; |
| if (cmdParams != null) { |
| if (rilMsg.mResCode == ResultCode.OK) { |
| handleProactiveCommand(cmdParams); |
| } else { |
| // for proactive commands that couldn't be decoded |
| // successfully respond with the code generated by the |
| // message decoder. |
| sendTerminalResponse(cmdParams.cmdDet, rilMsg.mResCode, |
| false, 0, null); |
| } |
| } |
| break; |
| case MSG_ID_REFRESH: |
| cmdParams = (CommandParams) rilMsg.mData; |
| if (cmdParams != null) { |
| handleProactiveCommand(cmdParams); |
| } |
| break; |
| case MSG_ID_SESSION_END: |
| handleSessionEnd(); |
| break; |
| case MSG_ID_CALL_SETUP: |
| // prior event notify command supplied all the information |
| // needed for set up call processing. |
| break; |
| } |
| } |
| |
| /** |
| * Handles RIL_UNSOL_STK_PROACTIVE_COMMAND unsolicited command from RIL. |
| * Sends valid proactive command data to the application using intents. |
| * |
| */ |
| private void handleProactiveCommand(CommandParams cmdParams) { |
| StkLog.d(this, cmdParams.getCommandType().name()); |
| |
| StkCmdMessage cmdMsg = new StkCmdMessage(cmdParams); |
| switch (cmdParams.getCommandType()) { |
| case SET_UP_MENU: |
| if (removeMenu(cmdMsg.getMenu())) { |
| mMenuCmd = null; |
| } else { |
| mMenuCmd = cmdMsg; |
| } |
| sendTerminalResponse(cmdParams.cmdDet, ResultCode.OK, false, 0, |
| null); |
| break; |
| case DISPLAY_TEXT: |
| // when application is not required to respond, send an immediate |
| // response. |
| if (!cmdMsg.geTextMessage().responseNeeded) { |
| sendTerminalResponse(cmdParams.cmdDet, ResultCode.OK, false, |
| 0, null); |
| } |
| break; |
| case REFRESH: |
| // ME side only handles refresh commands which meant to remove IDLE |
| // MODE TEXT. |
| cmdParams.cmdDet.typeOfCommand = CommandType.SET_UP_IDLE_MODE_TEXT |
| .value(); |
| break; |
| case SET_UP_IDLE_MODE_TEXT: |
| sendTerminalResponse(cmdParams.cmdDet, ResultCode.OK, false, |
| 0, null); |
| break; |
| case LAUNCH_BROWSER: |
| case SELECT_ITEM: |
| case GET_INPUT: |
| case GET_INKEY: |
| case SEND_DTMF: |
| case SEND_SMS: |
| case SEND_SS: |
| case SEND_USSD: |
| case PLAY_TONE: |
| case SET_UP_CALL: |
| // nothing to do on telephony! |
| break; |
| default: |
| StkLog.d(this, "Unsupported command"); |
| return; |
| } |
| mCurrntCmd = cmdMsg; |
| Intent intent = new Intent(AppInterface.STK_CMD_ACTION); |
| intent.putExtra("STK CMD", cmdMsg); |
| mContext.sendBroadcast(intent); |
| } |
| |
| /** |
| * Handles RIL_UNSOL_STK_SESSION_END unsolicited command from RIL. |
| * |
| */ |
| private void handleSessionEnd() { |
| StkLog.d(this, "SESSION END"); |
| |
| mCurrntCmd = mMenuCmd; |
| Intent intent = new Intent(AppInterface.STK_SESSION_END_ACTION); |
| mContext.sendBroadcast(intent); |
| } |
| |
| private void sendTerminalResponse(CommandDetails cmdDet, |
| ResultCode resultCode, boolean includeAdditionalInfo, |
| int additionalInfo, ResponseData resp) { |
| |
| if (cmdDet == null) { |
| return; |
| } |
| ByteArrayOutputStream buf = new ByteArrayOutputStream(); |
| |
| // command details |
| int tag = ComprehensionTlvTag.COMMAND_DETAILS.value(); |
| if (cmdDet.compRequired) { |
| tag |= 0x80; |
| } |
| buf.write(tag); |
| buf.write(0x03); // length |
| buf.write(cmdDet.commandNumber); |
| buf.write(cmdDet.typeOfCommand); |
| buf.write(cmdDet.commandQualifier); |
| |
| // device identities |
| tag = 0x80 | ComprehensionTlvTag.DEVICE_IDENTITIES.value(); |
| buf.write(tag); |
| buf.write(0x02); // length |
| buf.write(DEV_ID_TERMINAL); // source device id |
| buf.write(DEV_ID_UICC); // destination device id |
| |
| // result |
| tag = 0x80 | ComprehensionTlvTag.RESULT.value(); |
| buf.write(tag); |
| int length = includeAdditionalInfo ? 2 : 1; |
| buf.write(length); |
| buf.write(resultCode.value()); |
| |
| // additional info |
| if (includeAdditionalInfo) { |
| buf.write(additionalInfo); |
| } |
| |
| // Fill optional data for each corresponding command |
| if (resp != null) { |
| resp.format(buf); |
| } |
| |
| byte[] rawData = buf.toByteArray(); |
| String hexString = IccUtils.bytesToHexString(rawData); |
| if (Config.LOGD) { |
| StkLog.d(this, "TERMINAL RESPONSE: " + hexString); |
| } |
| |
| mCmdIf.sendTerminalResponse(hexString, null); |
| } |
| |
| |
| private void sendMenuSelection(int menuId, boolean helpRequired) { |
| |
| ByteArrayOutputStream buf = new ByteArrayOutputStream(); |
| |
| // tag |
| int tag = BerTlv.BER_MENU_SELECTION_TAG; |
| buf.write(tag); |
| |
| // length |
| buf.write(0x00); // place holder |
| |
| // device identities |
| tag = 0x80 | ComprehensionTlvTag.DEVICE_IDENTITIES.value(); |
| buf.write(tag); |
| buf.write(0x02); // length |
| buf.write(DEV_ID_KEYPAD); // source device id |
| buf.write(DEV_ID_UICC); // destination device id |
| |
| // item identifier |
| tag = 0x80 | ComprehensionTlvTag.ITEM_ID.value(); |
| buf.write(tag); |
| buf.write(0x01); // length |
| buf.write(menuId); // menu identifier chosen |
| |
| // help request |
| if (helpRequired) { |
| tag = ComprehensionTlvTag.HELP_REQUEST.value(); |
| buf.write(tag); |
| buf.write(0x00); // length |
| } |
| |
| byte[] rawData = buf.toByteArray(); |
| |
| // write real length |
| int len = rawData.length - 2; // minus (tag + length) |
| rawData[1] = (byte) len; |
| |
| String hexString = IccUtils.bytesToHexString(rawData); |
| |
| mCmdIf.sendEnvelope(hexString, null); |
| } |
| |
| private void eventDownload(int event, int sourceId, int destinationId, |
| byte[] additionalInfo, boolean oneShot) { |
| |
| ByteArrayOutputStream buf = new ByteArrayOutputStream(); |
| |
| // tag |
| int tag = BerTlv.BER_EVENT_DOWNLOAD_TAG; |
| buf.write(tag); |
| |
| // length |
| buf.write(0x00); // place holder, assume length < 128. |
| |
| // event list |
| tag = 0x80 | ComprehensionTlvTag.EVENT_LIST.value(); |
| buf.write(tag); |
| buf.write(0x01); // length |
| buf.write(event); // event value |
| |
| // device identities |
| tag = 0x80 | ComprehensionTlvTag.DEVICE_IDENTITIES.value(); |
| buf.write(tag); |
| buf.write(0x02); // length |
| buf.write(sourceId); // source device id |
| buf.write(destinationId); // destination device id |
| |
| // additional information |
| if (additionalInfo != null) { |
| for (byte b : additionalInfo) { |
| buf.write(b); |
| } |
| } |
| |
| byte[] rawData = buf.toByteArray(); |
| |
| // write real length |
| int len = rawData.length - 2; // minus (tag + length) |
| rawData[1] = (byte) len; |
| |
| String hexString = IccUtils.bytesToHexString(rawData); |
| |
| mCmdIf.sendEnvelope(hexString, null); |
| } |
| |
| /** |
| * Used for instantiating/updating the Service from the GsmPhone constructor. |
| * |
| * @param ci CommandsInterface object |
| * @param sr SIMRecords object |
| * @param context phone app context |
| * @param fh SIM file handler |
| * @param sc GSM SIM card |
| * @return The only Service object in the system |
| */ |
| public static StkService getInstance(CommandsInterface ci, SIMRecords sr, |
| Context context, SIMFileHandler fh, SimCard sc) { |
| if (sInstance == null) { |
| if (ci == null || sr == null || context == null || fh == null |
| || sc == null) { |
| return null; |
| } |
| HandlerThread thread = new HandlerThread("Stk Telephony service"); |
| thread.start(); |
| sInstance = new StkService(ci, sr, context, fh, sc); |
| StkLog.d(sInstance, "NEW sInstance"); |
| } else if ((sr != null) && (mSimRecords != sr)) { |
| StkLog.d(sInstance, "Reinitialize the Service with SIMRecords"); |
| mSimRecords = sr; |
| |
| // re-Register for SIM ready event. |
| mSimRecords.registerForRecordsLoaded(sInstance, MSG_ID_SIM_LOADED, null); |
| StkLog.d(sInstance, "sr changed reinitialize and return current sInstance"); |
| } else { |
| StkLog.d(sInstance, "Return current sInstance"); |
| } |
| return sInstance; |
| } |
| |
| /** |
| * Used by application to get an AppInterface object. |
| * |
| * @return The only Service object in the system |
| */ |
| public static AppInterface getInstance() { |
| return getInstance(null, null, null, null, null); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| |
| switch (msg.what) { |
| case MSG_ID_SESSION_END: |
| case MSG_ID_PROACTIVE_COMMAND: |
| case MSG_ID_EVENT_NOTIFY: |
| case MSG_ID_REFRESH: |
| StkLog.d(this, "ril message arrived"); |
| 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; |
| } |
| } |
| } |
| mMsgDecoder.sendStartDecodingMessageParams(new RilMessage(msg.what, data)); |
| break; |
| case MSG_ID_CALL_SETUP: |
| mMsgDecoder.sendStartDecodingMessageParams(new RilMessage(msg.what, null)); |
| break; |
| case MSG_ID_SIM_LOADED: |
| break; |
| case MSG_ID_RIL_MSG_DECODED: |
| handleRilMsg((RilMessage) msg.obj); |
| break; |
| case MSG_ID_RESPONSE: |
| handleCmdResponse((StkResponseMessage) msg.obj); |
| break; |
| default: |
| throw new AssertionError("Unrecognized STK command: " + msg.what); |
| } |
| } |
| |
| public synchronized void onCmdResponse(StkResponseMessage resMsg) { |
| if (resMsg == null) { |
| return; |
| } |
| // queue a response message. |
| Message msg = this.obtainMessage(MSG_ID_RESPONSE, resMsg); |
| msg.sendToTarget(); |
| } |
| |
| private boolean validateResponse(StkResponseMessage resMsg) { |
| if (mCurrntCmd != null) { |
| return (resMsg.cmdDet.compareTo(mCurrntCmd.mCmdDet)); |
| } |
| return false; |
| } |
| |
| private boolean removeMenu(Menu menu) { |
| try { |
| if (menu.items.size() == 1 && menu.items.get(0) == null) { |
| return true; |
| } |
| } catch (NullPointerException e) { |
| StkLog.d(this, "Unable to get Menu's items size"); |
| return true; |
| } |
| return false; |
| } |
| |
| private void handleCmdResponse(StkResponseMessage resMsg) { |
| // Make sure the response details match the last valid command. An invalid |
| // response is a one that doesn't have a corresponding proactive command |
| // and sending it can "confuse" the baseband/ril. |
| // One reason for out of order responses can be UI glitches. For example, |
| // if the application launch an activity, and that activity is stored |
| // by the framework inside the history stack. That activity will be |
| // available for relaunch using the latest application dialog |
| // (long press on the home button). Relaunching that activity can send |
| // the same command's result again to the StkService and can cause it to |
| // get out of sync with the SIM. |
| if (!validateResponse(resMsg)) { |
| return; |
| } |
| ResponseData resp = null; |
| boolean helpRequired = false; |
| CommandDetails cmdDet = resMsg.getCmdDetails(); |
| |
| switch (resMsg.resCode) { |
| case HELP_INFO_REQUIRED: |
| helpRequired = true; |
| // fall through |
| case OK: |
| case PRFRMD_WITH_PARTIAL_COMPREHENSION: |
| case PRFRMD_WITH_MISSING_INFO: |
| case PRFRMD_WITH_ADDITIONAL_EFS_READ: |
| case PRFRMD_ICON_NOT_DISPLAYED: |
| case PRFRMD_MODIFIED_BY_NAA: |
| case PRFRMD_LIMITED_SERVICE: |
| case PRFRMD_WITH_MODIFICATION: |
| case PRFRMD_NAA_NOT_ACTIVE: |
| case PRFRMD_TONE_NOT_PLAYED: |
| switch (AppInterface.CommandType.fromInt(cmdDet.typeOfCommand)) { |
| case SET_UP_MENU: |
| helpRequired = resMsg.resCode == ResultCode.HELP_INFO_REQUIRED; |
| sendMenuSelection(resMsg.usersMenuSelection, helpRequired); |
| return; |
| case SELECT_ITEM: |
| resp = new SelectItemResponseData(resMsg.usersMenuSelection); |
| break; |
| case GET_INPUT: |
| case GET_INKEY: |
| Input input = mCurrntCmd.geInput(); |
| if (!input.yesNo) { |
| // when help is requested there is no need to send the text |
| // string object. |
| if (!helpRequired) { |
| resp = new GetInkeyInputResponseData(resMsg.usersInput, |
| input.ucs2, input.packed); |
| } |
| } else { |
| resp = new GetInkeyInputResponseData( |
| resMsg.usersYesNoSelection); |
| } |
| break; |
| case DISPLAY_TEXT: |
| case LAUNCH_BROWSER: |
| break; |
| case SET_UP_CALL: |
| mCmdIf.handleCallSetupRequestFromSim(resMsg.usersConfirm, null); |
| // No need to send terminal response for SET UP CALL. The user's |
| // confirmation result is send back using a dedicated ril message |
| // invoked by the CommandInterface call above. |
| mCurrntCmd = null; |
| return; |
| } |
| break; |
| case NO_RESPONSE_FROM_USER: |
| case UICC_SESSION_TERM_BY_USER: |
| case BACKWARD_MOVE_BY_USER: |
| resp = null; |
| break; |
| default: |
| return; |
| } |
| sendTerminalResponse(cmdDet, resMsg.resCode, false, 0, resp); |
| mCurrntCmd = null; |
| } |
| } |