1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.stk; 18 19 import android.app.ActionBar; 20 import android.app.Activity; 21 import android.app.AlarmManager; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.res.Configuration; 25 import android.os.Bundle; 26 import android.os.SystemClock; 27 import android.text.Editable; 28 import android.text.InputFilter; 29 import android.text.TextUtils; 30 import android.text.TextWatcher; 31 import android.text.method.PasswordTransformationMethod; 32 import android.view.KeyEvent; 33 import android.view.Menu; 34 import android.view.MenuItem; 35 import android.view.View; 36 import android.view.WindowManager; 37 import android.view.inputmethod.EditorInfo; 38 import android.widget.Button; 39 import android.widget.EditText; 40 import android.widget.ImageView; 41 import android.widget.PopupMenu; 42 import android.widget.TextView; 43 import android.widget.TextView.BufferType; 44 45 import com.android.internal.telephony.cat.CatLog; 46 import com.android.internal.telephony.cat.Input; 47 48 /** 49 * Display a request for a text input a long with a text edit form. 50 */ 51 public class StkInputActivity extends Activity implements View.OnClickListener, 52 TextWatcher { 53 54 // Members 55 private int mState; 56 private EditText mTextIn = null; 57 private TextView mPromptView = null; 58 private View mMoreOptions = null; 59 private PopupMenu mPopupMenu = null; 60 private View mYesNoLayout = null; 61 private View mNormalLayout = null; 62 63 // Constants 64 private static final String className = new Object(){}.getClass().getEnclosingClass().getName(); 65 private static final String LOG_TAG = className.substring(className.lastIndexOf('.') + 1); 66 67 private Input mStkInput = null; 68 // Constants 69 private static final int STATE_TEXT = 1; 70 private static final int STATE_YES_NO = 2; 71 72 static final String YES_STR_RESPONSE = "YES"; 73 static final String NO_STR_RESPONSE = "NO"; 74 75 // Font size factor values. 76 static final float NORMAL_FONT_FACTOR = 1; 77 static final float LARGE_FONT_FACTOR = 2; 78 static final float SMALL_FONT_FACTOR = (1 / 2); 79 80 // Keys for saving the state of the activity in the bundle 81 private static final String RESPONSE_SENT_KEY = "response_sent"; 82 private static final String INPUT_STRING_KEY = "input_string"; 83 private static final String ALARM_TIME_KEY = "alarm_time"; 84 private static final String PENDING = "pending"; 85 86 private static final String INPUT_ALARM_TAG = LOG_TAG; 87 private static final long NO_INPUT_ALARM = -1; 88 private long mAlarmTime = NO_INPUT_ALARM; 89 90 private StkAppService appService = StkAppService.getInstance(); 91 92 private boolean mIsResponseSent = false; 93 // Determines whether this is in the pending state. 94 private boolean mIsPending = false; 95 private int mSlotId = -1; 96 97 // Click listener to handle buttons press.. onClick(View v)98 public void onClick(View v) { 99 String input = null; 100 if (mIsResponseSent) { 101 CatLog.d(LOG_TAG, "Already responded"); 102 return; 103 } 104 105 switch (v.getId()) { 106 case R.id.button_ok: 107 input = mTextIn.getText().toString(); 108 break; 109 case R.id.button_cancel: 110 sendResponse(StkAppService.RES_ID_END_SESSION); 111 finish(); 112 return; 113 // Yes/No layout buttons. 114 case R.id.button_yes: 115 input = YES_STR_RESPONSE; 116 break; 117 case R.id.button_no: 118 input = NO_STR_RESPONSE; 119 break; 120 case R.id.more: 121 if (mPopupMenu == null) { 122 mPopupMenu = new PopupMenu(this, v); 123 Menu menu = mPopupMenu.getMenu(); 124 createOptionsMenuInternal(menu); 125 prepareOptionsMenuInternal(menu); 126 mPopupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { 127 public boolean onMenuItemClick(MenuItem item) { 128 optionsItemSelectedInternal(item); 129 return true; 130 } 131 }); 132 mPopupMenu.setOnDismissListener(new PopupMenu.OnDismissListener() { 133 public void onDismiss(PopupMenu menu) { 134 mPopupMenu = null; 135 } 136 }); 137 mPopupMenu.show(); 138 } 139 return; 140 default: 141 break; 142 } 143 CatLog.d(LOG_TAG, "handleClick, ready to response"); 144 sendResponse(StkAppService.RES_ID_INPUT, input, false); 145 } 146 147 @Override onCreate(Bundle savedInstanceState)148 public void onCreate(Bundle savedInstanceState) { 149 super.onCreate(savedInstanceState); 150 151 CatLog.d(LOG_TAG, "onCreate - mIsResponseSent[" + mIsResponseSent + "]"); 152 153 // appService can be null if this activity is automatically recreated by the system 154 // with the saved instance state right after the phone process is killed. 155 if (appService == null) { 156 CatLog.d(LOG_TAG, "onCreate - appService is null"); 157 finish(); 158 return; 159 } 160 161 ActionBar actionBar = null; 162 if (getResources().getBoolean(R.bool.show_menu_title_only_on_menu)) { 163 actionBar = getActionBar(); 164 if (actionBar != null) { 165 actionBar.hide(); 166 } 167 } 168 169 // Set the layout for this activity. 170 setContentView(R.layout.stk_input); 171 172 if (actionBar != null) { 173 mMoreOptions = findViewById(R.id.more); 174 mMoreOptions.setVisibility(View.VISIBLE); 175 mMoreOptions.setOnClickListener(this); 176 } 177 178 // Initialize members 179 mTextIn = (EditText) this.findViewById(R.id.in_text); 180 mPromptView = (TextView) this.findViewById(R.id.prompt); 181 // Set buttons listeners. 182 Button okButton = (Button) findViewById(R.id.button_ok); 183 Button cancelButton = (Button) findViewById(R.id.button_cancel); 184 Button yesButton = (Button) findViewById(R.id.button_yes); 185 Button noButton = (Button) findViewById(R.id.button_no); 186 187 okButton.setOnClickListener(this); 188 cancelButton.setOnClickListener(this); 189 yesButton.setOnClickListener(this); 190 noButton.setOnClickListener(this); 191 192 mYesNoLayout = findViewById(R.id.yes_no_layout); 193 mNormalLayout = findViewById(R.id.normal_layout); 194 initFromIntent(getIntent()); 195 } 196 197 @Override onPostCreate(Bundle savedInstanceState)198 protected void onPostCreate(Bundle savedInstanceState) { 199 super.onPostCreate(savedInstanceState); 200 201 mTextIn.addTextChangedListener(this); 202 } 203 204 @Override onResume()205 public void onResume() { 206 super.onResume(); 207 CatLog.d(LOG_TAG, "onResume - mIsResponseSent[" + mIsResponseSent + 208 "], slot id: " + mSlotId); 209 // If the terminal has already sent response to the card when this activity is resumed, 210 // keep this as a pending activity as this should be finished when the session ends. 211 if (!mIsResponseSent) { 212 setPendingState(false); 213 } 214 215 if (mAlarmTime == NO_INPUT_ALARM) { 216 startTimeOut(); 217 } 218 } 219 220 @Override onPause()221 public void onPause() { 222 super.onPause(); 223 CatLog.d(LOG_TAG, "onPause - mIsResponseSent[" + mIsResponseSent + "]"); 224 if (mPopupMenu != null) { 225 mPopupMenu.dismiss(); 226 } 227 } 228 229 @Override onStop()230 public void onStop() { 231 super.onStop(); 232 CatLog.d(LOG_TAG, "onStop - mIsResponseSent[" + mIsResponseSent + "]"); 233 234 // Nothing should be done here if this activity is being finished or restarted now. 235 if (isFinishing() || isChangingConfigurations()) { 236 return; 237 } 238 239 if (mIsResponseSent) { 240 // It is unnecessary to keep this activity if the response was already sent and 241 // the dialog activity is NOT on the top of this activity. 242 if (!appService.isStkDialogActivated()) { 243 finish(); 244 } 245 } else { 246 // This should be registered as the pending activity here 247 // only when no response has been sent back to the card. 248 setPendingState(true); 249 } 250 } 251 252 @Override onDestroy()253 public void onDestroy() { 254 super.onDestroy(); 255 CatLog.d(LOG_TAG, "onDestroy - before Send End Session mIsResponseSent[" + 256 mIsResponseSent + " , " + mSlotId + "]"); 257 if (appService == null) { 258 return; 259 } 260 // Avoid sending the terminal response while the activty is being restarted 261 // due to some kind of configuration change. 262 if (!isChangingConfigurations()) { 263 // If the input activity is finished by stkappservice 264 // when receiving OP_LAUNCH_APP from the other SIM, we can not send TR here, 265 // since the input cmd is waiting user to process. 266 if (!mIsResponseSent && !appService.isInputPending(mSlotId)) { 267 CatLog.d(LOG_TAG, "handleDestroy - Send End Session"); 268 sendResponse(StkAppService.RES_ID_END_SESSION); 269 } 270 } 271 cancelTimeOut(); 272 } 273 274 @Override onConfigurationChanged(Configuration newConfig)275 public void onConfigurationChanged(Configuration newConfig) { 276 super.onConfigurationChanged(newConfig); 277 if (mPopupMenu != null) { 278 mPopupMenu.dismiss(); 279 } 280 } 281 282 @Override onKeyDown(int keyCode, KeyEvent event)283 public boolean onKeyDown(int keyCode, KeyEvent event) { 284 if (mIsResponseSent) { 285 CatLog.d(LOG_TAG, "Already responded"); 286 return true; 287 } 288 289 switch (keyCode) { 290 case KeyEvent.KEYCODE_BACK: 291 CatLog.d(LOG_TAG, "onKeyDown - KEYCODE_BACK"); 292 sendResponse(StkAppService.RES_ID_BACKWARD, null, false); 293 return true; 294 } 295 return super.onKeyDown(keyCode, event); 296 } 297 sendResponse(int resId)298 void sendResponse(int resId) { 299 sendResponse(resId, null, false); 300 } 301 sendResponse(int resId, String input, boolean help)302 void sendResponse(int resId, String input, boolean help) { 303 cancelTimeOut(); 304 305 if (mSlotId == -1) { 306 CatLog.d(LOG_TAG, "slot id is invalid"); 307 return; 308 } 309 310 if (StkAppService.getInstance() == null) { 311 CatLog.d(LOG_TAG, "StkAppService is null, Ignore response: id is " + resId); 312 return; 313 } 314 315 if (mMoreOptions != null) { 316 mMoreOptions.setVisibility(View.INVISIBLE); 317 } 318 319 CatLog.d(LOG_TAG, "sendResponse resID[" + resId + "] input[*****] help[" 320 + help + "]"); 321 mIsResponseSent = true; 322 Bundle args = new Bundle(); 323 args.putInt(StkAppService.RES_ID, resId); 324 if (input != null) { 325 args.putString(StkAppService.INPUT, input); 326 } 327 args.putBoolean(StkAppService.HELP, help); 328 appService.sendResponse(args, mSlotId); 329 330 // This instance should be set as a pending activity and finished by the service 331 if (resId != StkAppService.RES_ID_END_SESSION) { 332 setPendingState(true); 333 } 334 } 335 336 @Override onCreateOptionsMenu(android.view.Menu menu)337 public boolean onCreateOptionsMenu(android.view.Menu menu) { 338 super.onCreateOptionsMenu(menu); 339 createOptionsMenuInternal(menu); 340 return true; 341 } 342 createOptionsMenuInternal(Menu menu)343 private void createOptionsMenuInternal(Menu menu) { 344 menu.add(Menu.NONE, StkApp.MENU_ID_END_SESSION, 1, R.string.menu_end_session); 345 menu.add(0, StkApp.MENU_ID_HELP, 2, R.string.help); 346 } 347 348 @Override onPrepareOptionsMenu(android.view.Menu menu)349 public boolean onPrepareOptionsMenu(android.view.Menu menu) { 350 super.onPrepareOptionsMenu(menu); 351 prepareOptionsMenuInternal(menu); 352 return true; 353 } 354 prepareOptionsMenuInternal(Menu menu)355 private void prepareOptionsMenuInternal(Menu menu) { 356 menu.findItem(StkApp.MENU_ID_END_SESSION).setVisible(true); 357 menu.findItem(StkApp.MENU_ID_HELP).setVisible(mStkInput.helpAvailable); 358 } 359 360 @Override onOptionsItemSelected(MenuItem item)361 public boolean onOptionsItemSelected(MenuItem item) { 362 if (optionsItemSelectedInternal(item)) { 363 return true; 364 } 365 return super.onOptionsItemSelected(item); 366 } 367 optionsItemSelectedInternal(MenuItem item)368 private boolean optionsItemSelectedInternal(MenuItem item) { 369 if (mIsResponseSent) { 370 CatLog.d(LOG_TAG, "Already responded"); 371 return true; 372 } 373 switch (item.getItemId()) { 374 case StkApp.MENU_ID_END_SESSION: 375 sendResponse(StkAppService.RES_ID_END_SESSION); 376 finish(); 377 return true; 378 case StkApp.MENU_ID_HELP: 379 sendResponse(StkAppService.RES_ID_INPUT, "", true); 380 return true; 381 } 382 return false; 383 } 384 385 @Override onSaveInstanceState(Bundle outState)386 protected void onSaveInstanceState(Bundle outState) { 387 CatLog.d(LOG_TAG, "onSaveInstanceState: " + mSlotId); 388 outState.putBoolean(RESPONSE_SENT_KEY, mIsResponseSent); 389 outState.putString(INPUT_STRING_KEY, mTextIn.getText().toString()); 390 outState.putLong(ALARM_TIME_KEY, mAlarmTime); 391 outState.putBoolean(PENDING, mIsPending); 392 } 393 394 @Override onRestoreInstanceState(Bundle savedInstanceState)395 protected void onRestoreInstanceState(Bundle savedInstanceState) { 396 CatLog.d(LOG_TAG, "onRestoreInstanceState: " + mSlotId); 397 398 mIsResponseSent = savedInstanceState.getBoolean(RESPONSE_SENT_KEY); 399 if (mIsResponseSent && (mMoreOptions != null)) { 400 mMoreOptions.setVisibility(View.INVISIBLE); 401 } 402 403 String savedString = savedInstanceState.getString(INPUT_STRING_KEY); 404 mTextIn.setText(savedString); 405 updateButton(); 406 407 mAlarmTime = savedInstanceState.getLong(ALARM_TIME_KEY, NO_INPUT_ALARM); 408 if (mAlarmTime != NO_INPUT_ALARM) { 409 startTimeOut(); 410 } 411 412 if (!mIsResponseSent && !savedInstanceState.getBoolean(PENDING)) { 413 // If this is in the foreground and no response has been sent to the card, 414 // this must not be registered as pending activity by the previous instance. 415 // No need to renew nor clear pending activity in this case. 416 } else { 417 // Renew the instance of the pending activity. 418 setPendingState(true); 419 } 420 } 421 setPendingState(boolean on)422 private void setPendingState(boolean on) { 423 if (mIsPending != on) { 424 appService.getStkContext(mSlotId).setPendingActivityInstance(on ? this : null); 425 mIsPending = on; 426 } 427 } 428 beforeTextChanged(CharSequence s, int start, int count, int after)429 public void beforeTextChanged(CharSequence s, int start, int count, 430 int after) { 431 } 432 onTextChanged(CharSequence s, int start, int before, int count)433 public void onTextChanged(CharSequence s, int start, int before, int count) { 434 // Reset timeout. 435 cancelTimeOut(); 436 startTimeOut(); 437 updateButton(); 438 } 439 afterTextChanged(Editable s)440 public void afterTextChanged(Editable s) { 441 } 442 updateButton()443 private void updateButton() { 444 // Disable the button if the length of the input text does not meet the expectation. 445 Button okButton = (Button) findViewById(R.id.button_ok); 446 okButton.setEnabled((mTextIn.getText().length() < mStkInput.minLen) ? false : true); 447 } 448 449 private void cancelTimeOut() { 450 if (mAlarmTime != NO_INPUT_ALARM) { 451 CatLog.d(LOG_TAG, "cancelTimeOut - slot id: " + mSlotId); 452 AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); 453 am.cancel(mAlarmListener); 454 mAlarmTime = NO_INPUT_ALARM; 455 } 456 } 457 458 private void startTimeOut() { 459 // No need to set alarm if device sent TERMINAL RESPONSE already. 460 if (mIsResponseSent) { 461 return; 462 } 463 464 if (mAlarmTime == NO_INPUT_ALARM) { 465 int duration = StkApp.calculateDurationInMilis(mStkInput.duration); 466 if (duration <= 0) { 467 duration = StkApp.UI_TIMEOUT; 468 } 469 mAlarmTime = SystemClock.elapsedRealtime() + duration; 470 } 471 472 CatLog.d(LOG_TAG, "startTimeOut: " + mAlarmTime + "ms, slot id: " + mSlotId); 473 AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); 474 am.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, mAlarmTime, INPUT_ALARM_TAG, 475 mAlarmListener, null); 476 } 477 478 private void configInputDisplay() { 479 TextView numOfCharsView = (TextView) findViewById(R.id.num_of_chars); 480 TextView inTypeView = (TextView) findViewById(R.id.input_type); 481 482 int inTypeId = R.string.alphabet; 483 484 // set the prompt. 485 if ((mStkInput.icon == null || !mStkInput.iconSelfExplanatory) 486 && !TextUtils.isEmpty(mStkInput.text)) { 487 mPromptView.setText(mStkInput.text); 488 mPromptView.setVisibility(View.VISIBLE); 489 } 490 491 // Set input type (alphabet/digit) info close to the InText form. 492 if (mStkInput.digitOnly) { 493 mTextIn.setKeyListener(StkDigitsKeyListener.getInstance()); 494 inTypeId = R.string.digits; 495 } 496 inTypeView.setText(inTypeId); 497 498 setTitle(R.string.app_name); 499 500 if (mStkInput.icon != null) { 501 ImageView imageView = (ImageView) findViewById(R.id.icon); 502 imageView.setImageBitmap(mStkInput.icon); 503 imageView.setVisibility(View.VISIBLE); 504 } 505 506 // Handle specific global and text attributes. 507 switch (mState) { 508 case STATE_TEXT: 509 int maxLen = mStkInput.maxLen; 510 int minLen = mStkInput.minLen; 511 mTextIn.setFilters(new InputFilter[] {new InputFilter.LengthFilter( 512 maxLen)}); 513 514 // Set number of chars info. 515 String lengthLimit = String.valueOf(minLen); 516 if (maxLen != minLen) { 517 lengthLimit = minLen + " - " + maxLen; 518 } 519 numOfCharsView.setText(lengthLimit); 520 521 if (!mStkInput.echo) { 522 mTextIn.setTransformationMethod(PasswordTransformationMethod 523 .getInstance()); 524 } 525 mTextIn.setImeOptions(EditorInfo.IME_FLAG_NO_FULLSCREEN); 526 // Request the initial focus on the edit box and show the software keyboard. 527 mTextIn.requestFocus(); 528 getWindow().setSoftInputMode( 529 WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); 530 // Set default text if present. 531 if (mStkInput.defaultText != null) { 532 mTextIn.setText(mStkInput.defaultText); 533 } else { 534 // make sure the text is cleared 535 mTextIn.setText("", BufferType.EDITABLE); 536 } 537 updateButton(); 538 539 break; 540 case STATE_YES_NO: 541 // Set display mode - normal / yes-no layout 542 mYesNoLayout.setVisibility(View.VISIBLE); 543 mNormalLayout.setVisibility(View.GONE); 544 break; 545 } 546 } 547 548 private void initFromIntent(Intent intent) { 549 // Get the calling intent type: text/key, and setup the 550 // display parameters. 551 CatLog.d(LOG_TAG, "initFromIntent - slot id: " + mSlotId); 552 if (intent != null) { 553 mStkInput = intent.getParcelableExtra("INPUT"); 554 mSlotId = intent.getIntExtra(StkAppService.SLOT_ID, -1); 555 CatLog.d(LOG_TAG, "onCreate - slot id: " + mSlotId); 556 if (mStkInput == null) { 557 finish(); 558 } else { 559 mState = mStkInput.yesNo ? STATE_YES_NO : 560 STATE_TEXT; 561 configInputDisplay(); 562 } 563 } else { 564 finish(); 565 } 566 } 567 568 private final AlarmManager.OnAlarmListener mAlarmListener = 569 new AlarmManager.OnAlarmListener() { 570 @Override 571 public void onAlarm() { 572 CatLog.d(LOG_TAG, "The alarm time is reached"); 573 mAlarmTime = NO_INPUT_ALARM; 574 sendResponse(StkAppService.RES_ID_TIMEOUT); 575 } 576 }; 577 } 578