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