blob: 0ffb9c8f04b24285f08c12d325276c48f849c1a4 [file] [log] [blame]
/*
* 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.stk;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.telephony.TelephonyManager;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.RemoteViews;
import android.widget.TextView;
import android.widget.Toast;
import com.android.internal.telephony.cat.AppInterface;
import com.android.internal.telephony.cat.Menu;
import com.android.internal.telephony.cat.Item;
import com.android.internal.telephony.cat.Input;
import com.android.internal.telephony.cat.ResultCode;
import com.android.internal.telephony.cat.CatCmdMessage;
import com.android.internal.telephony.cat.CatCmdMessage.BrowserSettings;
import com.android.internal.telephony.cat.CatLog;
import com.android.internal.telephony.cat.CatResponseMessage;
import com.android.internal.telephony.cat.TextMessage;
import java.util.LinkedList;
/**
* SIM toolkit application level service. Interacts with Telephopny messages,
* application's launch and user input from STK UI elements.
*
*/
public class StkAppService extends Service implements Runnable {
// members
private volatile Looper mServiceLooper;
private volatile ServiceHandler mServiceHandler;
private AppInterface mStkService;
private Context mContext = null;
private CatCmdMessage mMainCmd = null;
private CatCmdMessage mCurrentCmd = null;
private Menu mCurrentMenu = null;
private String lastSelectedItem = null;
private boolean mMenuIsVisibile = false;
private boolean responseNeeded = true;
private boolean mCmdInProgress = false;
private NotificationManager mNotificationManager = null;
private LinkedList<DelayedCmd> mCmdsQ = null;
private boolean launchBrowser = false;
private BrowserSettings mBrowserSettings = null;
static StkAppService sInstance = null;
// Used for setting FLAG_ACTIVITY_NO_USER_ACTION when
// creating an intent.
private enum InitiatedByUserAction {
yes, // The action was started via a user initiated action
unknown, // Not known for sure if user initated the action
}
// constants
static final String OPCODE = "op";
static final String CMD_MSG = "cmd message";
static final String RES_ID = "response id";
static final String MENU_SELECTION = "menu selection";
static final String INPUT = "input";
static final String HELP = "help";
static final String CONFIRMATION = "confirm";
// operations ids for different service functionality.
static final int OP_CMD = 1;
static final int OP_RESPONSE = 2;
static final int OP_LAUNCH_APP = 3;
static final int OP_END_SESSION = 4;
static final int OP_BOOT_COMPLETED = 5;
private static final int OP_DELAYED_MSG = 6;
// Response ids
static final int RES_ID_MENU_SELECTION = 11;
static final int RES_ID_INPUT = 12;
static final int RES_ID_CONFIRM = 13;
static final int RES_ID_DONE = 14;
static final int RES_ID_TIMEOUT = 20;
static final int RES_ID_BACKWARD = 21;
static final int RES_ID_END_SESSION = 22;
static final int RES_ID_EXIT = 23;
private static final String PACKAGE_NAME = "com.android.stk";
private static final String MENU_ACTIVITY_NAME =
PACKAGE_NAME + ".StkMenuActivity";
private static final String INPUT_ACTIVITY_NAME =
PACKAGE_NAME + ".StkInputActivity";
// Notification id used to display Idle Mode text in NotificationManager.
private static final int STK_NOTIFICATION_ID = 333;
// Inner class used for queuing telephony messages (proactive commands,
// session end) while the service is busy processing a previous message.
private class DelayedCmd {
// members
int id;
CatCmdMessage msg;
DelayedCmd(int id, CatCmdMessage msg) {
this.id = id;
this.msg = msg;
}
}
@Override
public void onCreate() {
// Initialize members
mStkService = com.android.internal.telephony.cat.CatService
.getInstance();
// NOTE mStkService is a singleton and continues to exist even if the GSMPhone is disposed
// after the radio technology change from GSM to CDMA so the PHONE_TYPE_CDMA check is
// needed. In case of switching back from CDMA to GSM the GSMPhone constructor updates
// the instance. (TODO: test).
if ((mStkService == null)
&& (TelephonyManager.getDefault().getPhoneType()
!= TelephonyManager.PHONE_TYPE_CDMA)) {
CatLog.d(this, " Unable to get Service handle");
return;
}
mCmdsQ = new LinkedList<DelayedCmd>();
Thread serviceThread = new Thread(null, this, "Stk App Service");
serviceThread.start();
mContext = getBaseContext();
mNotificationManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
sInstance = this;
}
@Override
public void onStart(Intent intent, int startId) {
waitForLooper();
// onStart() method can be passed a null intent
// TODO: replace onStart() with onStartCommand()
if (intent == null) {
return;
}
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;
case OP_RESPONSE:
msg.obj = args;
/* falls through */
case OP_LAUNCH_APP:
case OP_END_SESSION:
case OP_BOOT_COMPLETED:
break;
default:
return;
}
mServiceHandler.sendMessage(msg);
}
@Override
public void onDestroy() {
waitForLooper();
mServiceLooper.quit();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
public void run() {
Looper.prepare();
mServiceLooper = Looper.myLooper();
mServiceHandler = new ServiceHandler();
Looper.loop();
}
/*
* Package api used by StkMenuActivity to indicate if its on the foreground.
*/
void indicateMenuVisibility(boolean visibility) {
mMenuIsVisibile = visibility;
}
/*
* Package api used by StkMenuActivity to get its Menu parameter.
*/
Menu getMenu() {
return mCurrentMenu;
}
/*
* Package api used by UI Activities and Dialogs to communicate directly
* with the service to deliver state information and parameters.
*/
static StkAppService getInstance() {
return sInstance;
}
private void waitForLooper() {
while (mServiceHandler == null) {
synchronized (this) {
try {
wait(100);
} catch (InterruptedException e) {
}
}
}
}
private final class ServiceHandler extends Handler {
@Override
public void handleMessage(Message msg) {
int opcode = msg.arg1;
switch (opcode) {
case OP_LAUNCH_APP:
if (mMainCmd == null) {
// nothing todo when no SET UP MENU command didn't arrive.
return;
}
launchMenuActivity(null);
break;
case OP_CMD:
CatCmdMessage cmdMsg = (CatCmdMessage) msg.obj;
// There are two types of commands:
// 1. Interactive - user's response is required.
// 2. Informative - display a message, no interaction with the user.
//
// Informative commands can be handled immediately without any delay.
// Interactive commands can't override each other. So if a command
// is already in progress, we need to queue the next command until
// the user has responded or a timeout expired.
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;
case OP_RESPONSE:
if (responseNeeded) {
handleCmdResponse((Bundle) msg.obj);
}
// call delayed commands if needed.
if (mCmdsQ.size() != 0) {
callDelayedMsg();
} else {
mCmdInProgress = false;
}
// reset response needed state var to its original value.
responseNeeded = true;
break;
case OP_END_SESSION:
if (!mCmdInProgress) {
mCmdInProgress = true;
handleSessionEnd();
} else {
mCmdsQ.addLast(new DelayedCmd(OP_END_SESSION, null));
}
break;
case OP_BOOT_COMPLETED:
CatLog.d(this, "OP_BOOT_COMPLETED");
if (mMainCmd == null) {
StkAppInstaller.unInstall(mContext);
}
break;
case OP_DELAYED_MSG:
handleDelayedCmd();
break;
}
}
}
private boolean isCmdInteractive(CatCmdMessage cmd) {
switch (cmd.getCmdType()) {
case SEND_DTMF:
case SEND_SMS:
case SEND_SS:
case SEND_USSD:
case SET_UP_IDLE_MODE_TEXT:
case SET_UP_MENU:
return false;
}
return true;
}
private void handleDelayedCmd() {
if (mCmdsQ.size() != 0) {
DelayedCmd cmd = mCmdsQ.poll();
switch (cmd.id) {
case OP_CMD:
handleCmd(cmd.msg);
break;
case OP_END_SESSION:
handleSessionEnd();
break;
}
}
}
private void callDelayedMsg() {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = OP_DELAYED_MSG;
mServiceHandler.sendMessage(msg);
}
private void handleSessionEnd() {
mCurrentCmd = mMainCmd;
lastSelectedItem = null;
// In case of SET UP MENU command which removed the app, don't
// update the current menu member.
if (mCurrentMenu != null && mMainCmd != null) {
mCurrentMenu = mMainCmd.getMenu();
}
if (mMenuIsVisibile) {
launchMenuActivity(null);
}
if (mCmdsQ.size() != 0) {
callDelayedMsg();
} else {
mCmdInProgress = false;
}
// In case a launch browser command was just confirmed, launch that url.
if (launchBrowser) {
launchBrowser = false;
launchBrowser(mBrowserSettings);
}
}
private void handleCmd(CatCmdMessage cmdMsg) {
if (cmdMsg == null) {
return;
}
// save local reference for state tracking.
mCurrentCmd = cmdMsg;
boolean waitForUsersResponse = true;
CatLog.d(this, cmdMsg.getCmdType().name());
switch (cmdMsg.getCmdType()) {
case DISPLAY_TEXT:
TextMessage msg = cmdMsg.geTextMessage();
responseNeeded = msg.responseNeeded;
if (lastSelectedItem != null) {
msg.title = lastSelectedItem;
} else if (mMainCmd != null){
msg.title = mMainCmd.getMenu().title;
} else {
// TODO: get the carrier name from the SIM
msg.title = "";
}
launchTextDialog();
break;
case SELECT_ITEM:
mCurrentMenu = cmdMsg.getMenu();
launchMenuActivity(cmdMsg.getMenu());
break;
case SET_UP_MENU:
mMainCmd = mCurrentCmd;
mCurrentMenu = cmdMsg.getMenu();
if (removeMenu()) {
CatLog.d(this, "Uninstall App");
mCurrentMenu = null;
StkAppInstaller.unInstall(mContext);
} else {
CatLog.d(this, "Install App");
StkAppInstaller.install(mContext);
}
if (mMenuIsVisibile) {
launchMenuActivity(null);
}
break;
case GET_INPUT:
case GET_INKEY:
launchInputActivity();
break;
case SET_UP_IDLE_MODE_TEXT:
waitForUsersResponse = false;
launchIdleText();
break;
case SEND_DTMF:
case SEND_SMS:
case SEND_SS:
case SEND_USSD:
waitForUsersResponse = false;
launchEventMessage();
break;
case LAUNCH_BROWSER:
launchConfirmationDialog(mCurrentCmd.geTextMessage());
break;
case SET_UP_CALL:
launchConfirmationDialog(mCurrentCmd.getCallSettings().confirmMsg);
break;
case PLAY_TONE:
launchToneDialog();
break;
}
if (!waitForUsersResponse) {
if (mCmdsQ.size() != 0) {
callDelayedMsg();
} else {
mCmdInProgress = false;
}
}
}
private void handleCmdResponse(Bundle args) {
if (mCurrentCmd == null) {
return;
}
CatResponseMessage resMsg = new CatResponseMessage(mCurrentCmd);
// set result code
boolean helpRequired = args.getBoolean(HELP, false);
switch(args.getInt(RES_ID)) {
case RES_ID_MENU_SELECTION:
CatLog.d(this, "RES_ID_MENU_SELECTION");
int menuSelection = args.getInt(MENU_SELECTION);
switch(mCurrentCmd.getCmdType()) {
case SET_UP_MENU:
case SELECT_ITEM:
lastSelectedItem = getItemName(menuSelection);
if (helpRequired) {
resMsg.setResultCode(ResultCode.HELP_INFO_REQUIRED);
} else {
resMsg.setResultCode(ResultCode.OK);
}
resMsg.setMenuSelection(menuSelection);
break;
}
break;
case RES_ID_INPUT:
CatLog.d(this, "RES_ID_INPUT");
String input = args.getString(INPUT);
Input cmdInput = mCurrentCmd.geInput();
if (cmdInput != null && cmdInput.yesNo) {
boolean yesNoSelection = input
.equals(StkInputActivity.YES_STR_RESPONSE);
resMsg.setYesNo(yesNoSelection);
} else {
if (helpRequired) {
resMsg.setResultCode(ResultCode.HELP_INFO_REQUIRED);
} else {
resMsg.setResultCode(ResultCode.OK);
resMsg.setInput(input);
}
}
break;
case RES_ID_CONFIRM:
CatLog.d(this, "RES_ID_CONFIRM");
boolean confirmed = args.getBoolean(CONFIRMATION);
switch (mCurrentCmd.getCmdType()) {
case DISPLAY_TEXT:
resMsg.setResultCode(confirmed ? ResultCode.OK
: ResultCode.UICC_SESSION_TERM_BY_USER);
break;
case LAUNCH_BROWSER:
resMsg.setResultCode(confirmed ? ResultCode.OK
: ResultCode.UICC_SESSION_TERM_BY_USER);
if (confirmed) {
launchBrowser = true;
mBrowserSettings = mCurrentCmd.getBrowserSettings();
}
break;
case SET_UP_CALL:
resMsg.setResultCode(ResultCode.OK);
resMsg.setConfirmation(confirmed);
if (confirmed) {
launchCallMsg();
}
break;
}
break;
case RES_ID_DONE:
resMsg.setResultCode(ResultCode.OK);
break;
case RES_ID_BACKWARD:
CatLog.d(this, "RES_ID_BACKWARD");
resMsg.setResultCode(ResultCode.BACKWARD_MOVE_BY_USER);
break;
case RES_ID_END_SESSION:
CatLog.d(this, "RES_ID_END_SESSION");
resMsg.setResultCode(ResultCode.UICC_SESSION_TERM_BY_USER);
break;
case RES_ID_TIMEOUT:
CatLog.d(this, "RES_ID_TIMEOUT");
// GCF test-case 27.22.4.1.1 Expected Sequence 1.5 (DISPLAY TEXT,
// Clear message after delay, successful) expects result code OK.
// If the command qualifier specifies no user response is required
// then send OK instead of NO_RESPONSE_FROM_USER
if ((mCurrentCmd.getCmdType().value() == AppInterface.CommandType.DISPLAY_TEXT
.value())
&& (mCurrentCmd.geTextMessage().userClear == false)) {
resMsg.setResultCode(ResultCode.OK);
} else {
resMsg.setResultCode(ResultCode.NO_RESPONSE_FROM_USER);
}
break;
default:
CatLog.d(this, "Unknown result id");
return;
}
mStkService.onCmdResponse(resMsg);
}
/**
* Returns 0 or FLAG_ACTIVITY_NO_USER_ACTION, 0 means the user initiated the action.
*
* @param userAction If the userAction is yes then we always return 0 otherwise
* mMenuIsVisible is used to determine what to return. If mMenuIsVisible is true
* then we are the foreground app and we'll return 0 as from our perspective a
* user action did cause. If it's false than we aren't the foreground app and
* FLAG_ACTIVITY_NO_USER_ACTION is returned.
*
* @return 0 or FLAG_ACTIVITY_NO_USER_ACTION
*/
private int getFlagActivityNoUserAction(InitiatedByUserAction userAction) {
return ((userAction == InitiatedByUserAction.yes) | mMenuIsVisibile) ?
0 : Intent.FLAG_ACTIVITY_NO_USER_ACTION;
}
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);
mContext.startActivity(newIntent);
}
private void launchInputActivity() {
Intent newIntent = new Intent(Intent.ACTION_VIEW);
newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| getFlagActivityNoUserAction(InitiatedByUserAction.unknown));
newIntent.setClassName(PACKAGE_NAME, INPUT_ACTIVITY_NAME);
newIntent.putExtra("INPUT", mCurrentCmd.geInput());
mContext.startActivity(newIntent);
}
private void launchTextDialog() {
Intent newIntent = new Intent(this, StkDialogActivity.class);
newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_MULTIPLE_TASK
| Intent.FLAG_ACTIVITY_NO_HISTORY
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| getFlagActivityNoUserAction(InitiatedByUserAction.unknown));
newIntent.putExtra("TEXT", mCurrentCmd.geTextMessage());
startActivity(newIntent);
}
private void launchEventMessage() {
TextMessage msg = mCurrentCmd.geTextMessage();
if (msg == null || msg.text == null) {
return;
}
Toast toast = new Toast(mContext.getApplicationContext());
LayoutInflater inflate = (LayoutInflater) mContext
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(R.layout.stk_event_msg, null);
TextView tv = (TextView) v
.findViewById(com.android.internal.R.id.message);
ImageView iv = (ImageView) v
.findViewById(com.android.internal.R.id.icon);
if (msg.icon != null) {
iv.setImageBitmap(msg.icon);
} else {
iv.setVisibility(View.GONE);
}
if (!msg.iconSelfExplanatory) {
tv.setText(msg.text);
}
toast.setView(v);
toast.setDuration(Toast.LENGTH_LONG);
toast.setGravity(Gravity.BOTTOM, 0, 0);
toast.show();
}
private void launchConfirmationDialog(TextMessage msg) {
msg.title = lastSelectedItem;
Intent newIntent = new Intent(this, StkDialogActivity.class);
newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_NO_HISTORY
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| getFlagActivityNoUserAction(InitiatedByUserAction.unknown));
newIntent.putExtra("TEXT", msg);
startActivity(newIntent);
}
private void launchBrowser(BrowserSettings settings) {
if (settings == null) {
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri data;
if (settings.url != null) {
data = Uri.parse(settings.url);
} else {
// If no URL specified, just bring up the "home page".
//
// (Note we need to specify *something* in the intent's data field
// here, since if you fire off a VIEW intent with no data at all
// you'll get an activity chooser rather than the browser. There's
// no specific URI that means "use the default home page", so
// instead let's just explicitly bring up http://google.com.)
data = Uri.parse("http://google.com/");
}
intent.setData(data);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
switch (settings.mode) {
case USE_EXISTING_BROWSER:
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
break;
case LAUNCH_NEW_BROWSER:
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
break;
case LAUNCH_IF_NOT_ALREADY_LAUNCHED:
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
break;
}
// start browser activity
startActivity(intent);
// a small delay, let the browser start, before processing the next command.
// this is good for scenarios where a related DISPLAY TEXT command is
// followed immediately.
try {
Thread.sleep(10000);
} catch (InterruptedException e) {}
}
private void launchCallMsg() {
TextMessage msg = mCurrentCmd.getCallSettings().callMsg;
if (msg.text == null || msg.text.length() == 0) {
return;
}
msg.title = lastSelectedItem;
Toast toast = Toast.makeText(mContext.getApplicationContext(), msg.text,
Toast.LENGTH_LONG);
toast.setGravity(Gravity.BOTTOM, 0, 0);
toast.show();
}
private void launchIdleText() {
TextMessage msg = mCurrentCmd.geTextMessage();
if (msg.text == null) {
mNotificationManager.cancel(STK_NOTIFICATION_ID);
} else {
Notification notification = new Notification();
RemoteViews contentView = new RemoteViews(
PACKAGE_NAME,
com.android.internal.R.layout.status_bar_latest_event_content);
notification.flags |= Notification.FLAG_NO_CLEAR;
notification.icon = com.android.internal.R.drawable.stat_notify_sim_toolkit;
// Set text and icon for the status bar and notification body.
if (!msg.iconSelfExplanatory) {
notification.tickerText = msg.text;
contentView.setTextViewText(com.android.internal.R.id.text,
msg.text);
}
if (msg.icon != null) {
contentView.setImageViewBitmap(com.android.internal.R.id.icon,
msg.icon);
} else {
contentView
.setImageViewResource(
com.android.internal.R.id.icon,
com.android.internal.R.drawable.stat_notify_sim_toolkit);
}
notification.contentView = contentView;
notification.contentIntent = PendingIntent.getService(mContext, 0,
new Intent(mContext, StkAppService.class), 0);
mNotificationManager.notify(STK_NOTIFICATION_ID, notification);
}
}
private void launchToneDialog() {
Intent newIntent = new Intent(this, ToneDialog.class);
newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_NO_HISTORY
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| getFlagActivityNoUserAction(InitiatedByUserAction.unknown));
newIntent.putExtra("TEXT", mCurrentCmd.geTextMessage());
newIntent.putExtra("TONE", mCurrentCmd.getToneSettings());
startActivity(newIntent);
}
private String getItemName(int itemId) {
Menu menu = mCurrentCmd.getMenu();
if (menu == null) {
return null;
}
for (Item item : menu.items) {
if (item.id == itemId) {
return item.text;
}
}
return null;
}
private boolean removeMenu() {
try {
if (mCurrentMenu.items.size() == 1 &&
mCurrentMenu.items.get(0) == null) {
return true;
}
} catch (NullPointerException e) {
CatLog.d(this, "Unable to get Menu's items size");
return true;
}
return false;
}
}