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.server.status; 18 19 import com.android.internal.R; 20 21 import android.content.Context; 22 import android.graphics.drawable.Drawable; 23 import android.os.Handler; 24 import android.text.StaticLayout; 25 import android.text.Layout.Alignment; 26 import android.text.TextPaint; 27 import android.text.TextUtils; 28 import android.util.Slog; 29 import android.view.View; 30 import android.view.animation.Animation; 31 import android.view.animation.AnimationUtils; 32 import android.widget.TextSwitcher; 33 import android.widget.TextView; 34 import android.widget.ImageSwitcher; 35 36 import java.util.ArrayList; 37 38 39 abstract class Ticker { 40 private static final int TICKER_SEGMENT_DELAY = 3000; 41 42 private final class Segment { 43 NotificationData notificationData; 44 Drawable icon; 45 CharSequence text; 46 int current; 47 int next; 48 boolean first; 49 50 StaticLayout getLayout(CharSequence substr) { 51 int w = mTextSwitcher.getWidth() - mTextSwitcher.getPaddingLeft() 52 - mTextSwitcher.getPaddingRight(); 53 return new StaticLayout(substr, mPaint, w, Alignment.ALIGN_NORMAL, 1, 0, true); 54 } 55 56 CharSequence rtrim(CharSequence substr, int start, int end) { 57 while (end > start && !TextUtils.isGraphic(substr.charAt(end-1))) { 58 end--; 59 } 60 if (end > start) { 61 return substr.subSequence(start, end); 62 } 63 return null; 64 } 65 66 /** returns null if there is no more text */ 67 CharSequence getText() { 68 if (this.current > this.text.length()) { 69 return null; 70 } 71 CharSequence substr = this.text.subSequence(this.current, this.text.length()); 72 StaticLayout l = getLayout(substr); 73 int lineCount = l.getLineCount(); 74 if (lineCount > 0) { 75 int start = l.getLineStart(0); 76 int end = l.getLineEnd(0); 77 this.next = this.current + end; 78 return rtrim(substr, start, end); 79 } else { 80 throw new RuntimeException("lineCount=" + lineCount + " current=" + current + 81 " text=" + text); 82 } 83 } 84 85 /** returns null if there is no more text */ 86 CharSequence advance() { 87 this.first = false; 88 int index = this.next; 89 final int len = this.text.length(); 90 while (index < len && !TextUtils.isGraphic(this.text.charAt(index))) { 91 index++; 92 } 93 if (index >= len) { 94 return null; 95 } 96 97 CharSequence substr = this.text.subSequence(index, this.text.length()); 98 StaticLayout l = getLayout(substr); 99 final int lineCount = l.getLineCount(); 100 int i; 101 for (i=0; i<lineCount; i++) { 102 int start = l.getLineStart(i); 103 int end = l.getLineEnd(i); 104 if (i == lineCount-1) { 105 this.next = len; 106 } else { 107 this.next = index + l.getLineStart(i+1); 108 } 109 CharSequence result = rtrim(substr, start, end); 110 if (result != null) { 111 this.current = index + start; 112 return result; 113 } 114 } 115 this.current = len; 116 return null; 117 } 118 119 Segment(NotificationData n, Drawable icon, CharSequence text) { 120 this.notificationData = n; 121 this.icon = icon; 122 this.text = text; 123 int index = 0; 124 final int len = text.length(); 125 while (index < len && !TextUtils.isGraphic(text.charAt(index))) { 126 index++; 127 } 128 this.current = index; 129 this.next = index; 130 this.first = true; 131 } 132 }; 133 134 private Handler mHandler = new Handler(); 135 private ArrayList<Segment> mSegments = new ArrayList(); 136 private TextPaint mPaint; 137 private View mTickerView; 138 private ImageSwitcher mIconSwitcher; 139 private TextSwitcher mTextSwitcher; 140 141 Ticker(Context context, StatusBarView sb) { 142 mTickerView = sb.findViewById(R.id.ticker); 143 144 mIconSwitcher = (ImageSwitcher)sb.findViewById(R.id.tickerIcon); 145 mIconSwitcher.setInAnimation( 146 AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_in)); 147 mIconSwitcher.setOutAnimation( 148 AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_out)); 149 150 mTextSwitcher = (TextSwitcher)sb.findViewById(R.id.tickerText); 151 mTextSwitcher.setInAnimation( 152 AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_in)); 153 mTextSwitcher.setOutAnimation( 154 AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_out)); 155 156 // Copy the paint style of one of the TextSwitchers children to use later for measuring 157 TextView text = (TextView)mTextSwitcher.getChildAt(0); 158 mPaint = text.getPaint(); 159 } 160 161 void addEntry(NotificationData n, Drawable icon, CharSequence text) { 162 int initialCount = mSegments.size(); 163 164 Segment newSegment = new Segment(n, icon, text); 165 166 // prune out any preexisting ones for this notification, but not the current one. 167 // let that finish, even if it's the same id 168 for (int i=1; i<initialCount; i++) { 169 Segment seg = mSegments.get(i); 170 if (n.id == seg.notificationData.id && n.pkg.equals(seg.notificationData.pkg)) { 171 // just update that one to use this new data instead 172 mSegments.set(i, newSegment); 173 // and since we know initialCount != 0, just return 174 return ; 175 } 176 } 177 178 mSegments.add(newSegment); 179 180 if (initialCount == 0 && mSegments.size() > 0) { 181 Segment seg = mSegments.get(0); 182 seg.first = false; 183 184 mIconSwitcher.setAnimateFirstView(false); 185 mIconSwitcher.reset(); 186 mIconSwitcher.setImageDrawable(seg.icon); 187 188 mTextSwitcher.setAnimateFirstView(false); 189 mTextSwitcher.reset(); 190 mTextSwitcher.setText(seg.getText()); 191 192 tickerStarting(); 193 scheduleAdvance(); 194 } 195 } 196 197 void halt() { 198 mHandler.removeCallbacks(mAdvanceTicker); 199 mSegments.clear(); 200 tickerHalting(); 201 } 202 203 void reflowText() { 204 if (mSegments.size() > 0) { 205 Segment seg = mSegments.get(0); 206 CharSequence text = seg.getText(); 207 mTextSwitcher.setCurrentText(text); 208 } 209 } 210 211 private Runnable mAdvanceTicker = new Runnable() { 212 public void run() { 213 while (mSegments.size() > 0) { 214 Segment seg = mSegments.get(0); 215 216 if (seg.first) { 217 // this makes the icon slide in for the first one for a given 218 // notification even if there are two notifications with the 219 // same icon in a row 220 mIconSwitcher.setImageDrawable(seg.icon); 221 } 222 CharSequence text = seg.advance(); 223 if (text == null) { 224 mSegments.remove(0); 225 continue; 226 } 227 mTextSwitcher.setText(text); 228 229 scheduleAdvance(); 230 break; 231 } 232 if (mSegments.size() == 0) { 233 tickerDone(); 234 } 235 } 236 }; 237 238 private void scheduleAdvance() { 239 mHandler.postDelayed(mAdvanceTicker, TICKER_SEGMENT_DELAY); 240 } 241 242 abstract void tickerStarting(); 243 abstract void tickerDone(); 244 abstract void tickerHalting(); 245 } 246 247