/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.statusbar.phone; import android.content.Context; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Handler; import android.text.StaticLayout; import android.text.Layout.Alignment; import android.text.TextPaint; import android.text.TextUtils; import android.util.Slog; import android.view.View; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.TextSwitcher; import android.widget.TextView; import android.widget.ImageSwitcher; import java.util.ArrayList; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.statusbar.StatusBarNotification; import com.android.internal.util.CharSequences; import com.android.systemui.R; import com.android.systemui.statusbar.StatusBarIconView; public abstract class Ticker { private static final int TICKER_SEGMENT_DELAY = 3000; private Context mContext; private Handler mHandler = new Handler(); private ArrayList<Segment> mSegments = new ArrayList(); private TextPaint mPaint; private View mTickerView; private ImageSwitcher mIconSwitcher; private TextSwitcher mTextSwitcher; private float mIconScale; private final class Segment { StatusBarNotification notification; Drawable icon; CharSequence text; int current; int next; boolean first; StaticLayout getLayout(CharSequence substr) { int w = mTextSwitcher.getWidth() - mTextSwitcher.getPaddingLeft() - mTextSwitcher.getPaddingRight(); return new StaticLayout(substr, mPaint, w, Alignment.ALIGN_NORMAL, 1, 0, true); } CharSequence rtrim(CharSequence substr, int start, int end) { while (end > start && !TextUtils.isGraphic(substr.charAt(end-1))) { end--; } if (end > start) { return substr.subSequence(start, end); } return null; } /** returns null if there is no more text */ CharSequence getText() { if (this.current > this.text.length()) { return null; } CharSequence substr = this.text.subSequence(this.current, this.text.length()); StaticLayout l = getLayout(substr); int lineCount = l.getLineCount(); if (lineCount > 0) { int start = l.getLineStart(0); int end = l.getLineEnd(0); this.next = this.current + end; return rtrim(substr, start, end); } else { throw new RuntimeException("lineCount=" + lineCount + " current=" + current + " text=" + text); } } /** returns null if there is no more text */ CharSequence advance() { this.first = false; int index = this.next; final int len = this.text.length(); while (index < len && !TextUtils.isGraphic(this.text.charAt(index))) { index++; } if (index >= len) { return null; } CharSequence substr = this.text.subSequence(index, this.text.length()); StaticLayout l = getLayout(substr); final int lineCount = l.getLineCount(); int i; for (i=0; i<lineCount; i++) { int start = l.getLineStart(i); int end = l.getLineEnd(i); if (i == lineCount-1) { this.next = len; } else { this.next = index + l.getLineStart(i+1); } CharSequence result = rtrim(substr, start, end); if (result != null) { this.current = index + start; return result; } } this.current = len; return null; } Segment(StatusBarNotification n, Drawable icon, CharSequence text) { this.notification = n; this.icon = icon; this.text = text; int index = 0; final int len = text.length(); while (index < len && !TextUtils.isGraphic(text.charAt(index))) { index++; } this.current = index; this.next = index; this.first = true; } }; public Ticker(Context context, View sb) { mContext = context; final Resources res = context.getResources(); final int outerBounds = res.getDimensionPixelSize(R.dimen.status_bar_icon_size); final int imageBounds = res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size); mIconScale = (float)imageBounds / (float)outerBounds; mTickerView = sb.findViewById(R.id.ticker); mIconSwitcher = (ImageSwitcher)sb.findViewById(R.id.tickerIcon); mIconSwitcher.setInAnimation( AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_in)); mIconSwitcher.setOutAnimation( AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_out)); mIconSwitcher.setScaleX(mIconScale); mIconSwitcher.setScaleY(mIconScale); mTextSwitcher = (TextSwitcher)sb.findViewById(R.id.tickerText); mTextSwitcher.setInAnimation( AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_in)); mTextSwitcher.setOutAnimation( AnimationUtils.loadAnimation(context, com.android.internal.R.anim.push_up_out)); // Copy the paint style of one of the TextSwitchers children to use later for measuring TextView text = (TextView)mTextSwitcher.getChildAt(0); mPaint = text.getPaint(); } public void addEntry(StatusBarNotification n) { int initialCount = mSegments.size(); // If what's being displayed has the same text and icon, just drop it // (which will let the current one finish, this happens when apps do // a notification storm). if (initialCount > 0) { final Segment seg = mSegments.get(0); if (n.pkg.equals(seg.notification.pkg) && n.notification.icon == seg.notification.notification.icon && n.notification.iconLevel == seg.notification.notification.iconLevel && CharSequences.equals(seg.notification.notification.tickerText, n.notification.tickerText)) { return; } } final Drawable icon = StatusBarIconView.getIcon(mContext, new StatusBarIcon(n.pkg, n.notification.icon, n.notification.iconLevel, 0, n.notification.tickerText)); final Segment newSegment = new Segment(n, icon, n.notification.tickerText); // If there's already a notification schedule for this package and id, remove it. for (int i=0; i<mSegments.size(); i++) { Segment seg = mSegments.get(i); if (n.id == seg.notification.id && n.pkg.equals(seg.notification.pkg)) { // just update that one to use this new data instead mSegments.remove(i--); // restart iteration here } } mSegments.add(newSegment); if (initialCount == 0 && mSegments.size() > 0) { Segment seg = mSegments.get(0); seg.first = false; mIconSwitcher.setAnimateFirstView(false); mIconSwitcher.reset(); mIconSwitcher.setImageDrawable(seg.icon); mTextSwitcher.setAnimateFirstView(false); mTextSwitcher.reset(); mTextSwitcher.setText(seg.getText()); tickerStarting(); scheduleAdvance(); } } public void removeEntry(StatusBarNotification n) { for (int i=mSegments.size()-1; i>=0; i--) { Segment seg = mSegments.get(i); if (n.id == seg.notification.id && n.pkg.equals(seg.notification.pkg)) { mSegments.remove(i); } } } public void halt() { mHandler.removeCallbacks(mAdvanceTicker); mSegments.clear(); tickerHalting(); } public void reflowText() { if (mSegments.size() > 0) { Segment seg = mSegments.get(0); CharSequence text = seg.getText(); mTextSwitcher.setCurrentText(text); } } private Runnable mAdvanceTicker = new Runnable() { public void run() { while (mSegments.size() > 0) { Segment seg = mSegments.get(0); if (seg.first) { // this makes the icon slide in for the first one for a given // notification even if there are two notifications with the // same icon in a row mIconSwitcher.setImageDrawable(seg.icon); } CharSequence text = seg.advance(); if (text == null) { mSegments.remove(0); continue; } mTextSwitcher.setText(text); scheduleAdvance(); break; } if (mSegments.size() == 0) { tickerDone(); } } }; private void scheduleAdvance() { mHandler.postDelayed(mAdvanceTicker, TICKER_SEGMENT_DELAY); } public abstract void tickerStarting(); public abstract void tickerDone(); public abstract void tickerHalting(); }