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