1/*
2 * Copyright (C) 2015 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.tv.ui;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ArgbEvaluator;
22import android.animation.ObjectAnimator;
23import android.animation.TimeInterpolator;
24import android.animation.TypeEvaluator;
25import android.animation.ValueAnimator;
26import android.animation.ValueAnimator.AnimatorUpdateListener;
27import android.annotation.SuppressLint;
28import android.content.Context;
29import android.content.SharedPreferences;
30import android.content.res.Resources;
31import android.graphics.Point;
32import android.hardware.display.DisplayManager;
33import android.os.Handler;
34import android.preference.PreferenceManager;
35import android.util.Log;
36import android.util.Property;
37import android.view.Display;
38import android.view.Gravity;
39import android.view.View;
40import android.view.ViewGroup;
41import android.view.ViewGroup.LayoutParams;
42import android.view.ViewGroup.MarginLayoutParams;
43import android.view.animation.AnimationUtils;
44import android.widget.FrameLayout;
45
46import com.android.tv.R;
47import com.android.tv.TvOptionsManager;
48import com.android.tv.data.DisplayMode;
49import com.android.tv.util.TvSettings;
50import com.android.tv.util.Utils;
51
52/**
53 * The TvViewUiManager is responsible for handling UI layouting and animation of main and PIP
54 * TvViews. It also control the settings regarding TvView UI such as display mode, PIP layout,
55 * and PIP size.
56 */
57public class TvViewUiManager {
58    private static final String TAG = "TvViewManager";
59    private static final boolean DEBUG = false;
60
61    private static final float DISPLAY_MODE_EPSILON = 0.001f;
62    private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f;
63
64    private final Context mContext;
65    private final Resources mResources;
66    private final FrameLayout mContentView;
67    private final TunableTvView mTvView;
68    private final TunableTvView mPipView;
69    private final TvOptionsManager mTvOptionsManager;
70    private final int mTvViewPapWidth;
71    private final int mTvViewShrunkenStartMargin;
72    private final int mTvViewShrunkenEndMargin;
73    private final int mTvViewPapStartMargin;
74    private final int mTvViewPapEndMargin;
75    private int mWindowWidth;
76    private int mWindowHeight;
77    private final int mPipViewHorizontalMargin;
78    private final int mPipViewTopMargin;
79    private final int mPipViewBottomMargin;
80    private final SharedPreferences mSharedPreferences;
81    private final TimeInterpolator mLinearOutSlowIn;
82    private final TimeInterpolator mFastOutLinearIn;
83    private final Handler mHandler = new Handler();
84    private int mDisplayMode;
85    // Used to restore the previous state from ShrunkenTvView state.
86    private int mTvViewStartMarginBeforeShrunken;
87    private int mTvViewEndMarginBeforeShrunken;
88    private int mDisplayModeBeforeShrunken;
89    private boolean mIsUnderShrunkenTvView;
90    private int mTvViewStartMargin;
91    private int mTvViewEndMargin;
92    private int mPipLayout;
93    private int mPipSize;
94    private boolean mPipStarted;
95    private ObjectAnimator mTvViewAnimator;
96    private FrameLayout.LayoutParams mTvViewLayoutParams;
97    // TV view's position when the display mode is FULL. It is used to compute PIP location relative
98    // to TV view's position.
99    private MarginLayoutParams mTvViewFrame;
100    private MarginLayoutParams mLastAnimatedTvViewFrame;
101    private MarginLayoutParams mOldTvViewFrame;
102    private ObjectAnimator mBackgroundAnimator;
103    private int mBackgroundColor;
104    private int mAppliedDisplayedMode = DisplayMode.MODE_NOT_DEFINED;
105    private int mAppliedTvViewStartMargin;
106    private int mAppliedTvViewEndMargin;
107    private float mAppliedVideoDisplayAspectRatio;
108
109    public TvViewUiManager(Context context, TunableTvView tvView, TunableTvView pipView,
110            FrameLayout contentView, TvOptionsManager tvOptionManager) {
111        mContext = context;
112        mResources = mContext.getResources();
113        mTvView = tvView;
114        mPipView = pipView;
115        mContentView = contentView;
116        mTvOptionsManager = tvOptionManager;
117
118        DisplayManager displayManager = (DisplayManager) mContext
119                .getSystemService(Context.DISPLAY_SERVICE);
120        Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
121        Point size = new Point();
122        display.getSize(size);
123        mWindowWidth = size.x;
124        mWindowHeight = size.y;
125
126        // Have an assumption that PIP and TvView Shrinking happens only in full screen.
127        mTvViewShrunkenStartMargin = mResources
128                .getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_start);
129        mTvViewShrunkenEndMargin =
130                mResources.getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_end)
131                        + mResources.getDimensionPixelSize(R.dimen.side_panel_width);
132        int papMarginHorizontal = mResources
133                .getDimensionPixelOffset(R.dimen.papview_margin_horizontal);
134        int papSpacing = mResources.getDimensionPixelOffset(R.dimen.papview_spacing);
135        mTvViewPapWidth = (mWindowWidth - papSpacing) / 2 - papMarginHorizontal;
136        mTvViewPapStartMargin = papMarginHorizontal + mTvViewPapWidth + papSpacing;
137        mTvViewPapEndMargin = papMarginHorizontal;
138        mTvViewFrame = createMarginLayoutParams(0, 0, 0, 0);
139
140        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
141
142        mLinearOutSlowIn = AnimationUtils
143                .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in);
144        mFastOutLinearIn = AnimationUtils
145                .loadInterpolator(mContext, android.R.interpolator.fast_out_linear_in);
146
147        mPipViewHorizontalMargin = mResources
148                .getDimensionPixelOffset(R.dimen.pipview_margin_horizontal);
149        mPipViewTopMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_top);
150        mPipViewBottomMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_bottom);
151        mContentView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
152            @Override
153            public void onLayoutChange(View v, int left, int top, int right, int bottom,
154                    int oldLeft, int oldTop, int oldRight, int oldBottom) {
155                int windowWidth = right - left;
156                int windowHeight = bottom - top;
157                if (windowWidth > 0 && windowHeight > 0) {
158                    if (mWindowWidth != windowWidth || mWindowHeight != windowHeight) {
159                        mWindowWidth = windowWidth;
160                        mWindowHeight = windowHeight;
161                        applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), false, true);
162                    }
163                }
164            }
165        });
166    }
167
168    /**
169     * Initializes animator in advance of using the animator to improve animation performance.
170     * For fast first tune, it is not expected to be called in Activity.onCreate, but called
171     * a few seconds later after onCreate.
172     */
173    public void initAnimatorIfNeeded() {
174        initTvAnimatorIfNeeded();
175        initBackgroundAnimatorIfNeeded();
176    }
177
178    /**
179     * It is called when shrunken TvView is desired, such as EditChannelFragment and
180     * ChannelsLockedFragment.
181     */
182    public void startShrunkenTvView() {
183        mIsUnderShrunkenTvView = true;
184
185        mTvViewStartMarginBeforeShrunken = mTvViewStartMargin;
186        mTvViewEndMarginBeforeShrunken = mTvViewEndMargin;
187        if (mPipStarted && getPipLayout() == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) {
188            float sidePanelWidth = mResources.getDimensionPixelOffset(R.dimen.side_panel_width);
189            float factor = 1.0f - sidePanelWidth / mWindowWidth;
190            int startMargin = (int) (mTvViewPapStartMargin * factor);
191            int endMargin = (int) (mTvViewPapEndMargin * factor + sidePanelWidth);
192            setTvViewMargin(startMargin, endMargin);
193        } else {
194            setTvViewMargin(mTvViewShrunkenStartMargin, mTvViewShrunkenEndMargin);
195        }
196        mDisplayModeBeforeShrunken = setDisplayMode(DisplayMode.MODE_NORMAL, false, true);
197    }
198
199    /**
200     * It is called when shrunken TvView is no longer desired, such as EditChannelFragment and
201     * ChannelsLockedFragment.
202     */
203    public void endShrunkenTvView() {
204        mIsUnderShrunkenTvView = false;
205        setTvViewMargin(mTvViewStartMarginBeforeShrunken, mTvViewEndMarginBeforeShrunken);
206        setDisplayMode(mDisplayModeBeforeShrunken, false, true);
207    }
208
209    /**
210     * Returns true, if TvView is shrunken.
211     */
212    public boolean isUnderShrunkenTvView() {
213        return mIsUnderShrunkenTvView;
214    }
215
216    /**
217     * Returns true, if {@code displayMode} is available now. If screen ratio is matched to
218     * video ratio, other display modes than {@link DisplayMode#MODE_NORMAL} are not available.
219     */
220    public boolean isDisplayModeAvailable(int displayMode) {
221        if (displayMode == DisplayMode.MODE_NORMAL) {
222            return true;
223        }
224
225        int viewWidth = mContentView.getWidth();
226        int viewHeight = mContentView.getHeight();
227
228        float videoDisplayAspectRatio = mTvView.getVideoDisplayAspectRatio();
229        if (viewWidth <= 0 || viewHeight <= 0 || videoDisplayAspectRatio <= 0f) {
230            Log.w(TAG, "Video size is currently unavailable");
231            if (DEBUG) {
232                Log.d(TAG, "isDisplayModeAvailable: "
233                        + "viewWidth=" + viewWidth
234                        + ", viewHeight=" + viewHeight
235                        + ", videoDisplayAspectRatio=" + videoDisplayAspectRatio
236                );
237            }
238            return false;
239        }
240
241        float viewRatio = viewWidth / (float) viewHeight;
242        return Math.abs(viewRatio - videoDisplayAspectRatio) >= DISPLAY_MODE_EPSILON;
243    }
244
245    /**
246     * Returns a constant defined in DisplayMode.
247     */
248    public int getDisplayMode() {
249        if (isDisplayModeAvailable(mDisplayMode)) {
250            return mDisplayMode;
251        }
252        return DisplayMode.MODE_NORMAL;
253    }
254
255    /**
256     * Sets the display mode to the given value.
257     *
258     * @return the previous display mode.
259     */
260    public int setDisplayMode(int displayMode, boolean storeInPreference, boolean animate) {
261        int prev = mDisplayMode;
262        mDisplayMode = displayMode;
263        if (storeInPreference) {
264            mSharedPreferences.edit().putInt(TvSettings.PREF_DISPLAY_MODE, displayMode).apply();
265        }
266        applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), animate, false);
267        return prev;
268    }
269
270    /**
271     * Restores the display mode to the display mode stored in preference.
272     */
273    public void restoreDisplayMode(boolean animate) {
274        int displayMode = mSharedPreferences
275                .getInt(TvSettings.PREF_DISPLAY_MODE, DisplayMode.MODE_NORMAL);
276        setDisplayMode(displayMode, false, animate);
277    }
278
279    /**
280     * Updates TvView. It is called when video resolution is updated.
281     */
282    public void updateTvView() {
283        applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), false, false);
284        if (mTvView.isVideoAvailable() && mTvView.isFadedOut()) {
285            mTvView.fadeIn(mResources.getInteger(R.integer.tvview_fade_in_duration),
286                    mFastOutLinearIn, null);
287        }
288    }
289
290    /**
291     * Fades in TvView.
292     */
293    public void fadeInTvView() {
294        if (mTvView.isFadedOut()) {
295            mTvView.fadeIn(mResources.getInteger(R.integer.tvview_fade_in_duration),
296                    mFastOutLinearIn, null);
297        }
298    }
299
300    /**
301     * Fades out TvView.
302     */
303    public void fadeOutTvView(Runnable postAction) {
304        if (!mTvView.isFadedOut()) {
305            mTvView.fadeOut(mResources.getInteger(R.integer.tvview_fade_out_duration),
306                    mLinearOutSlowIn, postAction);
307        }
308    }
309
310    /**
311     * Returns the current PIP layout. The layout should be one of
312     * {@link TvSettings#PIP_LAYOUT_BOTTOM_RIGHT}, {@link TvSettings#PIP_LAYOUT_TOP_RIGHT},
313     * {@link TvSettings#PIP_LAYOUT_TOP_LEFT}, {@link TvSettings#PIP_LAYOUT_BOTTOM_LEFT} and
314     * {@link TvSettings#PIP_LAYOUT_SIDE_BY_SIDE}.
315     */
316    public int getPipLayout() {
317        return mPipLayout;
318    }
319
320    /**
321     * Sets the PIP layout. The layout should be one of
322     * {@link TvSettings#PIP_LAYOUT_BOTTOM_RIGHT}, {@link TvSettings#PIP_LAYOUT_TOP_RIGHT},
323     * {@link TvSettings#PIP_LAYOUT_TOP_LEFT}, {@link TvSettings#PIP_LAYOUT_BOTTOM_LEFT} and
324     * {@link TvSettings#PIP_LAYOUT_SIDE_BY_SIDE}.
325     *
326     * @param storeInPreference if true, the stored value will be restored by
327     *                          {@link #restorePipLayout()}.
328     */
329    public void setPipLayout(int pipLayout, boolean storeInPreference) {
330        mPipLayout = pipLayout;
331        if (storeInPreference) {
332            TvSettings.setPipLayout(mContext, pipLayout);
333        }
334        updatePipView(mTvViewFrame);
335        if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) {
336            setTvViewMargin(mTvViewPapStartMargin, mTvViewPapEndMargin);
337            setDisplayMode(DisplayMode.MODE_NORMAL, false, false);
338        } else {
339            setTvViewMargin(0, 0);
340            restoreDisplayMode(false);
341        }
342        mTvOptionsManager.onPipLayoutChanged(pipLayout);
343    }
344
345    /**
346     * Restores the PIP layout which {@link #setPipLayout} lastly stores.
347     */
348    public void restorePipLayout() {
349        setPipLayout(TvSettings.getPipLayout(mContext), false);
350    }
351
352    /**
353     * Called when PIP is started.
354     */
355    public void onPipStart() {
356        mPipStarted = true;
357        updatePipView();
358        mPipView.setVisibility(View.VISIBLE);
359    }
360
361    /**
362     * Called when PIP is stopped.
363     */
364    public void onPipStop() {
365        setTvViewMargin(0, 0);
366        mPipView.setVisibility(View.GONE);
367        mPipStarted = false;
368    }
369
370    /**
371     * Called when PIP is resumed.
372     */
373    public void showPipForResume() {
374        mPipView.setVisibility(View.VISIBLE);
375    }
376
377    /**
378     * Called when PIP is paused.
379     */
380    public void hidePipForPause() {
381        if (mPipLayout != TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) {
382            mPipView.setVisibility(View.GONE);
383        }
384    }
385
386    /**
387     * Updates PIP view. It is usually called, when video resolution in PIP is updated.
388     */
389    public void updatePipView() {
390        updatePipView(mTvViewFrame);
391    }
392
393    /**
394     * Returns the size of the PIP view.
395     */
396    public int getPipSize() {
397        return mPipSize;
398    }
399
400    /**
401     * Sets PIP size and applies it immediately.
402     *
403     * @param pipSize           PIP size. The value should be one of {@link TvSettings#PIP_SIZE_BIG}
404     *                          and {@link TvSettings#PIP_SIZE_SMALL}.
405     * @param storeInPreference if true, the stored value will be restored by
406     *                          {@link #restorePipSize()}.
407     */
408    public void setPipSize(int pipSize, boolean storeInPreference) {
409        mPipSize = pipSize;
410        if (storeInPreference) {
411            TvSettings.setPipSize(mContext, pipSize);
412        }
413        updatePipView(mTvViewFrame);
414        mTvOptionsManager.onPipSizeChanged(pipSize);
415    }
416
417    /**
418     * Restores the PIP size which {@link #setPipSize} lastly stores.
419     */
420    public void restorePipSize() {
421        setPipSize(TvSettings.getPipSize(mContext), false);
422    }
423
424    /**
425     * This margins will be applied when applyDisplayMode is called.
426     */
427    private void setTvViewMargin(int tvViewStartMargin, int tvViewEndMargin) {
428        mTvViewStartMargin = tvViewStartMargin;
429        mTvViewEndMargin = tvViewEndMargin;
430    }
431
432    private boolean isTvViewFullScreen() {
433        return mTvViewStartMargin == 0 && mTvViewEndMargin == 0;
434    }
435
436    private void setBackgroundColor(int color, FrameLayout.LayoutParams targetLayoutParams,
437            boolean animate) {
438        if (animate) {
439            initBackgroundAnimatorIfNeeded();
440            if (mBackgroundAnimator.isStarted()) {
441                // Cancel the current animation and start new one.
442                mBackgroundAnimator.cancel();
443            }
444
445            int decorViewWidth = mContentView.getWidth();
446            int decorViewHeight = mContentView.getHeight();
447            boolean hasPillarBox = mTvView.getWidth() != decorViewWidth
448                    || mTvView.getHeight() != decorViewHeight;
449            boolean willHavePillarBox = ((targetLayoutParams.width != LayoutParams.MATCH_PARENT)
450                    && targetLayoutParams.width != decorViewWidth) || (
451                    (targetLayoutParams.height != LayoutParams.MATCH_PARENT)
452                            && targetLayoutParams.height != decorViewHeight);
453
454            if (!isTvViewFullScreen() && !hasPillarBox) {
455                // If there is no pillar box, no animation is needed.
456                mContentView.setBackgroundColor(color);
457            } else if (!isTvViewFullScreen() || willHavePillarBox) {
458                mBackgroundAnimator.setIntValues(mBackgroundColor, color);
459                mBackgroundAnimator.setEvaluator(new ArgbEvaluator());
460                mBackgroundAnimator.setInterpolator(mFastOutLinearIn);
461                mBackgroundAnimator.start();
462            }
463            // In the 'else' case (TV activity is getting out of the shrunken tv view mode and will
464            // have a pillar box), we keep the background color and don't show the animation.
465        } else {
466            mContentView.setBackgroundColor(color);
467        }
468        mBackgroundColor = color;
469    }
470
471    private void setTvViewPosition(final FrameLayout.LayoutParams layoutParams,
472            MarginLayoutParams tvViewFrame, boolean animate) {
473        if (DEBUG) {
474            Log.d(TAG, "setTvViewPosition: w=" + layoutParams.width + " h=" + layoutParams.height
475                    + " s=" + layoutParams.getMarginStart() + " t=" + layoutParams.topMargin
476                    + " e=" + layoutParams.getMarginEnd() + " b=" + layoutParams.bottomMargin
477                    + " animate=" + animate);
478        }
479        MarginLayoutParams oldTvViewFrame = mTvViewFrame;
480        mTvViewLayoutParams = layoutParams;
481        mTvViewFrame = tvViewFrame;
482        if (animate) {
483            initTvAnimatorIfNeeded();
484            if (mTvViewAnimator.isStarted()) {
485                // Cancel the current animation and start new one.
486                mTvViewAnimator.cancel();
487                mOldTvViewFrame = mLastAnimatedTvViewFrame;
488            } else {
489                mOldTvViewFrame = oldTvViewFrame;
490            }
491            mTvViewAnimator.setObjectValues(mTvView.getLayoutParams(), layoutParams);
492            mTvViewAnimator.setEvaluator(new TypeEvaluator<FrameLayout.LayoutParams>() {
493                FrameLayout.LayoutParams lp;
494                @Override
495                public FrameLayout.LayoutParams evaluate(float fraction,
496                        FrameLayout.LayoutParams startValue, FrameLayout.LayoutParams endValue) {
497                    if (lp == null) {
498                        lp = new FrameLayout.LayoutParams(0, 0);
499                        lp.gravity = startValue.gravity;
500                    }
501                    interpolateMarginsRelative(lp, startValue, endValue, fraction);
502                    return lp;
503                }
504            });
505            mTvViewAnimator
506                    .setInterpolator(isTvViewFullScreen() ? mFastOutLinearIn : mLinearOutSlowIn);
507            mTvViewAnimator.start();
508        } else {
509            if (mTvViewAnimator != null && mTvViewAnimator.isStarted()) {
510                // Continue the current animation.
511                // layoutParams will be applied when animation ends.
512                return;
513            }
514            // This block is also called when animation ends.
515            if (isTvViewFullScreen()) {
516                // When this layout is for full screen, fix the surface size after layout to make
517                // resize animation smooth.
518                mTvView.post(new Runnable() {
519                    @Override
520                    public void run() {
521                        if (DEBUG) {
522                            Log.d(TAG, "setFixedSize: w=" + layoutParams.width + " h="
523                                    + layoutParams.height);
524                        }
525                        mTvView.setLayoutParams(layoutParams);
526                        mTvView.setFixedSurfaceSize(layoutParams.width, layoutParams.height);
527                    }
528                });
529            } else {
530                mTvView.setLayoutParams(layoutParams);
531            }
532            updatePipView(mTvViewFrame);
533        }
534    }
535
536    /**
537     * The redlines assume that the ratio of the TV screen is 16:9. If the radio is not 16:9, the
538     * layout of PAP can be broken.
539     */
540    @SuppressLint("RtlHardcoded")
541    private void updatePipView(MarginLayoutParams tvViewFrame) {
542        if (!mPipStarted) {
543            return;
544        }
545        int width;
546        int height;
547        int startMargin;
548        int endMargin;
549        int topMargin;
550        int bottomMargin;
551        int gravity;
552
553        if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) {
554            gravity = Gravity.CENTER_VERTICAL | Gravity.START;
555            height = tvViewFrame.height;
556            float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio();
557            if (videoDisplayAspectRatio <= 0f) {
558                width = tvViewFrame.width;
559            } else {
560                width = (int) (height * videoDisplayAspectRatio);
561                if (width > tvViewFrame.width) {
562                    width = tvViewFrame.width;
563                }
564            }
565            startMargin = mResources.getDimensionPixelOffset(R.dimen.papview_margin_horizontal)
566                    * tvViewFrame.width / mTvViewPapWidth + (tvViewFrame.width - width) / 2;
567            endMargin = 0;
568            topMargin = 0;
569            bottomMargin = 0;
570        } else {
571            int tvViewWidth = tvViewFrame.width;
572            int tvViewHeight = tvViewFrame.height;
573            int tvStartMargin = tvViewFrame.getMarginStart();
574            int tvEndMargin = tvViewFrame.getMarginEnd();
575            int tvTopMargin = tvViewFrame.topMargin;
576            int tvBottomMargin = tvViewFrame.bottomMargin;
577            float horizontalScaleFactor = (float) tvViewWidth / mWindowWidth;
578            float verticalScaleFactor = (float) tvViewHeight / mWindowHeight;
579
580            int maxWidth;
581            if (mPipSize == TvSettings.PIP_SIZE_SMALL) {
582                maxWidth = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_small_size_width)
583                        * horizontalScaleFactor);
584                height = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_small_size_height)
585                        * verticalScaleFactor);
586            } else if (mPipSize == TvSettings.PIP_SIZE_BIG) {
587                maxWidth = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_large_size_width)
588                        * horizontalScaleFactor);
589                height = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_large_size_height)
590                        * verticalScaleFactor);
591            } else {
592                throw new IllegalArgumentException("Invalid PIP size: " + mPipSize);
593            }
594            float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio();
595            if (videoDisplayAspectRatio <= 0f) {
596                width = maxWidth;
597            } else {
598                width = (int) (height * videoDisplayAspectRatio);
599                if (width > maxWidth) {
600                    width = maxWidth;
601                }
602            }
603
604            startMargin = tvStartMargin + (int) (mPipViewHorizontalMargin * horizontalScaleFactor);
605            endMargin = tvEndMargin + (int) (mPipViewHorizontalMargin * horizontalScaleFactor);
606            topMargin = tvTopMargin + (int) (mPipViewTopMargin * verticalScaleFactor);
607            bottomMargin = tvBottomMargin + (int) (mPipViewBottomMargin * verticalScaleFactor);
608
609            switch (mPipLayout) {
610                case TvSettings.PIP_LAYOUT_TOP_LEFT:
611                    gravity = Gravity.TOP | Gravity.LEFT;
612                    break;
613                case TvSettings.PIP_LAYOUT_TOP_RIGHT:
614                    gravity = Gravity.TOP | Gravity.RIGHT;
615                    break;
616                case TvSettings.PIP_LAYOUT_BOTTOM_LEFT:
617                    gravity = Gravity.BOTTOM | Gravity.LEFT;
618                    break;
619                case TvSettings.PIP_LAYOUT_BOTTOM_RIGHT:
620                    gravity = Gravity.BOTTOM | Gravity.RIGHT;
621                    break;
622                default:
623                    throw new IllegalArgumentException("Invalid PIP location: " + mPipLayout);
624            }
625        }
626
627        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPipView.getLayoutParams();
628        if (lp.width != width || lp.height != height || lp.getMarginStart() != startMargin
629                || lp.getMarginEnd() != endMargin || lp.topMargin != topMargin
630                || lp.bottomMargin != bottomMargin || lp.gravity != gravity) {
631            lp.width = width;
632            lp.height = height;
633            lp.setMarginStart(startMargin);
634            lp.setMarginEnd(endMargin);
635            lp.topMargin = topMargin;
636            lp.bottomMargin = bottomMargin;
637            lp.gravity = gravity;
638            mPipView.setLayoutParams(lp);
639        }
640    }
641
642    private void initTvAnimatorIfNeeded() {
643        if (mTvViewAnimator != null) {
644            return;
645        }
646
647        // TvViewAnimator animates TvView by repeatedly re-layouting TvView.
648        // TvView includes a SurfaceView on which scale/translation effects do not work. Normally,
649        // SurfaceView can be animated by changing left/top/right/bottom directly using
650        // ObjectAnimator, although it would require calling getChildAt(0) against TvView (which is
651        // supposed to be opaque). More importantly, this method does not work in case of TvView,
652        // because TvView may request layout itself during animation and layout SurfaceView with
653        // its own parameters when TvInputService requests to do so.
654        mTvViewAnimator = new ObjectAnimator();
655        mTvViewAnimator.setTarget(mTvView);
656        mTvViewAnimator.setProperty(
657                Property.of(FrameLayout.class, ViewGroup.LayoutParams.class, "layoutParams"));
658        mTvViewAnimator.setDuration(mResources.getInteger(R.integer.tvview_anim_duration));
659        mTvViewAnimator.addListener(new AnimatorListenerAdapter() {
660            private boolean mCanceled = false;
661
662            @Override
663            public void onAnimationCancel(Animator animation) {
664                mCanceled = true;
665            }
666
667            @Override
668            public void onAnimationEnd(Animator animation) {
669                if (mCanceled) {
670                    mCanceled = false;
671                    return;
672                }
673                mHandler.post(new Runnable() {
674                    @Override
675                    public void run() {
676                        setTvViewPosition(mTvViewLayoutParams, mTvViewFrame, false);
677                    }
678                });
679            }
680        });
681        mTvViewAnimator.addUpdateListener(new AnimatorUpdateListener() {
682            @Override
683            public void onAnimationUpdate(ValueAnimator animator) {
684                float fraction = animator.getAnimatedFraction();
685                mLastAnimatedTvViewFrame = new MarginLayoutParams(0, 0);
686                interpolateMarginsRelative(mLastAnimatedTvViewFrame,
687                        mOldTvViewFrame, mTvViewFrame, fraction);
688                updatePipView(mLastAnimatedTvViewFrame);
689            }
690        });
691    }
692
693    private void initBackgroundAnimatorIfNeeded() {
694        if (mBackgroundAnimator != null) {
695            return;
696        }
697
698        mBackgroundAnimator = new ObjectAnimator();
699        mBackgroundAnimator.setTarget(mContentView);
700        mBackgroundAnimator.setPropertyName("backgroundColor");
701        mBackgroundAnimator
702                .setDuration(mResources.getInteger(R.integer.tvactivity_background_anim_duration));
703        mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
704            @Override
705            public void onAnimationEnd(Animator animation) {
706                mHandler.post(new Runnable() {
707                    @Override
708                    public void run() {
709                        mContentView.setBackgroundColor(mBackgroundColor);
710                    }
711                });
712            }
713        });
714    }
715
716    private void applyDisplayMode(float videoDisplayAspectRatio, boolean animate,
717            boolean forceUpdate) {
718        if (mAppliedDisplayedMode == mDisplayMode
719                && mAppliedTvViewStartMargin == mTvViewStartMargin
720                && mAppliedTvViewEndMargin == mTvViewEndMargin
721                && Math.abs(mAppliedVideoDisplayAspectRatio - videoDisplayAspectRatio) <
722                        DISPLAY_ASPECT_RATIO_EPSILON) {
723            if (!forceUpdate) {
724                return;
725            }
726        } else {
727            mAppliedDisplayedMode = mDisplayMode;
728            mAppliedTvViewStartMargin = mTvViewStartMargin;
729            mAppliedTvViewEndMargin = mTvViewEndMargin;
730            mAppliedVideoDisplayAspectRatio = videoDisplayAspectRatio;
731        }
732        int availableAreaWidth = mWindowWidth - mTvViewStartMargin - mTvViewEndMargin;
733        int availableAreaHeight = availableAreaWidth * mWindowHeight / mWindowWidth;
734        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(0, 0,
735                ((FrameLayout.LayoutParams) mTvView.getLayoutParams()).gravity);
736        int displayMode = mDisplayMode;
737        double availableAreaRatio = 0;
738        double videoRatio = 0;
739        if (availableAreaWidth <= 0 || availableAreaHeight <= 0) {
740            displayMode = DisplayMode.MODE_FULL;
741            Log.w(TAG, "Some resolution info is missing during applyDisplayMode. ("
742                    + "availableAreaWidth=" + availableAreaWidth + ", availableAreaHeight="
743                    + availableAreaHeight + ")");
744        } else {
745            availableAreaRatio = (double) availableAreaWidth / availableAreaHeight;
746            if (videoDisplayAspectRatio <= 0f) {
747                videoRatio = (double) mWindowWidth / mWindowHeight;
748            } else {
749                videoRatio = videoDisplayAspectRatio;
750            }
751        }
752
753        int tvViewFrameTop = (mWindowHeight - availableAreaHeight) / 2;
754        MarginLayoutParams tvViewFrame = createMarginLayoutParams(
755                mTvViewStartMargin, mTvViewEndMargin, tvViewFrameTop, tvViewFrameTop);
756        layoutParams.width = availableAreaWidth;
757        layoutParams.height = availableAreaHeight;
758        switch (displayMode) {
759            case DisplayMode.MODE_FULL:
760                layoutParams.width = availableAreaWidth;
761                layoutParams.height = availableAreaHeight;
762                break;
763            case DisplayMode.MODE_ZOOM:
764                if (videoRatio < availableAreaRatio) {
765                    // Y axis will be clipped.
766                    layoutParams.width = availableAreaWidth;
767                    layoutParams.height = (int) (availableAreaWidth / videoRatio);
768                } else {
769                    // X axis will be clipped.
770                    layoutParams.width = (int) (availableAreaHeight * videoRatio);
771                    layoutParams.height = availableAreaHeight;
772                }
773                break;
774            case DisplayMode.MODE_NORMAL:
775                if (videoRatio < availableAreaRatio) {
776                    // X axis has black area.
777                    layoutParams.width = (int) (availableAreaHeight * videoRatio);
778                    layoutParams.height = availableAreaHeight;
779                } else {
780                    // Y axis has black area.
781                    layoutParams.width = availableAreaWidth;
782                    layoutParams.height = (int) (availableAreaWidth / videoRatio);
783                }
784                break;
785        }
786
787        // FrameLayout has an issue with centering when left and right margins differ.
788        // So stick to Gravity.START | Gravity.CENTER_VERTICAL.
789        int marginStart = mTvViewStartMargin + (availableAreaWidth - layoutParams.width) / 2;
790        layoutParams.setMarginStart(marginStart);
791        // Set marginEnd as well because setTvViewPosition uses both start/end margin.
792        layoutParams.setMarginEnd(mWindowWidth - layoutParams.width - marginStart);
793
794        setBackgroundColor(Utils.getColor(mResources, isTvViewFullScreen()
795                ? R.color.tvactivity_background : R.color.tvactivity_background_on_shrunken_tvview),
796                layoutParams, animate);
797        setTvViewPosition(layoutParams, tvViewFrame, animate);
798
799        // Update the current display mode.
800        mTvOptionsManager.onDisplayModeChanged(displayMode);
801    }
802
803    private static int interpolate(int start, int end, float fraction) {
804        return (int) (start + (end - start) * fraction);
805    }
806
807    private static void interpolateMarginsRelative(MarginLayoutParams out,
808            MarginLayoutParams startValue, MarginLayoutParams endValue, float fraction) {
809        out.topMargin = interpolate(startValue.topMargin, endValue.topMargin, fraction);
810        out.bottomMargin = interpolate(startValue.bottomMargin, endValue.bottomMargin, fraction);
811        out.setMarginStart(interpolate(startValue.getMarginStart(), endValue.getMarginStart(),
812                fraction));
813        out.setMarginEnd(interpolate(startValue.getMarginEnd(), endValue.getMarginEnd(), fraction));
814        out.width = interpolate(startValue.width, endValue.width, fraction);
815        out.height = interpolate(startValue.height, endValue.height, fraction);
816    }
817
818    private MarginLayoutParams createMarginLayoutParams(
819            int startMargin, int endMargin, int topMargin, int bottomMargin) {
820        MarginLayoutParams lp = new MarginLayoutParams(0, 0);
821        lp.setMarginStart(startMargin);
822        lp.setMarginEnd(endMargin);
823        lp.topMargin = topMargin;
824        lp.bottomMargin = bottomMargin;
825        lp.width = mWindowWidth - startMargin - endMargin;
826        lp.height = mWindowHeight - topMargin - bottomMargin;
827        return lp;
828    }
829}
830