1/*
2 * Copyright (C) 2017 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 */
16package android.support.v17.leanback.app;
17
18import android.animation.PropertyValuesHolder;
19import android.graphics.Bitmap;
20import android.graphics.Color;
21import android.graphics.drawable.ColorDrawable;
22import android.graphics.drawable.Drawable;
23import android.support.annotation.ColorInt;
24import android.support.annotation.NonNull;
25import android.support.annotation.Nullable;
26import android.support.v17.leanback.R;
27import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
28import android.support.v17.leanback.media.PlaybackGlue;
29import android.support.v17.leanback.media.PlaybackGlueHost;
30import android.support.v17.leanback.widget.DetailsParallaxDrawable;
31import android.support.v17.leanback.widget.ParallaxTarget;
32import android.support.v4.app.Fragment;
33
34/**
35 * Controller for DetailsSupportFragment parallax background and embedded video play.
36 * <p>
37 * The parallax background drawable is made of two parts: cover drawable (by default
38 * {@link FitWidthBitmapDrawable}) above the details overview row and bottom drawable (by default
39 * {@link ColorDrawable}) below the details overview row. While vertically scrolling rows, the size
40 * of cover drawable and bottom drawable will be updated and the cover drawable will by default
41 * perform a parallax shift using {@link FitWidthBitmapDrawable#PROPERTY_VERTICAL_OFFSET}.
42 * </p>
43 * <pre>
44 *        ***************************
45 *        *      Cover Drawable     *
46 *        * (FitWidthBitmapDrawable)*
47 *        *                         *
48 *        ***************************
49 *        *    DetailsOverviewRow   *
50 *        *                         *
51 *        ***************************
52 *        *     Bottom Drawable     *
53 *        *      (ColorDrawable)    *
54 *        *         Related         *
55 *        *         Content         *
56 *        ***************************
57 * </pre>
58 * Both parallax background drawable and embedded video play are optional. App must call
59 * {@link #enableParallax()} and/or {@link #setupVideoPlayback(PlaybackGlue)} explicitly.
60 * The PlaybackGlue is automatically {@link PlaybackGlue#play()} when fragment starts and
61 * {@link PlaybackGlue#pause()} when fragment stops. When video is ready to play, cover drawable
62 * will be faded out.
63 * Example:
64 * <pre>
65 * DetailsSupportFragmentBackgroundController mController = new DetailsSupportFragmentBackgroundController(this);
66 *
67 * public void onCreate(Bundle savedInstance) {
68 *     super.onCreate(savedInstance);
69 *     MediaPlayerGlue player = new MediaPlayerGlue(..);
70 *     player.setUrl(...);
71 *     mController.enableParallax();
72 *     mController.setupVideoPlayback(player);
73 * }
74 *
75 * static class MyLoadBitmapTask extends ... {
76 *     WeakReference<MyFragment> mFragmentRef;
77 *     MyLoadBitmapTask(MyFragment fragment) {
78 *         mFragmentRef = new WeakReference(fragment);
79 *     }
80 *     protected void onPostExecute(Bitmap bitmap) {
81 *         MyFragment fragment = mFragmentRef.get();
82 *         if (fragment != null) {
83 *             fragment.mController.setCoverBitmap(bitmap);
84 *         }
85 *     }
86 * }
87 *
88 * public void onStart() {
89 *     new MyLoadBitmapTask(this).execute(url);
90 * }
91 *
92 * public void onStop() {
93 *     mController.setCoverBitmap(null);
94 * }
95 * </pre>
96 * <p>
97 * To customize cover drawable and/or bottom drawable, app should call
98 * {@link #enableParallax(Drawable, Drawable, ParallaxTarget.PropertyValuesHolderTarget)}.
99 * If app supplies a custom cover Drawable, it should not call {@link #setCoverBitmap(Bitmap)}.
100 * If app supplies a custom bottom Drawable, it should not call {@link #setSolidColor(int)}.
101 * </p>
102 * <p>
103 * To customize playback fragment, app should override {@link #onCreateVideoSupportFragment()} and
104 * {@link #onCreateGlueHost()}.
105 * </p>
106 *
107 */
108public class DetailsSupportFragmentBackgroundController {
109
110    final DetailsSupportFragment mFragment;
111    DetailsParallaxDrawable mParallaxDrawable;
112    int mParallaxDrawableMaxOffset;
113    PlaybackGlue mPlaybackGlue;
114    DetailsBackgroundVideoHelper mVideoHelper;
115    Bitmap mCoverBitmap;
116    int mSolidColor;
117    boolean mCanUseHost = false;
118    boolean mInitialControlVisible = false;
119
120    private Fragment mLastVideoSupportFragmentForGlueHost;
121
122    /**
123     * Creates a DetailsSupportFragmentBackgroundController for a DetailsSupportFragment. Note that
124     * each DetailsSupportFragment can only associate with one DetailsSupportFragmentBackgroundController.
125     *
126     * @param fragment The DetailsSupportFragment to control background and embedded video playing.
127     * @throws IllegalStateException If fragment was already associated with another controller.
128     */
129    public DetailsSupportFragmentBackgroundController(DetailsSupportFragment fragment) {
130        if (fragment.mDetailsBackgroundController != null) {
131            throw new IllegalStateException("Each DetailsSupportFragment is allowed to initialize "
132                    + "DetailsSupportFragmentBackgroundController once");
133        }
134        fragment.mDetailsBackgroundController = this;
135        mFragment = fragment;
136    }
137
138    /**
139     * Enables default parallax background using a {@link FitWidthBitmapDrawable} as cover drawable
140     * and {@link ColorDrawable} as bottom drawable. A vertical parallax movement will be applied
141     * to the FitWidthBitmapDrawable. App may use {@link #setSolidColor(int)} and
142     * {@link #setCoverBitmap(Bitmap)} to change the content of bottom drawable and cover drawable.
143     * This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}.
144     *
145     * @see #setCoverBitmap(Bitmap)
146     * @see #setSolidColor(int)
147     * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.
148     */
149    public void enableParallax() {
150        int offset = mParallaxDrawableMaxOffset;
151        if (offset == 0) {
152            offset = mFragment.getContext().getResources()
153                    .getDimensionPixelSize(R.dimen.lb_details_cover_drawable_parallax_movement);
154        }
155        Drawable coverDrawable = new FitWidthBitmapDrawable();
156        ColorDrawable colorDrawable = new ColorDrawable();
157        enableParallax(coverDrawable, colorDrawable,
158                new ParallaxTarget.PropertyValuesHolderTarget(
159                        coverDrawable,
160                        PropertyValuesHolder.ofInt(FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET,
161                                0, -offset)
162                ));
163    }
164
165    /**
166     * Enables parallax background using a custom cover drawable at top and a custom bottom
167     * drawable. This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}.
168     *
169     * @param coverDrawable Custom cover drawable shown at top. {@link #setCoverBitmap(Bitmap)}
170     *                      will not work if coverDrawable is not {@link FitWidthBitmapDrawable};
171     *                      in that case it's app's responsibility to set content into
172     *                      coverDrawable.
173     * @param bottomDrawable Drawable shown at bottom. {@link #setSolidColor(int)} will not work
174     *                       if bottomDrawable is not {@link ColorDrawable}; in that case it's app's
175     *                       responsibility to set content of bottomDrawable.
176     * @param coverDrawableParallaxTarget Target to perform parallax effect within coverDrawable.
177     *                                    Use null for no parallax movement effect.
178     *                                    Example to move bitmap within FitWidthBitmapDrawable:
179     *                                    new ParallaxTarget.PropertyValuesHolderTarget(
180     *                                        coverDrawable, PropertyValuesHolder.ofInt(
181     *                                            FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET,
182     *                                            0, -120))
183     * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.
184     */
185    public void enableParallax(@NonNull Drawable coverDrawable, @NonNull Drawable bottomDrawable,
186                               @Nullable ParallaxTarget.PropertyValuesHolderTarget
187                                       coverDrawableParallaxTarget) {
188        if (mParallaxDrawable != null) {
189            return;
190        }
191        // if bitmap is set before enableParallax, use it as initial value.
192        if (mCoverBitmap != null && coverDrawable instanceof FitWidthBitmapDrawable) {
193            ((FitWidthBitmapDrawable) coverDrawable).setBitmap(mCoverBitmap);
194        }
195        // if solid color is set before enableParallax, use it as initial value.
196        if (mSolidColor != Color.TRANSPARENT && bottomDrawable instanceof ColorDrawable) {
197            ((ColorDrawable) bottomDrawable).setColor(mSolidColor);
198        }
199        if (mPlaybackGlue != null) {
200            throw new IllegalStateException("enableParallaxDrawable must be called before "
201                    + "enableVideoPlayback");
202        }
203        mParallaxDrawable = new DetailsParallaxDrawable(
204                mFragment.getContext(),
205                mFragment.getParallax(),
206                coverDrawable,
207                bottomDrawable,
208                coverDrawableParallaxTarget);
209        mFragment.setBackgroundDrawable(mParallaxDrawable);
210        // create a VideoHelper with null PlaybackGlue for changing CoverDrawable visibility
211        // before PlaybackGlue is ready.
212        mVideoHelper = new DetailsBackgroundVideoHelper(null,
213                mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
214    }
215
216    /**
217     * Enable video playback and set proper {@link PlaybackGlueHost}. This method by default
218     * creates a VideoSupportFragment and VideoSupportFragmentGlueHost to host the PlaybackGlue.
219     * This method must be called after calling details Fragment super.onCreate(). This method
220     * can be called multiple times to replace existing PlaybackGlue or calling
221     * setupVideoPlayback(null) to clear. Note a typical {@link PlaybackGlue} subclass releases
222     * resources in {@link PlaybackGlue#onDetachedFromHost()}, when the {@link PlaybackGlue}
223     * subclass is not doing that, it's app's responsibility to release the resources.
224     *
225     * @param playbackGlue The new PlaybackGlue to set as background or null to clear existing one.
226     * @see #onCreateVideoSupportFragment()
227     * @see #onCreateGlueHost().
228     */
229    @SuppressWarnings("ReferenceEquality")
230    public void setupVideoPlayback(@NonNull PlaybackGlue playbackGlue) {
231        if (mPlaybackGlue == playbackGlue) {
232            return;
233        }
234
235        PlaybackGlueHost playbackGlueHost = null;
236        if (mPlaybackGlue != null) {
237            playbackGlueHost = mPlaybackGlue.getHost();
238            mPlaybackGlue.setHost(null);
239        }
240
241        mPlaybackGlue = playbackGlue;
242        mVideoHelper.setPlaybackGlue(mPlaybackGlue);
243        if (mCanUseHost && mPlaybackGlue != null) {
244            if (playbackGlueHost == null
245                    || mLastVideoSupportFragmentForGlueHost != findOrCreateVideoSupportFragment()) {
246                mPlaybackGlue.setHost(createGlueHost());
247                mLastVideoSupportFragmentForGlueHost = findOrCreateVideoSupportFragment();
248            } else {
249                mPlaybackGlue.setHost(playbackGlueHost);
250            }
251        }
252    }
253
254    /**
255     * Returns current PlaybackGlue or null if not set or cleared.
256     *
257     * @return Current PlaybackGlue or null
258     */
259    public final PlaybackGlue getPlaybackGlue() {
260        return mPlaybackGlue;
261    }
262
263    /**
264     * Precondition allows user navigate to video fragment using DPAD. Default implementation
265     * returns true if PlaybackGlue is not null. Subclass may override, e.g. only allow navigation
266     * when {@link PlaybackGlue#isPrepared()} is true. Note this method does not block
267     * app calls {@link #switchToVideo}.
268     *
269     * @return True allow to navigate to video fragment.
270     */
271    public boolean canNavigateToVideoSupportFragment() {
272        return mPlaybackGlue != null;
273    }
274
275    void switchToVideoBeforeCreate() {
276        mVideoHelper.crossFadeBackgroundToVideo(true, true);
277        mInitialControlVisible = true;
278    }
279
280    /**
281     * Switch to video fragment, note that this method is not affected by result of
282     * {@link #canNavigateToVideoSupportFragment()}. If the method is called in DetailsSupportFragment.onCreate()
283     * it will make video fragment to be initially focused once it is created.
284     * <p>
285     * Calling switchToVideo() in DetailsSupportFragment.onCreate() will clear the activity enter
286     * transition and shared element transition.
287     * </p>
288     * <p>
289     * If switchToVideo() is called after {@link DetailsSupportFragment#prepareEntranceTransition()} and
290     * before {@link DetailsSupportFragment#onEntranceTransitionEnd()}, it will be ignored.
291     * </p>
292     * <p>
293     * If {@link DetailsSupportFragment#prepareEntranceTransition()} is called after switchToVideo(), an
294     * IllegalStateException will be thrown.
295     * </p>
296     */
297    public final void switchToVideo() {
298        mFragment.switchToVideo();
299    }
300
301    /**
302     * Switch to rows fragment.
303     */
304    public final void switchToRows() {
305        mFragment.switchToRows();
306    }
307
308    /**
309     * When fragment is started and no running transition. First set host if not yet set, second
310     * start playing if it was paused before.
311     */
312    void onStart() {
313        if (!mCanUseHost) {
314            mCanUseHost = true;
315            if (mPlaybackGlue != null) {
316                mPlaybackGlue.setHost(createGlueHost());
317                mLastVideoSupportFragmentForGlueHost = findOrCreateVideoSupportFragment();
318            }
319        }
320        if (mPlaybackGlue != null && mPlaybackGlue.isPrepared()) {
321            mPlaybackGlue.play();
322        }
323    }
324
325    void onStop() {
326        if (mPlaybackGlue != null) {
327            mPlaybackGlue.pause();
328        }
329    }
330
331    /**
332     * Disable parallax that would auto-start video playback
333     * @return true if video fragment is visible or false otherwise.
334     */
335    boolean disableVideoParallax() {
336        if (mVideoHelper != null) {
337            mVideoHelper.stopParallax();
338            return mVideoHelper.isVideoVisible();
339        }
340        return false;
341    }
342
343    /**
344     * Returns the cover drawable at top. Returns null if {@link #enableParallax()} is not called.
345     * By default it's a {@link FitWidthBitmapDrawable}.
346     *
347     * @return The cover drawable at top.
348     */
349    public final Drawable getCoverDrawable() {
350        if (mParallaxDrawable == null) {
351            return null;
352        }
353        return mParallaxDrawable.getCoverDrawable();
354    }
355
356    /**
357     * Returns the drawable at bottom. Returns null if {@link #enableParallax()} is not called.
358     * By default it's a {@link ColorDrawable}.
359     *
360     * @return The bottom drawable.
361     */
362    public final Drawable getBottomDrawable() {
363        if (mParallaxDrawable == null) {
364            return null;
365        }
366        return mParallaxDrawable.getBottomDrawable();
367    }
368
369    /**
370     * Creates a Fragment to host {@link PlaybackGlue}. Returns a new {@link VideoSupportFragment} by
371     * default. App may override and return a different fragment and it also must override
372     * {@link #onCreateGlueHost()}.
373     *
374     * @return A new fragment used in {@link #onCreateGlueHost()}.
375     * @see #onCreateGlueHost()
376     * @see #setupVideoPlayback(PlaybackGlue)
377     */
378    public Fragment onCreateVideoSupportFragment() {
379        return new VideoSupportFragment();
380    }
381
382    /**
383     * Creates a PlaybackGlueHost to host PlaybackGlue. App may override this if it overrides
384     * {@link #onCreateVideoSupportFragment()}. This method must be called after calling Fragment
385     * super.onCreate(). When override this method, app may call
386     * {@link #findOrCreateVideoSupportFragment()} to get or create a fragment.
387     *
388     * @return A new PlaybackGlueHost to host PlaybackGlue.
389     * @see #onCreateVideoSupportFragment()
390     * @see #findOrCreateVideoSupportFragment()
391     * @see #setupVideoPlayback(PlaybackGlue)
392     */
393    public PlaybackGlueHost onCreateGlueHost() {
394        return new VideoSupportFragmentGlueHost((VideoSupportFragment) findOrCreateVideoSupportFragment());
395    }
396
397    PlaybackGlueHost createGlueHost() {
398        PlaybackGlueHost host = onCreateGlueHost();
399        if (mInitialControlVisible) {
400            host.showControlsOverlay(false);
401        } else {
402            host.hideControlsOverlay(false);
403        }
404        return host;
405    }
406
407    /**
408     * Adds or gets fragment for rendering video in DetailsSupportFragment. A subclass that
409     * overrides {@link #onCreateGlueHost()} should call this method to get a fragment for creating
410     * a {@link PlaybackGlueHost}.
411     *
412     * @return Fragment the added or restored fragment responsible for rendering video.
413     * @see #onCreateGlueHost()
414     */
415    public final Fragment findOrCreateVideoSupportFragment() {
416        return mFragment.findOrCreateVideoSupportFragment();
417    }
418
419    /**
420     * Convenient method to set Bitmap in cover drawable. If app is not using default
421     * {@link FitWidthBitmapDrawable}, app should not use this method  It's safe to call
422     * setCoverBitmap() before calling {@link #enableParallax()}.
423     *
424     * @param bitmap bitmap to set as cover.
425     */
426    public final void setCoverBitmap(Bitmap bitmap) {
427        mCoverBitmap = bitmap;
428        Drawable drawable = getCoverDrawable();
429        if (drawable instanceof FitWidthBitmapDrawable) {
430            ((FitWidthBitmapDrawable) drawable).setBitmap(mCoverBitmap);
431        }
432    }
433
434    /**
435     * Returns Bitmap set by {@link #setCoverBitmap(Bitmap)}.
436     *
437     * @return Bitmap for cover drawable.
438     */
439    public final Bitmap getCoverBitmap() {
440        return mCoverBitmap;
441    }
442
443    /**
444     * Returns color set by {@link #setSolidColor(int)}.
445     *
446     * @return Solid color used for bottom drawable.
447     */
448    public final @ColorInt int getSolidColor() {
449        return mSolidColor;
450    }
451
452    /**
453     * Convenient method to set color in bottom drawable. If app is not using default
454     * {@link ColorDrawable}, app should not use this method. It's safe to call setSolidColor()
455     * before calling {@link #enableParallax()}.
456     *
457     * @param color color for bottom drawable.
458     */
459    public final void setSolidColor(@ColorInt int color) {
460        mSolidColor = color;
461        Drawable bottomDrawable = getBottomDrawable();
462        if (bottomDrawable instanceof ColorDrawable) {
463            ((ColorDrawable) bottomDrawable).setColor(color);
464        }
465    }
466
467    /**
468     * Sets default parallax offset in pixels for bitmap moving vertically. This method must
469     * be called before {@link #enableParallax()}.
470     *
471     * @param offset Offset in pixels (e.g. 120).
472     * @see #enableParallax()
473     */
474    public final void setParallaxDrawableMaxOffset(int offset) {
475        if (mParallaxDrawable != null) {
476            throw new IllegalStateException("enableParallax already called");
477        }
478        mParallaxDrawableMaxOffset = offset;
479    }
480
481    /**
482     * Returns Default parallax offset in pixels for bitmap moving vertically.
483     * When 0, a default value would be used.
484     *
485     * @return Default parallax offset in pixels for bitmap moving vertically.
486     * @see #enableParallax()
487     */
488    public final int getParallaxDrawableMaxOffset() {
489        return mParallaxDrawableMaxOffset;
490    }
491
492}
493