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