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