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.InputType; 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.view.inputmethod.InputMethodManager; 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 = StkInputActivity.class.getSimpleName(); 71 72 private Input mStkInput = null; 73 // Constants 74 private static final int STATE_TEXT = 1; 75 private static final int STATE_YES_NO = 2; 76 77 static final String YES_STR_RESPONSE = "YES"; 78 static final String NO_STR_RESPONSE = "NO"; 79 80 // Font size factor values. 81 static final float NORMAL_FONT_FACTOR = 1; 82 static final float LARGE_FONT_FACTOR = 2; 83 static final float SMALL_FONT_FACTOR = (1 / 2); 84 85 // Keys for saving the state of the activity in the bundle 86 private static final String RESPONSE_SENT_KEY = "response_sent"; 87 private static final String INPUT_STRING_KEY = "input_string"; 88 private static final String ALARM_TIME_KEY = "alarm_time"; 89 90 private static final String INPUT_ALARM_TAG = LOG_TAG; 91 private static final long NO_INPUT_ALARM = -1; 92 private long mAlarmTime = NO_INPUT_ALARM; 93 94 private StkAppService appService = StkAppService.getInstance(); 95 96 private boolean mIsResponseSent = false; 97 private int mSlotId = -1; 98 99 // Click listener to handle buttons press.. onClick(View v)100 public void onClick(View v) { 101 String input = null; 102 if (mIsResponseSent) { 103 CatLog.d(LOG_TAG, "Already responded"); 104 return; 105 } 106 107 switch (v.getId()) { 108 case R.id.button_ok: 109 input = mTextIn.getText().toString(); 110 break; 111 case R.id.button_cancel: 112 sendResponse(StkAppService.RES_ID_END_SESSION); 113 finish(); 114 return; 115 // Yes/No layout buttons. 116 case R.id.button_yes: 117 input = YES_STR_RESPONSE; 118 break; 119 case R.id.button_no: 120 input = NO_STR_RESPONSE; 121 break; 122 case R.id.more: 123 if (mPopupMenu == null) { 124 mPopupMenu = new PopupMenu(this, v); 125 Menu menu = mPopupMenu.getMenu(); 126 createOptionsMenuInternal(menu); 127 prepareOptionsMenuInternal(menu); 128 mPopupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { 129 public boolean onMenuItemClick(MenuItem item) { 130 optionsItemSelectedInternal(item); 131 return true; 132 } 133 }); 134 mPopupMenu.setOnDismissListener(new PopupMenu.OnDismissListener() { 135 public void onDismiss(PopupMenu menu) { 136 mPopupMenu = null; 137 } 138 }); 139 mPopupMenu.show(); 140 } 141 return; 142 default: 143 break; 144 } 145 CatLog.d(LOG_TAG, "handleClick, ready to response"); 146 sendResponse(StkAppService.RES_ID_INPUT, input, false); 147 } 148 149 @Override onCreate(Bundle savedInstanceState)150 public void onCreate(Bundle savedInstanceState) { 151 super.onCreate(savedInstanceState); 152 getWindow().addSystemFlags( 153 WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 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 if (mTextIn != null) { 221 InputMethodManager imm = getSystemService(InputMethodManager.class); 222 imm.hideSoftInputFromWindow(mTextIn.getWindowToken(), 0); 223 } 224 } 225 226 @Override onStop()227 public void onStop() { 228 super.onStop(); 229 CatLog.d(LOG_TAG, "onStop - mIsResponseSent[" + mIsResponseSent + "]"); 230 } 231 232 @Override onDestroy()233 public void onDestroy() { 234 super.onDestroy(); 235 CatLog.d(LOG_TAG, "onDestroy - before Send End Session mIsResponseSent[" + 236 mIsResponseSent + " , " + mSlotId + "]"); 237 if (appService == null) { 238 return; 239 } 240 // Avoid sending the terminal response while the activty is being restarted 241 // due to some kind of configuration change. 242 if (!isChangingConfigurations()) { 243 // If the input activity is finished by stkappservice 244 // when receiving OP_LAUNCH_APP from the other SIM, we can not send TR here, 245 // since the input cmd is waiting user to process. 246 if (!mIsResponseSent && !appService.isInputPending(mSlotId)) { 247 CatLog.d(LOG_TAG, "handleDestroy - Send End Session"); 248 sendResponse(StkAppService.RES_ID_END_SESSION); 249 } 250 } 251 cancelTimeOut(); 252 } 253 254 @Override onConfigurationChanged(Configuration newConfig)255 public void onConfigurationChanged(Configuration newConfig) { 256 super.onConfigurationChanged(newConfig); 257 if (mPopupMenu != null) { 258 mPopupMenu.dismiss(); 259 } 260 } 261 262 @Override onKeyDown(int keyCode, KeyEvent event)263 public boolean onKeyDown(int keyCode, KeyEvent event) { 264 if (mIsResponseSent) { 265 CatLog.d(LOG_TAG, "Already responded"); 266 return true; 267 } 268 269 switch (keyCode) { 270 case KeyEvent.KEYCODE_BACK: 271 CatLog.d(LOG_TAG, "onKeyDown - KEYCODE_BACK"); 272 sendResponse(StkAppService.RES_ID_BACKWARD, null, false); 273 return true; 274 } 275 return super.onKeyDown(keyCode, event); 276 } 277 sendResponse(int resId)278 void sendResponse(int resId) { 279 sendResponse(resId, null, false); 280 } 281 sendResponse(int resId, String input, boolean help)282 void sendResponse(int resId, String input, boolean help) { 283 cancelTimeOut(); 284 285 if (mSlotId == -1) { 286 CatLog.d(LOG_TAG, "slot id is invalid"); 287 return; 288 } 289 290 if (StkAppService.getInstance() == null) { 291 CatLog.d(LOG_TAG, "StkAppService is null, Ignore response: id is " + resId); 292 return; 293 } 294 295 if (mMoreOptions != null) { 296 mMoreOptions.setVisibility(View.INVISIBLE); 297 } 298 299 CatLog.d(LOG_TAG, "sendResponse resID[" + resId + "] input[*****] help[" 300 + help + "]"); 301 mIsResponseSent = true; 302 Bundle args = new Bundle(); 303 args.putInt(StkAppService.RES_ID, resId); 304 if (input != null) { 305 args.putString(StkAppService.INPUT, input); 306 } 307 args.putBoolean(StkAppService.HELP, help); 308 appService.sendResponse(args, mSlotId); 309 } 310 311 @Override onCreateOptionsMenu(android.view.Menu menu)312 public boolean onCreateOptionsMenu(android.view.Menu menu) { 313 super.onCreateOptionsMenu(menu); 314 createOptionsMenuInternal(menu); 315 return true; 316 } 317 createOptionsMenuInternal(Menu menu)318 private void createOptionsMenuInternal(Menu menu) { 319 menu.add(Menu.NONE, StkApp.MENU_ID_END_SESSION, 1, R.string.menu_end_session); 320 menu.add(0, StkApp.MENU_ID_HELP, 2, R.string.help); 321 } 322 323 @Override onPrepareOptionsMenu(android.view.Menu menu)324 public boolean onPrepareOptionsMenu(android.view.Menu menu) { 325 super.onPrepareOptionsMenu(menu); 326 prepareOptionsMenuInternal(menu); 327 return true; 328 } 329 prepareOptionsMenuInternal(Menu menu)330 private void prepareOptionsMenuInternal(Menu menu) { 331 menu.findItem(StkApp.MENU_ID_END_SESSION).setVisible(true); 332 menu.findItem(StkApp.MENU_ID_HELP).setVisible(mStkInput.helpAvailable); 333 } 334 335 @Override onOptionsItemSelected(MenuItem item)336 public boolean onOptionsItemSelected(MenuItem item) { 337 if (optionsItemSelectedInternal(item)) { 338 return true; 339 } 340 return super.onOptionsItemSelected(item); 341 } 342 optionsItemSelectedInternal(MenuItem item)343 private boolean optionsItemSelectedInternal(MenuItem item) { 344 if (mIsResponseSent) { 345 CatLog.d(LOG_TAG, "Already responded"); 346 return true; 347 } 348 switch (item.getItemId()) { 349 case StkApp.MENU_ID_END_SESSION: 350 sendResponse(StkAppService.RES_ID_END_SESSION); 351 finish(); 352 return true; 353 case StkApp.MENU_ID_HELP: 354 sendResponse(StkAppService.RES_ID_INPUT, "", true); 355 return true; 356 } 357 return false; 358 } 359 360 @SuppressWarnings("MissingSuperCall") // TODO: Fix me 361 @Override onSaveInstanceState(Bundle outState)362 protected void onSaveInstanceState(Bundle outState) { 363 CatLog.d(LOG_TAG, "onSaveInstanceState: " + mSlotId); 364 outState.putBoolean(RESPONSE_SENT_KEY, mIsResponseSent); 365 outState.putString(INPUT_STRING_KEY, mTextIn.getText().toString()); 366 outState.putLong(ALARM_TIME_KEY, mAlarmTime); 367 } 368 369 @Override onRestoreInstanceState(Bundle savedInstanceState)370 protected void onRestoreInstanceState(Bundle savedInstanceState) { 371 CatLog.d(LOG_TAG, "onRestoreInstanceState: " + mSlotId); 372 373 mIsResponseSent = savedInstanceState.getBoolean(RESPONSE_SENT_KEY); 374 if (mIsResponseSent && (mMoreOptions != null)) { 375 mMoreOptions.setVisibility(View.INVISIBLE); 376 } 377 378 String savedString = savedInstanceState.getString(INPUT_STRING_KEY); 379 mTextIn.setText(savedString); 380 updateButton(); 381 382 mAlarmTime = savedInstanceState.getLong(ALARM_TIME_KEY, NO_INPUT_ALARM); 383 if (mAlarmTime != NO_INPUT_ALARM) { 384 startTimeOut(); 385 } 386 } 387 beforeTextChanged(CharSequence s, int start, int count, int after)388 public void beforeTextChanged(CharSequence s, int start, int count, 389 int after) { 390 } 391 onTextChanged(CharSequence s, int start, int before, int count)392 public void onTextChanged(CharSequence s, int start, int before, int count) { 393 // Reset timeout. 394 cancelTimeOut(); 395 startTimeOut(); 396 updateButton(); 397 } 398 afterTextChanged(Editable s)399 public void afterTextChanged(Editable s) { 400 } 401 updateButton()402 private void updateButton() { 403 // Disable the button if the length of the input text does not meet the expectation. 404 Button okButton = (Button) findViewById(R.id.button_ok); 405 okButton.setEnabled((mTextIn.getText().length() < mStkInput.minLen) ? false : true); 406 } 407 408 private void cancelTimeOut() { 409 if (mAlarmTime != NO_INPUT_ALARM) { 410 CatLog.d(LOG_TAG, "cancelTimeOut - slot id: " + mSlotId); 411 AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); 412 am.cancel(mAlarmListener); 413 mAlarmTime = NO_INPUT_ALARM; 414 } 415 } 416 417 private void startTimeOut() { 418 // No need to set alarm if device sent TERMINAL RESPONSE already. 419 if (mIsResponseSent) { 420 return; 421 } 422 423 if (mAlarmTime == NO_INPUT_ALARM) { 424 int duration = StkApp.calculateDurationInMilis(mStkInput.duration); 425 if (duration <= 0) { 426 duration = StkApp.UI_TIMEOUT; 427 } 428 mAlarmTime = SystemClock.elapsedRealtime() + duration; 429 } 430 431 CatLog.d(LOG_TAG, "startTimeOut: " + mAlarmTime + "ms, slot id: " + mSlotId); 432 AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); 433 am.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, mAlarmTime, INPUT_ALARM_TAG, 434 mAlarmListener, null); 435 } 436 437 private void configInputDisplay() { 438 TextInputLayout textInput = (TextInputLayout) findViewById(R.id.text_input_layout); 439 440 int inTypeId = R.string.alphabet; 441 442 // set the prompt. 443 if ((mStkInput.icon == null || !mStkInput.iconSelfExplanatory) 444 && !TextUtils.isEmpty(mStkInput.text)) { 445 mPromptView.setText(mStkInput.text); 446 mPromptView.setVisibility(View.VISIBLE); 447 } 448 449 // Set input type (alphabet/digit) info close to the InText form. 450 boolean hideHelper = false; 451 if (mStkInput.digitOnly) { 452 mTextIn.setKeyListener(StkDigitsKeyListener.getInstance()); 453 mTextIn.setInputType(InputType.TYPE_CLASS_PHONE); 454 inTypeId = R.string.digits; 455 hideHelper = StkAppService.getBooleanCarrierConfig(this, 456 CarrierConfigManager.KEY_HIDE_DIGITS_HELPER_TEXT_ON_STK_INPUT_SCREEN_BOOL, 457 mSlotId); 458 } 459 textInput.setHelperText(getResources().getString(inTypeId)); 460 textInput.setHelperTextEnabled(!hideHelper); 461 CatLog.d(LOG_TAG, 462 String.format("configInputDisplay: digitOnly=%s, hideHelper=%s", 463 mStkInput.digitOnly, hideHelper)); 464 setTitle(R.string.app_name); 465 466 if (mStkInput.icon != null) { 467 ImageView imageView = (ImageView) findViewById(R.id.icon); 468 imageView.setImageBitmap(mStkInput.icon); 469 imageView.setVisibility(View.VISIBLE); 470 } 471 472 // Handle specific global and text attributes. 473 switch (mState) { 474 case STATE_TEXT: 475 mTextIn.setFilters(new InputFilter[] {new InputFilter.LengthFilter(mStkInput.maxLen)}); 476 477 textInput.setCounterMaxLength(mStkInput.maxLen); 478 //do not show the length helper for the text input 479 textInput.setCounterEnabled(false); 480 481 if (!mStkInput.echo) { 482 mTextIn.setTransformationMethod(PasswordTransformationMethod 483 .getInstance()); 484 } 485 mTextIn.setImeOptions(EditorInfo.IME_FLAG_NO_FULLSCREEN); 486 // Request the initial focus on the edit box and show the software keyboard. 487 mTextIn.requestFocus(); 488 getWindow().setSoftInputMode( 489 WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); 490 // Set default text if present. 491 if (mStkInput.defaultText != null) { 492 mTextIn.setText(mStkInput.defaultText); 493 } else { 494 // make sure the text is cleared 495 mTextIn.setText("", BufferType.EDITABLE); 496 } 497 updateButton(); 498 499 break; 500 case STATE_YES_NO: 501 // Set display mode - normal / yes-no layout 502 mYesNoLayout.setVisibility(View.VISIBLE); 503 mNormalLayout.setVisibility(View.GONE); 504 break; 505 } 506 } 507 508 private void initFromIntent(Intent intent) { 509 // Get the calling intent type: text/key, and setup the 510 // display parameters. 511 CatLog.d(LOG_TAG, "initFromIntent - slot id: " + mSlotId); 512 if (intent != null) { 513 mStkInput = intent.getParcelableExtra("INPUT"); 514 mSlotId = intent.getIntExtra(StkAppService.SLOT_ID, -1); 515 CatLog.d(LOG_TAG, "onCreate - slot id: " + mSlotId); 516 if (mStkInput == null) { 517 finish(); 518 } else { 519 mState = mStkInput.yesNo ? STATE_YES_NO : 520 STATE_TEXT; 521 configInputDisplay(); 522 } 523 } else { 524 finish(); 525 } 526 } 527 528 private final AlarmManager.OnAlarmListener mAlarmListener = 529 new AlarmManager.OnAlarmListener() { 530 @Override 531 public void onAlarm() { 532 CatLog.d(LOG_TAG, "The alarm time is reached"); 533 mAlarmTime = NO_INPUT_ALARM; 534 sendResponse(StkAppService.RES_ID_TIMEOUT); 535 } 536 }; 537 } 538