TabletStatusBar.java revision cf2b199e98de71dcac042484800c07366363a734
1/*
2 * Copyright (C) 2010 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.systemui.statusbar.tablet;
18
19import java.io.FileDescriptor;
20import java.io.PrintWriter;
21import java.util.ArrayList;
22
23import android.animation.LayoutTransition;
24import android.animation.ObjectAnimator;
25import android.app.ActivityManagerNative;
26import android.app.PendingIntent;
27import android.app.Notification;
28import android.app.StatusBarManager;
29import android.content.Context;
30import android.content.Intent;
31import android.content.res.Resources;
32import android.graphics.PixelFormat;
33import android.graphics.Rect;
34import android.os.Handler;
35import android.os.IBinder;
36import android.os.Message;
37import android.os.RemoteException;
38import android.text.TextUtils;
39import android.util.Slog;
40import android.view.animation.Animation;
41import android.view.animation.AnimationUtils;
42import android.view.Gravity;
43import android.view.LayoutInflater;
44import android.view.MotionEvent;
45import android.view.VelocityTracker;
46import android.view.View;
47import android.view.ViewConfiguration;
48import android.view.ViewGroup;
49import android.view.WindowManager;
50import android.view.WindowManagerImpl;
51import android.widget.FrameLayout;
52import android.widget.ImageView;
53import android.widget.LinearLayout;
54import android.widget.RemoteViews;
55import android.widget.ScrollView;
56import android.widget.TextSwitcher;
57import android.widget.TextView;
58
59import com.android.internal.statusbar.StatusBarIcon;
60import com.android.internal.statusbar.StatusBarNotification;
61
62import com.android.systemui.R;
63import com.android.systemui.statusbar.*;
64import com.android.systemui.statusbar.policy.BatteryController;
65import com.android.systemui.statusbar.policy.NetworkController;
66import com.android.systemui.recent.RecentApplicationsActivity;
67
68public class TabletStatusBar extends StatusBar {
69    public static final boolean DEBUG = false;
70    public static final String TAG = "TabletStatusBar";
71
72    public static final int MSG_OPEN_NOTIFICATION_PANEL = 1000;
73    public static final int MSG_CLOSE_NOTIFICATION_PANEL = 1001;
74    public static final int MSG_OPEN_NOTIFICATION_PEEK = 1002;
75    public static final int MSG_CLOSE_NOTIFICATION_PEEK = 1003;
76    public static final int MSG_OPEN_RECENTS_PANEL = 1020;
77    public static final int MSG_CLOSE_RECENTS_PANEL = 1021;
78    public static final int MSG_LIGHTS_ON = 1030;
79    public static final int MSG_LIGHTS_OUT = 1031;
80
81    private static final int MAX_IMAGE_LEVEL = 10000;
82    private static final boolean USE_2D_RECENTS = true;
83
84    int mIconSize;
85
86    H mHandler = new H();
87
88    // tracking all current notifications
89    private NotificationData mNotns = new NotificationData();
90
91    TabletStatusBarView mStatusBarView;
92    View mNotificationArea;
93    View mNotificationTrigger;
94    NotificationIconArea mNotificationIconArea;
95    View mNavigationArea;
96    View mMenuButton;
97    View mRecentButton;
98
99    InputMethodButton mInputMethodButton;
100
101    NotificationPanel mNotificationPanel;
102    NotificationPeekPanel mNotificationPeekWindow;
103    ViewGroup mNotificationPeekRow;
104    int mNotificationPeekIndex;
105    LayoutTransition mNotificationPeekScrubLeft, mNotificationPeekScrubRight;
106
107    int mNotificationPeekTapDuration;
108    int mNotificationFlingVelocity;
109
110    ViewGroup mPile;
111
112    BatteryController mBatteryController;
113    NetworkController mNetworkController;
114
115    View mBarContents;
116    View mCurtains;
117
118    NotificationIconArea.IconLayout mIconLayout;
119
120    TabletTicker mTicker;
121    View mTickerView;
122    boolean mTicking;
123
124    // for disabling the status bar
125    int mDisabled = 0;
126
127    boolean mNotificationsOn = true;
128    private RecentAppsPanel mRecentsPanel;
129
130    protected void addPanelWindows() {
131        final Context context = mContext;
132
133        final Resources res = context.getResources();
134        final int barHeight= res.getDimensionPixelSize(
135            com.android.internal.R.dimen.status_bar_height);
136
137        // Notification Panel
138        mNotificationPanel = (NotificationPanel)View.inflate(context,
139                R.layout.sysbar_panel_notifications, null);
140        mNotificationPanel.setVisibility(View.GONE);
141        mNotificationPanel.setOnTouchListener(
142                new TouchOutsideListener(MSG_CLOSE_NOTIFICATION_PANEL, mNotificationPanel));
143
144        // the battery and network icons
145        mBatteryController.addIconView((ImageView)mNotificationPanel.findViewById(R.id.battery));
146        mBatteryController.addLabelView(
147                (TextView)mNotificationPanel.findViewById(R.id.battery_text));
148        mNetworkController.addCombinedDataIconView(
149                (ImageView)mNotificationPanel.findViewById(R.id.network));
150        mNetworkController.addLabelView(
151                (TextView)mNotificationPanel.findViewById(R.id.network_text));
152
153        mStatusBarView.setIgnoreChildren(0, mNotificationTrigger, mNotificationPanel);
154
155        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
156                ViewGroup.LayoutParams.WRAP_CONTENT,
157                ViewGroup.LayoutParams.MATCH_PARENT,
158                WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL,
159                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
160                    | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
161                    | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
162                PixelFormat.TRANSLUCENT);
163        lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
164        lp.setTitle("NotificationPanel");
165
166        WindowManagerImpl.getDefault().addView(mNotificationPanel, lp);
167
168        // Notification preview window
169        mNotificationPeekWindow = (NotificationPeekPanel) View.inflate(context,
170                R.layout.sysbar_panel_notification_peek, null);
171        mNotificationPeekRow = (ViewGroup) mNotificationPeekWindow.findViewById(R.id.content);
172        mNotificationPeekWindow.setVisibility(View.GONE);
173        mNotificationPeekWindow.setOnTouchListener(
174                new TouchOutsideListener(MSG_CLOSE_NOTIFICATION_PANEL, mNotificationPeekWindow));
175        mNotificationPeekScrubRight = new LayoutTransition();
176        mNotificationPeekScrubRight.setAnimator(LayoutTransition.APPEARING,
177                ObjectAnimator.ofInt(null, "left", -512, 0));
178        mNotificationPeekScrubRight.setAnimator(LayoutTransition.DISAPPEARING,
179                ObjectAnimator.ofInt(null, "left", -512, 0));
180        mNotificationPeekScrubRight.setDuration(500);
181
182        mNotificationPeekScrubLeft = new LayoutTransition();
183        mNotificationPeekScrubLeft.setAnimator(LayoutTransition.APPEARING,
184                ObjectAnimator.ofInt(null, "left", 512, 0));
185        mNotificationPeekScrubLeft.setAnimator(LayoutTransition.DISAPPEARING,
186                ObjectAnimator.ofInt(null, "left", 512, 0));
187        mNotificationPeekScrubLeft.setDuration(500);
188
189        // XXX: setIgnoreChildren?
190        lp = new WindowManager.LayoutParams(
191                512, // ViewGroup.LayoutParams.WRAP_CONTENT,
192                ViewGroup.LayoutParams.WRAP_CONTENT,
193                WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL,
194                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
195                    | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
196                    | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
197                PixelFormat.TRANSLUCENT);
198        lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
199        lp.setTitle("NotificationPeekWindow");
200        lp.windowAnimations = com.android.internal.R.style.Animation_Toast;
201
202        WindowManagerImpl.getDefault().addView(mNotificationPeekWindow, lp);
203
204        // Recents Panel
205        if (USE_2D_RECENTS) {
206            mRecentsPanel = (RecentAppsPanel) View.inflate(context, R.layout.sysbar_panel_recent,
207                    null);
208            mRecentsPanel.setVisibility(View.GONE);
209            mRecentsPanel.setOnTouchListener(new TouchOutsideListener(MSG_CLOSE_RECENTS_PANEL,
210                    mRecentsPanel));
211            mStatusBarView.setIgnoreChildren(2, mRecentButton, mRecentsPanel);
212
213            lp = new WindowManager.LayoutParams(
214                    ViewGroup.LayoutParams.WRAP_CONTENT,
215                    ViewGroup.LayoutParams.WRAP_CONTENT,
216                    WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL,
217                    WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
218                        | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
219                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
220                    PixelFormat.TRANSLUCENT);
221            lp.gravity = Gravity.BOTTOM | Gravity.LEFT;
222            lp.setTitle("RecentsPanel");
223            lp.windowAnimations = R.style.Animation_RecentPanel;
224
225            WindowManagerImpl.getDefault().addView(mRecentsPanel, lp);
226            mRecentsPanel.setBar(this);
227        }
228    }
229
230    @Override
231    public void start() {
232        super.start(); // will add the main bar view
233    }
234
235    protected View makeStatusBarView() {
236        final Context context = mContext;
237        final Resources res = context.getResources();
238
239        mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size);
240
241        final TabletStatusBarView sb = (TabletStatusBarView)View.inflate(
242                context, R.layout.status_bar, null);
243        mStatusBarView = sb;
244
245        sb.setHandler(mHandler);
246
247        mBarContents = sb.findViewById(R.id.bar_contents);
248        mCurtains = sb.findViewById(R.id.lights_out);
249
250        mRecentButton = sb.findViewById(R.id.recent_apps);
251        mRecentButton.setOnClickListener(mOnClickListener);
252
253        SetLightsOnListener on = new SetLightsOnListener(true);
254        mCurtains.setOnClickListener(on);
255        mCurtains.setOnLongClickListener(on);
256
257        // the whole right-hand side of the bar
258        mNotificationArea = sb.findViewById(R.id.notificationArea);
259
260        // the button to open the notification area
261        mNotificationTrigger = sb.findViewById(R.id.notificationTrigger);
262        mNotificationTrigger.setOnClickListener(mOnClickListener);
263
264        // the more notifications icon
265        mNotificationIconArea = (NotificationIconArea)sb.findViewById(R.id.notificationIcons);
266
267        // where the icons go
268        mIconLayout = (NotificationIconArea.IconLayout) sb.findViewById(R.id.icons);
269        mIconLayout.setOnTouchListener(new NotificationIconTouchListener());
270
271        ViewConfiguration vc = ViewConfiguration.get(context);
272        mNotificationPeekTapDuration = vc.getTapTimeout();
273        mNotificationFlingVelocity = 300; // px/s
274
275        mTicker = new TabletTicker(context, (FrameLayout)sb.findViewById(R.id.ticker));
276
277        // The icons
278        mBatteryController = new BatteryController(mContext);
279        mBatteryController.addIconView((ImageView)sb.findViewById(R.id.battery));
280        mNetworkController = new NetworkController(mContext);
281        mNetworkController.addCombinedDataIconView((ImageView)sb.findViewById(R.id.network));
282
283        // The navigation buttons
284        mNavigationArea = sb.findViewById(R.id.navigationArea);
285        mMenuButton = mNavigationArea.findViewById(R.id.menu);
286
287        // The bar contents buttons
288        mInputMethodButton = (InputMethodButton) mBarContents.findViewById(R.id.imeButton);
289
290        // set the initial view visibility
291        setAreThereNotifications();
292        refreshNotificationTrigger();
293
294        // Add the windows
295        addPanelWindows();
296
297        mPile = (ViewGroup)mNotificationPanel.findViewById(R.id.content);
298        mPile.removeAllViews();
299
300        ScrollView scroller = (ScrollView)mPile.getParent();
301        scroller.setFillViewport(true);
302
303        return sb;
304    }
305
306    protected int getStatusBarGravity() {
307        return Gravity.BOTTOM | Gravity.FILL_HORIZONTAL;
308    }
309
310    private class H extends Handler {
311        public void handleMessage(Message m) {
312            switch (m.what) {
313                case MSG_OPEN_NOTIFICATION_PEEK:
314                    if (DEBUG) Slog.d(TAG, "opening notification peek window; arg=" + m.arg1);
315                    if (m.arg1 >= 0) {
316                        final int N = mNotns.size();
317                        if (mNotificationPeekIndex < N) {
318                            NotificationData.Entry entry = mNotns.get(N-1-mNotificationPeekIndex);
319                            entry.icon.setBackgroundColor(0);
320                        }
321
322                        final int peekIndex = m.arg1;
323                        if (peekIndex < N) {
324                            Slog.d(TAG, "loading peek: " + peekIndex);
325                            NotificationData.Entry entry = mNotns.get(N-1-peekIndex);
326                            NotificationData.Entry copy = new NotificationData.Entry(
327                                    entry.key,
328                                    entry.notification,
329                                    entry.icon);
330                            inflateViews(copy, mNotificationPeekRow);
331
332                            entry.icon.setBackgroundColor(0x20FFFFFF);
333
334//                          mNotificationPeekRow.setLayoutTransition(
335//                              peekIndex < mNotificationPeekIndex
336//                                  ? mNotificationPeekScrubLeft
337//                                  : mNotificationPeekScrubRight);
338
339                            mNotificationPeekRow.removeAllViews();
340                            mNotificationPeekRow.addView(copy.row);
341
342                            mNotificationPeekWindow.setVisibility(View.VISIBLE);
343                            mNotificationPanel.setVisibility(View.GONE);
344
345                            mNotificationPeekIndex = peekIndex;
346                        }
347                    }
348                    break;
349                case MSG_CLOSE_NOTIFICATION_PEEK:
350                    if (DEBUG) Slog.d(TAG, "closing notification peek window");
351                    mNotificationPeekWindow.setVisibility(View.GONE);
352                    mNotificationPeekRow.removeAllViews();
353                    final int N = mNotns.size();
354                    if (mNotificationPeekIndex < N) {
355                        NotificationData.Entry entry = mNotns.get(N-1-mNotificationPeekIndex);
356                        entry.icon.setBackgroundColor(0);
357                    }
358                    break;
359                case MSG_OPEN_NOTIFICATION_PANEL:
360                    if (DEBUG) Slog.d(TAG, "opening notifications panel");
361                    if (mNotificationPanel.getVisibility() == View.GONE) {
362                        mNotificationPeekWindow.setVisibility(View.GONE);
363
364                        mNotificationPanel.setVisibility(View.VISIBLE);
365                        mNotificationArea.setVisibility(View.GONE);
366                    }
367                    break;
368                case MSG_CLOSE_NOTIFICATION_PANEL:
369                    if (DEBUG) Slog.d(TAG, "closing notifications panel");
370                    if (mNotificationPanel.getVisibility() == View.VISIBLE) {
371                        mNotificationPanel.setVisibility(View.GONE);
372                        mNotificationArea.setVisibility(View.VISIBLE);
373                    }
374                    break;
375                case MSG_OPEN_RECENTS_PANEL:
376                    if (DEBUG) Slog.d(TAG, "opening recents panel");
377                    if (mRecentsPanel != null) mRecentsPanel.setVisibility(View.VISIBLE);
378                    break;
379                case MSG_CLOSE_RECENTS_PANEL:
380                    if (DEBUG) Slog.d(TAG, "closing recents panel");
381                    if (mRecentsPanel != null) mRecentsPanel.setVisibility(View.GONE);
382                    break;
383                case MSG_LIGHTS_ON:
384                    setViewVisibility(mCurtains, View.GONE, R.anim.lights_out_out);
385                    setViewVisibility(mBarContents, View.VISIBLE, R.anim.status_bar_in);
386                    break;
387                case MSG_LIGHTS_OUT:
388                    animateCollapse();
389                    setViewVisibility(mCurtains, View.VISIBLE, R.anim.lights_out_in);
390                    setViewVisibility(mBarContents, View.GONE, R.anim.status_bar_out);
391                    break;
392            }
393        }
394    }
395
396    public void refreshNotificationTrigger() {
397        /*
398        if (mNotificationTrigger == null) return;
399
400        int resId;
401        boolean panel = (mNotificationPanel != null
402                && mNotificationPanel.getVisibility() == View.VISIBLE);
403        if (!mNotificationsOn) {
404            resId = R.drawable.ic_sysbar_noti_dnd;
405        } else if (mNotns.size() > 0) {
406            resId = panel ? R.drawable.ic_sysbar_noti_avail_open : R.drawable.ic_sysbar_noti_avail;
407        } else {
408            resId = panel ? R.drawable.ic_sysbar_noti_none_open : R.drawable.ic_sysbar_noti_none;
409        }
410        //mNotificationTrigger.setImageResource(resId);
411        */
412    }
413
414    public void addIcon(String slot, int index, int viewIndex, StatusBarIcon icon) {
415        if (DEBUG) Slog.d(TAG, "addIcon(" + slot + ") -> " + icon);
416    }
417
418    public void updateIcon(String slot, int index, int viewIndex,
419            StatusBarIcon old, StatusBarIcon icon) {
420        if (DEBUG) Slog.d(TAG, "updateIcon(" + slot + ") -> " + icon);
421    }
422
423    public void removeIcon(String slot, int index, int viewIndex) {
424        if (DEBUG) Slog.d(TAG, "removeIcon(" + slot + ")");
425    }
426
427    public void addNotification(IBinder key, StatusBarNotification notification) {
428        if (DEBUG) Slog.d(TAG, "addNotification(" + key + " -> " + notification + ")");
429        addNotificationViews(key, notification);
430
431        boolean immersive = false;
432        try {
433            immersive = ActivityManagerNative.getDefault().isTopActivityImmersive();
434            Slog.d(TAG, "Top activity is " + (immersive?"immersive":"not immersive"));
435        } catch (RemoteException ex) {
436        }
437        if (false && immersive) {
438            // TODO: immersive mode popups for tablet
439        } else if (notification.notification.fullScreenIntent != null) {
440            // not immersive & a full-screen alert should be shown
441            Slog.d(TAG, "Notification has fullScreenIntent and activity is not immersive;"
442                    + " sending fullScreenIntent");
443            try {
444                notification.notification.fullScreenIntent.send();
445            } catch (PendingIntent.CanceledException e) {
446            }
447        } else {
448            tick(notification);
449        }
450
451        setAreThereNotifications();
452    }
453
454    public void updateNotification(IBinder key, StatusBarNotification notification) {
455        if (DEBUG) Slog.d(TAG, "updateNotification(" + key + " -> " + notification + ") // TODO");
456
457        final NotificationData.Entry oldEntry = mNotns.findByKey(key);
458        if (oldEntry == null) {
459            Slog.w(TAG, "updateNotification for unknown key: " + key);
460            return;
461        }
462
463        final StatusBarNotification oldNotification = oldEntry.notification;
464        final RemoteViews oldContentView = oldNotification.notification.contentView;
465
466        final RemoteViews contentView = notification.notification.contentView;
467
468        if (false) {
469            Slog.d(TAG, "old notification: when=" + oldNotification.notification.when
470                    + " ongoing=" + oldNotification.isOngoing()
471                    + " expanded=" + oldEntry.expanded
472                    + " contentView=" + oldContentView);
473            Slog.d(TAG, "new notification: when=" + notification.notification.when
474                    + " ongoing=" + oldNotification.isOngoing()
475                    + " contentView=" + contentView);
476        }
477
478        // Can we just reapply the RemoteViews in place?  If when didn't change, the order
479        // didn't change.
480        if (notification.notification.when == oldNotification.notification.when
481                && notification.isOngoing() == oldNotification.isOngoing()
482                && oldEntry.expanded != null
483                && contentView != null
484                && oldContentView != null
485                && contentView.getPackage() != null
486                && oldContentView.getPackage() != null
487                && oldContentView.getPackage().equals(contentView.getPackage())
488                && oldContentView.getLayoutId() == contentView.getLayoutId()) {
489            if (DEBUG) Slog.d(TAG, "reusing notification for key: " + key);
490            oldEntry.notification = notification;
491            try {
492                // Reapply the RemoteViews
493                contentView.reapply(mContext, oldEntry.content);
494                // update the contentIntent
495                final PendingIntent contentIntent = notification.notification.contentIntent;
496                if (contentIntent != null) {
497                    oldEntry.content.setOnClickListener(new NotificationClicker(contentIntent,
498                                notification.pkg, notification.tag, notification.id));
499                } else {
500                    oldEntry.content.setOnClickListener(null);
501                }
502                // Update the icon.
503                final StatusBarIcon ic = new StatusBarIcon(notification.pkg,
504                        notification.notification.icon, notification.notification.iconLevel,
505                        notification.notification.number);
506                if (!oldEntry.icon.set(ic)) {
507                    handleNotificationError(key, notification, "Couldn't update icon: " + ic);
508                    return;
509                }
510            }
511            catch (RuntimeException e) {
512                // It failed to add cleanly.  Log, and remove the view from the panel.
513                Slog.w(TAG, "Couldn't reapply views for package " + contentView.getPackage(), e);
514                removeNotificationViews(key);
515                addNotificationViews(key, notification);
516            }
517        } else {
518            if (DEBUG) Slog.d(TAG, "not reusing notification for key: " + key);
519            removeNotificationViews(key);
520            addNotificationViews(key, notification);
521        }
522        // TODO: ticker; immersive mode
523
524        setAreThereNotifications();
525    }
526
527    public void removeNotification(IBinder key) {
528        if (DEBUG) Slog.d(TAG, "removeNotification(" + key + ") // TODO");
529        removeNotificationViews(key);
530        setAreThereNotifications();
531    }
532
533    public void disable(int state) {
534        int old = mDisabled;
535        int diff = state ^ old;
536        mDisabled = state;
537
538        // act accordingly
539        if ((diff & StatusBarManager.DISABLE_EXPAND) != 0) {
540            if ((state & StatusBarManager.DISABLE_EXPAND) != 0) {
541                Slog.d(TAG, "DISABLE_EXPAND: yes");
542                animateCollapse();
543            }
544        }
545        if ((diff & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) {
546            if ((state & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) {
547                Slog.d(TAG, "DISABLE_NOTIFICATION_ICONS: yes");
548                mNotificationIconArea.setVisibility(View.GONE);
549                mTicker.halt();
550            } else {
551                Slog.d(TAG, "DISABLE_NOTIFICATION_ICONS: no");
552                mNotificationIconArea.setVisibility(View.VISIBLE);
553            }
554        } else if ((diff & StatusBarManager.DISABLE_NOTIFICATION_TICKER) != 0) {
555            if ((state & StatusBarManager.DISABLE_NOTIFICATION_TICKER) != 0) {
556                mTicker.halt();
557            }
558        }
559        if ((diff & StatusBarManager.DISABLE_NAVIGATION) != 0) {
560            if ((state & StatusBarManager.DISABLE_NAVIGATION) != 0) {
561                Slog.d(TAG, "DISABLE_NAVIGATION: yes");
562                mNavigationArea.setVisibility(View.GONE);
563            } else {
564                Slog.d(TAG, "DISABLE_NAVIGATION: no");
565                mNavigationArea.setVisibility(View.VISIBLE);
566            }
567        }
568    }
569
570    private boolean hasTicker(Notification n) {
571        return !TextUtils.isEmpty(n.tickerText)
572                || !TextUtils.isEmpty(n.tickerTitle)
573                || !TextUtils.isEmpty(n.tickerSubtitle);
574    }
575
576    private void tick(StatusBarNotification n) {
577        // Don't show the ticker when the windowshade is open.
578        if (mNotificationPanel.getVisibility() == View.VISIBLE) {
579            return;
580        }
581        // Show the ticker if one is requested. Also don't do this
582        // until status bar window is attached to the window manager,
583        // because...  well, what's the point otherwise?  And trying to
584        // run a ticker without being attached will crash!
585        if (hasTicker(n.notification) && mStatusBarView.getWindowToken() != null) {
586            if (0 == (mDisabled & (StatusBarManager.DISABLE_NOTIFICATION_ICONS
587                            | StatusBarManager.DISABLE_NOTIFICATION_TICKER))) {
588                mTicker.add(n);
589            }
590        }
591    }
592
593    public void animateExpand() {
594        mHandler.removeMessages(MSG_OPEN_NOTIFICATION_PANEL);
595        mHandler.sendEmptyMessage(MSG_OPEN_NOTIFICATION_PANEL);
596    }
597
598    public void animateCollapse() {
599        mHandler.removeMessages(MSG_CLOSE_NOTIFICATION_PANEL);
600        mHandler.sendEmptyMessage(MSG_CLOSE_NOTIFICATION_PANEL);
601        mHandler.removeMessages(MSG_CLOSE_RECENTS_PANEL);
602        mHandler.sendEmptyMessage(MSG_CLOSE_RECENTS_PANEL);
603    }
604
605    // called by StatusBar
606    @Override
607    public void setLightsOn(boolean on) {
608        mHandler.removeMessages(MSG_LIGHTS_OUT);
609        mHandler.removeMessages(MSG_LIGHTS_ON);
610        mHandler.sendEmptyMessage(on ? MSG_LIGHTS_ON : MSG_LIGHTS_OUT);
611    }
612
613    public void setMenuKeyVisible(boolean visible) {
614        if (DEBUG) {
615            Slog.d(TAG, (visible?"showing":"hiding") + " the MENU button");
616        }
617        mMenuButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
618    }
619
620    public void setIMEButtonVisible(boolean visible) {
621        if (DEBUG) {
622            Slog.d(TAG, (visible?"showing":"hiding") + " the IME button");
623        }
624        mInputMethodButton.setIMEButtonVisible(visible);
625    }
626
627    private void setAreThereNotifications() {
628        final boolean hasClearable = mNotns.hasClearableItems();
629
630        //Slog.d(TAG, "setAreThereNotifications hasClerable=" + hasClearable);
631
632        /*
633        mOngoingTitle.setVisibility(ongoing ? View.VISIBLE : View.GONE);
634        mLatestTitle.setVisibility(latest ? View.VISIBLE : View.GONE);
635
636        if (ongoing || latest) {
637            mNoNotificationsTitle.setVisibility(View.GONE);
638        } else {
639            mNoNotificationsTitle.setVisibility(View.VISIBLE);
640        }
641        */
642    }
643
644    /**
645     * Cancel this notification and tell the status bar service about the failure. Hold no locks.
646     */
647    void handleNotificationError(IBinder key, StatusBarNotification n, String message) {
648        removeNotification(key);
649        try {
650            mBarService.onNotificationError(n.pkg, n.tag, n.id, n.uid, n.initialPid, message);
651        } catch (RemoteException ex) {
652            // The end is nigh.
653        }
654    }
655
656    private View.OnClickListener mOnClickListener = new View.OnClickListener() {
657        public void onClick(View v) {
658            if (v == mNotificationTrigger) {
659                onClickNotificationTrigger();
660            } else if (v == mRecentButton) {
661                onClickRecentButton();
662            }
663        }
664    };
665
666    public void onClickNotificationTrigger() {
667        if (DEBUG) Slog.d(TAG, "clicked notification icons");
668        if ((mDisabled & StatusBarManager.DISABLE_EXPAND) == 0) {
669            if (!mNotificationsOn) {
670                mNotificationsOn = true;
671                mIconLayout.setVisibility(View.VISIBLE); // TODO: animation
672                refreshNotificationTrigger();
673            } else {
674                int msg = (mNotificationPanel.getVisibility() == View.GONE)
675                    ? MSG_OPEN_NOTIFICATION_PANEL
676                    : MSG_CLOSE_NOTIFICATION_PANEL;
677                mHandler.removeMessages(msg);
678                mHandler.sendEmptyMessage(msg);
679            }
680        }
681    }
682
683    public void onClickRecentButton() {
684        if (DEBUG) Slog.d(TAG, "clicked recent apps");
685        if (mRecentsPanel == null) {
686            Intent intent = new Intent();
687            intent.setClass(mContext, RecentApplicationsActivity.class);
688            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
689                    | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
690            mContext.startActivity(intent);
691        } else {
692            if ((mDisabled & StatusBarManager.DISABLE_EXPAND) == 0) {
693                int msg = (mRecentsPanel.getVisibility() == View.GONE)
694                    ? MSG_OPEN_RECENTS_PANEL
695                    : MSG_CLOSE_RECENTS_PANEL;
696                mHandler.removeMessages(msg);
697                mHandler.sendEmptyMessage(msg);
698            }
699        }
700    }
701
702    private class NotificationClicker implements View.OnClickListener {
703        private PendingIntent mIntent;
704        private String mPkg;
705        private String mTag;
706        private int mId;
707
708        NotificationClicker(PendingIntent intent, String pkg, String tag, int id) {
709            mIntent = intent;
710            mPkg = pkg;
711            mTag = tag;
712            mId = id;
713        }
714
715        public void onClick(View v) {
716            try {
717                // The intent we are sending is for the application, which
718                // won't have permission to immediately start an activity after
719                // the user switches to home.  We know it is safe to do at this
720                // point, so make sure new activity switches are now allowed.
721                ActivityManagerNative.getDefault().resumeAppSwitches();
722            } catch (RemoteException e) {
723            }
724
725            if (mIntent != null) {
726                int[] pos = new int[2];
727                v.getLocationOnScreen(pos);
728                Intent overlay = new Intent();
729                overlay.setSourceBounds(
730                        new Rect(pos[0], pos[1], pos[0]+v.getWidth(), pos[1]+v.getHeight()));
731                try {
732                    mIntent.send(mContext, 0, overlay);
733                } catch (PendingIntent.CanceledException e) {
734                    // the stack trace isn't very helpful here.  Just log the exception message.
735                    Slog.w(TAG, "Sending contentIntent failed: " + e);
736                }
737            }
738
739            try {
740                mBarService.onNotificationClick(mPkg, mTag, mId);
741            } catch (RemoteException ex) {
742                // system process is dead if we're here.
743            }
744
745            // close the shade if it was open
746            animateCollapse();
747
748            // If this click was on the intruder alert, hide that instead
749//            mHandler.sendEmptyMessage(MSG_HIDE_INTRUDER);
750        }
751    }
752
753    StatusBarNotification removeNotificationViews(IBinder key) {
754        NotificationData.Entry entry = mNotns.remove(key);
755        if (entry == null) {
756            Slog.w(TAG, "removeNotification for unknown key: " + key);
757            return null;
758        }
759        // Remove the expanded view.
760        ViewGroup rowParent = (ViewGroup)entry.row.getParent();
761        if (rowParent != null) rowParent.removeView(entry.row);
762        // Remove the icon.
763//        ViewGroup iconParent = (ViewGroup)entry.icon.getParent();
764//        if (iconParent != null) iconParent.removeView(entry.icon);
765        refreshIcons();
766
767        return entry.notification;
768    }
769
770    private class NotificationIconTouchListener implements View.OnTouchListener {
771        VelocityTracker mVT;
772
773        public NotificationIconTouchListener() {
774        }
775
776        public boolean onTouch(View v, MotionEvent event) {
777            boolean peeking = mNotificationPeekWindow.getVisibility() != View.GONE;
778            boolean panelShowing = mNotificationPanel.getVisibility() != View.GONE;
779            if (panelShowing) return false;
780
781            switch (event.getAction()) {
782                case MotionEvent.ACTION_DOWN:
783                    mVT = VelocityTracker.obtain();
784
785                    // fall through
786                case MotionEvent.ACTION_OUTSIDE:
787                case MotionEvent.ACTION_MOVE:
788                    // peek and switch icons if necessary
789                    int numIcons = mIconLayout.getChildCount();
790                    int peekIndex =
791                            (int)((float)event.getX() * numIcons / mIconLayout.getWidth());
792                    if (peekIndex > numIcons - 1) peekIndex = numIcons - 1;
793                    else if (peekIndex < 0) peekIndex = 0;
794
795                    if (!peeking || mNotificationPeekIndex != peekIndex) {
796                        if (DEBUG) Slog.d(TAG, "will peek at notification #" + peekIndex);
797                        Message peekMsg = mHandler.obtainMessage(MSG_OPEN_NOTIFICATION_PEEK);
798                        peekMsg.arg1 = peekIndex;
799
800                        mHandler.removeMessages(MSG_OPEN_NOTIFICATION_PEEK);
801
802                        // no delay if we're scrubbing left-right
803                        mHandler.sendMessageDelayed(peekMsg,
804                                peeking ? 0 : mNotificationPeekTapDuration);
805                    }
806
807                    // check for fling
808                    if (mVT != null) {
809                        mVT.addMovement(event);
810                        mVT.computeCurrentVelocity(1000);
811                        // require a little more oomph once we're already in peekaboo mode
812                        if (!panelShowing && (
813                               (peeking && mVT.getYVelocity() < -mNotificationFlingVelocity*3)
814                            || (mVT.getYVelocity() < -mNotificationFlingVelocity))) {
815                            mHandler.removeMessages(MSG_OPEN_NOTIFICATION_PEEK);
816                            mHandler.removeMessages(MSG_OPEN_NOTIFICATION_PANEL);
817                            mHandler.sendEmptyMessage(MSG_CLOSE_NOTIFICATION_PEEK);
818                            mHandler.sendEmptyMessage(MSG_OPEN_NOTIFICATION_PANEL);
819                        }
820                    }
821                    return true;
822                case MotionEvent.ACTION_UP:
823                case MotionEvent.ACTION_CANCEL:
824                    mHandler.removeMessages(MSG_OPEN_NOTIFICATION_PEEK);
825                    if (peeking) {
826                        mHandler.sendEmptyMessageDelayed(MSG_CLOSE_NOTIFICATION_PEEK, 250);
827                    }
828                    mVT.recycle();
829                    mVT = null;
830                    return true;
831            }
832            return false;
833        }
834    }
835
836    StatusBarIconView addNotificationViews(IBinder key, StatusBarNotification notification) {
837        if (DEBUG) {
838            Slog.d(TAG, "addNotificationViews(key=" + key + ", notification=" + notification);
839        }
840        // Construct the icon.
841        final StatusBarIconView iconView = new StatusBarIconView(mContext,
842                notification.pkg + "/0x" + Integer.toHexString(notification.id));
843        iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
844
845        final StatusBarIcon ic = new StatusBarIcon(notification.pkg,
846                    notification.notification.icon,
847                    notification.notification.iconLevel,
848                    notification.notification.number);
849        if (!iconView.set(ic)) {
850            handleNotificationError(key, notification, "Couldn't attach StatusBarIcon: " + ic);
851            return null;
852        }
853        // Construct the expanded view.
854        NotificationData.Entry entry = new NotificationData.Entry(key, notification, iconView);
855        if (!inflateViews(entry, mPile)) {
856            handleNotificationError(key, notification, "Couldn't expand RemoteViews for: "
857                    + notification);
858            return null;
859        }
860
861        // Add the icon.
862        mNotns.add(entry);
863        refreshIcons();
864
865        return iconView;
866    }
867
868    private void refreshIcons() {
869        // XXX: need to implement a new limited linear layout class
870        // to avoid removing & readding everything
871
872        final int ICON_LIMIT = 4;
873        final LinearLayout.LayoutParams params
874            = new LinearLayout.LayoutParams(mIconSize, mIconSize);
875
876        int N = mNotns.size();
877
878        if (DEBUG) {
879            Slog.d(TAG, "refreshing icons: " + N + " notifications, mIconLayout=" + mIconLayout);
880        }
881
882        ArrayList<View> toShow = new ArrayList<View>();
883
884        for (int i=0; i<ICON_LIMIT; i++) {
885            if (i>=N) break;
886            toShow.add(mNotns.get(N-i-1).icon);
887        }
888
889        ArrayList<View> toRemove = new ArrayList<View>();
890        for (int i=0; i<mIconLayout.getChildCount(); i++) {
891            View child = mIconLayout.getChildAt(i);
892            if (!toShow.contains(child)) {
893                toRemove.add(child);
894            }
895        }
896
897        for (View remove : toRemove) {
898            mIconLayout.removeView(remove);
899        }
900
901        for (int i=0; i<toShow.size(); i++) {
902            View v = toShow.get(i);
903            if (v.getParent() == null) {
904                mIconLayout.addView(toShow.get(i), i, params);
905            }
906        }
907
908        loadNotificationPanel();
909        refreshNotificationTrigger();
910    }
911
912    private void loadNotificationPanel() {
913        int N = mNotns.size();
914
915        ArrayList<View> toShow = new ArrayList<View>();
916
917        for (int i=0; i<N; i++) {
918            View row = mNotns.get(N-i-1).row;
919            toShow.add(row);
920        }
921
922        ArrayList<View> toRemove = new ArrayList<View>();
923        for (int i=0; i<mPile.getChildCount(); i++) {
924            View child = mPile.getChildAt(i);
925            if (!toShow.contains(child)) {
926                toRemove.add(child);
927            }
928        }
929
930        for (View remove : toRemove) {
931            mPile.removeView(remove);
932        }
933
934        for (int i=0; i<toShow.size(); i++) {
935            View v = toShow.get(i);
936            if (v.getParent() == null) {
937                mPile.addView(toShow.get(i));
938            }
939        }
940    }
941
942    private boolean inflateViews(NotificationData.Entry entry, ViewGroup parent) {
943        StatusBarNotification sbn = entry.notification;
944        RemoteViews remoteViews = sbn.notification.contentView;
945        if (remoteViews == null) {
946            return false;
947        }
948
949        // create the row view
950        LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(
951                Context.LAYOUT_INFLATER_SERVICE);
952        View row = inflater.inflate(R.layout.status_bar_latest_event, parent, false);
953        View vetoButton = row.findViewById(R.id.veto);
954        if (entry.notification.isClearable()) {
955            final String _pkg = sbn.pkg;
956            final String _tag = sbn.tag;
957            final int _id = sbn.id;
958            vetoButton.setOnClickListener(new View.OnClickListener() {
959                    public void onClick(View v) {
960                        try {
961                            mBarService.onNotificationClear(_pkg, _tag, _id);
962                        } catch (RemoteException ex) {
963                            // system process is dead if we're here.
964                        }
965    //                    animateCollapse();
966                    }
967                });
968        } else {
969            vetoButton.setVisibility(View.INVISIBLE);
970        }
971
972        // bind the click event to the content area
973        ViewGroup content = (ViewGroup)row.findViewById(R.id.content);
974        // XXX: update to allow controls within notification views
975        content.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
976//        content.setOnFocusChangeListener(mFocusChangeListener);
977        PendingIntent contentIntent = sbn.notification.contentIntent;
978        if (contentIntent != null) {
979            content.setOnClickListener(new NotificationClicker(contentIntent,
980                        sbn.pkg, sbn.tag, sbn.id));
981        } else {
982            content.setOnClickListener(null);
983        }
984
985        View expanded = null;
986        Exception exception = null;
987        try {
988            expanded = remoteViews.apply(mContext, content);
989        }
990        catch (RuntimeException e) {
991            exception = e;
992        }
993        if (expanded == null) {
994            String ident = sbn.pkg + "/0x" + Integer.toHexString(sbn.id);
995            Slog.e(TAG, "couldn't inflate view for notification " + ident, exception);
996            return false;
997        } else {
998            content.addView(expanded);
999            row.setDrawingCacheEnabled(true);
1000        }
1001
1002        entry.row = row;
1003        entry.content = content;
1004        entry.expanded = expanded;
1005
1006        return true;
1007    }
1008
1009    public class SetLightsOnListener implements View.OnLongClickListener,
1010           View.OnClickListener {
1011        private boolean mOn;
1012
1013        SetLightsOnListener(boolean on) {
1014            mOn = on;
1015        }
1016
1017        public void onClick(View v) {
1018            setLightsOn(mOn);
1019        }
1020
1021        public boolean onLongClick(View v) {
1022            setLightsOn(mOn);
1023            return true;
1024        }
1025
1026    }
1027
1028    public class TouchOutsideListener implements View.OnTouchListener {
1029        private int mMsg;
1030        private StatusBarPanel mPanel;
1031
1032        public TouchOutsideListener(int msg, StatusBarPanel panel) {
1033            mMsg = msg;
1034            mPanel = panel;
1035        }
1036
1037        public boolean onTouch(View v, MotionEvent ev) {
1038            final int action = ev.getAction();
1039            if (action == MotionEvent.ACTION_OUTSIDE
1040                    || (action == MotionEvent.ACTION_DOWN
1041                        && !mPanel.isInContentArea((int)ev.getX(), (int)ev.getY()))) {
1042                mHandler.removeMessages(mMsg);
1043                mHandler.sendEmptyMessage(mMsg);
1044                return true;
1045            }
1046            return false;
1047        }
1048    }
1049
1050    private void setViewVisibility(View v, int vis, int anim) {
1051        if (v.getVisibility() != vis) {
1052            //Slog.d(TAG, "setViewVisibility vis=" + (vis == View.VISIBLE) + " v=" + v);
1053            v.setAnimation(AnimationUtils.loadAnimation(mContext, anim));
1054            v.setVisibility(vis);
1055        }
1056    }
1057
1058
1059    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1060        pw.print("mDisabled=0x");
1061        pw.println(Integer.toHexString(mDisabled));
1062    }
1063}
1064
1065
1066