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.AlarmManager;
21 import android.app.ListActivity;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.os.Bundle;
27 import android.os.SystemClock;
28 import android.telephony.SubscriptionManager;
29 import android.view.ContextMenu;
30 import android.view.ContextMenu.ContextMenuInfo;
31 import android.view.KeyEvent;
32 import android.view.MenuItem;
33 import android.view.View;
34 import android.widget.AdapterView;
35 import android.widget.ImageView;
36 import android.widget.ListView;
37 import android.widget.ProgressBar;
38 import android.widget.TextView;
39 
40 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
41 
42 import com.android.internal.telephony.cat.CatLog;
43 import com.android.internal.telephony.cat.Item;
44 import com.android.internal.telephony.cat.Menu;
45 
46 /**
47  * ListActivity used for displaying STK menus. These can be SET UP MENU and
48  * SELECT ITEM menus. This activity is started multiple times with different
49  * menu content.
50  *
51  */
52 public class StkMenuActivity extends ListActivity implements View.OnCreateContextMenuListener {
53     private Menu mStkMenu = null;
54     private int mState = STATE_MAIN;
55     private boolean mAcceptUsersInput = true;
56     private int mSlotId = -1;
57     private boolean mIsResponseSent = false;
58 
59     private TextView mTitleTextView = null;
60     private ImageView mTitleIconView = null;
61     private ProgressBar mProgressView = null;
62 
63     private static final String LOG_TAG =
64             new Object(){}.getClass().getEnclosingClass().getSimpleName();
65 
66     private StkAppService appService = StkAppService.getInstance();
67 
68     // Keys for saving the state of the dialog in the bundle
69     private static final String STATE_KEY = "state";
70     private static final String ACCEPT_USERS_INPUT_KEY = "accept_users_input";
71     private static final String RESPONSE_SENT_KEY = "response_sent";
72     private static final String ALARM_TIME_KEY = "alarm_time";
73 
74     private static final String SELECT_ALARM_TAG = LOG_TAG;
75     private static final long NO_SELECT_ALARM = -1;
76     private long mAlarmTime = NO_SELECT_ALARM;
77 
78     // Internal state values
79     static final int STATE_INIT = 0;
80     static final int STATE_MAIN = 1;
81     static final int STATE_SECONDARY = 2;
82 
83     private static final int CONTEXT_MENU_HELP = 0;
84 
85     @Override
onCreate(Bundle savedInstanceState)86     public void onCreate(Bundle savedInstanceState) {
87         super.onCreate(savedInstanceState);
88 
89         CatLog.d(LOG_TAG, "onCreate");
90 
91         ActionBar actionBar = getActionBar();
92         actionBar.setCustomView(R.layout.stk_title);
93         actionBar.setDisplayShowCustomEnabled(true);
94 
95         // Set the layout for this activity.
96         setContentView(R.layout.stk_menu_list);
97         mTitleTextView = (TextView) findViewById(R.id.title_text);
98         mTitleIconView = (ImageView) findViewById(R.id.title_icon);
99         mProgressView = (ProgressBar) findViewById(R.id.progress_bar);
100         getListView().setOnCreateContextMenuListener(this);
101 
102         // appService can be null if this activity is automatically recreated by the system
103         // with the saved instance state right after the phone process is killed.
104         if (appService == null) {
105             CatLog.d(LOG_TAG, "onCreate - appService is null");
106             finish();
107             return;
108         }
109 
110         LocalBroadcastManager.getInstance(this).registerReceiver(mLocalBroadcastReceiver,
111                 new IntentFilter(StkAppService.SESSION_ENDED));
112         initFromIntent(getIntent());
113         if (!SubscriptionManager.isValidSlotIndex(mSlotId)) {
114             finish();
115             return;
116         }
117         if (mState == STATE_SECONDARY) {
118             appService.getStkContext(mSlotId).setPendingActivityInstance(this);
119         }
120     }
121 
122     @Override
onListItemClick(ListView l, View v, int position, long id)123     protected void onListItemClick(ListView l, View v, int position, long id) {
124         super.onListItemClick(l, v, position, id);
125 
126         if (!mAcceptUsersInput) {
127             CatLog.d(LOG_TAG, "mAcceptUsersInput:false");
128             return;
129         }
130 
131         Item item = getSelectedItem(position);
132         if (item == null) {
133             CatLog.d(LOG_TAG, "Item is null");
134             return;
135         }
136 
137         CatLog.d(LOG_TAG, "onListItemClick Id: " + item.id + ", mState: " + mState);
138         sendResponse(StkAppService.RES_ID_MENU_SELECTION, item.id, false);
139         invalidateOptionsMenu();
140     }
141 
142     @Override
onKeyDown(int keyCode, KeyEvent event)143     public boolean onKeyDown(int keyCode, KeyEvent event) {
144         CatLog.d(LOG_TAG, "mAcceptUsersInput: " + mAcceptUsersInput);
145         if (!mAcceptUsersInput) {
146             return true;
147         }
148 
149         switch (keyCode) {
150         case KeyEvent.KEYCODE_BACK:
151             CatLog.d(LOG_TAG, "KEYCODE_BACK - mState[" + mState + "]");
152             switch (mState) {
153             case STATE_SECONDARY:
154                 CatLog.d(LOG_TAG, "STATE_SECONDARY");
155                 sendResponse(StkAppService.RES_ID_BACKWARD);
156                 return true;
157             case STATE_MAIN:
158                 CatLog.d(LOG_TAG, "STATE_MAIN");
159                 finish();
160                 return true;
161             }
162             break;
163         }
164         return super.onKeyDown(keyCode, event);
165     }
166 
167     @Override
onResume()168     public void onResume() {
169         super.onResume();
170 
171         CatLog.d(LOG_TAG, "onResume, slot id: " + mSlotId + "," + mState);
172         appService.indicateMenuVisibility(true, mSlotId);
173         if (mState == STATE_MAIN) {
174             mStkMenu = appService.getMainMenu(mSlotId);
175         } else {
176             mStkMenu = appService.getMenu(mSlotId);
177         }
178         if (mStkMenu == null) {
179             CatLog.d(LOG_TAG, "menu is null");
180             cancelTimeOut();
181             finish();
182             return;
183         }
184         displayMenu();
185 
186         if (mAlarmTime == NO_SELECT_ALARM) {
187             startTimeOut();
188         }
189 
190         invalidateOptionsMenu();
191     }
192 
193     @Override
onPause()194     public void onPause() {
195         super.onPause();
196         CatLog.d(LOG_TAG, "onPause, slot id: " + mSlotId + "," + mState);
197         //If activity is finished in onResume and it reaults from null appService.
198         if (appService != null) {
199             appService.indicateMenuVisibility(false, mSlotId);
200         } else {
201             CatLog.d(LOG_TAG, "onPause: null appService.");
202         }
203 
204         /*
205          * do not cancel the timer here cancelTimeOut(). If any higher/lower
206          * priority events such as incoming call, new sms, screen off intent,
207          * notification alerts, user actions such as 'User moving to another activtiy'
208          * etc.. occur during SELECT ITEM ongoing session,
209          * this activity would receive 'onPause()' event resulting in
210          * cancellation of the timer. As a result no terminal response is
211          * sent to the card.
212          */
213 
214     }
215 
216     @Override
onStop()217     public void onStop() {
218         super.onStop();
219         CatLog.d(LOG_TAG, "onStop, slot id: " + mSlotId + "," + mIsResponseSent + "," + mState);
220     }
221 
222     @Override
onDestroy()223     public void onDestroy() {
224         getListView().setOnCreateContextMenuListener(null);
225         super.onDestroy();
226         CatLog.d(LOG_TAG, "onDestroy" + ", " + mState);
227         if (appService == null || !SubscriptionManager.isValidSlotIndex(mSlotId)) {
228             return;
229         }
230         //isMenuPending: if input act is finish by stkappservice when OP_LAUNCH_APP again,
231         //we can not send TR here, since the input cmd is waiting user to process.
232         if (mState == STATE_SECONDARY && !mIsResponseSent && !appService.isMenuPending(mSlotId)) {
233             // Avoid sending the terminal response while the activty is being restarted
234             // due to some kind of configuration change.
235             if (!isChangingConfigurations()) {
236                 CatLog.d(LOG_TAG, "handleDestroy - Send End Session");
237                 sendResponse(StkAppService.RES_ID_END_SESSION);
238             }
239         }
240         cancelTimeOut();
241         LocalBroadcastManager.getInstance(this).unregisterReceiver(mLocalBroadcastReceiver);
242     }
243 
244     @Override
onCreateOptionsMenu(android.view.Menu menu)245     public boolean onCreateOptionsMenu(android.view.Menu menu) {
246         super.onCreateOptionsMenu(menu);
247         menu.add(0, StkApp.MENU_ID_END_SESSION, 1, R.string.menu_end_session);
248         return true;
249     }
250 
251     @Override
onPrepareOptionsMenu(android.view.Menu menu)252     public boolean onPrepareOptionsMenu(android.view.Menu menu) {
253         super.onPrepareOptionsMenu(menu);
254         boolean mainVisible = false;
255 
256         if (mState == STATE_SECONDARY && mAcceptUsersInput) {
257             mainVisible = true;
258         }
259 
260         menu.findItem(StkApp.MENU_ID_END_SESSION).setVisible(mainVisible);
261 
262         return mainVisible;
263     }
264 
265     @Override
onOptionsItemSelected(MenuItem item)266     public boolean onOptionsItemSelected(MenuItem item) {
267         if (!mAcceptUsersInput) {
268             return true;
269         }
270         switch (item.getItemId()) {
271         case StkApp.MENU_ID_END_SESSION:
272             // send session end response.
273             sendResponse(StkAppService.RES_ID_END_SESSION);
274             finish();
275             return true;
276         default:
277             break;
278         }
279         return super.onOptionsItemSelected(item);
280     }
281 
282     @Override
onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo)283     public void onCreateContextMenu(ContextMenu menu, View v,
284             ContextMenuInfo menuInfo) {
285         CatLog.d(LOG_TAG, "onCreateContextMenu");
286         boolean helpVisible = false;
287         if (mStkMenu != null) {
288             helpVisible = mStkMenu.helpAvailable;
289         }
290         if (helpVisible) {
291             CatLog.d(LOG_TAG, "add menu");
292             menu.add(0, CONTEXT_MENU_HELP, 0, R.string.help);
293         }
294     }
295 
296     @Override
onContextItemSelected(MenuItem item)297     public boolean onContextItemSelected(MenuItem item) {
298         AdapterView.AdapterContextMenuInfo info;
299         try {
300             info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
301         } catch (ClassCastException e) {
302             return false;
303         }
304         switch (item.getItemId()) {
305             case CONTEXT_MENU_HELP:
306                 int position = info.position;
307                 CatLog.d(LOG_TAG, "Position:" + position);
308                 Item stkItem = getSelectedItem(position);
309                 if (stkItem != null) {
310                     CatLog.d(LOG_TAG, "item id:" + stkItem.id);
311                     sendResponse(StkAppService.RES_ID_MENU_SELECTION, stkItem.id, true);
312                 }
313                 return true;
314 
315             default:
316                 return super.onContextItemSelected(item);
317         }
318     }
319 
320     @Override
onSaveInstanceState(Bundle outState)321     protected void onSaveInstanceState(Bundle outState) {
322         CatLog.d(LOG_TAG, "onSaveInstanceState: " + mSlotId);
323         outState.putInt(STATE_KEY, mState);
324         outState.putBoolean(ACCEPT_USERS_INPUT_KEY, mAcceptUsersInput);
325         outState.putBoolean(RESPONSE_SENT_KEY, mIsResponseSent);
326         outState.putLong(ALARM_TIME_KEY, mAlarmTime);
327     }
328 
329     @Override
onRestoreInstanceState(Bundle savedInstanceState)330     protected void onRestoreInstanceState(Bundle savedInstanceState) {
331         CatLog.d(LOG_TAG, "onRestoreInstanceState: " + mSlotId);
332         mState = savedInstanceState.getInt(STATE_KEY);
333         mAcceptUsersInput = savedInstanceState.getBoolean(ACCEPT_USERS_INPUT_KEY);
334         if (!mAcceptUsersInput) {
335             // Check the latest information as the saved instance state can be outdated.
336             if ((mState == STATE_MAIN) && appService.isMainMenuAvailable(mSlotId)) {
337                 mAcceptUsersInput = true;
338             } else {
339                 showProgressBar(true);
340             }
341         }
342         mIsResponseSent = savedInstanceState.getBoolean(RESPONSE_SENT_KEY);
343 
344         mAlarmTime = savedInstanceState.getLong(ALARM_TIME_KEY, NO_SELECT_ALARM);
345         if (mAlarmTime != NO_SELECT_ALARM) {
346             startTimeOut();
347         }
348     }
349 
cancelTimeOut()350     private void cancelTimeOut() {
351         if (mAlarmTime != NO_SELECT_ALARM) {
352             CatLog.d(LOG_TAG, "cancelTimeOut - slot id: " + mSlotId);
353             AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
354             am.cancel(mAlarmListener);
355             mAlarmTime = NO_SELECT_ALARM;
356         }
357     }
358 
startTimeOut()359     private void startTimeOut() {
360         // No need to set alarm if this is the main menu or device sent TERMINAL RESPONSE already.
361         if (mState != STATE_SECONDARY || mIsResponseSent) {
362             return;
363         }
364 
365         if (mAlarmTime == NO_SELECT_ALARM) {
366             mAlarmTime = SystemClock.elapsedRealtime() + StkApp.UI_TIMEOUT;
367         }
368 
369         CatLog.d(LOG_TAG, "startTimeOut: " + mAlarmTime + "ms, slot id: " + mSlotId);
370         AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
371         am.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, mAlarmTime, SELECT_ALARM_TAG,
372                 mAlarmListener, null);
373     }
374 
375     // Bind list adapter to the items list.
displayMenu()376     private void displayMenu() {
377 
378         if (mStkMenu != null) {
379             String title = mStkMenu.title == null ? getString(R.string.app_name) : mStkMenu.title;
380             // Display title & title icon
381             if (mStkMenu.titleIcon != null) {
382                 mTitleIconView.setImageBitmap(mStkMenu.titleIcon);
383                 mTitleIconView.setVisibility(View.VISIBLE);
384                 mTitleTextView.setVisibility(View.INVISIBLE);
385                 if (!mStkMenu.titleIconSelfExplanatory) {
386                     mTitleTextView.setText(title);
387                     mTitleTextView.setVisibility(View.VISIBLE);
388                 }
389             } else {
390                 mTitleIconView.setVisibility(View.GONE);
391                 mTitleTextView.setVisibility(View.VISIBLE);
392                 mTitleTextView.setText(title);
393             }
394             // create an array adapter for the menu list
395             StkMenuAdapter adapter = new StkMenuAdapter(this,
396                     mStkMenu.items, mStkMenu.itemsIconSelfExplanatory);
397             // Bind menu list to the new adapter.
398             setListAdapter(adapter);
399             // Set default item
400             setSelection(mStkMenu.defaultItem);
401         }
402     }
403 
showProgressBar(boolean show)404     private void showProgressBar(boolean show) {
405         if (show) {
406             mProgressView.setIndeterminate(true);
407             mProgressView.setVisibility(View.VISIBLE);
408         } else {
409             mProgressView.setIndeterminate(false);
410             mProgressView.setVisibility(View.GONE);
411         }
412     }
413 
initFromIntent(Intent intent)414     private void initFromIntent(Intent intent) {
415 
416         if (intent != null) {
417             mState = intent.getIntExtra("STATE", STATE_MAIN);
418             mSlotId = intent.getIntExtra(StkAppService.SLOT_ID, -1);
419             CatLog.d(LOG_TAG, "slot id: " + mSlotId + ", state: " + mState);
420         } else {
421             CatLog.d(LOG_TAG, "finish!");
422             finish();
423         }
424     }
425 
getSelectedItem(int position)426     private Item getSelectedItem(int position) {
427         Item item = null;
428         if (mStkMenu != null) {
429             try {
430                 item = mStkMenu.items.get(position);
431             } catch (IndexOutOfBoundsException e) {
432                 if (StkApp.DBG) {
433                     CatLog.d(LOG_TAG, "IOOBE Invalid menu");
434                 }
435             } catch (NullPointerException e) {
436                 if (StkApp.DBG) {
437                     CatLog.d(LOG_TAG, "NPE Invalid menu");
438                 }
439             }
440         }
441         return item;
442     }
443 
sendResponse(int resId)444     private void sendResponse(int resId) {
445         sendResponse(resId, 0, false);
446     }
447 
sendResponse(int resId, int itemId, boolean help)448     private void sendResponse(int resId, int itemId, boolean help) {
449         CatLog.d(LOG_TAG, "sendResponse resID[" + resId + "] itemId[" + itemId +
450             "] help[" + help + "]");
451 
452         // Disallow user operation temporarily until receiving the result of the response.
453         mAcceptUsersInput = false;
454         if (resId == StkAppService.RES_ID_MENU_SELECTION) {
455             showProgressBar(true);
456         }
457         cancelTimeOut();
458 
459         mIsResponseSent = true;
460         Bundle args = new Bundle();
461         args.putInt(StkAppService.RES_ID, resId);
462         args.putInt(StkAppService.MENU_SELECTION, itemId);
463         args.putBoolean(StkAppService.HELP, help);
464         appService.sendResponse(args, mSlotId);
465     }
466 
467     private final BroadcastReceiver mLocalBroadcastReceiver = new BroadcastReceiver() {
468         @Override
469         public void onReceive(Context context, Intent intent) {
470             if (StkAppService.SESSION_ENDED.equals(intent.getAction())) {
471                 int slotId = intent.getIntExtra(StkAppService.SLOT_ID, 0);
472                 if ((mState == STATE_MAIN) && (mSlotId == slotId)) {
473                     mAcceptUsersInput = true;
474                     showProgressBar(false);
475                 }
476             }
477         }
478     };
479 
480     private final AlarmManager.OnAlarmListener mAlarmListener =
481             new AlarmManager.OnAlarmListener() {
482                 @Override
483                 public void onAlarm() {
484                     CatLog.d(LOG_TAG, "The alarm time is reached");
485                     mAlarmTime = NO_SELECT_ALARM;
486                     sendResponse(StkAppService.RES_ID_TIMEOUT);
487                 }
488             };
489 }
490