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