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