1/*
2 * Copyright (C) 2010 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.statusbar.tablet;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.content.Context;
24import android.graphics.Rect;
25import android.util.AttributeSet;
26import android.util.Slog;
27import android.view.Gravity;
28import android.view.KeyEvent;
29import android.view.LayoutInflater;
30import android.view.MotionEvent;
31import android.view.View;
32import android.view.ViewGroup;
33import android.view.ViewTreeObserver;
34import android.view.animation.AccelerateInterpolator;
35import android.view.animation.DecelerateInterpolator;
36import android.view.animation.Interpolator;
37import android.widget.RelativeLayout;
38
39import com.android.systemui.ExpandHelper;
40import com.android.systemui.R;
41import com.android.systemui.statusbar.policy.NotificationRowLayout;
42
43public class NotificationPanel extends RelativeLayout implements StatusBarPanel,
44        View.OnClickListener {
45    private ExpandHelper mExpandHelper;
46    private NotificationRowLayout latestItems;
47
48    static final String TAG = "Tablet/NotificationPanel";
49    static final boolean DEBUG = false;
50
51    final static int PANEL_FADE_DURATION = 150;
52
53    boolean mShowing;
54    boolean mHasClearableNotifications = false;
55    int mNotificationCount = 0;
56    NotificationPanelTitle mTitleArea;
57    View mSettingsButton;
58    View mNotificationButton;
59    View mNotificationScroller;
60    ViewGroup mContentFrame;
61    Rect mContentArea = new Rect();
62    View mSettingsView;
63    ViewGroup mContentParent;
64    TabletStatusBar mBar;
65    View mClearButton;
66    static Interpolator sAccelerateInterpolator = new AccelerateInterpolator();
67    static Interpolator sDecelerateInterpolator = new DecelerateInterpolator();
68
69    // amount to slide mContentParent down by when mContentFrame is missing
70    float mContentFrameMissingTranslation;
71
72    Choreographer mChoreo = new Choreographer();
73
74    public NotificationPanel(Context context, AttributeSet attrs) {
75        this(context, attrs, 0);
76    }
77
78    public NotificationPanel(Context context, AttributeSet attrs, int defStyle) {
79        super(context, attrs, defStyle);
80    }
81
82    public void setBar(TabletStatusBar b) {
83        mBar = b;
84    }
85
86    @Override
87    public void onFinishInflate() {
88        super.onFinishInflate();
89
90        setWillNotDraw(false);
91
92        mContentParent = (ViewGroup)findViewById(R.id.content_parent);
93        mContentParent.bringToFront();
94        mTitleArea = (NotificationPanelTitle) findViewById(R.id.title_area);
95        mTitleArea.setPanel(this);
96
97        mSettingsButton = findViewById(R.id.settings_button);
98        mNotificationButton = findViewById(R.id.notification_button);
99
100        mNotificationScroller = findViewById(R.id.notification_scroller);
101        mContentFrame = (ViewGroup)findViewById(R.id.content_frame);
102        mContentFrameMissingTranslation = 0; // not needed with current assets
103
104        // the "X" that appears in place of the clock when the panel is showing notifications
105        mClearButton = findViewById(R.id.clear_all_button);
106        mClearButton.setOnClickListener(mClearButtonListener);
107
108        mShowing = false;
109    }
110
111    @Override
112    protected void onAttachedToWindow () {
113        super.onAttachedToWindow();
114        latestItems = (NotificationRowLayout) findViewById(R.id.content);
115        int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_min_height);
116        int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_max_height);
117        mExpandHelper = new ExpandHelper(mContext, latestItems, minHeight, maxHeight);
118        mExpandHelper.setEventSource(this);
119        mExpandHelper.setGravity(Gravity.BOTTOM);
120    }
121
122    private View.OnClickListener mClearButtonListener = new View.OnClickListener() {
123        public void onClick(View v) {
124            mBar.clearAll();
125        }
126    };
127
128    public View getClearButton() {
129        return mClearButton;
130    }
131
132    public void show(boolean show, boolean animate) {
133        if (animate) {
134            if (mShowing != show) {
135                mShowing = show;
136                if (show) {
137                    setVisibility(View.VISIBLE);
138                    // Don't start the animation until we've created the layer, which is done
139                    // right before we are drawn
140                    mContentParent.setLayerType(View.LAYER_TYPE_HARDWARE, null);
141                    getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
142                } else {
143                    mChoreo.startAnimation(show);
144                }
145            }
146        } else {
147            mShowing = show;
148            setVisibility(show ? View.VISIBLE : View.GONE);
149        }
150    }
151
152    /**
153     * This is used only when we've created a hardware layer and are waiting until it's
154     * been created in order to start the appearing animation.
155     */
156    private ViewTreeObserver.OnPreDrawListener mPreDrawListener =
157            new ViewTreeObserver.OnPreDrawListener() {
158        @Override
159        public boolean onPreDraw() {
160            getViewTreeObserver().removeOnPreDrawListener(this);
161            mChoreo.startAnimation(true);
162            return false;
163        }
164    };
165
166    /**
167     * Whether the panel is showing, or, if it's animating, whether it will be
168     * when the animation is done.
169     */
170    public boolean isShowing() {
171        return mShowing;
172    }
173
174    @Override
175    public void onVisibilityChanged(View v, int vis) {
176        super.onVisibilityChanged(v, vis);
177        // when we hide, put back the notifications
178        if (vis != View.VISIBLE) {
179            if (mSettingsView != null) removeSettingsView();
180            mNotificationScroller.setVisibility(View.VISIBLE);
181            mNotificationScroller.setAlpha(1f);
182            mNotificationScroller.scrollTo(0, 0);
183            updatePanelModeButtons();
184        }
185    }
186
187    @Override
188    public boolean dispatchHoverEvent(MotionEvent event) {
189        // Ignore hover events outside of this panel bounds since such events
190        // generate spurious accessibility events with the panel content when
191        // tapping outside of it, thus confusing the user.
192        final int x = (int) event.getX();
193        final int y = (int) event.getY();
194        if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) {
195            return super.dispatchHoverEvent(event);
196        }
197        return true;
198    }
199
200    @Override
201    public boolean dispatchKeyEvent(KeyEvent event) {
202    final int keyCode = event.getKeyCode();
203        switch (keyCode) {
204            // We exclusively handle the back key by hiding this panel.
205            case KeyEvent.KEYCODE_BACK: {
206                if (event.getAction() == KeyEvent.ACTION_UP) {
207                    mBar.animateCollapsePanels();
208                }
209                return true;
210            }
211            // We react to the home key but let the system handle it.
212            case KeyEvent.KEYCODE_HOME: {
213                if (event.getAction() == KeyEvent.ACTION_UP) {
214                    mBar.animateCollapsePanels();
215                }
216            } break;
217        }
218        return super.dispatchKeyEvent(event);
219    }
220
221    /*
222    @Override
223    protected void onLayout(boolean changed, int l, int t, int r, int b) {
224        super.onLayout(changed, l, t, r, b);
225
226        if (DEBUG) Slog.d(TAG, String.format("PANEL: onLayout: (%d, %d, %d, %d)", l, t, r, b));
227    }
228
229    @Override
230    public void onSizeChanged(int w, int h, int oldw, int oldh) {
231        super.onSizeChanged(w, h, oldw, oldh);
232
233        if (DEBUG) {
234            Slog.d(TAG, String.format("PANEL: onSizeChanged: (%d -> %d, %d -> %d)",
235                        oldw, w, oldh, h));
236        }
237    }
238    */
239
240    public void onClick(View v) {
241        if (mSettingsButton.isEnabled() && v == mTitleArea) {
242            swapPanels();
243        }
244    }
245
246    public void setNotificationCount(int n) {
247        mNotificationCount = n;
248    }
249
250    public void setContentFrameVisible(final boolean showing, boolean animate) {
251    }
252
253    public void swapPanels() {
254        final View toShow, toHide;
255        if (mSettingsView == null) {
256            addSettingsView();
257            toShow = mSettingsView;
258            toHide = mNotificationScroller;
259        } else {
260            toShow = mNotificationScroller;
261            toHide = mSettingsView;
262        }
263        Animator a = ObjectAnimator.ofFloat(toHide, "alpha", 1f, 0f)
264                .setDuration(PANEL_FADE_DURATION);
265        a.addListener(new AnimatorListenerAdapter() {
266            @Override
267            public void onAnimationEnd(Animator _a) {
268                toHide.setVisibility(View.GONE);
269                if (toShow != null) {
270                    toShow.setVisibility(View.VISIBLE);
271                    if (toShow == mSettingsView || mNotificationCount > 0) {
272                        ObjectAnimator.ofFloat(toShow, "alpha", 0f, 1f)
273                                .setDuration(PANEL_FADE_DURATION)
274                                .start();
275                    }
276
277                    if (toHide == mSettingsView) {
278                        removeSettingsView();
279                    }
280                }
281                updateClearButton();
282                updatePanelModeButtons();
283            }
284        });
285        a.start();
286    }
287
288    public void updateClearButton() {
289        if (mBar != null) {
290            final boolean showX
291                = (isShowing()
292                        && mHasClearableNotifications
293                        && mNotificationScroller.getVisibility() == View.VISIBLE);
294            getClearButton().setVisibility(showX ? View.VISIBLE : View.INVISIBLE);
295        }
296    }
297
298    public void setClearable(boolean clearable) {
299        mHasClearableNotifications = clearable;
300    }
301
302    public void updatePanelModeButtons() {
303        final boolean settingsVisible = (mSettingsView != null);
304        mSettingsButton.setVisibility(!settingsVisible && mSettingsButton.isEnabled() ? View.VISIBLE : View.GONE);
305        mNotificationButton.setVisibility(settingsVisible ? View.VISIBLE : View.GONE);
306    }
307
308    public boolean isInContentArea(int x, int y) {
309        mContentArea.left = mContentFrame.getLeft() + mContentFrame.getPaddingLeft();
310        mContentArea.top = mContentFrame.getTop() + mContentFrame.getPaddingTop()
311            + (int)mContentParent.getTranslationY(); // account for any adjustment
312        mContentArea.right = mContentFrame.getRight() - mContentFrame.getPaddingRight();
313        mContentArea.bottom = mContentFrame.getBottom() - mContentFrame.getPaddingBottom();
314
315        offsetDescendantRectToMyCoords(mContentParent, mContentArea);
316        return mContentArea.contains(x, y);
317    }
318
319    void removeSettingsView() {
320        if (mSettingsView != null) {
321            mContentFrame.removeView(mSettingsView);
322            mSettingsView = null;
323        }
324    }
325
326    // NB: it will be invisible until you show it
327    void addSettingsView() {
328        LayoutInflater infl = LayoutInflater.from(getContext());
329        mSettingsView = infl.inflate(R.layout.system_bar_settings_view, mContentFrame, false);
330        mSettingsView.setVisibility(View.GONE);
331        mContentFrame.addView(mSettingsView);
332    }
333
334    private class Choreographer implements Animator.AnimatorListener {
335        boolean mVisible;
336        int mPanelHeight;
337        AnimatorSet mContentAnim;
338
339        // should group this into a multi-property animation
340        final static int OPEN_DURATION = 250;
341        final static int CLOSE_DURATION = 250;
342
343        // the panel will start to appear this many px from the end
344        final int HYPERSPACE_OFFRAMP = 200;
345
346        Choreographer() {
347        }
348
349        void createAnimation(boolean appearing) {
350            // mVisible: previous state; appearing: new state
351
352            float start, end;
353
354            // 0: on-screen
355            // height: off-screen
356            float y = mContentParent.getTranslationY();
357            if (appearing) {
358                // we want to go from near-the-top to the top, unless we're half-open in the right
359                // general vicinity
360                end = 0;
361                if (mNotificationCount == 0) {
362                    end += mContentFrameMissingTranslation;
363                }
364                start = HYPERSPACE_OFFRAMP+end;
365            } else {
366                start = y;
367                end = y + HYPERSPACE_OFFRAMP;
368            }
369
370            Animator posAnim = ObjectAnimator.ofFloat(mContentParent, "translationY",
371                    start, end);
372            posAnim.setInterpolator(appearing ? sDecelerateInterpolator : sAccelerateInterpolator);
373
374            if (mContentAnim != null && mContentAnim.isRunning()) {
375                mContentAnim.cancel();
376            }
377
378            Animator fadeAnim = ObjectAnimator.ofFloat(mContentParent, "alpha",
379                    appearing ? 1.0f : 0.0f);
380            fadeAnim.setInterpolator(appearing ? sAccelerateInterpolator : sDecelerateInterpolator);
381
382            mContentAnim = new AnimatorSet();
383            mContentAnim
384                .play(fadeAnim)
385                .with(posAnim)
386                ;
387            mContentAnim.setDuration((DEBUG?10:1)*(appearing ? OPEN_DURATION : CLOSE_DURATION));
388            mContentAnim.addListener(this);
389        }
390
391        void startAnimation(boolean appearing) {
392            if (DEBUG) Slog.d(TAG, "startAnimation(appearing=" + appearing + ")");
393
394            createAnimation(appearing);
395            mContentAnim.start();
396
397            mVisible = appearing;
398
399            // we want to start disappearing promptly
400            if (!mVisible) updateClearButton();
401        }
402
403        public void onAnimationCancel(Animator animation) {
404            if (DEBUG) Slog.d(TAG, "onAnimationCancel");
405        }
406
407        public void onAnimationEnd(Animator animation) {
408            if (DEBUG) Slog.d(TAG, "onAnimationEnd");
409            if (! mVisible) {
410                setVisibility(View.GONE);
411            }
412            mContentParent.setLayerType(View.LAYER_TYPE_NONE, null);
413            mContentAnim = null;
414
415            // we want to show the X lazily
416            if (mVisible) updateClearButton();
417        }
418
419        public void onAnimationRepeat(Animator animation) {
420        }
421
422        public void onAnimationStart(Animator animation) {
423        }
424    }
425
426    @Override
427    public boolean onInterceptTouchEvent(MotionEvent ev) {
428        MotionEvent cancellation = MotionEvent.obtain(ev);
429        cancellation.setAction(MotionEvent.ACTION_CANCEL);
430
431        boolean intercept = mExpandHelper.onInterceptTouchEvent(ev) ||
432                super.onInterceptTouchEvent(ev);
433        if (intercept) {
434            latestItems.onInterceptTouchEvent(cancellation);
435        }
436        return intercept;
437    }
438
439    @Override
440    public boolean onTouchEvent(MotionEvent ev) {
441        boolean handled = mExpandHelper.onTouchEvent(ev) ||
442                super.onTouchEvent(ev);
443        return handled;
444    }
445
446    public void setSettingsEnabled(boolean settingsEnabled) {
447        if (mSettingsButton != null) {
448            mSettingsButton.setEnabled(settingsEnabled);
449            mSettingsButton.setVisibility(settingsEnabled ? View.VISIBLE : View.GONE);
450        }
451    }
452}
453
454