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