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
226        final AlertDialog dialog = ab.create();
227        dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
228        if (!mContext.getResources().getBoolean(
229                com.android.internal.R.bool.config_sf_slowBlur)) {
230            dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND,
231                    WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
232        }
233
234        dialog.setOnDismissListener(this);
235
236        return dialog;
237    }
238
239    private void prepareDialog() {
240        final boolean silentModeOn =
241                mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
242        mSilentModeToggle.updateState(
243                silentModeOn ? ToggleAction.State.On : ToggleAction.State.Off);
244        mAirplaneModeOn.updateState(mAirplaneState);
245        mAdapter.notifyDataSetChanged();
246        if (mKeyguardShowing) {
247            mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
248        } else {
249            mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
250        }
251        mDialog.setTitle(R.string.global_actions);
252    }
253
254
255    /** {@inheritDoc} */
256    public void onDismiss(DialogInterface dialog) {
257        mStatusBar.disable(StatusBarManager.DISABLE_NONE);
258    }
259
260    /** {@inheritDoc} */
261    public void onClick(DialogInterface dialog, int which) {
262        dialog.dismiss();
263        mAdapter.getItem(which).onPress();
264    }
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 + " out of "
320                    + "range of showable actions, filtered count = "
321                    + "= " + getCount() + ", keyguardshowing=" + mKeyguardShowing
322                    + ", provisioned=" + mDeviceProvisioned);
323        }
324
325
326        public long getItemId(int position) {
327            return position;
328        }
329
330        public View getView(int position, View convertView, ViewGroup parent) {
331            Action action = getItem(position);
332            return action.create(mContext, convertView, parent, LayoutInflater.from(mContext));
333        }
334    }
335
336    // note: the scheme below made more sense when we were planning on having
337    // 8 different things in the global actions dialog.  seems overkill with
338    // only 3 items now, but may as well keep this flexible approach so it will
339    // be easy should someone decide at the last minute to include something
340    // else, such as 'enable wifi', or 'enable bluetooth'
341
342    /**
343     * What each item in the global actions dialog must be able to support.
344     */
345    private interface Action {
346        View create(Context context, View convertView, ViewGroup parent, LayoutInflater inflater);
347
348        void onPress();
349
350        /**
351         * @return whether this action should appear in the dialog when the keygaurd
352         *    is showing.
353         */
354        boolean showDuringKeyguard();
355
356        /**
357         * @return whether this action should appear in the dialog before the
358         *   device is provisioned.
359         */
360        boolean showBeforeProvisioning();
361
362        boolean isEnabled();
363    }
364
365    /**
366     * A single press action maintains no state, just responds to a press
367     * and takes an action.
368     */
369    private static abstract class SinglePressAction implements Action {
370        private final int mIconResId;
371        private final int mMessageResId;
372
373        protected SinglePressAction(int iconResId, int messageResId) {
374            mIconResId = iconResId;
375            mMessageResId = messageResId;
376        }
377
378        public boolean isEnabled() {
379            return true;
380        }
381
382        abstract public void onPress();
383
384        public View create(
385                Context context, View convertView, ViewGroup parent, LayoutInflater inflater) {
386            View v = (convertView != null) ?
387                    convertView :
388                    inflater.inflate(R.layout.global_actions_item, parent, false);
389
390            ImageView icon = (ImageView) v.findViewById(R.id.icon);
391            TextView messageView = (TextView) v.findViewById(R.id.message);
392
393            v.findViewById(R.id.status).setVisibility(View.GONE);
394
395            icon.setImageDrawable(context.getResources().getDrawable(mIconResId));
396            messageView.setText(mMessageResId);
397
398            return v;
399        }
400    }
401
402    /**
403     * A toggle action knows whether it is on or off, and displays an icon
404     * and status message accordingly.
405     */
406    private static abstract class ToggleAction implements Action {
407
408        enum State {
409            Off(false),
410            TurningOn(true),
411            TurningOff(true),
412            On(false);
413
414            private final boolean inTransition;
415
416            State(boolean intermediate) {
417                inTransition = intermediate;
418            }
419
420            public boolean inTransition() {
421                return inTransition;
422            }
423        }
424
425        protected State mState = State.Off;
426
427        // prefs
428        protected int mEnabledIconResId;
429        protected int mDisabledIconResid;
430        protected int mMessageResId;
431        protected int mEnabledStatusMessageResId;
432        protected int mDisabledStatusMessageResId;
433
434        /**
435         * @param enabledIconResId The icon for when this action is on.
436         * @param disabledIconResid The icon for when this action is off.
437         * @param essage The general information message, e.g 'Silent Mode'
438         * @param enabledStatusMessageResId The on status message, e.g 'sound disabled'
439         * @param disabledStatusMessageResId The off status message, e.g. 'sound enabled'
440         */
441        public ToggleAction(int enabledIconResId,
442                int disabledIconResid,
443                int essage,
444                int enabledStatusMessageResId,
445                int disabledStatusMessageResId) {
446            mEnabledIconResId = enabledIconResId;
447            mDisabledIconResid = disabledIconResid;
448            mMessageResId = essage;
449            mEnabledStatusMessageResId = enabledStatusMessageResId;
450            mDisabledStatusMessageResId = disabledStatusMessageResId;
451        }
452
453        /**
454         * Override to make changes to resource IDs just before creating the
455         * View.
456         */
457        void willCreate() {
458
459        }
460
461        public View create(Context context, View convertView, ViewGroup parent,
462                LayoutInflater inflater) {
463            willCreate();
464
465            View v = (convertView != null) ?
466                    convertView :
467                    inflater.inflate(R
468                            .layout.global_actions_item, parent, false);
469
470            ImageView icon = (ImageView) v.findViewById(R.id.icon);
471            TextView messageView = (TextView) v.findViewById(R.id.message);
472            TextView statusView = (TextView) v.findViewById(R.id.status);
473
474            messageView.setText(mMessageResId);
475
476            boolean on = ((mState == State.On) || (mState == State.TurningOn));
477            icon.setImageDrawable(context.getResources().getDrawable(
478                    (on ? mEnabledIconResId : mDisabledIconResid)));
479            statusView.setText(on ? mEnabledStatusMessageResId : mDisabledStatusMessageResId);
480            statusView.setVisibility(View.VISIBLE);
481
482            final boolean enabled = isEnabled();
483            messageView.setEnabled(enabled);
484            statusView.setEnabled(enabled);
485            icon.setEnabled(enabled);
486            v.setEnabled(enabled);
487
488            return v;
489        }
490
491        public final void onPress() {
492            if (mState.inTransition()) {
493                Log.w(TAG, "shouldn't be able to toggle when in transition");
494                return;
495            }
496
497            final boolean nowOn = !(mState == State.On);
498            onToggle(nowOn);
499            changeStateFromPress(nowOn);
500        }
501
502        public boolean isEnabled() {
503            return !mState.inTransition();
504        }
505
506        /**
507         * Implementations may override this if their state can be in on of the intermediate
508         * states until some notification is received (e.g airplane mode is 'turning off' until
509         * we know the wireless connections are back online
510         * @param buttonOn Whether the button was turned on or off
511         */
512        protected void changeStateFromPress(boolean buttonOn) {
513            mState = buttonOn ? State.On : State.Off;
514        }
515
516        abstract void onToggle(boolean on);
517
518        public void updateState(State state) {
519            mState = state;
520        }
521    }
522
523    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
524        public void onReceive(Context context, Intent intent) {
525            String action = intent.getAction();
526            if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)
527                    || Intent.ACTION_SCREEN_OFF.equals(action)) {
528                String reason = intent.getStringExtra(PhoneWindowManager.SYSTEM_DIALOG_REASON_KEY);
529                if (!PhoneWindowManager.SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS.equals(reason)) {
530                    mHandler.sendEmptyMessage(MESSAGE_DISMISS);
531                }
532            } else if (TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED.equals(action)) {
533                // Airplane mode can be changed after ECM exits if airplane toggle button
534                // is pressed during ECM mode
535                if (!(intent.getBooleanExtra("PHONE_IN_ECM_STATE", false)) &&
536                        mIsWaitingForEcmExit) {
537                    mIsWaitingForEcmExit = false;
538                    changeAirplaneModeSystemSetting(true);
539                }
540            }
541        }
542    };
543
544    PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
545        @Override
546        public void onServiceStateChanged(ServiceState serviceState) {
547            final boolean inAirplaneMode = serviceState.getState() == ServiceState.STATE_POWER_OFF;
548            mAirplaneState = inAirplaneMode ? ToggleAction.State.On : ToggleAction.State.Off;
549            mAirplaneModeOn.updateState(mAirplaneState);
550            mAdapter.notifyDataSetChanged();
551        }
552    };
553
554    private static final int MESSAGE_DISMISS = 0;
555    private Handler mHandler = new Handler() {
556        public void handleMessage(Message msg) {
557            if (msg.what == MESSAGE_DISMISS) {
558                if (mDialog != null) {
559                    mDialog.dismiss();
560                }
561            }
562        }
563    };
564
565    /**
566     * Change the airplane mode system setting
567     */
568    private void changeAirplaneModeSystemSetting(boolean on) {
569        Settings.System.putInt(
570                mContext.getContentResolver(),
571                Settings.System.AIRPLANE_MODE_ON,
572                on ? 1 : 0);
573        Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
574        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
575        intent.putExtra("state", on);
576        mContext.sendBroadcast(intent);
577    }
578}
579