1/*
2 * Copyright (C) 2014 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.settings.dialog;
18
19import android.animation.Animator;
20import android.animation.AnimatorInflater;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.AnimatorSet;
23import android.animation.ObjectAnimator;
24import android.animation.TimeInterpolator;
25import android.app.Fragment;
26import android.app.FragmentManager;
27import android.app.FragmentTransaction;
28import android.content.Context;
29import android.content.Intent;
30import android.content.pm.PackageManager;
31import android.content.res.Resources;
32import android.graphics.Bitmap;
33import android.graphics.Color;
34import android.graphics.drawable.BitmapDrawable;
35import android.graphics.drawable.ColorDrawable;
36import android.graphics.drawable.Drawable;
37import android.net.Uri;
38import android.os.Bundle;
39import android.os.Parcel;
40import android.os.Parcelable;
41import android.support.v17.leanback.widget.VerticalGridView;
42import android.support.v4.view.ViewCompat;
43import android.support.v7.widget.RecyclerView;
44import android.util.Log;
45import android.view.LayoutInflater;
46import android.view.View;
47import android.view.ViewGroup;
48import android.view.ViewGroup.LayoutParams;
49import android.view.ViewPropertyAnimator;
50import android.view.ViewTreeObserver;
51import android.view.animation.DecelerateInterpolator;
52import android.widget.ImageView;
53import android.widget.TextView;
54
55import com.android.tv.settings.R;
56import com.android.tv.settings.util.AccessibilityHelper;
57import com.android.tv.settings.widget.BitmapWorkerOptions;
58import com.android.tv.settings.widget.DrawableDownloader;
59import com.android.tv.settings.widget.DrawableDownloader.BitmapCallback;
60
61import java.util.ArrayList;
62
63/**
64 * Displays content on the left and actions on the right.
65 */
66public class DialogFragment extends Fragment {
67
68    private static final String TAG_LEAN_BACK_DIALOG_FRAGMENT = "leanBackDialogFragment";
69    private static final String EXTRA_CONTENT_TITLE = "title";
70    private static final String EXTRA_CONTENT_BREADCRUMB = "breadcrumb";
71    private static final String EXTRA_CONTENT_DESCRIPTION = "description";
72    private static final String EXTRA_CONTENT_ICON_RESOURCE_ID = "iconResourceId";
73    private static final String EXTRA_CONTENT_ICON_URI = "iconUri";
74    private static final String EXTRA_CONTENT_ICON_BITMAP = "iconBitmap";
75    private static final String EXTRA_CONTENT_ICON_BACKGROUND = "iconBackground";
76    private static final String EXTRA_ACTION_NAME = "name";
77    private static final String EXTRA_ACTION_ACTIONS = "actions";
78    private static final String EXTRA_ACTION_SELECTED_INDEX = "selectedIndex";
79    private static final String EXTRA_ENTRY_TRANSITION_PERFORMED = "entryTransitionPerformed";
80    private static final int ANIMATE_IN_DURATION = 250;
81    private static final int ANIMATE_DELAY = 550;
82    private static final int SECONDARY_ANIMATE_DELAY = 120;
83    private static final int SLIDE_IN_DISTANCE = 120;
84    private static final int ANIMATION_FRAGMENT_ENTER = 1;
85    private static final int ANIMATION_FRAGMENT_EXIT = 2;
86    private static final int ANIMATION_FRAGMENT_ENTER_POP = 3;
87    private static final int ANIMATION_FRAGMENT_EXIT_POP = 4;
88
89    /**
90     * Builds a LeanBackDialogFragment object.
91     */
92    public static class Builder {
93
94        private String mContentTitle;
95        private String mContentBreadcrumb;
96        private String mContentDescription;
97        private int mIconResourceId;
98        private Uri mIconUri;
99        private Bitmap mIconBitmap;
100        private int mIconBackgroundColor = Color.TRANSPARENT;
101        private ArrayList<Action> mActions;
102        private String mName;
103        private int mSelectedIndex;
104
105        public DialogFragment build() {
106            DialogFragment fragment = new DialogFragment();
107            Bundle args = new Bundle();
108            args.putString(EXTRA_CONTENT_TITLE, mContentTitle);
109            args.putString(EXTRA_CONTENT_BREADCRUMB, mContentBreadcrumb);
110            args.putString(EXTRA_CONTENT_DESCRIPTION, mContentDescription);
111            args.putInt(EXTRA_CONTENT_ICON_RESOURCE_ID, mIconResourceId);
112            args.putParcelable(EXTRA_CONTENT_ICON_URI, mIconUri);
113            args.putParcelable(EXTRA_CONTENT_ICON_BITMAP, mIconBitmap);
114            args.putInt(EXTRA_CONTENT_ICON_BACKGROUND, mIconBackgroundColor);
115            args.putParcelableArrayList(EXTRA_ACTION_ACTIONS, mActions);
116            args.putString(EXTRA_ACTION_NAME, mName);
117            args.putInt(EXTRA_ACTION_SELECTED_INDEX, mSelectedIndex);
118            fragment.setArguments(args);
119            return fragment;
120        }
121
122        public Builder title(String title) {
123            mContentTitle = title;
124            return this;
125        }
126
127        public Builder breadcrumb(String breadcrumb) {
128            mContentBreadcrumb = breadcrumb;
129            return this;
130        }
131
132        public Builder description(String description) {
133            mContentDescription = description;
134            return this;
135        }
136
137        public Builder iconResourceId(int iconResourceId) {
138            mIconResourceId = iconResourceId;
139            return this;
140        }
141
142        public Builder iconUri(Uri iconUri) {
143            mIconUri = iconUri;
144            return this;
145        }
146
147        public Builder iconBitmap(Bitmap iconBitmap) {
148            mIconBitmap = iconBitmap;
149            return this;
150        }
151
152        public Builder iconBackgroundColor(int iconBackgroundColor) {
153            mIconBackgroundColor = iconBackgroundColor;
154            return this;
155        }
156
157        public Builder actions(ArrayList<Action> actions) {
158            mActions = actions;
159            return this;
160        }
161
162        public Builder name(String name) {
163            mName = name;
164            return this;
165        }
166
167        public Builder selectedIndex(int selectedIndex) {
168            mSelectedIndex = selectedIndex;
169            return this;
170        }
171    }
172
173    public static void add(FragmentManager fm, DialogFragment f) {
174        boolean hasDialog = fm.findFragmentByTag(TAG_LEAN_BACK_DIALOG_FRAGMENT) != null;
175        FragmentTransaction ft = fm.beginTransaction();
176
177        if (hasDialog) {
178            ft.setCustomAnimations(ANIMATION_FRAGMENT_ENTER,
179                    ANIMATION_FRAGMENT_EXIT, ANIMATION_FRAGMENT_ENTER_POP,
180                    ANIMATION_FRAGMENT_EXIT_POP);
181            ft.addToBackStack(null);
182        }
183        ft.replace(android.R.id.content, f, TAG_LEAN_BACK_DIALOG_FRAGMENT).commit();
184    }
185
186    private DialogActionAdapter mAdapter;
187    private SelectorAnimator mSelectorAnimator;
188    private VerticalGridView mListView;
189    private Action.Listener mListener;
190    private String mTitle;
191    private String mBreadcrumb;
192    private String mDescription;
193    private int mIconResourceId;
194    private Uri mIconUri;
195    private Bitmap mIconBitmap;
196    private int mIconBackgroundColor = Color.TRANSPARENT;
197    private ArrayList<Action> mActions;
198    private String mName;
199    private int mSelectedIndex = -1;
200    private boolean mEntryTransitionPerformed;
201    private boolean mIntroAnimationInProgress;
202    private BitmapCallback mBitmapCallBack;
203
204    @Override
205    public void onCreate(Bundle savedInstanceState) {
206        android.util.Log.v("DialogFragment", "onCreate");
207        super.onCreate(savedInstanceState);
208        Bundle state = (savedInstanceState != null) ? savedInstanceState : getArguments();
209        if (mTitle == null) {
210            mTitle = state.getString(EXTRA_CONTENT_TITLE);
211        }
212        if (mBreadcrumb == null) {
213            mBreadcrumb = state.getString(EXTRA_CONTENT_BREADCRUMB);
214        }
215        if (mDescription == null) {
216            mDescription = state.getString(EXTRA_CONTENT_DESCRIPTION);
217        }
218        if (mIconResourceId == 0) {
219            mIconResourceId = state.getInt(EXTRA_CONTENT_ICON_RESOURCE_ID, 0);
220        }
221        if (mIconUri == null) {
222            mIconUri = state.getParcelable(EXTRA_CONTENT_ICON_URI);
223        }
224        if (mIconBitmap == null) {
225            mIconBitmap = state.getParcelable(EXTRA_CONTENT_ICON_BITMAP);
226        }
227        if (mIconBackgroundColor == Color.TRANSPARENT) {
228            mIconBackgroundColor = state.getInt(EXTRA_CONTENT_ICON_BACKGROUND, Color.TRANSPARENT);
229        }
230        if (mActions == null) {
231            mActions = state.getParcelableArrayList(EXTRA_ACTION_ACTIONS);
232        }
233        if (mName == null) {
234            mName = state.getString(EXTRA_ACTION_NAME);
235        }
236        if (mSelectedIndex == -1) {
237            mSelectedIndex = state.getInt(EXTRA_ACTION_SELECTED_INDEX, -1);
238        }
239        mEntryTransitionPerformed = state.getBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, false);
240    }
241
242    @Override
243    public View onCreateView(LayoutInflater inflater, ViewGroup container,
244            Bundle savedInstanceState) {
245
246        View v = inflater.inflate(R.layout.lb_dialog_fragment, container, false);
247
248        View contentContainer = v.findViewById(R.id.content_fragment);
249        View content = inflater.inflate(R.layout.lb_dialog_content, container, false);
250        ((ViewGroup) contentContainer).addView(content);
251        setContentView(content);
252        v.setTag(R.id.content_fragment, content);
253
254        View actionContainer = v.findViewById(R.id.action_fragment);
255        View action = inflater.inflate(R.layout.lb_dialog_action_list, container, false);
256        ((ViewGroup) actionContainer).addView(action);
257        setActionView(action);
258        v.setTag(R.id.action_fragment, action);
259
260        return v;
261    }
262
263    @Override
264    public void onSaveInstanceState(Bundle outState) {
265        super.onSaveInstanceState(outState);
266        outState.putString(EXTRA_CONTENT_TITLE, mTitle);
267        outState.putString(EXTRA_CONTENT_BREADCRUMB, mBreadcrumb);
268        outState.putString(EXTRA_CONTENT_DESCRIPTION, mDescription);
269        outState.putInt(EXTRA_CONTENT_ICON_RESOURCE_ID, mIconResourceId);
270        outState.putParcelable(EXTRA_CONTENT_ICON_URI, mIconUri);
271        outState.putParcelable(EXTRA_CONTENT_ICON_BITMAP, mIconBitmap);
272        outState.putInt(EXTRA_CONTENT_ICON_BACKGROUND, mIconBackgroundColor);
273        outState.putParcelableArrayList(EXTRA_ACTION_ACTIONS, mActions);
274        outState.putInt(EXTRA_ACTION_SELECTED_INDEX,
275                (mListView != null) ? getSelectedItemPosition() : mSelectedIndex);
276        outState.putString(EXTRA_ACTION_NAME, mName);
277        outState.putBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, mEntryTransitionPerformed);
278    }
279
280    @Override
281    public void onStart() {
282        super.onStart();
283        if (!mEntryTransitionPerformed) {
284            mEntryTransitionPerformed = true;
285            performEntryTransition();
286        } else {
287            performSelectorTransition();
288        }
289    }
290
291    @Override
292    public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
293        View dialogView = getView();
294        View contentView = (View) dialogView.getTag(R.id.content_fragment);
295        View actionView = (View) dialogView.getTag(R.id.action_fragment);
296        View actionContainerView = dialogView.findViewById(R.id.action_fragment);
297        View titleView = (View) contentView.getTag(R.id.title);
298        View breadcrumbView = (View) contentView.getTag(R.id.breadcrumb);
299        View descriptionView = (View) contentView.getTag(R.id.description);
300        View iconView = (View) contentView.getTag(R.id.icon);
301        View listView = (View) actionView.getTag(R.id.list);
302        View selectorView = (View) actionView.getTag(R.id.selector);
303
304        ArrayList<Animator> animators = new ArrayList<>();
305
306        switch (nextAnim) {
307            case ANIMATION_FRAGMENT_ENTER:
308                animators.add(createSlideInFromEndAnimator(titleView));
309                animators.add(createSlideInFromEndAnimator(breadcrumbView));
310                animators.add(createSlideInFromEndAnimator(descriptionView));
311                animators.add(createSlideInFromEndAnimator(iconView));
312                animators.add(createSlideInFromEndAnimator(listView));
313                animators.add(createSlideInFromEndAnimator(selectorView));
314                break;
315            case ANIMATION_FRAGMENT_EXIT:
316                animators.add(createSlideOutToStartAnimator(titleView));
317                animators.add(createSlideOutToStartAnimator(breadcrumbView));
318                animators.add(createSlideOutToStartAnimator(descriptionView));
319                animators.add(createSlideOutToStartAnimator(iconView));
320                animators.add(createSlideOutToStartAnimator(listView));
321                animators.add(createSlideOutToStartAnimator(selectorView));
322                animators.add(createFadeOutAnimator(actionContainerView));
323                break;
324            case ANIMATION_FRAGMENT_ENTER_POP:
325                animators.add(createSlideInFromStartAnimator(titleView));
326                animators.add(createSlideInFromStartAnimator(breadcrumbView));
327                animators.add(createSlideInFromStartAnimator(descriptionView));
328                animators.add(createSlideInFromStartAnimator(iconView));
329                animators.add(createSlideInFromStartAnimator(listView));
330                animators.add(createSlideInFromStartAnimator(selectorView));
331                break;
332            case ANIMATION_FRAGMENT_EXIT_POP:
333                animators.add(createSlideOutToEndAnimator(titleView));
334                animators.add(createSlideOutToEndAnimator(breadcrumbView));
335                animators.add(createSlideOutToEndAnimator(descriptionView));
336                animators.add(createSlideOutToEndAnimator(iconView));
337                animators.add(createSlideOutToEndAnimator(listView));
338                animators.add(createSlideOutToEndAnimator(selectorView));
339                animators.add(createFadeOutAnimator(actionContainerView));
340                break;
341            default:
342                return super.onCreateAnimator(transit, enter, nextAnim);
343        }
344
345        mEntryTransitionPerformed = true;
346        return createDummyAnimator(dialogView, animators);
347    }
348
349    public void setIcon(int iconResourceId) {
350        mIconResourceId = iconResourceId;
351        View v = getView();
352        if (v != null) {
353            final ImageView iconImageView = (ImageView) v.findViewById(R.id.icon);
354            if (iconImageView != null) {
355                if (iconResourceId != 0) {
356                    iconImageView.setImageResource(iconResourceId);
357                    iconImageView.setVisibility(View.VISIBLE);
358                    updateViewSize(iconImageView);
359                }
360            }
361        }
362    }
363
364    /**
365     * Fragments need to call this method in its {@link #onResume()} to set the
366     * custom listener. <br/>
367     * Activities do not need to call this method
368     *
369     * @param listener
370     */
371    public void setListener(Action.Listener listener) {
372        mListener = listener;
373    }
374
375    public boolean hasListener() {
376        return mListener != null;
377    }
378
379    public ArrayList<Action> getActions() {
380        return mActions;
381    }
382
383    public void setActions(ArrayList<Action> actions) {
384        mActions = actions;
385        if (mAdapter != null) {
386            mAdapter.setActions(mActions);
387        }
388    }
389
390    public View getItemView(int position) {
391        return mListView.getChildAt(position);
392    }
393
394    public void setSelectedPosition(int position) {
395        mListView.setSelectedPosition(position);
396    }
397
398    public int getSelectedItemPosition() {
399        return mListView.indexOfChild(mListView.getFocusedChild());
400    }
401
402    /**
403     * Called when intro animation is finished.
404     * <p>
405     * If a subclass is going to alter the view, should wait until this is
406     * called.
407     */
408    public void onIntroAnimationFinished() {
409        mIntroAnimationInProgress = false;
410    }
411
412    public boolean isIntroAnimationInProgress() {
413        return mIntroAnimationInProgress;
414    }
415
416    private void setContentView(View content) {
417        TextView titleView = (TextView) content.findViewById(R.id.title);
418        TextView breadcrumbView = (TextView) content.findViewById(R.id.breadcrumb);
419        TextView descriptionView = (TextView) content.findViewById(R.id.description);
420        titleView.setText(mTitle);
421        breadcrumbView.setText(mBreadcrumb);
422        descriptionView.setText(mDescription);
423        final ImageView iconImageView = (ImageView) content.findViewById(R.id.icon);
424        if (mIconBackgroundColor != Color.TRANSPARENT) {
425            iconImageView.setBackgroundColor(mIconBackgroundColor);
426        }
427
428        if (AccessibilityHelper.forceFocusableViews(getActivity())) {
429            titleView.setFocusable(true);
430            titleView.setFocusableInTouchMode(true);
431            descriptionView.setFocusable(true);
432            descriptionView.setFocusableInTouchMode(true);
433            breadcrumbView.setFocusable(true);
434            breadcrumbView.setFocusableInTouchMode(true);
435        }
436
437        if (mIconResourceId != 0) {
438            iconImageView.setImageResource(mIconResourceId);
439            updateViewSize(iconImageView);
440        } else {
441            if (mIconBitmap != null) {
442                iconImageView.setImageBitmap(mIconBitmap);
443                updateViewSize(iconImageView);
444            } else {
445                if (mIconUri != null) {
446                    iconImageView.setVisibility(View.INVISIBLE);
447
448                    DrawableDownloader bitmapDownloader = DrawableDownloader.getInstance(
449                            content.getContext());
450                    mBitmapCallBack = new BitmapCallback() {
451                        @Override
452                        public void onBitmapRetrieved(Drawable bitmap) {
453                            if (bitmap != null) {
454                                mIconBitmap = (bitmap instanceof BitmapDrawable) ? ((BitmapDrawable) bitmap)
455                                        .getBitmap()
456                                        : null;
457                                iconImageView.setVisibility(View.VISIBLE);
458                                iconImageView.setImageDrawable(bitmap);
459                                updateViewSize(iconImageView);
460                            }
461                        }
462                    };
463
464                    bitmapDownloader.getBitmap(new BitmapWorkerOptions.Builder(
465                            content.getContext()).resource(mIconUri)
466                            .width(iconImageView.getLayoutParams().width).build(),
467                            mBitmapCallBack);
468                } else {
469                    iconImageView.setVisibility(View.GONE);
470                }
471            }
472        }
473
474        content.setTag(R.id.title, titleView);
475        content.setTag(R.id.breadcrumb, breadcrumbView);
476        content.setTag(R.id.description, descriptionView);
477        content.setTag(R.id.icon, iconImageView);
478    }
479
480    private void setActionView(View action) {
481        mAdapter = new DialogActionAdapter(new Action.Listener() {
482            @Override
483            public void onActionClicked(Action action) {
484                // eat events if action is disabled or only displays info
485                if (!action.isEnabled() || action.infoOnly()) {
486                    return;
487                }
488
489                /**
490                 * If the custom lister has been set using
491                 * {@link #setListener(DialogActionAdapter.Listener)}, use it.
492                 * If not, use the activity's default listener.
493                 */
494                if (mListener != null) {
495                    mListener.onActionClicked(action);
496                } else if (getActivity() instanceof Action.Listener) {
497                    Action.Listener listener = (Action.Listener) getActivity();
498                    listener.onActionClicked(action);
499                }
500            }
501        }, new Action.OnFocusListener() {
502            @Override
503            public void onActionFocused(Action action) {
504                if (getActivity() instanceof Action.OnFocusListener) {
505                    Action.OnFocusListener listener = (Action.OnFocusListener) getActivity();
506                    listener.onActionFocused(action);
507                }
508            }
509        }, mActions);
510
511        if (action instanceof VerticalGridView) {
512            mListView = (VerticalGridView) action;
513        } else {
514            mListView = (VerticalGridView) action.findViewById(R.id.list);
515            if (mListView == null) {
516                throw new IllegalStateException("No ListView exists.");
517            }
518//            mListView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
519//            mListView.setWindowAlignmentOffsetPercent(0.5f);
520//            mListView.setItemAlignmentOffset(0);
521//            mListView.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
522            mListView.setWindowAlignmentOffset(0);
523            mListView.setWindowAlignmentOffsetPercent(50f);
524            mListView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
525            View selectorView = action.findViewById(R.id.selector);
526            if (selectorView != null) {
527                mSelectorAnimator = new SelectorAnimator(selectorView, mListView);
528                mListView.setOnScrollListener(mSelectorAnimator);
529            }
530        }
531
532        mListView.requestFocusFromTouch();
533        mListView.setAdapter(mAdapter);
534        mListView.setSelectedPosition(
535                (mSelectedIndex >= 0 && mSelectedIndex < mActions.size()) ? mSelectedIndex
536                : getFirstCheckedAction());
537
538        action.setTag(R.id.list, mListView);
539        action.setTag(R.id.selector, action.findViewById(R.id.selector));
540    }
541
542    private int getFirstCheckedAction() {
543        for (int i = 0, size = mActions.size(); i < size; i++) {
544            if (mActions.get(i).isChecked()) {
545                return i;
546            }
547        }
548        return 0;
549    }
550
551    private void updateViewSize(ImageView iconView) {
552        int intrinsicWidth = iconView.getDrawable().getIntrinsicWidth();
553        LayoutParams lp = iconView.getLayoutParams();
554        if (intrinsicWidth > 0) {
555            lp.height = lp.width * iconView.getDrawable().getIntrinsicHeight()
556                    / intrinsicWidth;
557        } else {
558            // If no intrinsic width, then just mke this a square.
559            lp.height = lp.width;
560        }
561    }
562
563    private void fadeIn(View v) {
564        v.setAlpha(0f);
565        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v, "alpha", 1f);
566        alphaAnimator.setDuration(v.getContext().getResources().getInteger(
567                android.R.integer.config_mediumAnimTime));
568        alphaAnimator.start();
569    }
570
571    private void runDelayedAnim(final Runnable runnable) {
572        final View dialogView = getView();
573        final View contentView = (View) dialogView.getTag(R.id.content_fragment);
574
575        contentView.getViewTreeObserver().addOnGlobalLayoutListener(
576                new ViewTreeObserver.OnGlobalLayoutListener() {
577                    @Override
578                    public void onGlobalLayout() {
579                        contentView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
580                        // if we buildLayer() at this time, the texture is
581                        // actually not created delay a little so we can make
582                        // sure all hardware layer is created before animation,
583                        // in that way we can avoid the jittering of start
584                        // animation
585                        contentView.postOnAnimationDelayed(runnable, ANIMATE_DELAY);
586                    }
587                });
588
589    }
590
591    private void performSelectorTransition() {
592        runDelayedAnim(new Runnable() {
593            @Override
594            public void run() {
595                // Fade in the selector.
596                if (mSelectorAnimator != null) {
597                    mSelectorAnimator.fadeIn();
598                }
599            }
600        });
601    }
602
603    private void performEntryTransition() {
604        final View dialogView = getView();
605        final View contentView = (View) dialogView.getTag(R.id.content_fragment);
606        final View actionContainerView = dialogView.findViewById(R.id.action_fragment);
607
608        mIntroAnimationInProgress = true;
609
610        // Fade out the old activity.
611        getActivity().overridePendingTransition(0, R.anim.lb_dialog_fade_out);
612
613        int bgColor = contentView.getContext().getResources()
614                .getColor(R.color.lb_dialog_activity_background);
615        final ColorDrawable bgDrawable = new ColorDrawable();
616        bgDrawable.setColor(bgColor);
617        bgDrawable.setAlpha(0);
618        dialogView.setBackground(bgDrawable);
619        dialogView.setVisibility(View.INVISIBLE);
620
621        runDelayedAnim(new Runnable() {
622            @Override
623            public void run() {
624                if (!isAdded()) {
625                    // We have been detached before this could run,
626                    // so just bail
627                    return;
628                }
629
630                dialogView.setVisibility(View.VISIBLE);
631
632                // Fade in the activity background protection
633                ObjectAnimator oa = ObjectAnimator.ofInt(bgDrawable, "alpha", 255);
634                oa.setDuration(ANIMATE_IN_DURATION);
635                oa.setStartDelay(SECONDARY_ANIMATE_DELAY);
636                oa.setInterpolator(new DecelerateInterpolator(1.0f));
637                oa.start();
638
639                boolean isRtl = ViewCompat.getLayoutDirection(contentView) ==
640                        ViewCompat.LAYOUT_DIRECTION_RTL;
641                int startDist = isRtl ? SLIDE_IN_DISTANCE : -SLIDE_IN_DISTANCE;
642                int endDist = isRtl ? -actionContainerView.getMeasuredWidth() :
643                        actionContainerView.getMeasuredWidth();
644
645                // Fade in and slide in the ContentFragment
646                // TextViews from the start.
647                prepareAndAnimateView((View) contentView.getTag(R.id.title),
648                        startDist, false);
649                prepareAndAnimateView((View) contentView.getTag(R.id.breadcrumb),
650                        startDist, false);
651                prepareAndAnimateView((View) contentView.getTag(R.id.description),
652                        startDist, false);
653
654                // Fade in and slide in the ActionFragment from the
655                // end.
656                prepareAndAnimateView(actionContainerView,
657                        endDist, false);
658                prepareAndAnimateView((View) contentView.getTag(R.id.icon),
659                        startDist, true);
660
661                // Fade in the selector.
662                if (mSelectorAnimator != null) {
663                    mSelectorAnimator.fadeIn();
664                }
665            }
666        });
667    }
668
669    private void prepareAndAnimateView(final View v, float initTransX,
670            final boolean notifyAnimationFinished) {
671        v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
672        v.buildLayer();
673        v.setAlpha(0);
674        v.setTranslationX(initTransX);
675        v.animate().alpha(1f).translationX(0).setDuration(ANIMATE_IN_DURATION)
676                .setStartDelay(SECONDARY_ANIMATE_DELAY);
677        v.animate().setInterpolator(new DecelerateInterpolator(1.0f));
678        v.animate().setListener(new AnimatorListenerAdapter() {
679                @Override
680            public void onAnimationEnd(Animator animation) {
681                v.setLayerType(View.LAYER_TYPE_NONE, null);
682                if (notifyAnimationFinished) {
683                    onIntroAnimationFinished();
684                }
685            }
686        });
687        v.animate().start();
688    }
689
690    private Animator createDummyAnimator(final View v, ArrayList<Animator> animators) {
691        final AnimatorSet animatorSet = new AnimatorSet();
692        animatorSet.playTogether(animators);
693        return new UntargetableAnimatorSet(animatorSet);
694    }
695
696    private Animator createAnimator(View v, int resourceId) {
697        Animator animator = AnimatorInflater.loadAnimator(v.getContext(), resourceId);
698        animator.setTarget(v);
699        return animator;
700    }
701
702    private Animator createSlideOutToStartAnimator(View v) {
703        boolean isRtl = ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_RTL;
704        return createTranslateAlphaAnimator(v, 0, isRtl ? 200f : -200f, 1f, 0);
705    }
706
707    private Animator createSlideInFromEndAnimator(View v) {
708        boolean isRtl = ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_RTL;
709        return createTranslateAlphaAnimator(v, isRtl ? -200f : 200f, 0, 0, 1f);
710    }
711
712    private Animator createSlideInFromStartAnimator(View v) {
713        boolean isRtl = ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_RTL;
714        return createTranslateAlphaAnimator(v, isRtl ? 200f : -200f, 0, 0, 1f);
715    }
716
717    private Animator createSlideOutToEndAnimator(View v) {
718        boolean isRtl = ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_RTL;
719        return createTranslateAlphaAnimator(v, 0, isRtl ? -200f : 200f, 1f, 0);
720    }
721
722    private Animator createFadeOutAnimator(View v) {
723        return createAlphaAnimator(v, 1f, 0);
724    }
725
726    private Animator createTranslateAlphaAnimator(View v, float fromTranslateX, float toTranslateX,
727            float fromAlpha, float toAlpha) {
728        ObjectAnimator translateAnimator = ObjectAnimator.ofFloat(v, "translationX", fromTranslateX,
729                toTranslateX);
730        translateAnimator.setDuration(
731                getResources().getInteger(android.R.integer.config_longAnimTime));
732        Animator alphaAnimator = createAlphaAnimator(v, fromAlpha, toAlpha);
733        AnimatorSet animatorSet = new AnimatorSet();
734        animatorSet.play(translateAnimator).with(alphaAnimator);
735        return animatorSet;
736    }
737
738    private Animator createAlphaAnimator(View v, float fromAlpha, float toAlpha) {
739        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v, "alpha", fromAlpha, toAlpha);
740        alphaAnimator.setDuration(getResources().getInteger(android.R.integer.config_longAnimTime));
741        return alphaAnimator;
742    }
743
744    private static class SelectorAnimator extends RecyclerView.OnScrollListener {
745
746        private final View mSelectorView;
747        private final ViewGroup mParentView;
748        private final int mAnimationDuration;
749        private volatile boolean mFadedOut = true;
750
751        SelectorAnimator(View selectorView, ViewGroup parentView) {
752            mSelectorView = selectorView;
753            mParentView = parentView;
754            mAnimationDuration = selectorView.getContext()
755                    .getResources().getInteger(R.integer.lb_dialog_animation_duration);
756        }
757
758        // We want to fade in the selector if we've stopped scrolling on it. If
759        // we're scrolling, we want to ensure to dim the selector if we haven't
760        // already. We dim the last highlighted view so that while a user is
761        // scrolling, nothing is highlighted.
762        @Override
763        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
764            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
765                fadeIn();
766            } else {
767                fadeOut();
768            }
769        }
770
771        public void fadeIn() {
772            // The selector starts with a height of 0. In order to scale up
773            // from
774            // 0 we first need the set the height to 1 and scale form there.
775            int selectorHeight = mSelectorView.getHeight();
776            if (selectorHeight == 0) {
777                LayoutParams lp = mSelectorView.getLayoutParams();
778                lp.height = selectorHeight = mSelectorView.getContext().getResources()
779                        .getDimensionPixelSize(R.dimen.lb_action_fragment_selector_min_height);
780                mSelectorView.setLayoutParams(lp);
781            }
782            View focusedChild = mParentView.getFocusedChild();
783            if (focusedChild != null) {
784                float scaleY = (float) focusedChild.getHeight() / selectorHeight;
785                ViewPropertyAnimator animation = mSelectorView.animate()
786                        .alpha(1f)
787                        .setListener(new Listener(false))
788                        .setDuration(mAnimationDuration)
789                        .setInterpolator(new DecelerateInterpolator(2f));
790                if (mFadedOut) {
791                    // selector is completely faded out, so we can just
792                    // scale
793                    // before fading in.
794                    mSelectorView.setScaleY(scaleY);
795                } else {
796                    // selector is not faded out, so we must animate the
797                    // scale
798                    // as we fade in.
799                    animation.scaleY(scaleY);
800                }
801                animation.start();
802            }
803        }
804
805        public void fadeOut() {
806            mSelectorView.animate()
807            .alpha(0f)
808            .setDuration(mAnimationDuration)
809            .setInterpolator(new DecelerateInterpolator(2f))
810            .setListener(new Listener(true))
811            .start();
812        }
813
814        /**
815         * Sets {@link BaseScrollAdapterFragment#mFadedOut}
816         * {@link BaseScrollAdapterFragment#mFadedOut} is true, iff
817         * {@link BaseScrollAdapterFragment#mSelectorView} has an alpha of 0
818         * (faded out). If false the view either has an alpha of 1 (visible) or
819         * is in the process of animating.
820         */
821        private class Listener implements Animator.AnimatorListener {
822            private final boolean mFadingOut;
823            private boolean mCanceled;
824
825            public Listener(boolean fadingOut) {
826                mFadingOut = fadingOut;
827            }
828
829            @Override
830            public void onAnimationStart(Animator animation) {
831                if (!mFadingOut) {
832                    mFadedOut = false;
833                }
834            }
835
836            @Override
837            public void onAnimationEnd(Animator animation) {
838                if (!mCanceled && mFadingOut) {
839                    mFadedOut = true;
840                }
841            }
842
843            @Override
844            public void onAnimationCancel(Animator animation) {
845                mCanceled = true;
846            }
847
848            @Override
849            public void onAnimationRepeat(Animator animation) {
850            }
851        }
852    }
853
854    private static class UntargetableAnimatorSet extends Animator {
855
856        private final AnimatorSet mAnimatorSet;
857
858        UntargetableAnimatorSet(AnimatorSet animatorSet) {
859            mAnimatorSet = animatorSet;
860        }
861
862        @Override
863        public void addListener(Animator.AnimatorListener listener) {
864            mAnimatorSet.addListener(listener);
865        }
866
867        @Override
868        public void cancel() {
869            mAnimatorSet.cancel();
870        }
871
872        @Override
873        public Animator clone() {
874            return mAnimatorSet.clone();
875        }
876
877        @Override
878        public void end() {
879            mAnimatorSet.end();
880        }
881
882        @Override
883        public long getDuration() {
884            return mAnimatorSet.getDuration();
885        }
886
887        @Override
888        public ArrayList<Animator.AnimatorListener> getListeners() {
889            return mAnimatorSet.getListeners();
890        }
891
892        @Override
893        public long getStartDelay() {
894            return mAnimatorSet.getStartDelay();
895        }
896
897        @Override
898        public boolean isRunning() {
899            return mAnimatorSet.isRunning();
900        }
901
902        @Override
903        public boolean isStarted() {
904            return mAnimatorSet.isStarted();
905        }
906
907        @Override
908        public void removeAllListeners() {
909            mAnimatorSet.removeAllListeners();
910        }
911
912        @Override
913        public void removeListener(Animator.AnimatorListener listener) {
914            mAnimatorSet.removeListener(listener);
915        }
916
917        @Override
918        public Animator setDuration(long duration) {
919            return mAnimatorSet.setDuration(duration);
920        }
921
922        @Override
923        public void setInterpolator(TimeInterpolator value) {
924            mAnimatorSet.setInterpolator(value);
925        }
926
927        @Override
928        public void setStartDelay(long startDelay) {
929            mAnimatorSet.setStartDelay(startDelay);
930        }
931
932        @Override
933        public void setTarget(Object target) {
934            // ignore
935        }
936
937        @Override
938        public void setupEndValues() {
939            mAnimatorSet.setupEndValues();
940        }
941
942        @Override
943        public void setupStartValues() {
944            mAnimatorSet.setupStartValues();
945        }
946
947        @Override
948        public void start() {
949            mAnimatorSet.start();
950        }
951    }
952
953    /**
954     * An data class which represents an action within an
955     * {@link DialogFragment}. A list of Actions represent a list of choices the
956     * user can make, a radio-button list of configuration options, or just a
957     * list of information.
958     */
959    public static class Action implements Parcelable {
960
961        private static final String TAG = "Action";
962
963        public static final int NO_DRAWABLE = 0;
964        public static final int NO_CHECK_SET = 0;
965        public static final int DEFAULT_CHECK_SET_ID = 1;
966
967        /**
968         * Object listening for adapter events.
969         */
970        public interface Listener {
971
972            /**
973             * Called when the user clicks on an action.
974             */
975            public void onActionClicked(Action action);
976        }
977
978        public interface OnFocusListener {
979
980            /**
981             * Called when the user focuses on an action.
982             */
983            public void onActionFocused(Action action);
984        }
985
986        /**
987         * Builds a Action object.
988         */
989        public static class Builder {
990            private String mKey;
991            private String mTitle;
992            private String mDescription;
993            private Intent mIntent;
994            private String mResourcePackageName;
995            private int mDrawableResource = NO_DRAWABLE;
996            private Uri mIconUri;
997            private boolean mChecked;
998            private boolean mMultilineDescription;
999            private boolean mHasNext;
1000            private boolean mInfoOnly;
1001            private int mCheckSetId = NO_CHECK_SET;
1002            private boolean mEnabled = true;
1003
1004            public Action build() {
1005                Action action = new Action();
1006                action.mKey = mKey;
1007                action.mTitle = mTitle;
1008                action.mDescription = mDescription;
1009                action.mIntent = mIntent;
1010                action.mResourcePackageName = mResourcePackageName;
1011                action.mDrawableResource = mDrawableResource;
1012                action.mIconUri = mIconUri;
1013                action.mChecked = mChecked;
1014                action.mMultilineDescription = mMultilineDescription;
1015                action.mHasNext = mHasNext;
1016                action.mInfoOnly = mInfoOnly;
1017                action.mCheckSetId = mCheckSetId;
1018                action.mEnabled = mEnabled;
1019                return action;
1020            }
1021
1022            public Builder key(String key) {
1023                mKey = key;
1024                return this;
1025            }
1026
1027            public Builder title(String title) {
1028                mTitle = title;
1029                return this;
1030            }
1031
1032            public Builder description(String description) {
1033                mDescription = description;
1034                return this;
1035            }
1036
1037            public Builder intent(Intent intent) {
1038                mIntent = intent;
1039                return this;
1040            }
1041
1042            public Builder resourcePackageName(String resourcePackageName) {
1043                mResourcePackageName = resourcePackageName;
1044                return this;
1045            }
1046
1047            public Builder drawableResource(int drawableResource) {
1048                mDrawableResource = drawableResource;
1049                return this;
1050            }
1051
1052            public Builder iconUri(Uri iconUri) {
1053                mIconUri = iconUri;
1054                return this;
1055            }
1056
1057            public Builder checked(boolean checked) {
1058                mChecked = checked;
1059                return this;
1060            }
1061
1062            public Builder multilineDescription(boolean multilineDescription) {
1063                mMultilineDescription = multilineDescription;
1064                return this;
1065            }
1066
1067            public Builder hasNext(boolean hasNext) {
1068                mHasNext = hasNext;
1069                return this;
1070            }
1071
1072            public Builder infoOnly(boolean infoOnly) {
1073                mInfoOnly = infoOnly;
1074                return this;
1075            }
1076
1077            public Builder checkSetId(int checkSetId) {
1078                mCheckSetId = checkSetId;
1079                return this;
1080            }
1081
1082            public Builder enabled(boolean enabled) {
1083                mEnabled = enabled;
1084                return this;
1085            }
1086        }
1087
1088        private String mKey;
1089        private String mTitle;
1090        private String mDescription;
1091        private Intent mIntent;
1092
1093        /**
1094         * If not {@code null}, the package name to use to retrieve
1095         * {@link #mDrawableResource}.
1096         */
1097        private String mResourcePackageName;
1098
1099        private int mDrawableResource;
1100        private Uri mIconUri;
1101        private boolean mChecked;
1102        private boolean mMultilineDescription;
1103        private boolean mHasNext;
1104        private boolean mInfoOnly;
1105        private int mCheckSetId;
1106        private boolean mEnabled;
1107
1108        private Action() {
1109        }
1110
1111        public String getKey() {
1112            return mKey;
1113        }
1114
1115        public String getTitle() {
1116            return mTitle;
1117        }
1118
1119        public String getDescription() {
1120            return mDescription;
1121        }
1122
1123        public void setDescription(String description) {
1124            mDescription = description;
1125        }
1126
1127        public Intent getIntent() {
1128            return mIntent;
1129        }
1130
1131        public boolean isChecked() {
1132            return mChecked;
1133        }
1134
1135        public int getDrawableResource() {
1136            return mDrawableResource;
1137        }
1138
1139        public Uri getIconUri() {
1140            return mIconUri;
1141        }
1142
1143        public String getResourcePackageName() {
1144            return mResourcePackageName;
1145        }
1146
1147        /**
1148         * Returns the check set id this action is a part of. All actions in the
1149         * same list with the same check set id are considered linked. When one
1150         * of the actions within that set is selected that action becomes
1151         * checked while all the other actions become unchecked.
1152         *
1153         * @return an integer representing the check set this action is a part
1154         *         of or {@link NO_CHECK_SET} if this action isn't a
1155         *         part of a check set.
1156         */
1157        public int getCheckSetId() {
1158            return mCheckSetId;
1159        }
1160
1161        public boolean hasMultilineDescription() {
1162            return mMultilineDescription;
1163        }
1164
1165        public boolean isEnabled() {
1166            return mEnabled;
1167        }
1168
1169        public void setChecked(boolean checked) {
1170            mChecked = checked;
1171        }
1172
1173        public void setEnabled(boolean enabled) {
1174            mEnabled = enabled;
1175        }
1176
1177        /**
1178         * @return true if the action will request further user input when
1179         *         selected (such as showing another dialog or launching a new
1180         *         activity). False, otherwise.
1181         */
1182        public boolean hasNext() {
1183            return mHasNext;
1184        }
1185
1186        /**
1187         * @return true if the action will only display information and is thus
1188         *         unactionable. If both this and {@link #hasNext()} are true,
1189         *         infoOnly takes precedence. (default is false) e.g. the amount
1190         *         of storage a document uses or cost of an app.
1191         */
1192        public boolean infoOnly() {
1193            return mInfoOnly;
1194        }
1195
1196        /**
1197         * Returns an indicator to be drawn. If null is returned, no space for
1198         * the indicator will be made.
1199         *
1200         * @param context the context of the Activity this Action belongs to
1201         * @return an indicator to draw or null if no indicator space should
1202         *         exist.
1203         */
1204        public Drawable getIndicator(Context context) {
1205            if (mDrawableResource == NO_DRAWABLE) {
1206                return null;
1207            }
1208            if (mResourcePackageName == null) {
1209                return context.getResources().getDrawable(mDrawableResource);
1210            }
1211            // If we get to here, need to load the resources.
1212            Drawable icon = null;
1213            try {
1214                Context packageContext = context.createPackageContext(mResourcePackageName, 0);
1215                icon = packageContext.getResources().getDrawable(mDrawableResource);
1216            } catch (PackageManager.NameNotFoundException e) {
1217                if (Log.isLoggable(TAG, Log.WARN)) {
1218                    Log.w(TAG, "No icon for this action.");
1219                }
1220            } catch (Resources.NotFoundException e) {
1221                if (Log.isLoggable(TAG, Log.WARN)) {
1222                    Log.w(TAG, "No icon for this action.");
1223                }
1224            }
1225            return icon;
1226        }
1227
1228        public static Parcelable.Creator<Action> CREATOR = new Parcelable.Creator<Action>() {
1229
1230                @Override
1231            public Action createFromParcel(Parcel source) {
1232
1233                return new Action.Builder()
1234                        .key(source.readString())
1235                        .title(source.readString())
1236                        .description(source.readString())
1237                        .intent((Intent) source.readParcelable(Intent.class.getClassLoader()))
1238                        .resourcePackageName(source.readString())
1239                        .drawableResource(source.readInt())
1240                        .iconUri((Uri) source.readParcelable(Uri.class.getClassLoader()))
1241                        .checked(source.readInt() != 0)
1242                        .multilineDescription(source.readInt() != 0)
1243                        .checkSetId(source.readInt())
1244                        .build();
1245            }
1246
1247                @Override
1248            public Action[] newArray(int size) {
1249                return new Action[size];
1250            }
1251        };
1252
1253        @Override
1254        public int describeContents() {
1255            return 0;
1256        }
1257
1258        @Override
1259        public void writeToParcel(Parcel dest, int flags) {
1260            dest.writeString(mKey);
1261            dest.writeString(mTitle);
1262            dest.writeString(mDescription);
1263            dest.writeParcelable(mIntent, flags);
1264            dest.writeString(mResourcePackageName);
1265            dest.writeInt(mDrawableResource);
1266            dest.writeParcelable(mIconUri, flags);
1267            dest.writeInt(mChecked ? 1 : 0);
1268            dest.writeInt(mMultilineDescription ? 1 : 0);
1269            dest.writeInt(mCheckSetId);
1270        }
1271    }
1272}
1273