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