/* * Copyright (C) 2010 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; import android.app.Service; import com.android.internal.statusbar.IStatusBar; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.statusbar.StatusBarIconList; import com.android.internal.statusbar.StatusBarNotification; import android.app.ActivityManagerNative; import android.app.Dialog; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.app.StatusBarManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.IBinder; import android.os.RemoteException; import android.os.Binder; import android.os.Handler; import android.os.Message; import android.os.ServiceManager; import android.os.SystemClock; import android.util.Slog; import android.util.Log; import android.view.Display; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.view.WindowManagerImpl; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RemoteViews; import android.widget.ScrollView; import android.widget.TextView; import android.widget.FrameLayout; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.Set; import com.android.systemui.R; import com.android.systemui.statusbar.policy.StatusBarPolicy; public class StatusBarService extends Service implements CommandQueue.Callbacks { static final String TAG = "StatusBarService"; static final boolean SPEW = false; public static final String ACTION_STATUSBAR_START = "com.android.internal.policy.statusbar.START"; static final int EXPANDED_LEAVE_ALONE = -10000; static final int EXPANDED_FULL_OPEN = -10001; private static final int MSG_ANIMATE = 1000; private static final int MSG_ANIMATE_REVEAL = 1001; private static final int MSG_SHOW_INTRUDER = 1002; private static final int MSG_HIDE_INTRUDER = 1003; // will likely move to a resource or other tunable param at some point private static final int INTRUDER_ALERT_DECAY_MS = 10000; StatusBarPolicy mIconPolicy; CommandQueue mCommandQueue; IStatusBarService mBarService; int mIconSize; Display mDisplay; StatusBarView mStatusBarView; int mPixelFormat; H mHandler = new H(); Object mQueueLock = new Object(); // icons LinearLayout mIcons; IconMerger mNotificationIcons; LinearLayout mStatusIcons; // expanded notifications Dialog mExpandedDialog; ExpandedView mExpandedView; WindowManager.LayoutParams mExpandedParams; ScrollView mScrollView; View mNotificationLinearLayout; View mExpandedContents; // top bar TextView mNoNotificationsTitle; TextView mClearButton; // drag bar CloseDragHandle mCloseView; // ongoing NotificationData mOngoing = new NotificationData(); TextView mOngoingTitle; LinearLayout mOngoingItems; // latest NotificationData mLatest = new NotificationData(); TextView mLatestTitle; LinearLayout mLatestItems; // position int[] mPositionTmp = new int[2]; boolean mExpanded; boolean mExpandedVisible; // the date view DateView mDateView; // the tracker view TrackingView mTrackingView; WindowManager.LayoutParams mTrackingParams; int mTrackingPosition; // the position of the top of the tracking view. private boolean mPanelSlightlyVisible; // ticker private Ticker mTicker; private View mTickerView; private boolean mTicking; // Tracking finger for opening/closing. int mEdgeBorder; // corresponds to R.dimen.status_bar_edge_ignore boolean mTracking; VelocityTracker mVelocityTracker; static final int ANIM_FRAME_DURATION = (1000/60); boolean mAnimating; long mCurAnimationTime; float mDisplayHeight; float mAnimY; float mAnimVel; float mAnimAccel; long mAnimLastTime; boolean mAnimatingReveal = false; int mViewDelta; int[] mAbsPos = new int[2]; // for disabling the status bar int mDisabled = 0; private class ExpandedDialog extends Dialog { ExpandedDialog(Context context) { super(context, com.android.internal.R.style.Theme_Light_NoTitleBar); } @Override public boolean dispatchKeyEvent(KeyEvent event) { boolean down = event.getAction() == KeyEvent.ACTION_DOWN; switch (event.getKeyCode()) { case KeyEvent.KEYCODE_BACK: if (!down) { animateCollapse(); } return true; } return super.dispatchKeyEvent(event); } } @Override public void onCreate() { // First set up our views and stuff. mDisplay = ((WindowManager)getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); makeStatusBarView(this); // Connect in to the status bar manager service StatusBarIconList iconList = new StatusBarIconList(); ArrayList notificationKeys = new ArrayList(); ArrayList notifications = new ArrayList(); mCommandQueue = new CommandQueue(this, iconList); mBarService = IStatusBarService.Stub.asInterface( ServiceManager.getService(Context.STATUS_BAR_SERVICE)); try { mBarService.registerStatusBar(mCommandQueue, iconList, notificationKeys, notifications); } catch (RemoteException ex) { // If the system process isn't there we're doomed anyway. } // Set up the initial icon state int N = iconList.size(); int viewIndex = 0; for (int i=0; i s final float y = mAnimY; final float v = mAnimVel; // px/s final float a = mAnimAccel; // px/s/s mAnimY = y + (v*t) + (0.5f*a*t*t); // px mAnimVel = v + (a*t); // px/s mAnimLastTime = now; // ms //Slog.d(TAG, "y=" + y + " v=" + v + " a=" + a + " t=" + t + " mAnimY=" + mAnimY // + " mAnimAccel=" + mAnimAccel); } void doRevealAnimation() { final int h = mCloseView.getHeight() + mStatusBarView.getHeight(); if (mAnimatingReveal && mAnimating && mAnimY < h) { incrementAnim(); if (mAnimY >= h) { mAnimY = h; updateExpandedViewPos((int)mAnimY); } else { updateExpandedViewPos((int)mAnimY); mCurAnimationTime += ANIM_FRAME_DURATION; mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE_REVEAL), mCurAnimationTime); } } } void prepareTracking(int y, boolean opening) { mTracking = true; mVelocityTracker = VelocityTracker.obtain(); if (opening) { mAnimAccel = 2000.0f; mAnimVel = 200; mAnimY = mStatusBarView.getHeight(); updateExpandedViewPos((int)mAnimY); mAnimating = true; mAnimatingReveal = true; mHandler.removeMessages(MSG_ANIMATE); mHandler.removeMessages(MSG_ANIMATE_REVEAL); long now = SystemClock.uptimeMillis(); mAnimLastTime = now; mCurAnimationTime = now + ANIM_FRAME_DURATION; mAnimating = true; mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE_REVEAL), mCurAnimationTime); makeExpandedVisible(); } else { // it's open, close it? if (mAnimating) { mAnimating = false; mHandler.removeMessages(MSG_ANIMATE); } updateExpandedViewPos(y + mViewDelta); } } void performFling(int y, float vel, boolean always) { mAnimatingReveal = false; mDisplayHeight = mDisplay.getHeight(); mAnimY = y; mAnimVel = vel; //Slog.d(TAG, "starting with mAnimY=" + mAnimY + " mAnimVel=" + mAnimVel); if (mExpanded) { if (!always && ( vel > 200.0f || (y > (mDisplayHeight-25) && vel > -200.0f))) { // We are expanded, but they didn't move sufficiently to cause // us to retract. Animate back to the expanded position. mAnimAccel = 2000.0f; if (vel < 0) { mAnimVel = 0; } } else { // We are expanded and are now going to animate away. mAnimAccel = -2000.0f; if (vel > 0) { mAnimVel = 0; } } } else { if (always || ( vel > 200.0f || (y > (mDisplayHeight/2) && vel > -200.0f))) { // We are collapsed, and they moved enough to allow us to // expand. Animate in the notifications. mAnimAccel = 2000.0f; if (vel < 0) { mAnimVel = 0; } } else { // We are collapsed, but they didn't move sufficiently to cause // us to retract. Animate back to the collapsed position. mAnimAccel = -2000.0f; if (vel > 0) { mAnimVel = 0; } } } //Slog.d(TAG, "mAnimY=" + mAnimY + " mAnimVel=" + mAnimVel // + " mAnimAccel=" + mAnimAccel); long now = SystemClock.uptimeMillis(); mAnimLastTime = now; mCurAnimationTime = now + ANIM_FRAME_DURATION; mAnimating = true; mHandler.removeMessages(MSG_ANIMATE); mHandler.removeMessages(MSG_ANIMATE_REVEAL); mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE), mCurAnimationTime); stopTracking(); } boolean interceptTouchEvent(MotionEvent event) { if (SPEW) { Slog.d(TAG, "Touch: rawY=" + event.getRawY() + " event=" + event + " mDisabled=" + mDisabled); } if ((mDisabled & StatusBarManager.DISABLE_EXPAND) != 0) { return false; } final int statusBarSize = mStatusBarView.getHeight(); final int hitSize = statusBarSize*2; if (event.getAction() == MotionEvent.ACTION_DOWN) { final int y = (int)event.getRawY(); if (!mExpanded) { mViewDelta = statusBarSize - y; } else { mTrackingView.getLocationOnScreen(mAbsPos); mViewDelta = mAbsPos[1] + mTrackingView.getHeight() - y; } if ((!mExpanded && y < hitSize) || (mExpanded && y > (mDisplay.getHeight()-hitSize))) { // We drop events at the edge of the screen to make the windowshade come // down by accident less, especially when pushing open a device with a keyboard // that rotates (like g1 and droid) int x = (int)event.getRawX(); final int edgeBorder = mEdgeBorder; if (x >= edgeBorder && x < mDisplay.getWidth() - edgeBorder) { prepareTracking(y, !mExpanded);// opening if we're not already fully visible mVelocityTracker.addMovement(event); } } } else if (mTracking) { mVelocityTracker.addMovement(event); final int minY = statusBarSize + mCloseView.getHeight(); if (event.getAction() == MotionEvent.ACTION_MOVE) { int y = (int)event.getRawY(); if (mAnimatingReveal && y < minY) { // nothing } else { mAnimatingReveal = false; updateExpandedViewPos(y + mViewDelta); } } else if (event.getAction() == MotionEvent.ACTION_UP) { mVelocityTracker.computeCurrentVelocity(1000); float yVel = mVelocityTracker.getYVelocity(); boolean negative = yVel < 0; float xVel = mVelocityTracker.getXVelocity(); if (xVel < 0) { xVel = -xVel; } if (xVel > 150.0f) { xVel = 150.0f; // limit how much we care about the x axis } float vel = (float)Math.hypot(yVel, xVel); if (negative) { vel = -vel; } performFling((int)event.getRawY(), vel, false); } } return false; } private class Launcher implements View.OnClickListener { private PendingIntent mIntent; private String mPkg; private String mTag; private int mId; Launcher(PendingIntent intent, String pkg, String tag, int id) { mIntent = intent; mPkg = pkg; mTag = tag; mId = id; } public void onClick(View v) { try { // The intent we are sending is for the application, which // won't have permission to immediately start an activity after // the user switches to home. We know it is safe to do at this // point, so make sure new activity switches are now allowed. ActivityManagerNative.getDefault().resumeAppSwitches(); } catch (RemoteException e) { } if (mIntent != null) { int[] pos = new int[2]; v.getLocationOnScreen(pos); Intent overlay = new Intent(); overlay.setSourceBounds( new Rect(pos[0], pos[1], pos[0]+v.getWidth(), pos[1]+v.getHeight())); try { mIntent.send(StatusBarService.this, 0, overlay); } catch (PendingIntent.CanceledException e) { // the stack trace isn't very helpful here. Just log the exception message. Slog.w(TAG, "Sending contentIntent failed: " + e); } } try { mBarService.onNotificationClick(mPkg, mTag, mId); } catch (RemoteException ex) { // system process is dead if we're here. } // close the shade if it was open animateCollapse(); // If this click was on the intruder alert, hide that instead mHandler.sendEmptyMessage(MSG_HIDE_INTRUDER); } } private void tick(StatusBarNotification n) { // Show the ticker if one is requested. Also don't do this // until status bar window is attached to the window manager, // because... well, what's the point otherwise? And trying to // run a ticker without being attached will crash! if (n.notification.tickerText != null && mStatusBarView.getWindowToken() != null) { if (0 == (mDisabled & (StatusBarManager.DISABLE_NOTIFICATION_ICONS | StatusBarManager.DISABLE_NOTIFICATION_TICKER))) { mTicker.addEntry(n); } } } /** * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService * about the failure. * * WARNING: this will call back into us. Don't hold any locks. */ void handleNotificationError(IBinder key, StatusBarNotification n, String message) { removeNotification(key); try { mBarService.onNotificationError(n.pkg, n.tag, n.id, n.uid, n.initialPid, message); } catch (RemoteException ex) { // The end is nigh. } } private class MyTicker extends Ticker { MyTicker(Context context, StatusBarView sb) { super(context, sb); } @Override void tickerStarting() { mTicking = true; mIcons.setVisibility(View.GONE); mTickerView.setVisibility(View.VISIBLE); mTickerView.startAnimation(loadAnim(com.android.internal.R.anim.push_up_in, null)); mIcons.startAnimation(loadAnim(com.android.internal.R.anim.push_up_out, null)); if (mExpandedVisible) { setDateViewVisibility(false, com.android.internal.R.anim.push_up_out); } } @Override void tickerDone() { mIcons.setVisibility(View.VISIBLE); mTickerView.setVisibility(View.GONE); mIcons.startAnimation(loadAnim(com.android.internal.R.anim.push_down_in, null)); mTickerView.startAnimation(loadAnim(com.android.internal.R.anim.push_down_out, mTickingDoneListener)); if (mExpandedVisible) { setDateViewVisibility(true, com.android.internal.R.anim.push_down_in); } } void tickerHalting() { mIcons.setVisibility(View.VISIBLE); mTickerView.setVisibility(View.GONE); mIcons.startAnimation(loadAnim(com.android.internal.R.anim.fade_in, null)); mTickerView.startAnimation(loadAnim(com.android.internal.R.anim.fade_out, mTickingDoneListener)); if (mExpandedVisible) { setDateViewVisibility(true, com.android.internal.R.anim.fade_in); } } } Animation.AnimationListener mTickingDoneListener = new Animation.AnimationListener() {; public void onAnimationEnd(Animation animation) { mTicking = false; } public void onAnimationRepeat(Animation animation) { } public void onAnimationStart(Animation animation) { } }; private Animation loadAnim(int id, Animation.AnimationListener listener) { Animation anim = AnimationUtils.loadAnimation(StatusBarService.this, id); if (listener != null) { anim.setAnimationListener(listener); } return anim; } public String viewInfo(View v) { return "(" + v.getLeft() + "," + v.getTop() + ")(" + v.getRight() + "," + v.getBottom() + " " + v.getWidth() + "x" + v.getHeight() + ")"; } protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (checkCallingOrSelfPermission(android.Manifest.permission.DUMP) != PackageManager.PERMISSION_GRANTED) { pw.println("Permission Denial: can't dump StatusBar from from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()); return; } synchronized (mQueueLock) { pw.println("Current Status Bar state:"); pw.println(" mExpanded=" + mExpanded + ", mExpandedVisible=" + mExpandedVisible); pw.println(" mTicking=" + mTicking); pw.println(" mTracking=" + mTracking); pw.println(" mAnimating=" + mAnimating + ", mAnimY=" + mAnimY + ", mAnimVel=" + mAnimVel + ", mAnimAccel=" + mAnimAccel); pw.println(" mCurAnimationTime=" + mCurAnimationTime + " mAnimLastTime=" + mAnimLastTime); pw.println(" mDisplayHeight=" + mDisplayHeight + " mAnimatingReveal=" + mAnimatingReveal + " mViewDelta=" + mViewDelta); pw.println(" mDisplayHeight=" + mDisplayHeight); pw.println(" mExpandedParams: " + mExpandedParams); pw.println(" mExpandedView: " + viewInfo(mExpandedView)); pw.println(" mExpandedDialog: " + mExpandedDialog); pw.println(" mTrackingParams: " + mTrackingParams); pw.println(" mTrackingView: " + viewInfo(mTrackingView)); pw.println(" mOngoingTitle: " + viewInfo(mOngoingTitle)); pw.println(" mOngoingItems: " + viewInfo(mOngoingItems)); pw.println(" mLatestTitle: " + viewInfo(mLatestTitle)); pw.println(" mLatestItems: " + viewInfo(mLatestItems)); pw.println(" mNoNotificationsTitle: " + viewInfo(mNoNotificationsTitle)); pw.println(" mCloseView: " + viewInfo(mCloseView)); pw.println(" mTickerView: " + viewInfo(mTickerView)); pw.println(" mScrollView: " + viewInfo(mScrollView) + " scroll " + mScrollView.getScrollX() + "," + mScrollView.getScrollY()); pw.println("mNotificationLinearLayout: " + viewInfo(mNotificationLinearLayout)); } /* synchronized (mNotificationData) { int N = mNotificationData.ongoingCount(); pw.println(" ongoingCount.size=" + N); for (int i=0; i