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