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