Home | History | Annotate | Download | only in phone
      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 
     17 package com.android.systemui.statusbar.phone;
     18 
     19 import android.service.notification.StatusBarNotification;
     20 import android.content.Context;
     21 import android.content.res.Resources;
     22 import android.graphics.drawable.Drawable;
     23 import android.os.Handler;
     24 import android.text.StaticLayout;
     25 import android.text.Layout.Alignment;
     26 import android.text.TextPaint;
     27 import android.view.View;
     28 import android.view.animation.AnimationUtils;
     29 import android.widget.TextSwitcher;
     30 import android.widget.TextView;
     31 import android.widget.ImageSwitcher;
     32 
     33 import java.util.ArrayList;
     34 
     35 import com.android.internal.statusbar.StatusBarIcon;
     36 import com.android.internal.util.CharSequences;
     37 
     38 import com.android.systemui.R;
     39 import com.android.systemui.statusbar.StatusBarIconView;
     40 
     41 public 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.getPackageName().equals(seg.notification.getPackageName())
    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.getPackageName(), 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.getPackageName().equals(seg.notification.getPackageName())) {
    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.getPackageName().equals(seg.notification.getPackageName())) {
    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