Ticker.java revision e6f7f2e3a01b8deb00e03ccfa93751c315f14ef0
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.systemui.statusbar.phone;
18
19import android.service.notification.StatusBarNotification;
20import android.content.Context;
21import android.content.res.Resources;
22import android.graphics.drawable.Drawable;
23import android.os.Handler;
24import android.text.StaticLayout;
25import android.text.Layout.Alignment;
26import android.text.TextPaint;
27import android.view.View;
28import android.view.animation.AnimationUtils;
29import android.widget.TextSwitcher;
30import android.widget.TextView;
31import android.widget.ImageSwitcher;
32
33import java.util.ArrayList;
34
35import com.android.internal.statusbar.StatusBarIcon;
36import com.android.internal.util.CharSequences;
37
38import com.android.systemui.R;
39import com.android.systemui.statusbar.StatusBarIconView;
40
41public abstract class Ticker {
42    private static final int TICKER_SEGMENT_DELAY = 3000;
43
44    private Context mContext;
45    private Handler mHandler = new Handler();
46    private ArrayList<Segment> mSegments = new ArrayList();
47    private TextPaint mPaint;
48    private View mTickerView;
49    private ImageSwitcher mIconSwitcher;
50    private TextSwitcher mTextSwitcher;
51    private float mIconScale;
52
53    public static boolean isGraphicOrEmoji(char c) {
54        int gc = Character.getType(c);
55        return     gc != Character.CONTROL
56                && gc != Character.FORMAT
57                && gc != Character.UNASSIGNED
58                && gc != Character.LINE_SEPARATOR
59                && gc != Character.PARAGRAPH_SEPARATOR
60                && gc != Character.SPACE_SEPARATOR;
61    }
62
63    private final class Segment {
64        StatusBarNotification notification;
65        Drawable icon;
66        CharSequence text;
67        int current;
68        int next;
69        boolean first;
70
71        StaticLayout getLayout(CharSequence substr) {
72            int w = mTextSwitcher.getWidth() - mTextSwitcher.getPaddingLeft()
73                    - mTextSwitcher.getPaddingRight();
74            return new StaticLayout(substr, mPaint, w, Alignment.ALIGN_NORMAL, 1, 0, true);
75        }
76
77        CharSequence rtrim(CharSequence substr, int start, int end) {
78            while (end > start && !isGraphicOrEmoji(substr.charAt(end-1))) {
79                end--;
80            }
81            if (end > start) {
82                return substr.subSequence(start, end);
83            }
84            return null;
85        }
86
87        /** returns null if there is no more text */
88        CharSequence getText() {
89            if (this.current > this.text.length()) {
90                return null;
91            }
92            CharSequence substr = this.text.subSequence(this.current, this.text.length());
93            StaticLayout l = getLayout(substr);
94            int lineCount = l.getLineCount();
95            if (lineCount > 0) {
96                int start = l.getLineStart(0);
97                int end = l.getLineEnd(0);
98                this.next = this.current + end;
99                return rtrim(substr, start, end);
100            } else {
101                throw new RuntimeException("lineCount=" + lineCount + " current=" + current +
102                        " text=" + text);
103            }
104        }
105
106        /** returns null if there is no more text */
107        CharSequence advance() {
108            this.first = false;
109            int index = this.next;
110            final int len = this.text.length();
111            while (index < len && !isGraphicOrEmoji(this.text.charAt(index))) {
112                index++;
113            }
114            if (index >= len) {
115                return null;
116            }
117
118            CharSequence substr = this.text.subSequence(index, this.text.length());
119            StaticLayout l = getLayout(substr);
120            final int lineCount = l.getLineCount();
121            int i;
122            for (i=0; i<lineCount; i++) {
123                int start = l.getLineStart(i);
124                int end = l.getLineEnd(i);
125                if (i == lineCount-1) {
126                    this.next = len;
127                } else {
128                    this.next = index + l.getLineStart(i+1);
129                }
130                CharSequence result = rtrim(substr, start, end);
131                if (result != null) {
132                    this.current = index + start;
133                    return result;
134                }
135            }
136            this.current = len;
137            return null;
138        }
139
140        Segment(StatusBarNotification n, Drawable icon, CharSequence text) {
141            this.notification = n;
142            this.icon = icon;
143            this.text = text;
144            int index = 0;
145            final int len = text.length();
146            while (index < len && !isGraphicOrEmoji(text.charAt(index))) {
147                index++;
148            }
149            this.current = index;
150            this.next = index;
151            this.first = true;
152        }
153    };
154
155    public Ticker(Context context, View sb) {
156        mContext = context;
157        final Resources res = context.getResources();
158        final int outerBounds = res.getDimensionPixelSize(R.dimen.status_bar_icon_size);
159        final int imageBounds = res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size);
160        mIconScale = (float)imageBounds / (float)outerBounds;
161
162        mTickerView = sb.findViewById(R.id.ticker);
163
164        mIconSwitcher = (ImageSwitcher)sb.findViewById(R.id.tickerIcon);
165        mIconSwitcher.setInAnimation(
166                    AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_in));
167        mIconSwitcher.setOutAnimation(
168                    AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_out));
169        mIconSwitcher.setScaleX(mIconScale);
170        mIconSwitcher.setScaleY(mIconScale);
171
172        mTextSwitcher = (TextSwitcher)sb.findViewById(R.id.tickerText);
173        mTextSwitcher.setInAnimation(
174                    AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_in));
175        mTextSwitcher.setOutAnimation(
176                    AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_out));
177
178        // Copy the paint style of one of the TextSwitchers children to use later for measuring
179        TextView text = (TextView)mTextSwitcher.getChildAt(0);
180        mPaint = text.getPaint();
181    }
182
183
184    public void addEntry(StatusBarNotification n) {
185        int initialCount = mSegments.size();
186
187        // If what's being displayed has the same text and icon, just drop it
188        // (which will let the current one finish, this happens when apps do
189        // a notification storm).
190        if (initialCount > 0) {
191            final Segment seg = mSegments.get(0);
192            if (n.getPkg().equals(seg.notification.getPkg())
193                    && n.getNotification().icon == seg.notification.getNotification().icon
194                    && n.getNotification().iconLevel == seg.notification.getNotification().iconLevel
195                    && CharSequences.equals(seg.notification.getNotification().tickerText,
196                        n.getNotification().tickerText)) {
197                return;
198            }
199        }
200
201        final Drawable icon = StatusBarIconView.getIcon(mContext,
202                new StatusBarIcon(n.getPkg(), n.getUser(), n.getNotification().icon, n.getNotification().iconLevel, 0,
203                        n.getNotification().tickerText));
204        final CharSequence text = n.getNotification().tickerText;
205        final Segment newSegment = new Segment(n, icon, text);
206
207        // If there's already a notification schedule for this package and id, remove it.
208        for (int i=0; i<mSegments.size(); i++) {
209            Segment seg = mSegments.get(i);
210            if (n.getId() == seg.notification.getId() && n.getPkg().equals(seg.notification.getPkg())) {
211                // just update that one to use this new data instead
212                mSegments.remove(i--); // restart iteration here
213            }
214        }
215
216        mSegments.add(newSegment);
217
218        if (initialCount == 0 && mSegments.size() > 0) {
219            Segment seg = mSegments.get(0);
220            seg.first = false;
221
222            mIconSwitcher.setAnimateFirstView(false);
223            mIconSwitcher.reset();
224            mIconSwitcher.setImageDrawable(seg.icon);
225
226            mTextSwitcher.setAnimateFirstView(false);
227            mTextSwitcher.reset();
228            mTextSwitcher.setText(seg.getText());
229
230            tickerStarting();
231            scheduleAdvance();
232        }
233    }
234
235    public void removeEntry(StatusBarNotification n) {
236        for (int i=mSegments.size()-1; i>=0; i--) {
237            Segment seg = mSegments.get(i);
238            if (n.getId() == seg.notification.getId() && n.getPkg().equals(seg.notification.getPkg())) {
239                mSegments.remove(i);
240            }
241        }
242    }
243
244    public void halt() {
245        mHandler.removeCallbacks(mAdvanceTicker);
246        mSegments.clear();
247        tickerHalting();
248    }
249
250    public void reflowText() {
251        if (mSegments.size() > 0) {
252            Segment seg = mSegments.get(0);
253            CharSequence text = seg.getText();
254            mTextSwitcher.setCurrentText(text);
255        }
256    }
257
258    private Runnable mAdvanceTicker = new Runnable() {
259        public void run() {
260            while (mSegments.size() > 0) {
261                Segment seg = mSegments.get(0);
262
263                if (seg.first) {
264                    // this makes the icon slide in for the first one for a given
265                    // notification even if there are two notifications with the
266                    // same icon in a row
267                    mIconSwitcher.setImageDrawable(seg.icon);
268                }
269                CharSequence text = seg.advance();
270                if (text == null) {
271                    mSegments.remove(0);
272                    continue;
273                }
274                mTextSwitcher.setText(text);
275
276                scheduleAdvance();
277                break;
278            }
279            if (mSegments.size() == 0) {
280                tickerDone();
281            }
282        }
283    };
284
285    private void scheduleAdvance() {
286        mHandler.postDelayed(mAdvanceTicker, TICKER_SEGMENT_DELAY);
287    }
288
289    public abstract void tickerStarting();
290    public abstract void tickerDone();
291    public abstract void tickerHalting();
292}
293
294