RecentApplicationsDialog.java revision 8c769cb9cc02fe0496c16b51bc555729accf70c4
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.ActivityManager;
20import android.app.Dialog;
21import android.app.StatusBarManager;
22import android.content.ActivityNotFoundException;
23import android.content.BroadcastReceiver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.IntentFilter;
27import android.content.pm.ActivityInfo;
28import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
30import android.graphics.drawable.Drawable;
31import android.os.Bundle;
32import android.os.Handler;
33import android.util.Log;
34import android.view.KeyEvent;
35import android.view.SoundEffectConstants;
36import android.view.View;
37import android.view.Window;
38import android.view.WindowManager;
39import android.view.View.OnClickListener;
40import android.widget.TextView;
41
42import java.util.List;
43
44public class RecentApplicationsDialog extends Dialog implements OnClickListener {
45    // Elements for debugging support
46//  private static final String LOG_TAG = "RecentApplicationsDialog";
47    private static final boolean DBG_FORCE_EMPTY_LIST = false;
48
49    static private StatusBarManager sStatusBar;
50
51    private static final int NUM_BUTTONS = 8;
52    private static final int MAX_RECENT_TASKS = NUM_BUTTONS * 2;    // allow for some discards
53
54    final TextView[] mIcons = new TextView[NUM_BUTTONS];
55    View mNoAppsText;
56    IntentFilter mBroadcastIntentFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
57
58    class RecentTag {
59        ActivityManager.RecentTaskInfo info;
60        Intent intent;
61    }
62
63    Handler mHandler = new Handler();
64    Runnable mCleanup = new Runnable() {
65        public void run() {
66            // dump extra memory we're hanging on to
67            for (TextView icon: mIcons) {
68                icon.setCompoundDrawables(null, null, null, null);
69                icon.setTag(null);
70            }
71        }
72    };
73
74    private int mInitialModifiers;
75
76    public RecentApplicationsDialog(Context context, int initialModifiers) {
77        super(context, com.android.internal.R.style.Theme_Dialog_RecentApplications);
78
79        mInitialModifiers = initialModifiers;
80    }
81
82    /**
83     * We create the recent applications dialog just once, and it stays around (hidden)
84     * until activated by the user.
85     *
86     * @see PhoneWindowManager#showRecentAppsDialog
87     */
88    @Override
89    protected void onCreate(Bundle savedInstanceState) {
90        super.onCreate(savedInstanceState);
91
92        Context context = getContext();
93
94        if (sStatusBar == null) {
95            sStatusBar = (StatusBarManager)context.getSystemService(Context.STATUS_BAR_SERVICE);
96        }
97
98        Window window = getWindow();
99        window.requestFeature(Window.FEATURE_NO_TITLE);
100        window.setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
101        window.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
102                WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
103        window.setTitle("Recents");
104
105        setContentView(com.android.internal.R.layout.recent_apps_dialog);
106
107        final WindowManager.LayoutParams params = window.getAttributes();
108        params.width = WindowManager.LayoutParams.MATCH_PARENT;
109        params.height = WindowManager.LayoutParams.MATCH_PARENT;
110        window.setAttributes(params);
111        window.setFlags(0, WindowManager.LayoutParams.FLAG_DIM_BEHIND);
112
113        mIcons[0] = (TextView)findViewById(com.android.internal.R.id.button0);
114        mIcons[1] = (TextView)findViewById(com.android.internal.R.id.button1);
115        mIcons[2] = (TextView)findViewById(com.android.internal.R.id.button2);
116        mIcons[3] = (TextView)findViewById(com.android.internal.R.id.button3);
117        mIcons[4] = (TextView)findViewById(com.android.internal.R.id.button4);
118        mIcons[5] = (TextView)findViewById(com.android.internal.R.id.button5);
119        mIcons[6] = (TextView)findViewById(com.android.internal.R.id.button6);
120        mIcons[7] = (TextView)findViewById(com.android.internal.R.id.button7);
121        mNoAppsText = findViewById(com.android.internal.R.id.no_applications_message);
122
123        for (TextView b: mIcons) {
124            b.setOnClickListener(this);
125        }
126    }
127
128    @Override
129    public boolean onKeyDown(int keyCode, KeyEvent event) {
130        if (keyCode == KeyEvent.KEYCODE_APP_SWITCH || keyCode == KeyEvent.KEYCODE_TAB) {
131            // Ignore all meta keys other than SHIFT.  The app switch key could be a
132            // fallback action chorded with ALT, META or even CTRL depending on the key map.
133            // DPad navigation is handled by the ViewRoot elsewhere.
134            final boolean backward = event.isShiftPressed();
135            final int numIcons = mIcons.length;
136            int numButtons = 0;
137            while (numButtons < numIcons && mIcons[numButtons].getVisibility() == View.VISIBLE) {
138                numButtons += 1;
139            }
140            if (numButtons != 0) {
141                int nextFocus = backward ? numButtons - 1 : 0;
142                for (int i = 0; i < numButtons; i++) {
143                    if (mIcons[i].hasFocus()) {
144                        if (backward) {
145                            nextFocus = (i + numButtons - 1) % numButtons;
146                        } else {
147                            nextFocus = (i + 1) % numButtons;
148                        }
149                        break;
150                    }
151                }
152                final int direction = backward ? View.FOCUS_BACKWARD : View.FOCUS_FORWARD;
153                if (mIcons[nextFocus].requestFocus(direction)) {
154                    mIcons[nextFocus].playSoundEffect(
155                            SoundEffectConstants.getContantForFocusDirection(direction));
156                }
157            }
158
159            // The dialog always handles the key to prevent the ViewRoot from
160            // performing the default navigation itself.
161            return true;
162        }
163
164        return super.onKeyDown(keyCode, event);
165    }
166
167    @Override
168    public boolean onKeyUp(int keyCode, KeyEvent event) {
169        if (mInitialModifiers != 0 && event.hasNoModifiers()) {
170            final int numIcons = mIcons.length;
171            RecentTag tag = null;
172            for (int i = 0; i < numIcons; i++) {
173                if (mIcons[i].getVisibility() != View.VISIBLE) {
174                    break;
175                }
176                if (i == 0 || mIcons[i].hasFocus()) {
177                    tag = (RecentTag) mIcons[i].getTag();
178                    if (mIcons[i].hasFocus()) {
179                        break;
180                    }
181                }
182            }
183            if (tag != null) {
184                switchTo(tag);
185            }
186            dismiss();
187            return true;
188        }
189
190        return super.onKeyUp(keyCode, event);
191    }
192
193    /**
194     * Handler for user clicks.  If a button was clicked, launch the corresponding activity.
195     */
196    public void onClick(View v) {
197        for (TextView b: mIcons) {
198            if (b == v) {
199                RecentTag tag = (RecentTag)b.getTag();
200                switchTo(tag);
201                break;
202            }
203        }
204        dismiss();
205    }
206
207    private void switchTo(RecentTag tag) {
208        if (tag.info.id >= 0) {
209            // This is an active task; it should just go to the foreground.
210            final ActivityManager am = (ActivityManager)
211                    getContext().getSystemService(Context.ACTIVITY_SERVICE);
212            am.moveTaskToFront(tag.info.id, ActivityManager.MOVE_TASK_WITH_HOME);
213        } else if (tag.intent != null) {
214            tag.intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
215                    | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
216            try {
217                getContext().startActivity(tag.intent);
218            } catch (ActivityNotFoundException e) {
219                Log.w("Recent", "Unable to launch recent task", e);
220            }
221        }
222    }
223
224    /**
225     * Set up and show the recent activities dialog.
226     */
227    @Override
228    public void onStart() {
229        super.onStart();
230        reloadButtons();
231        if (sStatusBar != null) {
232            sStatusBar.disable(StatusBarManager.DISABLE_EXPAND);
233        }
234
235        // receive broadcasts
236        getContext().registerReceiver(mBroadcastReceiver, mBroadcastIntentFilter);
237
238        mHandler.removeCallbacks(mCleanup);
239    }
240
241    /**
242     * Dismiss the recent activities dialog.
243     */
244    @Override
245    public void onStop() {
246        super.onStop();
247
248        if (sStatusBar != null) {
249            sStatusBar.disable(StatusBarManager.DISABLE_NONE);
250        }
251
252        // stop receiving broadcasts
253        getContext().unregisterReceiver(mBroadcastReceiver);
254
255        mHandler.postDelayed(mCleanup, 100);
256     }
257
258    /**
259     * Reload the 6 buttons with recent activities
260     */
261    private void reloadButtons() {
262
263        final Context context = getContext();
264        final PackageManager pm = context.getPackageManager();
265        final ActivityManager am = (ActivityManager)
266                context.getSystemService(Context.ACTIVITY_SERVICE);
267        final List<ActivityManager.RecentTaskInfo> recentTasks =
268                am.getRecentTasks(MAX_RECENT_TASKS, ActivityManager.RECENT_IGNORE_UNAVAILABLE);
269
270        ActivityInfo homeInfo =
271            new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
272                    .resolveActivityInfo(pm, 0);
273
274        IconUtilities iconUtilities = new IconUtilities(getContext());
275
276        // Performance note:  Our android performance guide says to prefer Iterator when
277        // using a List class, but because we know that getRecentTasks() always returns
278        // an ArrayList<>, we'll use a simple index instead.
279        int index = 0;
280        int numTasks = recentTasks.size();
281        for (int i = 0; i < numTasks && (index < NUM_BUTTONS); ++i) {
282            final ActivityManager.RecentTaskInfo info = recentTasks.get(i);
283
284            // for debug purposes only, disallow first result to create empty lists
285            if (DBG_FORCE_EMPTY_LIST && (i == 0)) continue;
286
287            Intent intent = new Intent(info.baseIntent);
288            if (info.origActivity != null) {
289                intent.setComponent(info.origActivity);
290            }
291
292            // Skip the current home activity.
293            if (homeInfo != null) {
294                if (homeInfo.packageName.equals(
295                        intent.getComponent().getPackageName())
296                        && homeInfo.name.equals(
297                                intent.getComponent().getClassName())) {
298                    continue;
299                }
300            }
301
302            intent.setFlags((intent.getFlags()&~Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
303                    | Intent.FLAG_ACTIVITY_NEW_TASK);
304            final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
305            if (resolveInfo != null) {
306                final ActivityInfo activityInfo = resolveInfo.activityInfo;
307                final String title = activityInfo.loadLabel(pm).toString();
308                Drawable icon = activityInfo.loadIcon(pm);
309
310                if (title != null && title.length() > 0 && icon != null) {
311                    final TextView tv = mIcons[index];
312                    tv.setText(title);
313                    icon = iconUtilities.createIconDrawable(icon);
314                    tv.setCompoundDrawables(null, icon, null, null);
315                    RecentTag tag = new RecentTag();
316                    tag.info = info;
317                    tag.intent = intent;
318                    tv.setTag(tag);
319                    tv.setVisibility(View.VISIBLE);
320                    tv.setPressed(false);
321                    tv.clearFocus();
322                    ++index;
323                }
324            }
325        }
326
327        // handle the case of "no icons to show"
328        mNoAppsText.setVisibility((index == 0) ? View.VISIBLE : View.GONE);
329
330        // hide the rest
331        for (; index < NUM_BUTTONS; ++index) {
332            mIcons[index].setVisibility(View.GONE);
333        }
334    }
335
336    /**
337     * This is the listener for the ACTION_CLOSE_SYSTEM_DIALOGS intent.  It's an indication that
338     * we should close ourselves immediately, in order to allow a higher-priority UI to take over
339     * (e.g. phone call received).
340     */
341    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
342        @Override
343        public void onReceive(Context context, Intent intent) {
344            String action = intent.getAction();
345            if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
346                String reason = intent.getStringExtra(PhoneWindowManager.SYSTEM_DIALOG_REASON_KEY);
347                if (! PhoneWindowManager.SYSTEM_DIALOG_REASON_RECENT_APPS.equals(reason)) {
348                    dismiss();
349                }
350            }
351        }
352    };
353}
354