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