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