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.server.policy;
18
19import com.android.internal.app.AlertController;
20import com.android.internal.app.AlertController.AlertParams;
21import com.android.internal.logging.MetricsLogger;
22import com.android.internal.logging.MetricsProto.MetricsEvent;
23import com.android.internal.telephony.TelephonyIntents;
24import com.android.internal.telephony.TelephonyProperties;
25import com.android.internal.R;
26import com.android.internal.widget.LockPatternUtils;
27
28import android.app.ActivityManager;
29import android.app.ActivityManagerNative;
30import android.app.AlertDialog;
31import android.app.Dialog;
32import android.content.BroadcastReceiver;
33import android.content.Context;
34import android.content.DialogInterface;
35import android.content.Intent;
36import android.content.IntentFilter;
37import android.content.pm.UserInfo;
38import android.database.ContentObserver;
39import android.graphics.drawable.Drawable;
40import android.media.AudioManager;
41import android.net.ConnectivityManager;
42import android.os.Build;
43import android.os.Bundle;
44import android.os.Handler;
45import android.os.Message;
46import android.os.RemoteException;
47import android.os.ServiceManager;
48import android.os.SystemClock;
49import android.os.SystemProperties;
50import android.os.UserHandle;
51import android.os.UserManager;
52import android.os.Vibrator;
53import android.provider.Settings;
54import android.service.dreams.DreamService;
55import android.service.dreams.IDreamManager;
56import android.telephony.PhoneStateListener;
57import android.telephony.ServiceState;
58import android.telephony.TelephonyManager;
59import android.text.TextUtils;
60import android.util.ArraySet;
61import android.util.Log;
62import android.util.TypedValue;
63import android.view.InputDevice;
64import android.view.KeyEvent;
65import android.view.LayoutInflater;
66import android.view.MotionEvent;
67import android.view.View;
68import android.view.ViewConfiguration;
69import android.view.ViewGroup;
70import android.view.WindowManager;
71import android.view.WindowManagerGlobal;
72import android.view.WindowManagerPolicy.WindowManagerFuncs;
73import android.view.accessibility.AccessibilityEvent;
74import android.widget.AdapterView;
75import android.widget.BaseAdapter;
76import android.widget.ImageView;
77import android.widget.ImageView.ScaleType;
78import android.widget.ListView;
79import android.widget.TextView;
80
81import java.util.ArrayList;
82import java.util.List;
83
84/**
85 * Helper to show the global actions dialog.  Each item is an {@link Action} that
86 * may show depending on whether the keyguard is showing, and whether the device
87 * is provisioned.
88 */
89class GlobalActions implements DialogInterface.OnDismissListener, DialogInterface.OnClickListener  {
90
91    private static final String TAG = "GlobalActions";
92
93    private static final boolean SHOW_SILENT_TOGGLE = true;
94
95    /* Valid settings for global actions keys.
96     * see config.xml config_globalActionList */
97    private static final String GLOBAL_ACTION_KEY_POWER = "power";
98    private static final String GLOBAL_ACTION_KEY_AIRPLANE = "airplane";
99    private static final String GLOBAL_ACTION_KEY_BUGREPORT = "bugreport";
100    private static final String GLOBAL_ACTION_KEY_SILENT = "silent";
101    private static final String GLOBAL_ACTION_KEY_USERS = "users";
102    private static final String GLOBAL_ACTION_KEY_SETTINGS = "settings";
103    private static final String GLOBAL_ACTION_KEY_LOCKDOWN = "lockdown";
104    private static final String GLOBAL_ACTION_KEY_VOICEASSIST = "voiceassist";
105    private static final String GLOBAL_ACTION_KEY_ASSIST = "assist";
106
107    private final Context mContext;
108    private final WindowManagerFuncs mWindowManagerFuncs;
109    private final AudioManager mAudioManager;
110    private final IDreamManager mDreamManager;
111
112    private ArrayList<Action> mItems;
113    private GlobalActionsDialog mDialog;
114
115    private Action mSilentModeAction;
116    private ToggleAction mAirplaneModeOn;
117
118    private MyAdapter mAdapter;
119
120    private boolean mKeyguardShowing = false;
121    private boolean mDeviceProvisioned = false;
122    private ToggleAction.State mAirplaneState = ToggleAction.State.Off;
123    private boolean mIsWaitingForEcmExit = false;
124    private boolean mHasTelephony;
125    private boolean mHasVibrator;
126    private final boolean mShowSilentToggle;
127
128    /**
129     * @param context everything needs a context :(
130     */
131    public GlobalActions(Context context, WindowManagerFuncs windowManagerFuncs) {
132        mContext = context;
133        mWindowManagerFuncs = windowManagerFuncs;
134        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
135        mDreamManager = IDreamManager.Stub.asInterface(
136                ServiceManager.getService(DreamService.DREAM_SERVICE));
137
138        // receive broadcasts
139        IntentFilter filter = new IntentFilter();
140        filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
141        filter.addAction(Intent.ACTION_SCREEN_OFF);
142        filter.addAction(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED);
143        context.registerReceiver(mBroadcastReceiver, filter);
144
145        ConnectivityManager cm = (ConnectivityManager)
146                context.getSystemService(Context.CONNECTIVITY_SERVICE);
147        mHasTelephony = cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE);
148
149        // get notified of phone state changes
150        TelephonyManager telephonyManager =
151                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
152        telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE);
153        mContext.getContentResolver().registerContentObserver(
154                Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON), true,
155                mAirplaneModeObserver);
156        Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
157        mHasVibrator = vibrator != null && vibrator.hasVibrator();
158
159        mShowSilentToggle = SHOW_SILENT_TOGGLE && !mContext.getResources().getBoolean(
160                com.android.internal.R.bool.config_useFixedVolume);
161    }
162
163    /**
164     * Show the global actions dialog (creating if necessary)
165     * @param keyguardShowing True if keyguard is showing
166     */
167    public void showDialog(boolean keyguardShowing, boolean isDeviceProvisioned) {
168        mKeyguardShowing = keyguardShowing;
169        mDeviceProvisioned = isDeviceProvisioned;
170        if (mDialog != null) {
171            mDialog.dismiss();
172            mDialog = null;
173            // Show delayed, so that the dismiss of the previous dialog completes
174            mHandler.sendEmptyMessage(MESSAGE_SHOW);
175        } else {
176            handleShow();
177        }
178    }
179
180    private void awakenIfNecessary() {
181        if (mDreamManager != null) {
182            try {
183                if (mDreamManager.isDreaming()) {
184                    mDreamManager.awaken();
185                }
186            } catch (RemoteException e) {
187                // we tried
188            }
189        }
190    }
191
192    private void handleShow() {
193        awakenIfNecessary();
194        mDialog = createDialog();
195        prepareDialog();
196
197        // If we only have 1 item and it's a simple press action, just do this action.
198        if (mAdapter.getCount() == 1
199                && mAdapter.getItem(0) instanceof SinglePressAction
200                && !(mAdapter.getItem(0) instanceof LongPressAction)) {
201            ((SinglePressAction) mAdapter.getItem(0)).onPress();
202        } else {
203            WindowManager.LayoutParams attrs = mDialog.getWindow().getAttributes();
204            attrs.setTitle("GlobalActions");
205            mDialog.getWindow().setAttributes(attrs);
206            mDialog.show();
207            mDialog.getWindow().getDecorView().setSystemUiVisibility(View.STATUS_BAR_DISABLE_EXPAND);
208        }
209    }
210
211    /**
212     * Create the global actions dialog.
213     * @return A new dialog.
214     */
215    private GlobalActionsDialog createDialog() {
216        // Simple toggle style if there's no vibrator, otherwise use a tri-state
217        if (!mHasVibrator) {
218            mSilentModeAction = new SilentModeToggleAction();
219        } else {
220            mSilentModeAction = new SilentModeTriStateAction(mContext, mAudioManager, mHandler);
221        }
222        mAirplaneModeOn = new ToggleAction(
223                R.drawable.ic_lock_airplane_mode,
224                R.drawable.ic_lock_airplane_mode_off,
225                R.string.global_actions_toggle_airplane_mode,
226                R.string.global_actions_airplane_mode_on_status,
227                R.string.global_actions_airplane_mode_off_status) {
228
229            void onToggle(boolean on) {
230                if (mHasTelephony && Boolean.parseBoolean(
231                        SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE))) {
232                    mIsWaitingForEcmExit = true;
233                    // Launch ECM exit dialog
234                    Intent ecmDialogIntent =
235                            new Intent(TelephonyIntents.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null);
236                    ecmDialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
237                    mContext.startActivity(ecmDialogIntent);
238                } else {
239                    changeAirplaneModeSystemSetting(on);
240                }
241            }
242
243            @Override
244            protected void changeStateFromPress(boolean buttonOn) {
245                if (!mHasTelephony) return;
246
247                // In ECM mode airplane state cannot be changed
248                if (!(Boolean.parseBoolean(
249                        SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE)))) {
250                    mState = buttonOn ? State.TurningOn : State.TurningOff;
251                    mAirplaneState = mState;
252                }
253            }
254
255            public boolean showDuringKeyguard() {
256                return true;
257            }
258
259            public boolean showBeforeProvisioning() {
260                return false;
261            }
262        };
263        onAirplaneModeChanged();
264
265        mItems = new ArrayList<Action>();
266        String[] defaultActions = mContext.getResources().getStringArray(
267                com.android.internal.R.array.config_globalActionsList);
268
269        ArraySet<String> addedKeys = new ArraySet<String>();
270        for (int i = 0; i < defaultActions.length; i++) {
271            String actionKey = defaultActions[i];
272            if (addedKeys.contains(actionKey)) {
273                // If we already have added this, don't add it again.
274                continue;
275            }
276            if (GLOBAL_ACTION_KEY_POWER.equals(actionKey)) {
277                mItems.add(new PowerAction());
278            } else if (GLOBAL_ACTION_KEY_AIRPLANE.equals(actionKey)) {
279                mItems.add(mAirplaneModeOn);
280            } else if (GLOBAL_ACTION_KEY_BUGREPORT.equals(actionKey)) {
281                if (Settings.Global.getInt(mContext.getContentResolver(),
282                        Settings.Global.BUGREPORT_IN_POWER_MENU, 0) != 0 && isCurrentUserOwner()) {
283                    mItems.add(new BugReportAction());
284                }
285            } else if (GLOBAL_ACTION_KEY_SILENT.equals(actionKey)) {
286                if (mShowSilentToggle) {
287                    mItems.add(mSilentModeAction);
288                }
289            } else if (GLOBAL_ACTION_KEY_USERS.equals(actionKey)) {
290                if (SystemProperties.getBoolean("fw.power_user_switcher", false)) {
291                    addUsersToMenu(mItems);
292                }
293            } else if (GLOBAL_ACTION_KEY_SETTINGS.equals(actionKey)) {
294                mItems.add(getSettingsAction());
295            } else if (GLOBAL_ACTION_KEY_LOCKDOWN.equals(actionKey)) {
296                mItems.add(getLockdownAction());
297            } else if (GLOBAL_ACTION_KEY_VOICEASSIST.equals(actionKey)) {
298                mItems.add(getVoiceAssistAction());
299            } else if (GLOBAL_ACTION_KEY_ASSIST.equals(actionKey)) {
300                mItems.add(getAssistAction());
301            } else {
302                Log.e(TAG, "Invalid global action key " + actionKey);
303            }
304            // Add here so we don't add more than one.
305            addedKeys.add(actionKey);
306        }
307
308        mAdapter = new MyAdapter();
309
310        AlertParams params = new AlertParams(mContext);
311        params.mAdapter = mAdapter;
312        params.mOnClickListener = this;
313        params.mForceInverseBackground = true;
314
315        GlobalActionsDialog dialog = new GlobalActionsDialog(mContext, params);
316        dialog.setCanceledOnTouchOutside(false); // Handled by the custom class.
317
318        dialog.getListView().setItemsCanFocus(true);
319        dialog.getListView().setLongClickable(true);
320        dialog.getListView().setOnItemLongClickListener(
321                new AdapterView.OnItemLongClickListener() {
322                    @Override
323                    public boolean onItemLongClick(AdapterView<?> parent, View view, int position,
324                            long id) {
325                        final Action action = mAdapter.getItem(position);
326                        if (action instanceof LongPressAction) {
327                            return ((LongPressAction) action).onLongPress();
328                        }
329                        return false;
330                    }
331        });
332        dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
333
334        dialog.setOnDismissListener(this);
335
336        return dialog;
337    }
338
339    private final class PowerAction extends SinglePressAction implements LongPressAction {
340        private PowerAction() {
341            super(com.android.internal.R.drawable.ic_lock_power_off,
342                R.string.global_action_power_off);
343        }
344
345        @Override
346        public boolean onLongPress() {
347            UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
348            if (!um.hasUserRestriction(UserManager.DISALLOW_SAFE_BOOT)) {
349                mWindowManagerFuncs.rebootSafeMode(true);
350                return true;
351            }
352            return false;
353        }
354
355        @Override
356        public boolean showDuringKeyguard() {
357            return true;
358        }
359
360        @Override
361        public boolean showBeforeProvisioning() {
362            return true;
363        }
364
365        @Override
366        public void onPress() {
367            // shutdown by making sure radio and power are handled accordingly.
368            mWindowManagerFuncs.shutdown(false /* confirm */);
369        }
370    }
371
372    private class BugReportAction extends SinglePressAction implements LongPressAction {
373
374        public BugReportAction() {
375            super(com.android.internal.R.drawable.ic_lock_bugreport, R.string.bugreport_title);
376        }
377
378        @Override
379        public void onPress() {
380            // don't actually trigger the bugreport if we are running stability
381            // tests via monkey
382            if (ActivityManager.isUserAMonkey()) {
383                return;
384            }
385            // Add a little delay before executing, to give the
386            // dialog a chance to go away before it takes a
387            // screenshot.
388            mHandler.postDelayed(new Runnable() {
389                @Override
390                public void run() {
391                    try {
392                        // Take an "interactive" bugreport.
393                        MetricsLogger.action(mContext,
394                                MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_INTERACTIVE);
395                        ActivityManagerNative.getDefault().requestBugReport(
396                                ActivityManager.BUGREPORT_OPTION_INTERACTIVE);
397                    } catch (RemoteException e) {
398                    }
399                }
400            }, 500);
401        }
402
403        @Override
404        public boolean onLongPress() {
405            // don't actually trigger the bugreport if we are running stability
406            // tests via monkey
407            if (ActivityManager.isUserAMonkey()) {
408                return false;
409            }
410            try {
411                // Take a "full" bugreport.
412                MetricsLogger.action(mContext, MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_FULL);
413                ActivityManagerNative.getDefault().requestBugReport(
414                        ActivityManager.BUGREPORT_OPTION_FULL);
415            } catch (RemoteException e) {
416            }
417            return false;
418        }
419
420        public boolean showDuringKeyguard() {
421            return true;
422        }
423
424        @Override
425        public boolean showBeforeProvisioning() {
426            return false;
427        }
428
429        @Override
430        public String getStatus() {
431            return mContext.getString(
432                    com.android.internal.R.string.bugreport_status,
433                    Build.VERSION.RELEASE,
434                    Build.ID);
435        }
436    }
437
438    private Action getSettingsAction() {
439        return new SinglePressAction(com.android.internal.R.drawable.ic_settings,
440                R.string.global_action_settings) {
441
442            @Override
443            public void onPress() {
444                Intent intent = new Intent(Settings.ACTION_SETTINGS);
445                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
446                mContext.startActivity(intent);
447            }
448
449            @Override
450            public boolean showDuringKeyguard() {
451                return true;
452            }
453
454            @Override
455            public boolean showBeforeProvisioning() {
456                return true;
457            }
458        };
459    }
460
461    private Action getAssistAction() {
462        return new SinglePressAction(com.android.internal.R.drawable.ic_action_assist_focused,
463                R.string.global_action_assist) {
464            @Override
465            public void onPress() {
466                Intent intent = new Intent(Intent.ACTION_ASSIST);
467                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
468                mContext.startActivity(intent);
469            }
470
471            @Override
472            public boolean showDuringKeyguard() {
473                return true;
474            }
475
476            @Override
477            public boolean showBeforeProvisioning() {
478                return true;
479            }
480        };
481    }
482
483    private Action getVoiceAssistAction() {
484        return new SinglePressAction(com.android.internal.R.drawable.ic_voice_search,
485                R.string.global_action_voice_assist) {
486            @Override
487            public void onPress() {
488                Intent intent = new Intent(Intent.ACTION_VOICE_ASSIST);
489                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
490                mContext.startActivity(intent);
491            }
492
493            @Override
494            public boolean showDuringKeyguard() {
495                return true;
496            }
497
498            @Override
499            public boolean showBeforeProvisioning() {
500                return true;
501            }
502        };
503    }
504
505    private Action getLockdownAction() {
506        return new SinglePressAction(com.android.internal.R.drawable.ic_lock_lock,
507                R.string.global_action_lockdown) {
508
509            @Override
510            public void onPress() {
511                new LockPatternUtils(mContext).requireCredentialEntry(UserHandle.USER_ALL);
512                try {
513                    WindowManagerGlobal.getWindowManagerService().lockNow(null);
514                } catch (RemoteException e) {
515                    Log.e(TAG, "Error while trying to lock device.", e);
516                }
517            }
518
519            @Override
520            public boolean showDuringKeyguard() {
521                return true;
522            }
523
524            @Override
525            public boolean showBeforeProvisioning() {
526                return false;
527            }
528        };
529    }
530
531    private UserInfo getCurrentUser() {
532        try {
533            return ActivityManagerNative.getDefault().getCurrentUser();
534        } catch (RemoteException re) {
535            return null;
536        }
537    }
538
539    private boolean isCurrentUserOwner() {
540        UserInfo currentUser = getCurrentUser();
541        return currentUser == null || currentUser.isPrimary();
542    }
543
544    private void addUsersToMenu(ArrayList<Action> items) {
545        UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
546        if (um.isUserSwitcherEnabled()) {
547            List<UserInfo> users = um.getUsers();
548            UserInfo currentUser = getCurrentUser();
549            for (final UserInfo user : users) {
550                if (user.supportsSwitchToByUser()) {
551                    boolean isCurrentUser = currentUser == null
552                            ? user.id == 0 : (currentUser.id == user.id);
553                    Drawable icon = user.iconPath != null ? Drawable.createFromPath(user.iconPath)
554                            : null;
555                    SinglePressAction switchToUser = new SinglePressAction(
556                            com.android.internal.R.drawable.ic_menu_cc, icon,
557                            (user.name != null ? user.name : "Primary")
558                            + (isCurrentUser ? " \u2714" : "")) {
559                        public void onPress() {
560                            try {
561                                ActivityManagerNative.getDefault().switchUser(user.id);
562                            } catch (RemoteException re) {
563                                Log.e(TAG, "Couldn't switch user " + re);
564                            }
565                        }
566
567                        public boolean showDuringKeyguard() {
568                            return true;
569                        }
570
571                        public boolean showBeforeProvisioning() {
572                            return false;
573                        }
574                    };
575                    items.add(switchToUser);
576                }
577            }
578        }
579    }
580
581    private void prepareDialog() {
582        refreshSilentMode();
583        mAirplaneModeOn.updateState(mAirplaneState);
584        mAdapter.notifyDataSetChanged();
585        mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
586        if (mShowSilentToggle) {
587            IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION);
588            mContext.registerReceiver(mRingerModeReceiver, filter);
589        }
590    }
591
592    private void refreshSilentMode() {
593        if (!mHasVibrator) {
594            final boolean silentModeOn =
595                    mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
596            ((ToggleAction)mSilentModeAction).updateState(
597                    silentModeOn ? ToggleAction.State.On : ToggleAction.State.Off);
598        }
599    }
600
601    /** {@inheritDoc} */
602    public void onDismiss(DialogInterface dialog) {
603        if (mShowSilentToggle) {
604            try {
605                mContext.unregisterReceiver(mRingerModeReceiver);
606            } catch (IllegalArgumentException ie) {
607                // ignore this
608                Log.w(TAG, ie);
609            }
610        }
611    }
612
613    /** {@inheritDoc} */
614    public void onClick(DialogInterface dialog, int which) {
615        if (!(mAdapter.getItem(which) instanceof SilentModeTriStateAction)) {
616            dialog.dismiss();
617        }
618        mAdapter.getItem(which).onPress();
619    }
620
621    /**
622     * The adapter used for the list within the global actions dialog, taking
623     * into account whether the keyguard is showing via
624     * {@link GlobalActions#mKeyguardShowing} and whether the device is provisioned
625     * via {@link GlobalActions#mDeviceProvisioned}.
626     */
627    private class MyAdapter extends BaseAdapter {
628
629        public int getCount() {
630            int count = 0;
631
632            for (int i = 0; i < mItems.size(); i++) {
633                final Action action = mItems.get(i);
634
635                if (mKeyguardShowing && !action.showDuringKeyguard()) {
636                    continue;
637                }
638                if (!mDeviceProvisioned && !action.showBeforeProvisioning()) {
639                    continue;
640                }
641                count++;
642            }
643            return count;
644        }
645
646        @Override
647        public boolean isEnabled(int position) {
648            return getItem(position).isEnabled();
649        }
650
651        @Override
652        public boolean areAllItemsEnabled() {
653            return false;
654        }
655
656        public Action getItem(int position) {
657
658            int filteredPos = 0;
659            for (int i = 0; i < mItems.size(); i++) {
660                final Action action = mItems.get(i);
661                if (mKeyguardShowing && !action.showDuringKeyguard()) {
662                    continue;
663                }
664                if (!mDeviceProvisioned && !action.showBeforeProvisioning()) {
665                    continue;
666                }
667                if (filteredPos == position) {
668                    return action;
669                }
670                filteredPos++;
671            }
672
673            throw new IllegalArgumentException("position " + position
674                    + " out of range of showable actions"
675                    + ", filtered count=" + getCount()
676                    + ", keyguardshowing=" + mKeyguardShowing
677                    + ", provisioned=" + mDeviceProvisioned);
678        }
679
680
681        public long getItemId(int position) {
682            return position;
683        }
684
685        public View getView(int position, View convertView, ViewGroup parent) {
686            Action action = getItem(position);
687            return action.create(mContext, convertView, parent, LayoutInflater.from(mContext));
688        }
689    }
690
691    // note: the scheme below made more sense when we were planning on having
692    // 8 different things in the global actions dialog.  seems overkill with
693    // only 3 items now, but may as well keep this flexible approach so it will
694    // be easy should someone decide at the last minute to include something
695    // else, such as 'enable wifi', or 'enable bluetooth'
696
697    /**
698     * What each item in the global actions dialog must be able to support.
699     */
700    private interface Action {
701        /**
702         * @return Text that will be announced when dialog is created.  null
703         *     for none.
704         */
705        CharSequence getLabelForAccessibility(Context context);
706
707        View create(Context context, View convertView, ViewGroup parent, LayoutInflater inflater);
708
709        void onPress();
710
711        /**
712         * @return whether this action should appear in the dialog when the keygaurd
713         *    is showing.
714         */
715        boolean showDuringKeyguard();
716
717        /**
718         * @return whether this action should appear in the dialog before the
719         *   device is provisioned.
720         */
721        boolean showBeforeProvisioning();
722
723        boolean isEnabled();
724    }
725
726    /**
727     * An action that also supports long press.
728     */
729    private interface LongPressAction extends Action {
730        boolean onLongPress();
731    }
732
733    /**
734     * A single press action maintains no state, just responds to a press
735     * and takes an action.
736     */
737    private static abstract class SinglePressAction implements Action {
738        private final int mIconResId;
739        private final Drawable mIcon;
740        private final int mMessageResId;
741        private final CharSequence mMessage;
742
743        protected SinglePressAction(int iconResId, int messageResId) {
744            mIconResId = iconResId;
745            mMessageResId = messageResId;
746            mMessage = null;
747            mIcon = null;
748        }
749
750        protected SinglePressAction(int iconResId, Drawable icon, CharSequence message) {
751            mIconResId = iconResId;
752            mMessageResId = 0;
753            mMessage = message;
754            mIcon = icon;
755        }
756
757        public boolean isEnabled() {
758            return true;
759        }
760
761        public String getStatus() {
762            return null;
763        }
764
765        abstract public void onPress();
766
767        public CharSequence getLabelForAccessibility(Context context) {
768            if (mMessage != null) {
769                return mMessage;
770            } else {
771                return context.getString(mMessageResId);
772            }
773        }
774
775        public View create(
776                Context context, View convertView, ViewGroup parent, LayoutInflater inflater) {
777            View v = inflater.inflate(R.layout.global_actions_item, parent, false);
778
779            ImageView icon = (ImageView) v.findViewById(R.id.icon);
780            TextView messageView = (TextView) v.findViewById(R.id.message);
781
782            TextView statusView = (TextView) v.findViewById(R.id.status);
783            final String status = getStatus();
784            if (!TextUtils.isEmpty(status)) {
785                statusView.setText(status);
786            } else {
787                statusView.setVisibility(View.GONE);
788            }
789            if (mIcon != null) {
790                icon.setImageDrawable(mIcon);
791                icon.setScaleType(ScaleType.CENTER_CROP);
792            } else if (mIconResId != 0) {
793                icon.setImageDrawable(context.getDrawable(mIconResId));
794            }
795            if (mMessage != null) {
796                messageView.setText(mMessage);
797            } else {
798                messageView.setText(mMessageResId);
799            }
800
801            return v;
802        }
803    }
804
805    /**
806     * A toggle action knows whether it is on or off, and displays an icon
807     * and status message accordingly.
808     */
809    private static abstract class ToggleAction implements Action {
810
811        enum State {
812            Off(false),
813            TurningOn(true),
814            TurningOff(true),
815            On(false);
816
817            private final boolean inTransition;
818
819            State(boolean intermediate) {
820                inTransition = intermediate;
821            }
822
823            public boolean inTransition() {
824                return inTransition;
825            }
826        }
827
828        protected State mState = State.Off;
829
830        // prefs
831        protected int mEnabledIconResId;
832        protected int mDisabledIconResid;
833        protected int mMessageResId;
834        protected int mEnabledStatusMessageResId;
835        protected int mDisabledStatusMessageResId;
836
837        /**
838         * @param enabledIconResId The icon for when this action is on.
839         * @param disabledIconResid The icon for when this action is off.
840         * @param essage The general information message, e.g 'Silent Mode'
841         * @param enabledStatusMessageResId The on status message, e.g 'sound disabled'
842         * @param disabledStatusMessageResId The off status message, e.g. 'sound enabled'
843         */
844        public ToggleAction(int enabledIconResId,
845                int disabledIconResid,
846                int message,
847                int enabledStatusMessageResId,
848                int disabledStatusMessageResId) {
849            mEnabledIconResId = enabledIconResId;
850            mDisabledIconResid = disabledIconResid;
851            mMessageResId = message;
852            mEnabledStatusMessageResId = enabledStatusMessageResId;
853            mDisabledStatusMessageResId = disabledStatusMessageResId;
854        }
855
856        /**
857         * Override to make changes to resource IDs just before creating the
858         * View.
859         */
860        void willCreate() {
861
862        }
863
864        @Override
865        public CharSequence getLabelForAccessibility(Context context) {
866            return context.getString(mMessageResId);
867        }
868
869        public View create(Context context, View convertView, ViewGroup parent,
870                LayoutInflater inflater) {
871            willCreate();
872
873            View v = inflater.inflate(R
874                            .layout.global_actions_item, parent, false);
875
876            ImageView icon = (ImageView) v.findViewById(R.id.icon);
877            TextView messageView = (TextView) v.findViewById(R.id.message);
878            TextView statusView = (TextView) v.findViewById(R.id.status);
879            final boolean enabled = isEnabled();
880
881            if (messageView != null) {
882                messageView.setText(mMessageResId);
883                messageView.setEnabled(enabled);
884            }
885
886            boolean on = ((mState == State.On) || (mState == State.TurningOn));
887            if (icon != null) {
888                icon.setImageDrawable(context.getDrawable(
889                        (on ? mEnabledIconResId : mDisabledIconResid)));
890                icon.setEnabled(enabled);
891            }
892
893            if (statusView != null) {
894                statusView.setText(on ? mEnabledStatusMessageResId : mDisabledStatusMessageResId);
895                statusView.setVisibility(View.VISIBLE);
896                statusView.setEnabled(enabled);
897            }
898            v.setEnabled(enabled);
899
900            return v;
901        }
902
903        public final void onPress() {
904            if (mState.inTransition()) {
905                Log.w(TAG, "shouldn't be able to toggle when in transition");
906                return;
907            }
908
909            final boolean nowOn = !(mState == State.On);
910            onToggle(nowOn);
911            changeStateFromPress(nowOn);
912        }
913
914        public boolean isEnabled() {
915            return !mState.inTransition();
916        }
917
918        /**
919         * Implementations may override this if their state can be in on of the intermediate
920         * states until some notification is received (e.g airplane mode is 'turning off' until
921         * we know the wireless connections are back online
922         * @param buttonOn Whether the button was turned on or off
923         */
924        protected void changeStateFromPress(boolean buttonOn) {
925            mState = buttonOn ? State.On : State.Off;
926        }
927
928        abstract void onToggle(boolean on);
929
930        public void updateState(State state) {
931            mState = state;
932        }
933    }
934
935    private class SilentModeToggleAction extends ToggleAction {
936        public SilentModeToggleAction() {
937            super(R.drawable.ic_audio_vol_mute,
938                    R.drawable.ic_audio_vol,
939                    R.string.global_action_toggle_silent_mode,
940                    R.string.global_action_silent_mode_on_status,
941                    R.string.global_action_silent_mode_off_status);
942        }
943
944        void onToggle(boolean on) {
945            if (on) {
946                mAudioManager.setRingerMode(AudioManager.RINGER_MODE_SILENT);
947            } else {
948                mAudioManager.setRingerMode(AudioManager.RINGER_MODE_NORMAL);
949            }
950        }
951
952        public boolean showDuringKeyguard() {
953            return true;
954        }
955
956        public boolean showBeforeProvisioning() {
957            return false;
958        }
959    }
960
961    private static class SilentModeTriStateAction implements Action, View.OnClickListener {
962
963        private final int[] ITEM_IDS = { R.id.option1, R.id.option2, R.id.option3 };
964
965        private final AudioManager mAudioManager;
966        private final Handler mHandler;
967        private final Context mContext;
968
969        SilentModeTriStateAction(Context context, AudioManager audioManager, Handler handler) {
970            mAudioManager = audioManager;
971            mHandler = handler;
972            mContext = context;
973        }
974
975        private int ringerModeToIndex(int ringerMode) {
976            // They just happen to coincide
977            return ringerMode;
978        }
979
980        private int indexToRingerMode(int index) {
981            // They just happen to coincide
982            return index;
983        }
984
985        @Override
986        public CharSequence getLabelForAccessibility(Context context) {
987            return null;
988        }
989
990        public View create(Context context, View convertView, ViewGroup parent,
991                LayoutInflater inflater) {
992            View v = inflater.inflate(R.layout.global_actions_silent_mode, parent, false);
993
994            int selectedIndex = ringerModeToIndex(mAudioManager.getRingerMode());
995            for (int i = 0; i < 3; i++) {
996                View itemView = v.findViewById(ITEM_IDS[i]);
997                itemView.setSelected(selectedIndex == i);
998                // Set up click handler
999                itemView.setTag(i);
1000                itemView.setOnClickListener(this);
1001            }
1002            return v;
1003        }
1004
1005        public void onPress() {
1006        }
1007
1008        public boolean showDuringKeyguard() {
1009            return true;
1010        }
1011
1012        public boolean showBeforeProvisioning() {
1013            return false;
1014        }
1015
1016        public boolean isEnabled() {
1017            return true;
1018        }
1019
1020        void willCreate() {
1021        }
1022
1023        public void onClick(View v) {
1024            if (!(v.getTag() instanceof Integer)) return;
1025
1026            int index = (Integer) v.getTag();
1027            mAudioManager.setRingerMode(indexToRingerMode(index));
1028            mHandler.sendEmptyMessageDelayed(MESSAGE_DISMISS, DIALOG_DISMISS_DELAY);
1029        }
1030    }
1031
1032    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
1033        public void onReceive(Context context, Intent intent) {
1034            String action = intent.getAction();
1035            if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)
1036                    || Intent.ACTION_SCREEN_OFF.equals(action)) {
1037                String reason = intent.getStringExtra(PhoneWindowManager.SYSTEM_DIALOG_REASON_KEY);
1038                if (!PhoneWindowManager.SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS.equals(reason)) {
1039                    mHandler.sendEmptyMessage(MESSAGE_DISMISS);
1040                }
1041            } else if (TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED.equals(action)) {
1042                // Airplane mode can be changed after ECM exits if airplane toggle button
1043                // is pressed during ECM mode
1044                if (!(intent.getBooleanExtra("PHONE_IN_ECM_STATE", false)) &&
1045                        mIsWaitingForEcmExit) {
1046                    mIsWaitingForEcmExit = false;
1047                    changeAirplaneModeSystemSetting(true);
1048                }
1049            }
1050        }
1051    };
1052
1053    PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
1054        @Override
1055        public void onServiceStateChanged(ServiceState serviceState) {
1056            if (!mHasTelephony) return;
1057            final boolean inAirplaneMode = serviceState.getState() == ServiceState.STATE_POWER_OFF;
1058            mAirplaneState = inAirplaneMode ? ToggleAction.State.On : ToggleAction.State.Off;
1059            mAirplaneModeOn.updateState(mAirplaneState);
1060            mAdapter.notifyDataSetChanged();
1061        }
1062    };
1063
1064    private BroadcastReceiver mRingerModeReceiver = new BroadcastReceiver() {
1065        @Override
1066        public void onReceive(Context context, Intent intent) {
1067            if (intent.getAction().equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
1068                mHandler.sendEmptyMessage(MESSAGE_REFRESH);
1069            }
1070        }
1071    };
1072
1073    private ContentObserver mAirplaneModeObserver = new ContentObserver(new Handler()) {
1074        @Override
1075        public void onChange(boolean selfChange) {
1076            onAirplaneModeChanged();
1077        }
1078    };
1079
1080    private static final int MESSAGE_DISMISS = 0;
1081    private static final int MESSAGE_REFRESH = 1;
1082    private static final int MESSAGE_SHOW = 2;
1083    private static final int DIALOG_DISMISS_DELAY = 300; // ms
1084
1085    private Handler mHandler = new Handler() {
1086        public void handleMessage(Message msg) {
1087            switch (msg.what) {
1088            case MESSAGE_DISMISS:
1089                if (mDialog != null) {
1090                    mDialog.dismiss();
1091                    mDialog = null;
1092                }
1093                break;
1094            case MESSAGE_REFRESH:
1095                refreshSilentMode();
1096                mAdapter.notifyDataSetChanged();
1097                break;
1098            case MESSAGE_SHOW:
1099                handleShow();
1100                break;
1101            }
1102        }
1103    };
1104
1105    private void onAirplaneModeChanged() {
1106        // Let the service state callbacks handle the state.
1107        if (mHasTelephony) return;
1108
1109        boolean airplaneModeOn = Settings.Global.getInt(
1110                mContext.getContentResolver(),
1111                Settings.Global.AIRPLANE_MODE_ON,
1112                0) == 1;
1113        mAirplaneState = airplaneModeOn ? ToggleAction.State.On : ToggleAction.State.Off;
1114        mAirplaneModeOn.updateState(mAirplaneState);
1115    }
1116
1117    /**
1118     * Change the airplane mode system setting
1119     */
1120    private void changeAirplaneModeSystemSetting(boolean on) {
1121        Settings.Global.putInt(
1122                mContext.getContentResolver(),
1123                Settings.Global.AIRPLANE_MODE_ON,
1124                on ? 1 : 0);
1125        Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
1126        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
1127        intent.putExtra("state", on);
1128        mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
1129        if (!mHasTelephony) {
1130            mAirplaneState = on ? ToggleAction.State.On : ToggleAction.State.Off;
1131        }
1132    }
1133
1134    private static final class GlobalActionsDialog extends Dialog implements DialogInterface {
1135        private final Context mContext;
1136        private final int mWindowTouchSlop;
1137        private final AlertController mAlert;
1138        private final MyAdapter mAdapter;
1139
1140        private EnableAccessibilityController mEnableAccessibilityController;
1141
1142        private boolean mIntercepted;
1143        private boolean mCancelOnUp;
1144
1145        public GlobalActionsDialog(Context context, AlertParams params) {
1146            super(context, getDialogTheme(context));
1147            mContext = getContext();
1148            mAlert = new AlertController(mContext, this, getWindow());
1149            mAdapter = (MyAdapter) params.mAdapter;
1150            mWindowTouchSlop = ViewConfiguration.get(context).getScaledWindowTouchSlop();
1151            params.apply(mAlert);
1152        }
1153
1154        private static int getDialogTheme(Context context) {
1155            TypedValue outValue = new TypedValue();
1156            context.getTheme().resolveAttribute(com.android.internal.R.attr.alertDialogTheme,
1157                    outValue, true);
1158            return outValue.resourceId;
1159        }
1160
1161        @Override
1162        protected void onStart() {
1163            // If global accessibility gesture can be performed, we will take care
1164            // of dismissing the dialog on touch outside. This is because the dialog
1165            // is dismissed on the first down while the global gesture is a long press
1166            // with two fingers anywhere on the screen.
1167            if (EnableAccessibilityController.canEnableAccessibilityViaGesture(mContext)) {
1168                mEnableAccessibilityController = new EnableAccessibilityController(mContext,
1169                        new Runnable() {
1170                    @Override
1171                    public void run() {
1172                        dismiss();
1173                    }
1174                });
1175                super.setCanceledOnTouchOutside(false);
1176            } else {
1177                mEnableAccessibilityController = null;
1178                super.setCanceledOnTouchOutside(true);
1179            }
1180
1181            super.onStart();
1182        }
1183
1184        @Override
1185        protected void onStop() {
1186            if (mEnableAccessibilityController != null) {
1187                mEnableAccessibilityController.onDestroy();
1188            }
1189            super.onStop();
1190        }
1191
1192        @Override
1193        public boolean dispatchTouchEvent(MotionEvent event) {
1194            if (mEnableAccessibilityController != null) {
1195                final int action = event.getActionMasked();
1196                if (action == MotionEvent.ACTION_DOWN) {
1197                    View decor = getWindow().getDecorView();
1198                    final int eventX = (int) event.getX();
1199                    final int eventY = (int) event.getY();
1200                    if (eventX < -mWindowTouchSlop
1201                            || eventY < -mWindowTouchSlop
1202                            || eventX >= decor.getWidth() + mWindowTouchSlop
1203                            || eventY >= decor.getHeight() + mWindowTouchSlop) {
1204                        mCancelOnUp = true;
1205                    }
1206                }
1207                try {
1208                    if (!mIntercepted) {
1209                        mIntercepted = mEnableAccessibilityController.onInterceptTouchEvent(event);
1210                        if (mIntercepted) {
1211                            final long now = SystemClock.uptimeMillis();
1212                            event = MotionEvent.obtain(now, now,
1213                                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
1214                            event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
1215                            mCancelOnUp = true;
1216                        }
1217                    } else {
1218                        return mEnableAccessibilityController.onTouchEvent(event);
1219                    }
1220                } finally {
1221                    if (action == MotionEvent.ACTION_UP) {
1222                        if (mCancelOnUp) {
1223                            cancel();
1224                        }
1225                        mCancelOnUp = false;
1226                        mIntercepted = false;
1227                    }
1228                }
1229            }
1230            return super.dispatchTouchEvent(event);
1231        }
1232
1233        public ListView getListView() {
1234            return mAlert.getListView();
1235        }
1236
1237        @Override
1238        protected void onCreate(Bundle savedInstanceState) {
1239            super.onCreate(savedInstanceState);
1240            mAlert.installContent();
1241        }
1242
1243        @Override
1244        public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
1245            if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
1246                for (int i = 0; i < mAdapter.getCount(); ++i) {
1247                    CharSequence label =
1248                            mAdapter.getItem(i).getLabelForAccessibility(getContext());
1249                    if (label != null) {
1250                        event.getText().add(label);
1251                    }
1252                }
1253            }
1254            return super.dispatchPopulateAccessibilityEvent(event);
1255        }
1256
1257        @Override
1258        public boolean onKeyDown(int keyCode, KeyEvent event) {
1259            if (mAlert.onKeyDown(keyCode, event)) {
1260                return true;
1261            }
1262            return super.onKeyDown(keyCode, event);
1263        }
1264
1265        @Override
1266        public boolean onKeyUp(int keyCode, KeyEvent event) {
1267            if (mAlert.onKeyUp(keyCode, event)) {
1268                return true;
1269            }
1270            return super.onKeyUp(keyCode, event);
1271        }
1272    }
1273}
1274