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.util.Arrays;
20
21import android.animation.LayoutTransition;
22import android.app.Notification;
23import android.app.PendingIntent;
24import android.content.Context;
25import android.content.res.Resources;
26import android.graphics.Bitmap;
27import android.graphics.PixelFormat;
28import android.graphics.drawable.Drawable;
29import android.os.Handler;
30import android.os.IBinder;
31import android.os.Message;
32import android.util.Slog;
33import android.view.Gravity;
34import android.view.LayoutInflater;
35import android.view.View;
36import android.view.ViewGroup;
37import android.view.WindowManager;
38import android.widget.FrameLayout;
39import android.widget.ImageView;
40import android.widget.FrameLayout;
41import android.widget.TextView;
42
43import com.android.internal.statusbar.StatusBarIcon;
44import com.android.internal.statusbar.StatusBarNotification;
45
46import com.android.systemui.R;
47import com.android.systemui.statusbar.StatusBarIconView;
48
49public class TabletTicker
50        extends Handler
51        implements LayoutTransition.TransitionListener {
52
53    private static final String TAG = "StatusBar.TabletTicker";
54
55    private static final boolean CLICKABLE_TICKER = true;
56
57    // 3 is enough to let us see most cases, but not get so far behind that it's too annoying.
58    private static final int QUEUE_LENGTH = 3;
59
60    private static final int MSG_ADVANCE = 1;
61
62    private static final int ADVANCE_DELAY = 5000; // 5 seconds
63
64    private final Context mContext;
65    private final WindowManager mWindowManager;
66
67    private ViewGroup mWindow;
68    private IBinder mCurrentKey;
69    private StatusBarNotification mCurrentNotification;
70    private View mCurrentView;
71
72    private IBinder[] mKeys = new IBinder[QUEUE_LENGTH];
73    private StatusBarNotification[] mQueue = new StatusBarNotification[QUEUE_LENGTH];
74    private int mQueuePos;
75
76    private final int mLargeIconHeight;
77
78    private TabletStatusBar mBar;
79
80    private LayoutTransition mLayoutTransition;
81    private boolean mWindowShouldClose;
82
83    public TabletTicker(TabletStatusBar bar) {
84        mBar = bar;
85        mContext = bar.getContext();
86        mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
87        final Resources res = mContext.getResources();
88        mLargeIconHeight = res.getDimensionPixelSize(
89                android.R.dimen.notification_large_icon_height);
90    }
91
92    public void add(IBinder key, StatusBarNotification notification) {
93        if (false) {
94            Slog.d(TAG, "add 1 mCurrentNotification=" + mCurrentNotification
95                    + " mQueuePos=" + mQueuePos + " mQueue=" + Arrays.toString(mQueue));
96        }
97
98        // If it's already in here, remove whatever's in there and put the new one at the end.
99        remove(key, false);
100
101        mKeys[mQueuePos] = key;
102        mQueue[mQueuePos] = notification;
103
104        // If nothing is running now, start the next one.
105        if (mQueuePos == 0 && mCurrentNotification == null) {
106            sendEmptyMessage(MSG_ADVANCE);
107        }
108
109        if (mQueuePos < QUEUE_LENGTH - 1) {
110            mQueuePos++;
111        }
112    }
113
114    public void remove(IBinder key) {
115        remove(key, true);
116    }
117
118    public void remove(IBinder key, boolean advance) {
119        if (mCurrentKey == key) {
120            // Showing now
121            if (advance) {
122                removeMessages(MSG_ADVANCE);
123                sendEmptyMessage(MSG_ADVANCE);
124            }
125        } else {
126            // In the queue
127            for (int i=0; i<QUEUE_LENGTH; i++) {
128                if (mKeys[i] == key) {
129                    for (; i<QUEUE_LENGTH-1; i++) {
130                        mKeys[i] = mKeys[i+1];
131                        mQueue[i] = mQueue[i+1];
132                    }
133                    mKeys[QUEUE_LENGTH-1] = null;
134                    mQueue[QUEUE_LENGTH-1] = null;
135                    if (mQueuePos > 0) {
136                        mQueuePos--;
137                    }
138                    break;
139                }
140            }
141        }
142    }
143
144    public void halt() {
145        removeMessages(MSG_ADVANCE);
146        if (mCurrentView != null || mQueuePos != 0) {
147            for (int i=0; i<QUEUE_LENGTH; i++) {
148                mKeys[i] = null;
149                mQueue[i] = null;
150            }
151            mQueuePos = 0;
152            sendEmptyMessage(MSG_ADVANCE);
153        }
154    }
155
156    public void handleMessage(Message msg) {
157        switch (msg.what) {
158            case MSG_ADVANCE:
159                advance();
160                break;
161        }
162    }
163
164    private void advance() {
165        // Out with the old...
166        if (mCurrentView != null) {
167            if (mWindow != null) {
168                mWindow.removeView(mCurrentView);
169            }
170            mCurrentView = null;
171            mCurrentKey = null;
172            mCurrentNotification = null;
173        }
174
175        // In with the new...
176        dequeue();
177        while (mCurrentNotification != null) {
178            mCurrentView = makeTickerView(mCurrentNotification);
179            if (mCurrentView != null) {
180                if (mWindow == null) {
181                    mWindow = makeWindow();
182                    mWindowManager.addView(mWindow, mWindow.getLayoutParams());
183                }
184
185                mWindow.addView(mCurrentView);
186                sendEmptyMessageDelayed(MSG_ADVANCE, ADVANCE_DELAY);
187                break;
188            }
189            dequeue();
190        }
191
192        // if there's nothing left, close the window
193        mWindowShouldClose = (mCurrentView == null && mWindow != null);
194    }
195
196    private void dequeue() {
197        mCurrentKey = mKeys[0];
198        mCurrentNotification = mQueue[0];
199        if (false) {
200            Slog.d(TAG, "dequeue mQueuePos=" + mQueuePos + " mQueue=" + Arrays.toString(mQueue));
201        }
202        final int N = mQueuePos;
203        for (int i=0; i<N; i++) {
204            mKeys[i] = mKeys[i+1];
205            mQueue[i] = mQueue[i+1];
206        }
207        mKeys[N] = null;
208        mQueue[N] = null;
209        if (mQueuePos > 0) {
210            mQueuePos--;
211        }
212    }
213
214    private ViewGroup makeWindow() {
215        final Resources res = mContext.getResources();
216        final FrameLayout view = new FrameLayout(mContext);
217        final int width = res.getDimensionPixelSize(R.dimen.notification_ticker_width);
218        int windowFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
219                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
220                    | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
221        if (CLICKABLE_TICKER) {
222            windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
223        } else {
224            windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
225        }
226        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(width, mLargeIconHeight,
227                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, windowFlags,
228                PixelFormat.TRANSLUCENT);
229        lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
230//        lp.windowAnimations = com.android.internal.R.style.Animation_Toast;
231
232        mLayoutTransition = new LayoutTransition();
233        mLayoutTransition.addTransitionListener(this);
234        view.setLayoutTransition(mLayoutTransition);
235        lp.setTitle("NotificationTicker");
236        view.setLayoutParams(lp);
237        return view;
238    }
239
240    public void startTransition(LayoutTransition transition, ViewGroup container,
241            View view, int transitionType) {}
242
243    public void endTransition(LayoutTransition transition, ViewGroup container,
244            View view, int transitionType) {
245        if (mWindowShouldClose) {
246            mWindowManager.removeView(mWindow);
247            mWindow = null;
248            mWindowShouldClose = false;
249            mBar.doneTicking();
250        }
251    }
252
253    private View makeTickerView(StatusBarNotification notification) {
254        final Notification n = notification.notification;
255
256        LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(
257                Context.LAYOUT_INFLATER_SERVICE);
258
259        ViewGroup group;
260        int layoutId;
261        int iconId;
262        if (n.largeIcon != null) {
263            iconId = R.id.right_icon;
264        } else {
265            iconId = R.id.left_icon;
266        }
267        if (n.tickerView != null) {
268            group = (ViewGroup)inflater.inflate(R.layout.system_bar_ticker_panel, null, false);
269            ViewGroup content = (FrameLayout) group.findViewById(R.id.ticker_expanded);
270            View expanded = null;
271            Exception exception = null;
272            try {
273                expanded = n.tickerView.apply(mContext, content);
274            }
275            catch (RuntimeException e) {
276                exception = e;
277            }
278            if (expanded == null) {
279                final String ident = notification.pkg
280                        + "/0x" + Integer.toHexString(notification.id);
281                Slog.e(TAG, "couldn't inflate view for notification " + ident, exception);
282                return null;
283            }
284            FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
285                    ViewGroup.LayoutParams.MATCH_PARENT,
286                    ViewGroup.LayoutParams.MATCH_PARENT);
287            content.addView(expanded, lp);
288        } else if (n.tickerText != null) {
289            group = (ViewGroup)inflater.inflate(R.layout.system_bar_ticker_compat, mWindow, false);
290            final Drawable icon = StatusBarIconView.getIcon(mContext,
291                    new StatusBarIcon(notification.pkg, notification.user, n.icon, n.iconLevel, 0,
292                            n.tickerText));
293            ImageView iv = (ImageView)group.findViewById(iconId);
294            iv.setImageDrawable(icon);
295            iv.setVisibility(View.VISIBLE);
296            TextView tv = (TextView)group.findViewById(R.id.text);
297            tv.setText(n.tickerText);
298        } else {
299            throw new RuntimeException("tickerView==null && tickerText==null");
300        }
301        ImageView largeIcon = (ImageView)group.findViewById(R.id.large_icon);
302        if (n.largeIcon != null) {
303            largeIcon.setImageBitmap(n.largeIcon);
304            largeIcon.setVisibility(View.VISIBLE);
305            final ViewGroup.LayoutParams lp = largeIcon.getLayoutParams();
306            final int statusBarHeight = mBar.getStatusBarHeight();
307            if (n.largeIcon.getHeight() <= statusBarHeight) {
308                // for smallish largeIcons, it looks a little odd to have them floating halfway up
309                // the ticker, so we vertically center them in the status bar area instead
310                lp.height = statusBarHeight;
311            } else {
312                lp.height = mLargeIconHeight;
313            }
314            largeIcon.setLayoutParams(lp);
315        }
316
317        if (CLICKABLE_TICKER) {
318            PendingIntent contentIntent = notification.notification.contentIntent;
319            if (contentIntent != null) {
320                // create the usual notification clicker, but chain it together with a halt() call
321                // to abort the ticker too
322                final View.OnClickListener clicker = mBar.makeClicker(contentIntent,
323                                            notification.pkg, notification.tag, notification.id);
324                group.setOnClickListener(new View.OnClickListener() {
325                    public void onClick(View v) {
326                        halt();
327                        clicker.onClick(v);
328                    }
329                });
330            } else {
331                group.setOnClickListener(null);
332            }
333        }
334
335        return group;
336    }
337}
338
339