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