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