1/*
2 * Copyright (C) 2012 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
17package com.android.systemui;
18
19import android.app.ActivityManager;
20import android.content.Context;
21import android.content.res.Configuration;
22import android.content.res.Resources;
23import android.graphics.Matrix;
24import android.graphics.PixelFormat;
25import android.os.RemoteException;
26import android.util.Log;
27import android.util.Slog;
28import android.view.Choreographer;
29import android.view.Display;
30import android.view.IWindowSession;
31import android.view.MotionEvent;
32import android.view.VelocityTracker;
33import android.view.View;
34import android.view.ViewRootImpl;
35import android.view.WindowManager;
36import android.view.WindowManagerGlobal;
37import android.view.animation.Transformation;
38import android.widget.FrameLayout;
39
40public class UniverseBackground extends FrameLayout {
41    static final String TAG = "UniverseBackground";
42    static final boolean SPEW = false;
43    static final boolean CHATTY = false;
44
45    final IWindowSession mSession;
46    final View mContent;
47    final View mBottomAnchor;
48
49    final Runnable mAnimationCallback = new Runnable() {
50        @Override
51        public void run() {
52            doAnimation(mChoreographer.getFrameTimeNanos());
53        }
54    };
55
56    // fling gesture tuning parameters, scaled to display density
57    private float mSelfExpandVelocityPx; // classic value: 2000px/s
58    private float mSelfCollapseVelocityPx; // classic value: 2000px/s (will be negated to collapse "up")
59    private float mFlingExpandMinVelocityPx; // classic value: 200px/s
60    private float mFlingCollapseMinVelocityPx; // classic value: 200px/s
61    private float mCollapseMinDisplayFraction; // classic value: 0.08 (25px/min(320px,480px) on G1)
62    private float mExpandMinDisplayFraction; // classic value: 0.5 (drag open halfway to expand)
63    private float mFlingGestureMaxXVelocityPx; // classic value: 150px/s
64
65    private float mExpandAccelPx; // classic value: 2000px/s/s
66    private float mCollapseAccelPx; // classic value: 2000px/s/s (will be negated to collapse "up")
67
68    static final int STATE_CLOSED = 0;
69    static final int STATE_OPENING = 1;
70    static final int STATE_OPEN = 2;
71    private int mState = STATE_CLOSED;
72
73    private float mDragStartX, mDragStartY;
74    private float mAverageX, mAverageY;
75
76    // position
77    private int[] mPositionTmp = new int[2];
78    private boolean mExpanded;
79    private boolean mExpandedVisible;
80
81    private boolean mTracking;
82    private VelocityTracker mVelocityTracker;
83
84    private Choreographer mChoreographer;
85    private boolean mAnimating;
86    private boolean mClosing; // only valid when mAnimating; indicates the initial acceleration
87    private float mAnimY;
88    private float mAnimVel;
89    private float mAnimAccel;
90    private long mAnimLastTimeNanos;
91    private boolean mAnimatingReveal = false;
92
93    private int mYDelta = 0;
94    private Transformation mUniverseTransform = new Transformation();
95    private final float[] mTmpFloats = new float[9];
96
97    public UniverseBackground(Context context) {
98        super(context);
99        setBackgroundColor(0xff000000);
100        mSession = WindowManagerGlobal.getWindowSession(context.getMainLooper());
101        mContent = View.inflate(context, R.layout.universe, null);
102        addView(mContent);
103        mContent.findViewById(R.id.close).setOnClickListener(new View.OnClickListener() {
104            @Override public void onClick(View v) {
105                animateCollapse();
106            }
107        });
108        mBottomAnchor = mContent.findViewById(R.id.bottom);
109        mChoreographer = Choreographer.getInstance();
110        loadDimens();
111    }
112
113    @Override
114    protected void onConfigurationChanged(Configuration newConfig) {
115        super.onConfigurationChanged(newConfig);
116        loadDimens();
117    }
118
119    private void loadDimens() {
120        final Resources res = getContext().getResources();
121        mSelfExpandVelocityPx = res.getDimension(R.dimen.self_expand_velocity);
122        mSelfCollapseVelocityPx = res.getDimension(R.dimen.self_collapse_velocity);
123        mFlingExpandMinVelocityPx = res.getDimension(R.dimen.fling_expand_min_velocity);
124        mFlingCollapseMinVelocityPx = res.getDimension(R.dimen.fling_collapse_min_velocity);
125
126        mCollapseMinDisplayFraction = res.getFraction(R.dimen.collapse_min_display_fraction, 1, 1);
127        mExpandMinDisplayFraction = res.getFraction(R.dimen.expand_min_display_fraction, 1, 1);
128
129        mExpandAccelPx = res.getDimension(R.dimen.expand_accel);
130        mCollapseAccelPx = res.getDimension(R.dimen.collapse_accel);
131
132        mFlingGestureMaxXVelocityPx = res.getDimension(R.dimen.fling_gesture_max_x_velocity);
133    }
134
135    private void computeAveragePos(MotionEvent event) {
136        final int num = event.getPointerCount();
137        float x = 0, y = 0;
138        for (int i=0; i<num; i++) {
139            x += event.getX(i);
140            y += event.getY(i);
141        }
142        mAverageX = x / num;
143        mAverageY = y / num;
144    }
145
146    private void sendUniverseTransform() {
147        if (getWindowToken() != null) {
148            mUniverseTransform.getMatrix().getValues(mTmpFloats);
149            try {
150                mSession.setUniverseTransform(getWindowToken(), mUniverseTransform.getAlpha(),
151                        mTmpFloats[Matrix.MTRANS_X], mTmpFloats[Matrix.MTRANS_Y],
152                        mTmpFloats[Matrix.MSCALE_X], mTmpFloats[Matrix.MSKEW_Y],
153                        mTmpFloats[Matrix.MSKEW_X], mTmpFloats[Matrix.MSCALE_Y]);
154            } catch (RemoteException e) {
155            }
156        }
157    }
158
159    public WindowManager.LayoutParams getLayoutParams() {
160        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
161                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
162                WindowManager.LayoutParams.TYPE_UNIVERSE_BACKGROUND,
163                    0
164                    | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
165                    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
166                    | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
167                PixelFormat.OPAQUE);
168        // this will allow the window to run in an overlay on devices that support this
169        if (ActivityManager.isHighEndGfx()) {
170            lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
171        }
172        lp.setTitle("UniverseBackground");
173        lp.windowAnimations = 0;
174        return lp;
175    }
176
177    private int getExpandedViewMaxHeight() {
178        return mBottomAnchor.getTop();
179    }
180
181    public void animateCollapse() {
182        animateCollapse(1.0f);
183    }
184
185    public void animateCollapse(float velocityMultiplier) {
186        if (SPEW) {
187            Slog.d(TAG, "animateCollapse(): mExpanded=" + mExpanded
188                    + " mExpandedVisible=" + mExpandedVisible
189                    + " mExpanded=" + mExpanded
190                    + " mAnimating=" + mAnimating
191                    + " mAnimY=" + mAnimY
192                    + " mAnimVel=" + mAnimVel);
193        }
194
195        mState = STATE_CLOSED;
196        if (!mExpandedVisible) {
197            return;
198        }
199
200        int y;
201        if (mAnimating) {
202            y = (int)mAnimY;
203        } else {
204            y = getExpandedViewMaxHeight()-1;
205        }
206        // Let the fling think that we're open so it goes in the right direction
207        // and doesn't try to re-open the windowshade.
208        mExpanded = true;
209        prepareTracking(y, false);
210        performFling(y, -mSelfCollapseVelocityPx*velocityMultiplier, true);
211    }
212
213    private void updateUniverseScale() {
214        if (mYDelta > 0) {
215            int w = getWidth();
216            int h = getHeight();
217            float scale = (h-mYDelta+.5f) / (float)h;
218            mUniverseTransform.getMatrix().setScale(scale, scale, w/2, h);
219            if (CHATTY) Log.i(TAG, "w=" + w + " h=" + h + " scale=" + scale
220                    + ": " + mUniverseTransform);
221            sendUniverseTransform();
222            if (getVisibility() != VISIBLE) {
223                setVisibility(VISIBLE);
224            }
225        } else {
226            if (CHATTY) Log.i(TAG, "mYDelta=" + mYDelta);
227            mUniverseTransform.clear();
228            sendUniverseTransform();
229            if (getVisibility() == VISIBLE) {
230                setVisibility(GONE);
231            }
232        }
233    }
234
235    void resetLastAnimTime() {
236        mAnimLastTimeNanos = System.nanoTime();
237        if (SPEW) {
238            Throwable t = new Throwable();
239            t.fillInStackTrace();
240            Slog.d(TAG, "resetting last anim time=" + mAnimLastTimeNanos, t);
241        }
242    }
243
244    void doAnimation(long frameTimeNanos) {
245        if (mAnimating) {
246            if (SPEW) Slog.d(TAG, "doAnimation dt=" + (frameTimeNanos - mAnimLastTimeNanos));
247            if (SPEW) Slog.d(TAG, "doAnimation before mAnimY=" + mAnimY);
248            incrementAnim(frameTimeNanos);
249            if (SPEW) {
250                Slog.d(TAG, "doAnimation after  mAnimY=" + mAnimY);
251            }
252
253            if (mAnimY >= getExpandedViewMaxHeight()-1 && !mClosing) {
254                if (SPEW) Slog.d(TAG, "Animation completed to expanded state.");
255                mAnimating = false;
256                mYDelta = getExpandedViewMaxHeight();
257                updateUniverseScale();
258                mExpanded = true;
259                mState = STATE_OPEN;
260                return;
261            }
262
263            if (mAnimY <= 0 && mClosing) {
264                if (SPEW) Slog.d(TAG, "Animation completed to collapsed state.");
265                mAnimating = false;
266                mYDelta = 0;
267                updateUniverseScale();
268                mExpanded = false;
269                mState = STATE_CLOSED;
270                return;
271            }
272
273            mYDelta = (int)mAnimY;
274            updateUniverseScale();
275            mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION,
276                    mAnimationCallback, null);
277        }
278    }
279
280    void stopTracking() {
281        mTracking = false;
282        mVelocityTracker.recycle();
283        mVelocityTracker = null;
284    }
285
286    void incrementAnim(long frameTimeNanos) {
287        final long deltaNanos = Math.max(frameTimeNanos - mAnimLastTimeNanos, 0);
288        final float t = deltaNanos * 0.000000001f;                  // ns -> s
289        final float y = mAnimY;
290        final float v = mAnimVel;                                   // px/s
291        final float a = mAnimAccel;                                 // px/s/s
292        mAnimY = y + (v*t) + (0.5f*a*t*t);                          // px
293        mAnimVel = v + (a*t);                                       // px/s
294        mAnimLastTimeNanos = frameTimeNanos;                        // ns
295        //Slog.d(TAG, "y=" + y + " v=" + v + " a=" + a + " t=" + t + " mAnimY=" + mAnimY
296        //        + " mAnimAccel=" + mAnimAccel);
297    }
298
299    void prepareTracking(int y, boolean opening) {
300        if (CHATTY) {
301            Slog.d(TAG, "panel: beginning to track the user's touch, y=" + y + " opening=" + opening);
302        }
303
304        mTracking = true;
305        mVelocityTracker = VelocityTracker.obtain();
306        if (opening) {
307            mAnimAccel = mExpandAccelPx;
308            mAnimVel = mFlingExpandMinVelocityPx;
309            mAnimY = y;
310            mAnimating = true;
311            mAnimatingReveal = true;
312            resetLastAnimTime();
313            mExpandedVisible = true;
314        }
315        if (mAnimating) {
316            mAnimating = false;
317            mChoreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION,
318                    mAnimationCallback, null);
319        }
320    }
321
322    void performFling(int y, float vel, boolean always) {
323        if (CHATTY) {
324            Slog.d(TAG, "panel: will fling, y=" + y + " vel=" + vel);
325        }
326
327        mAnimatingReveal = false;
328
329        mAnimY = y;
330        mAnimVel = vel;
331
332        //Slog.d(TAG, "starting with mAnimY=" + mAnimY + " mAnimVel=" + mAnimVel);
333
334        if (mExpanded) {
335            if (!always && (
336                    vel > mFlingCollapseMinVelocityPx
337                    || (y > (getExpandedViewMaxHeight()*(1f-mCollapseMinDisplayFraction)) &&
338                        vel > -mFlingExpandMinVelocityPx))) {
339                // We are expanded, but they didn't move sufficiently to cause
340                // us to retract.  Animate back to the expanded position.
341                mAnimAccel = mExpandAccelPx;
342                if (vel < 0) {
343                    mAnimVel = 0;
344                }
345            }
346            else {
347                // We are expanded and are now going to animate away.
348                mAnimAccel = -mCollapseAccelPx;
349                if (vel > 0) {
350                    mAnimVel = 0;
351                }
352            }
353        } else {
354            if (always || (
355                    vel > mFlingExpandMinVelocityPx
356                    || (y > (getExpandedViewMaxHeight()*(1f-mExpandMinDisplayFraction)) &&
357                        vel > -mFlingCollapseMinVelocityPx))) {
358                // We are collapsed, and they moved enough to allow us to
359                // expand.  Animate in the notifications.
360                mAnimAccel = mExpandAccelPx;
361                if (vel < 0) {
362                    mAnimVel = 0;
363                }
364            }
365            else {
366                // We are collapsed, but they didn't move sufficiently to cause
367                // us to retract.  Animate back to the collapsed position.
368                mAnimAccel = -mCollapseAccelPx;
369                if (vel > 0) {
370                    mAnimVel = 0;
371                }
372            }
373        }
374        //Slog.d(TAG, "mAnimY=" + mAnimY + " mAnimVel=" + mAnimVel
375        //        + " mAnimAccel=" + mAnimAccel);
376
377        resetLastAnimTime();
378        mAnimating = true;
379        mClosing = mAnimAccel < 0;
380        mChoreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION,
381                mAnimationCallback, null);
382        mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION,
383                mAnimationCallback, null);
384
385        stopTracking();
386    }
387
388    private void trackMovement(MotionEvent event) {
389        mVelocityTracker.addMovement(event);
390    }
391
392    public boolean consumeEvent(MotionEvent event) {
393        if (mState == STATE_CLOSED) {
394            if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
395                // Second finger down, time to start opening!
396                computeAveragePos(event);
397                mDragStartX = mAverageX;
398                mDragStartY = mAverageY;
399                mYDelta = 0;
400                mUniverseTransform.clear();
401                sendUniverseTransform();
402                setVisibility(VISIBLE);
403                mState = STATE_OPENING;
404                prepareTracking((int)mDragStartY, true);
405                mVelocityTracker.clear();
406                trackMovement(event);
407                return true;
408            }
409            return false;
410        }
411
412        if (mState == STATE_OPENING) {
413            if (event.getActionMasked() == MotionEvent.ACTION_UP
414                    || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
415                mVelocityTracker.computeCurrentVelocity(1000);
416                computeAveragePos(event);
417
418                float yVel = mVelocityTracker.getYVelocity();
419                boolean negative = yVel < 0;
420
421                float xVel = mVelocityTracker.getXVelocity();
422                if (xVel < 0) {
423                    xVel = -xVel;
424                }
425                if (xVel > mFlingGestureMaxXVelocityPx) {
426                    xVel = mFlingGestureMaxXVelocityPx; // limit how much we care about the x axis
427                }
428
429                float vel = (float)Math.hypot(yVel, xVel);
430                if (negative) {
431                    vel = -vel;
432                }
433
434                if (CHATTY) {
435                    Slog.d(TAG, String.format("gesture: vraw=(%f,%f) vnorm=(%f,%f) vlinear=%f",
436                        mVelocityTracker.getXVelocity(),
437                        mVelocityTracker.getYVelocity(),
438                        xVel, yVel,
439                        vel));
440                }
441
442                performFling((int)mAverageY, vel, false);
443                mState = STATE_OPEN;
444                return true;
445            }
446
447            computeAveragePos(event);
448            mYDelta = (int)(mAverageY - mDragStartY);
449            if (mYDelta > getExpandedViewMaxHeight()) {
450                mYDelta = getExpandedViewMaxHeight();
451            }
452            updateUniverseScale();
453            return true;
454        }
455
456        return false;
457    }
458}
459