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