GlobalActions.java revision 742a67127366c376fdf188ff99ba30b27d3bf90c
1/*
2 * Copyright (C) 2008 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
17package com.android.internal.policy.impl;
18
19import com.android.internal.app.ShutdownThread;
20import com.android.internal.telephony.TelephonyIntents;
21import com.android.internal.telephony.TelephonyProperties;
22import com.android.internal.R;
23
24import android.app.ActivityManagerNative;
25import android.app.AlertDialog;
26import android.content.BroadcastReceiver;
27import android.content.Context;
28import android.content.DialogInterface;
29import android.content.Intent;
30import android.content.IntentFilter;
31import android.content.pm.UserInfo;
32import android.media.AudioManager;
33import android.os.Handler;
34import android.os.Message;
35import android.os.RemoteException;
36import android.os.SystemProperties;
37import android.provider.Settings;
38import android.telephony.PhoneStateListener;
39import android.telephony.ServiceState;
40import android.telephony.TelephonyManager;
41import android.util.Log;
42import android.view.LayoutInflater;
43import android.view.View;
44import android.view.ViewGroup;
45import android.view.WindowManager;
46import android.widget.BaseAdapter;
47import android.widget.ImageView;
48import android.widget.TextView;
49
50import java.util.ArrayList;
51import java.util.List;
52
53/**
54 * Helper to show the global actions dialog.  Each item is an {@link Action} that
55 * may show depending on whether the keyguard is showing, and whether the device
56 * is provisioned.
57 */
58class GlobalActions implements DialogInterface.OnDismissListener, DialogInterface.OnClickListener  {
59
60    private static final String TAG = "GlobalActions";
61
62    private static final boolean SHOW_SILENT_TOGGLE = true;
63
64    private final Context mContext;
65    private final AudioManager mAudioManager;
66
67    private ArrayList<Action> mItems;
68    private AlertDialog mDialog;
69
70    private SilentModeAction mSilentModeAction;
71    private ToggleAction mAirplaneModeOn;
72
73    private MyAdapter mAdapter;
74
75    private boolean mKeyguardShowing = false;
76    private boolean mDeviceProvisioned = false;
77    private ToggleAction.State mAirplaneState = ToggleAction.State.Off;
78    private boolean mIsWaitingForEcmExit = false;
79
80    /**
81     * @param context everything needs a context :(
82     */
83    public GlobalActions(Context context) {
84        mContext = context;
85        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
86
87        // receive broadcasts
88        IntentFilter filter = new IntentFilter();
89        filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
90        filter.addAction(Intent.ACTION_SCREEN_OFF);
91        filter.addAction(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED);
92        context.registerReceiver(mBroadcastReceiver, filter);
93
94        // get notified of phone state changes
95        TelephonyManager telephonyManager =
96                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
97        telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE);
98    }
99
100    /**
101     * Show the global actions dialog (creating if necessary)
102     * @param keyguardShowing True if keyguard is showing
103     */
104    public void showDialog(boolean keyguardShowing, boolean isDeviceProvisioned) {
105        mKeyguardShowing = keyguardShowing;
106        mDeviceProvisioned = isDeviceProvisioned;
107        if (mDialog != null) {
108            mDialog.dismiss();
109        }
110        mDialog = createDialog();
111        prepareDialog();
112
113        mDialog.show();
114        mDialog.getWindow().getDecorView().setSystemUiVisibility(View.STATUS_BAR_DISABLE_EXPAND);
115    }
116
117    /**
118     * Create the global actions dialog.
119     * @return A new dialog.
120     */
121    private AlertDialog createDialog() {
122        mSilentModeAction = new SilentModeAction(mAudioManager, mHandler);
123
124        mAirplaneModeOn = new ToggleAction(
125                R.drawable.ic_lock_airplane_mode,
126                R.drawable.ic_lock_airplane_mode_off,
127                R.string.global_actions_toggle_airplane_mode,
128                R.string.global_actions_airplane_mode_on_status,
129                R.string.global_actions_airplane_mode_off_status) {
130
131            void onToggle(boolean on) {
132                if (Boolean.parseBoolean(
133                        SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE))) {
134                    mIsWaitingForEcmExit = true;
135                    // Launch ECM exit dialog
136                    Intent ecmDialogIntent =
137                            new Intent(TelephonyIntents.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null);
138                    ecmDialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
139                    mContext.startActivity(ecmDialogIntent);
140                } else {
141                    changeAirplaneModeSystemSetting(on);
142                }
143            }
144
145            @Override
146            protected void changeStateFromPress(boolean buttonOn) {
147                // In ECM mode airplane state cannot be changed
148                if (!(Boolean.parseBoolean(
149                        SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE)))) {
150                    mState = buttonOn ? State.TurningOn : State.TurningOff;
151                    mAirplaneState = mState;
152                }
153            }
154
155            public boolean showDuringKeyguard() {
156                return true;
157            }
158
159            public boolean showBeforeProvisioning() {
160                return false;
161            }
162        };
163
164        mItems = new ArrayList<Action>();
165
166        // first: power off
167        mItems.add(
168            new SinglePressAction(
169                    com.android.internal.R.drawable.ic_lock_power_off,
170                    R.string.global_action_power_off) {
171
172                public void onPress() {
173                    // shutdown by making sure radio and power are handled accordingly.
174                    ShutdownThread.shutdown(mContext, true);
175                }
176
177                public boolean showDuringKeyguard() {
178                    return true;
179                }
180
181                public boolean showBeforeProvisioning() {
182                    return true;
183                }
184            });
185
186        // next: airplane mode
187        mItems.add(mAirplaneModeOn);
188
189        // last: silent mode
190        if (SHOW_SILENT_TOGGLE) {
191            mItems.add(mSilentModeAction);
192        }
193
194        List<UserInfo> users = mContext.getPackageManager().getUsers();
195        if (users.size() > 1) {
196            for (final UserInfo user : users) {
197                SinglePressAction switchToUser = new SinglePressAction(
198                        com.android.internal.R.drawable.ic_menu_cc,
199                        user.name != null ? user.name : "Primary") {
200                    public void onPress() {
201                        try {
202                            ActivityManagerNative.getDefault().switchUser(user.id);
203                        } catch (RemoteException re) {
204                            Log.e(TAG, "Couldn't switch user " + re);
205                        }
206                    }
207
208                    public boolean showDuringKeyguard() {
209                        return true;
210                    }
211
212                    public boolean showBeforeProvisioning() {
213                        return false;
214                    }
215                };
216                mItems.add(switchToUser);
217            }
218        }
219        mAdapter = new MyAdapter();
220
221        final AlertDialog.Builder ab = new AlertDialog.Builder(mContext);
222
223        ab.setAdapter(mAdapter, this)
224                .setInverseBackgroundForced(true);
225
226        final AlertDialog dialog = ab.create();
227        dialog.getListView().setItemsCanFocus(true);
228        dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
229
230        dialog.setOnDismissListener(this);
231
232        return dialog;
233    }
234
235    private void prepareDialog() {
236        final boolean silentModeOn =
237                mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
238        mAirplaneModeOn.updateState(mAirplaneState);
239        mAdapter.notifyDataSetChanged();
240        if (mKeyguardShowing) {
241            mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
242        } else {
243            mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
244        }
245        if (SHOW_SILENT_TOGGLE) {
246            IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION);
247            mContext.registerReceiver(mRingerModeReceiver, filter);
248        }
249    }
250
251
252    /** {@inheritDoc} */
253    public void onDismiss(DialogInterface dialog) {
254        if (SHOW_SILENT_TOGGLE) {
255            mContext.unregisterReceiver(mRingerModeReceiver);
256        }
257    }
258
259    /** {@inheritDoc} */
260    public void onClick(DialogInterface dialog, int which) {
261        if (!(mAdapter.getItem(which) instanceof SilentModeAction)) {
262            dialog.dismiss();
263        }
264        mAdapter.getItem(which).onPress();
265    }
266
267    /**
268     * The adapter used for the list within the global actions dialog, taking
269     * into account whether the keyguard is showing via
270     * {@link GlobalActions#mKeyguardShowing} and whether the device is provisioned
271     * via {@link GlobalActions#mDeviceProvisioned}.
272     */
273    private class MyAdapter extends BaseAdapter {
274
275        public int getCount() {
276            int count = 0;
277
278            for (int i = 0; i < mItems.size(); i++) {
279                final Action action = mItems.get(i);
280
281                if (mKeyguardShowing && !action.showDuringKeyguard()) {
282                    continue;
283                }
284                if (!mDeviceProvisioned && !action.showBeforeProvisioning()) {
285                    continue;
286                }
287                count++;
288            }
289            return count;
290        }
291
292        @Override
293        public boolean isEnabled(int position) {
294            return getItem(position).isEnabled();
295        }
296
297        @Override
298        public boolean areAllItemsEnabled() {
299            return false;
300        }
301
302        public Action getItem(int position) {
303
304            int filteredPos = 0;
305            for (int i = 0; i < mItems.size(); i++) {
306                final Action action = mItems.get(i);
307                if (mKeyguardShowing && !action.showDuringKeyguard()) {
308                    continue;
309                }
310                if (!mDeviceProvisioned && !action.showBeforeProvisioning()) {
311                    continue;
312                }
313                if (filteredPos == position) {
314                    return action;
315                }
316                filteredPos++;
317            }
318
319            throw new IllegalArgumentException("position " + position
320                    + " out of range of showable actions"
321                    + ", filtered count=" + getCount()
322                    + ", keyguardshowing=" + mKeyguardShowing
323                    + ", provisioned=" + mDeviceProvisioned);
324        }
325
326
327        public long getItemId(int position) {
328            return position;
329        }
330
331        public View getView(int position, View convertView, ViewGroup parent) {
332            Action action = getItem(position);
333            return action.create(mContext, convertView, parent, LayoutInflater.from(mContext));
334        }
335    }
336
337    // note: the scheme below made more sense when we were planning on having
338    // 8 different things in the global actions dialog.  seems overkill with
339    // only 3 items now, but may as well keep this flexible approach so it will
340    // be easy should someone decide at the last minute to include something
341    // else, such as 'enable wifi', or 'enable bluetooth'
342
343    /**
344     * What each item in the global actions dialog must be able to support.
345     */
346    private interface Action {
347        View create(Context context, View convertView, ViewGroup parent, LayoutInflater inflater);
348
349        void onPress();
350
351        /**
352         * @return whether this action should appear in the dialog when the keygaurd
353         *    is showing.
354         */
355        boolean showDuringKeyguard();
356
357        /**
358         * @return whether this action should appear in the dialog before the
359         *   device is provisioned.
360         */
361        boolean showBeforeProvisioning();
362
363        boolean isEnabled();
364    }
365
366    /**
367     * A single press action maintains no state, just responds to a press
368     * and takes an action.
369     */
370    private static abstract class SinglePressAction implements Action {
371        private final int mIconResId;
372        private final int mMessageResId;
373        private final CharSequence mMessage;
374
375        protected SinglePressAction(int iconResId, int messageResId) {
376            mIconResId = iconResId;
377            mMessageResId = messageResId;
378            mMessage = null;
379        }
380
381        protected SinglePressAction(int iconResId, CharSequence message) {
382            mIconResId = iconResId;
383            mMessageResId = 0;
384            mMessage = message;
385        }
386        public boolean isEnabled() {
387            return true;
388        }
389
390        abstract public void onPress();
391
392        public View create(
393                Context context, View convertView, ViewGroup parent, LayoutInflater inflater) {
394            View v = inflater.inflate(R.layout.global_actions_item, parent, false);
395
396            ImageView icon = (ImageView) v.findViewById(R.id.icon);
397            TextView messageView = (TextView) v.findViewById(R.id.message);
398
399            v.findViewById(R.id.status).setVisibility(View.GONE);
400
401            icon.setImageDrawable(context.getResources().getDrawable(mIconResId));
402            if (mMessage != null) {
403                messageView.setText(mMessage);
404            } else {
405                messageView.setText(mMessageResId);
406            }
407
408            return v;
409        }
410    }
411
412    /**
413     * A toggle action knows whether it is on or off, and displays an icon
414     * and status message accordingly.
415     */
416    private static abstract class ToggleAction implements Action {
417
418        enum State {
419            Off(false),
420            TurningOn(true),
421            TurningOff(true),
422            On(false);
423
424            private final boolean inTransition;
425
426            State(boolean intermediate) {
427                inTransition = intermediate;
428            }
429
430            public boolean inTransition() {
431                return inTransition;
432            }
433        }
434
435        protected State mState = State.Off;
436
437        // prefs
438        protected int mEnabledIconResId;
439        protected int mDisabledIconResid;
440        protected int mMessageResId;
441        protected int mEnabledStatusMessageResId;
442        protected int mDisabledStatusMessageResId;
443
444        /**
445         * @param enabledIconResId The icon for when this action is on.
446         * @param disabledIconResid The icon for when this action is off.
447         * @param essage The general information message, e.g 'Silent Mode'
448         * @param enabledStatusMessageResId The on status message, e.g 'sound disabled'
449         * @param disabledStatusMessageResId The off status message, e.g. 'sound enabled'
450         */
451        public ToggleAction(int enabledIconResId,
452                int disabledIconResid,
453                int essage,
454                int enabledStatusMessageResId,
455                int disabledStatusMessageResId) {
456            mEnabledIconResId = enabledIconResId;
457            mDisabledIconResid = disabledIconResid;
458            mMessageResId = essage;
459            mEnabledStatusMessageResId = enabledStatusMessageResId;
460            mDisabledStatusMessageResId = disabledStatusMessageResId;
461        }
462
463        /**
464         * Override to make changes to resource IDs just before creating the
465         * View.
466         */
467        void willCreate() {
468
469        }
470
471        public View create(Context context, View convertView, ViewGroup parent,
472                LayoutInflater inflater) {
473            willCreate();
474
475            View v = inflater.inflate(R
476                            .layout.global_actions_item, parent, false);
477
478            ImageView icon = (ImageView) v.findViewById(R.id.icon);
479            TextView messageView = (TextView) v.findViewById(R.id.message);
480            TextView statusView = (TextView) v.findViewById(R.id.status);
481            final boolean enabled = isEnabled();
482
483            if (messageView != null) {
484                messageView.setText(mMessageResId);
485                messageView.setEnabled(enabled);
486            }
487
488            boolean on = ((mState == State.On) || (mState == State.TurningOn));
489            if (icon != null) {
490                icon.setImageDrawable(context.getResources().getDrawable(
491                        (on ? mEnabledIconResId : mDisabledIconResid)));
492                icon.setEnabled(enabled);
493            }
494
495            if (statusView != null) {
496                statusView.setText(on ? mEnabledStatusMessageResId : mDisabledStatusMessageResId);
497                statusView.setVisibility(View.VISIBLE);
498                statusView.setEnabled(enabled);
499            }
500            v.setEnabled(enabled);
501
502            return v;
503        }
504
505        public final void onPress() {
506            if (mState.inTransition()) {
507                Log.w(TAG, "shouldn't be able to toggle when in transition");
508                return;
509            }
510
511            final boolean nowOn = !(mState == State.On);
512            onToggle(nowOn);
513            changeStateFromPress(nowOn);
514        }
515
516        public boolean isEnabled() {
517            return !mState.inTransition();
518        }
519
520        /**
521         * Implementations may override this if their state can be in on of the intermediate
522         * states until some notification is received (e.g airplane mode is 'turning off' until
523         * we know the wireless connections are back online
524         * @param buttonOn Whether the button was turned on or off
525         */
526        protected void changeStateFromPress(boolean buttonOn) {
527            mState = buttonOn ? State.On : State.Off;
528        }
529
530        abstract void onToggle(boolean on);
531
532        public void updateState(State state) {
533            mState = state;
534        }
535    }
536
537    private static class SilentModeAction implements Action, View.OnClickListener {
538
539        private final int[] ITEM_IDS = { R.id.option1, R.id.option2, R.id.option3 };
540
541        private final AudioManager mAudioManager;
542        private final Handler mHandler;
543
544        SilentModeAction(AudioManager audioManager, Handler handler) {
545            mAudioManager = audioManager;
546            mHandler = handler;
547        }
548
549        private int ringerModeToIndex(int ringerMode) {
550            // They just happen to coincide
551            return ringerMode;
552        }
553
554        private int indexToRingerMode(int index) {
555            // They just happen to coincide
556            return index;
557        }
558
559        public View create(Context context, View convertView, ViewGroup parent,
560                LayoutInflater inflater) {
561            View v = inflater.inflate(R.layout.global_actions_silent_mode, parent, false);
562
563            int selectedIndex = ringerModeToIndex(mAudioManager.getRingerMode());
564            for (int i = 0; i < 3; i++) {
565                View itemView = v.findViewById(ITEM_IDS[i]);
566                itemView.setSelected(selectedIndex == i);
567                // Set up click handler
568                itemView.setTag(i);
569                itemView.setOnClickListener(this);
570            }
571            return v;
572        }
573
574        public void onPress() {
575        }
576
577        public boolean showDuringKeyguard() {
578            return true;
579        }
580
581        public boolean showBeforeProvisioning() {
582            return false;
583        }
584
585        public boolean isEnabled() {
586            return true;
587        }
588
589        void willCreate() {
590        }
591
592        public void onClick(View v) {
593            if (!(v.getTag() instanceof Integer)) return;
594
595            int index = (Integer) v.getTag();
596            mAudioManager.setRingerMode(indexToRingerMode(index));
597            mHandler.sendEmptyMessageDelayed(MESSAGE_DISMISS, DIALOG_DISMISS_DELAY);
598        }
599    }
600
601    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
602        public void onReceive(Context context, Intent intent) {
603            String action = intent.getAction();
604            if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)
605                    || Intent.ACTION_SCREEN_OFF.equals(action)) {
606                String reason = intent.getStringExtra(PhoneWindowManager.SYSTEM_DIALOG_REASON_KEY);
607                if (!PhoneWindowManager.SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS.equals(reason)) {
608                    mHandler.sendEmptyMessage(MESSAGE_DISMISS);
609                }
610            } else if (TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED.equals(action)) {
611                // Airplane mode can be changed after ECM exits if airplane toggle button
612                // is pressed during ECM mode
613                if (!(intent.getBooleanExtra("PHONE_IN_ECM_STATE", false)) &&
614                        mIsWaitingForEcmExit) {
615                    mIsWaitingForEcmExit = false;
616                    changeAirplaneModeSystemSetting(true);
617                }
618            }
619        }
620    };
621
622    PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
623        @Override
624        public void onServiceStateChanged(ServiceState serviceState) {
625            final boolean inAirplaneMode = serviceState.getState() == ServiceState.STATE_POWER_OFF;
626            mAirplaneState = inAirplaneMode ? ToggleAction.State.On : ToggleAction.State.Off;
627            mAirplaneModeOn.updateState(mAirplaneState);
628            mAdapter.notifyDataSetChanged();
629        }
630    };
631
632    private BroadcastReceiver mRingerModeReceiver = new BroadcastReceiver() {
633        @Override
634        public void onReceive(Context context, Intent intent) {
635            if (intent.getAction().equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
636                mHandler.sendEmptyMessage(MESSAGE_REFRESH);
637            }
638        }
639    };
640
641    private static final int MESSAGE_DISMISS = 0;
642    private static final int MESSAGE_REFRESH = 1;
643    private static final int DIALOG_DISMISS_DELAY = 300; // ms
644
645    private Handler mHandler = new Handler() {
646        public void handleMessage(Message msg) {
647            if (msg.what == MESSAGE_DISMISS) {
648                if (mDialog != null) {
649                    mDialog.dismiss();
650                }
651            } else if (msg.what == MESSAGE_REFRESH) {
652                mAdapter.notifyDataSetChanged();
653            }
654        }
655    };
656
657    /**
658     * Change the airplane mode system setting
659     */
660    private void changeAirplaneModeSystemSetting(boolean on) {
661        Settings.System.putInt(
662                mContext.getContentResolver(),
663                Settings.System.AIRPLANE_MODE_ON,
664                on ? 1 : 0);
665        Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
666        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
667        intent.putExtra("state", on);
668        mContext.sendBroadcast(intent);
669    }
670}
671