1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.app;
15
16import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
17
18import android.animation.Animator;
19import android.animation.AnimatorSet;
20import android.app.Activity;
21import android.app.Fragment;
22import android.app.FragmentManager;
23import android.app.FragmentManager.BackStackEntry;
24import android.app.FragmentTransaction;
25import android.content.Context;
26import android.os.Build;
27import android.os.Bundle;
28import android.support.annotation.NonNull;
29import android.support.annotation.RestrictTo;
30import android.support.v17.leanback.R;
31import android.support.v17.leanback.transition.TransitionHelper;
32import android.support.v17.leanback.widget.GuidanceStylist;
33import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
34import android.support.v17.leanback.widget.GuidedAction;
35import android.support.v17.leanback.widget.GuidedActionAdapter;
36import android.support.v17.leanback.widget.GuidedActionAdapterGroup;
37import android.support.v17.leanback.widget.GuidedActionsStylist;
38import android.support.v4.app.ActivityCompat;
39import android.support.v7.widget.RecyclerView;
40import android.util.Log;
41import android.util.TypedValue;
42import android.view.ContextThemeWrapper;
43import android.view.Gravity;
44import android.view.LayoutInflater;
45import android.view.View;
46import android.view.ViewGroup;
47import android.widget.FrameLayout;
48import android.widget.LinearLayout;
49
50import java.util.ArrayList;
51import java.util.List;
52
53/**
54 * A GuidedStepFragment is used to guide the user through a decision or series of decisions.
55 * It is composed of a guidance view on the left and a view on the right containing a list of
56 * possible actions.
57 * <p>
58 * <h3>Basic Usage</h3>
59 * <p>
60 * Clients of GuidedStepFragment must create a custom subclass to attach to their Activities.
61 * This custom subclass provides the information necessary to construct the user interface and
62 * respond to user actions. At a minimum, subclasses should override:
63 * <ul>
64 * <li>{@link #onCreateGuidance}, to provide instructions to the user</li>
65 * <li>{@link #onCreateActions}, to provide a set of {@link GuidedAction}s the user can take</li>
66 * <li>{@link #onGuidedActionClicked}, to respond to those actions</li>
67 * </ul>
68 * <p>
69 * Clients use following helper functions to add GuidedStepFragment to Activity or FragmentManager:
70 * <ul>
71 * <li>{@link #addAsRoot(Activity, GuidedStepFragment, int)}, to be called during Activity onCreate,
72 * adds GuidedStepFragment as the first Fragment in activity.</li>
73 * <li>{@link #add(FragmentManager, GuidedStepFragment)} or {@link #add(FragmentManager,
74 * GuidedStepFragment, int)}, to add GuidedStepFragment on top of existing Fragments or
75 * replacing existing GuidedStepFragment when moving forward to next step.</li>
76 * <li>{@link #finishGuidedStepFragments()} can either finish the activity or pop all
77 * GuidedStepFragment from stack.
78 * <li>If app chooses not to use the helper function, it is the app's responsibility to call
79 * {@link #setUiStyle(int)} to select fragment transition and remember the stack entry where it
80 * need pops to.
81 * </ul>
82 * <h3>Theming and Stylists</h3>
83 * <p>
84 * GuidedStepFragment delegates its visual styling to classes called stylists. The {@link
85 * GuidanceStylist} is responsible for the left guidance view, while the {@link
86 * GuidedActionsStylist} is responsible for the right actions view. The stylists use theme
87 * attributes to derive values associated with the presentation, such as colors, animations, etc.
88 * Most simple visual aspects of GuidanceStylist and GuidedActionsStylist can be customized
89 * via theming; see their documentation for more information.
90 * <p>
91 * GuidedStepFragments must have access to an appropriate theme in order for the stylists to
92 * function properly.  Specifically, the fragment must receive {@link
93 * android.support.v17.leanback.R.style#Theme_Leanback_GuidedStep}, or a theme whose parent is
94 * is set to that theme. Themes can be provided in one of three ways:
95 * <ul>
96 * <li>The simplest way is to set the theme for the host Activity to the GuidedStep theme or a
97 * theme that derives from it.</li>
98 * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the
99 * existing Activity theme can have an entry added for the attribute {@link
100 * android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme}. If present,
101 * this theme will be used by GuidedStepFragment as an overlay to the Activity's theme.</li>
102 * <li>Finally, custom subclasses of GuidedStepFragment may provide a theme through the {@link
103 * #onProvideTheme} method. This can be useful if a subclass is used across multiple
104 * Activities.</li>
105 * </ul>
106 * <p>
107 * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by
108 * the Activity's theme.  (Themes whose parent theme is already set to the guided step theme do not
109 * need to set the guidedStepTheme attribute; if set, it will be ignored.)
110 * <p>
111 * If themes do not provide enough customizability, the stylists themselves may be subclassed and
112 * provided to the GuidedStepFragment through the {@link #onCreateGuidanceStylist} and {@link
113 * #onCreateActionsStylist} methods.  The stylists have simple hooks so that subclasses
114 * may override layout files; subclasses may also have more complex logic to determine styling.
115 * <p>
116 * <h3>Guided sequences</h3>
117 * <p>
118 * GuidedStepFragments can be grouped together to provide a guided sequence. GuidedStepFragments
119 * grouped as a sequence use custom animations provided by {@link GuidanceStylist} and
120 * {@link GuidedActionsStylist} (or subclasses) during transitions between steps. Clients
121 * should use {@link #add} to place subsequent GuidedFragments onto the fragment stack so that
122 * custom animations are properly configured. (Custom animations are triggered automatically when
123 * the fragment stack is subsequently popped by any normal mechanism.)
124 * <p>
125 * <i>Note: Currently GuidedStepFragments grouped in this way must all be defined programmatically,
126 * rather than in XML. This restriction may be removed in the future.</i>
127 *
128 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme
129 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepBackground
130 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthWeight
131 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthWeightTwoPanels
132 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsBackground
133 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsBackgroundDark
134 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsElevation
135 * @see GuidanceStylist
136 * @see GuidanceStylist.Guidance
137 * @see GuidedAction
138 * @see GuidedActionsStylist
139 */
140public class GuidedStepFragment extends Fragment implements GuidedActionAdapter.FocusListener {
141
142    private static final String TAG_LEAN_BACK_ACTIONS_FRAGMENT = "leanBackGuidedStepFragment";
143    private static final String EXTRA_ACTION_PREFIX = "action_";
144    private static final String EXTRA_BUTTON_ACTION_PREFIX = "buttonaction_";
145
146    private static final String ENTRY_NAME_REPLACE = "GuidedStepDefault";
147
148    private static final String ENTRY_NAME_ENTRANCE = "GuidedStepEntrance";
149
150    private static final boolean IS_FRAMEWORK_FRAGMENT = true;
151
152    /**
153     * Fragment argument name for UI style.  The argument value is persisted in fragment state and
154     * used to select fragment transition. The value is initially {@link #UI_STYLE_ENTRANCE} and
155     * might be changed in one of the three helper functions:
156     * <ul>
157     * <li>{@link #addAsRoot(Activity, GuidedStepFragment, int)} sets to
158     * {@link #UI_STYLE_ACTIVITY_ROOT}</li>
159     * <li>{@link #add(FragmentManager, GuidedStepFragment)} or {@link #add(FragmentManager,
160     * GuidedStepFragment, int)} sets it to {@link #UI_STYLE_REPLACE} if there is already a
161     * GuidedStepFragment on stack.</li>
162     * <li>{@link #finishGuidedStepFragments()} changes current GuidedStepFragment to
163     * {@link #UI_STYLE_ENTRANCE} for the non activity case.  This is a special case that changes
164     * the transition settings after fragment has been created,  in order to force current
165     * GuidedStepFragment run a return transition of {@link #UI_STYLE_ENTRANCE}</li>
166     * </ul>
167     * <p>
168     * Argument value can be either:
169     * <ul>
170     * <li>{@link #UI_STYLE_REPLACE}</li>
171     * <li>{@link #UI_STYLE_ENTRANCE}</li>
172     * <li>{@link #UI_STYLE_ACTIVITY_ROOT}</li>
173     * </ul>
174     */
175    public static final String EXTRA_UI_STYLE = "uiStyle";
176
177    /**
178     * This is the case that we use GuidedStepFragment to replace another existing
179     * GuidedStepFragment when moving forward to next step. Default behavior of this style is:
180     * <ul>
181     * <li>Enter transition slides in from END(right), exit transition same as
182     * {@link #UI_STYLE_ENTRANCE}.
183     * </li>
184     * </ul>
185     */
186    public static final int UI_STYLE_REPLACE = 0;
187
188    /**
189     * @deprecated Same value as {@link #UI_STYLE_REPLACE}.
190     */
191    @Deprecated
192    public static final int UI_STYLE_DEFAULT = 0;
193
194    /**
195     * Default value for argument {@link #EXTRA_UI_STYLE}. The default value is assigned in
196     * GuidedStepFragment constructor. This is the case that we show GuidedStepFragment on top of
197     * other content. The default behavior of this style:
198     * <ul>
199     * <li>Enter transition slides in from two sides, exit transition slide out to START(left).
200     * Background will be faded in. Note: Changing exit transition by UI style is not working
201     * because fragment transition asks for exit transition before UI style is restored in Fragment
202     * .onCreate().</li>
203     * </ul>
204     * When popping multiple GuidedStepFragment, {@link #finishGuidedStepFragments()} also changes
205     * the top GuidedStepFragment to UI_STYLE_ENTRANCE in order to run the return transition
206     * (reverse of enter transition) of UI_STYLE_ENTRANCE.
207     */
208    public static final int UI_STYLE_ENTRANCE = 1;
209
210    /**
211     * One possible value of argument {@link #EXTRA_UI_STYLE}. This is the case that we show first
212     * GuidedStepFragment in a separate activity. The default behavior of this style:
213     * <ul>
214     * <li>Enter transition is assigned null (will rely on activity transition), exit transition is
215     * same as {@link #UI_STYLE_ENTRANCE}. Note: Changing exit transition by UI style is not working
216     * because fragment transition asks for exit transition before UI style is restored in
217     * Fragment.onCreate().</li>
218     * </ul>
219     */
220    public static final int UI_STYLE_ACTIVITY_ROOT = 2;
221
222    /**
223     * Animation to slide the contents from the side (left/right).
224     * @hide
225     */
226    @RestrictTo(LIBRARY_GROUP)
227    public static final int SLIDE_FROM_SIDE = 0;
228
229    /**
230     * Animation to slide the contents from the bottom.
231     * @hide
232     */
233    @RestrictTo(LIBRARY_GROUP)
234    public static final int SLIDE_FROM_BOTTOM = 1;
235
236    private static final String TAG = "GuidedStepFragment";
237    private static final boolean DEBUG = false;
238
239    /**
240     * @hide
241     */
242    @RestrictTo(LIBRARY_GROUP)
243    public static class DummyFragment extends Fragment {
244        @Override
245        public View onCreateView(LayoutInflater inflater, ViewGroup container,
246                Bundle savedInstanceState) {
247            final View v = new View(inflater.getContext());
248            v.setVisibility(View.GONE);
249            return v;
250        }
251    }
252
253    private ContextThemeWrapper mThemeWrapper;
254    private GuidanceStylist mGuidanceStylist;
255    GuidedActionsStylist mActionsStylist;
256    private GuidedActionsStylist mButtonActionsStylist;
257    private GuidedActionAdapter mAdapter;
258    private GuidedActionAdapter mSubAdapter;
259    private GuidedActionAdapter mButtonAdapter;
260    private GuidedActionAdapterGroup mAdapterGroup;
261    private List<GuidedAction> mActions = new ArrayList<GuidedAction>();
262    private List<GuidedAction> mButtonActions = new ArrayList<GuidedAction>();
263    private int entranceTransitionType = SLIDE_FROM_SIDE;
264
265    public GuidedStepFragment() {
266        mGuidanceStylist = onCreateGuidanceStylist();
267        mActionsStylist = onCreateActionsStylist();
268        mButtonActionsStylist = onCreateButtonActionsStylist();
269        onProvideFragmentTransitions();
270    }
271
272    /**
273     * Creates the presenter used to style the guidance panel. The default implementation returns
274     * a basic GuidanceStylist.
275     * @return The GuidanceStylist used in this fragment.
276     */
277    public GuidanceStylist onCreateGuidanceStylist() {
278        return new GuidanceStylist();
279    }
280
281    /**
282     * Creates the presenter used to style the guided actions panel. The default implementation
283     * returns a basic GuidedActionsStylist.
284     * @return The GuidedActionsStylist used in this fragment.
285     */
286    public GuidedActionsStylist onCreateActionsStylist() {
287        return new GuidedActionsStylist();
288    }
289
290    /**
291     * Creates the presenter used to style a sided actions panel for button only.
292     * The default implementation returns a basic GuidedActionsStylist.
293     * @return The GuidedActionsStylist used in this fragment.
294     */
295    public GuidedActionsStylist onCreateButtonActionsStylist() {
296        GuidedActionsStylist stylist = new GuidedActionsStylist();
297        stylist.setAsButtonActions();
298        return stylist;
299    }
300
301    /**
302     * Returns the theme used for styling the fragment. The default returns -1, indicating that the
303     * host Activity's theme should be used.
304     * @return The theme resource ID of the theme to use in this fragment, or -1 to use the
305     * host Activity's theme.
306     */
307    public int onProvideTheme() {
308        return -1;
309    }
310
311    /**
312     * Returns the information required to provide guidance to the user. This hook is called during
313     * {@link #onCreateView}.  May be overridden to return a custom subclass of {@link
314     * GuidanceStylist.Guidance} for use in a subclass of {@link GuidanceStylist}. The default
315     * returns a Guidance object with empty fields; subclasses should override.
316     * @param savedInstanceState The saved instance state from onCreateView.
317     * @return The Guidance object representing the information used to guide the user.
318     */
319    public @NonNull Guidance onCreateGuidance(Bundle savedInstanceState) {
320        return new Guidance("", "", "", null);
321    }
322
323    /**
324     * Fills out the set of actions available to the user. This hook is called during {@link
325     * #onCreate}. The default leaves the list of actions empty; subclasses should override.
326     * @param actions A non-null, empty list ready to be populated.
327     * @param savedInstanceState The saved instance state from onCreate.
328     */
329    public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
330    }
331
332    /**
333     * Fills out the set of actions shown at right available to the user. This hook is called during
334     * {@link #onCreate}. The default leaves the list of actions empty; subclasses may override.
335     * @param actions A non-null, empty list ready to be populated.
336     * @param savedInstanceState The saved instance state from onCreate.
337     */
338    public void onCreateButtonActions(@NonNull List<GuidedAction> actions,
339            Bundle savedInstanceState) {
340    }
341
342    /**
343     * Callback invoked when an action is taken by the user. Subclasses should override in
344     * order to act on the user's decisions.
345     * @param action The chosen action.
346     */
347    public void onGuidedActionClicked(GuidedAction action) {
348    }
349
350    /**
351     * Callback invoked when an action in sub actions is taken by the user. Subclasses should
352     * override in order to act on the user's decisions.  Default return value is true to close
353     * the sub actions list.
354     * @param action The chosen action.
355     * @return true to collapse the sub actions list, false to keep it expanded.
356     */
357    public boolean onSubGuidedActionClicked(GuidedAction action) {
358        return true;
359    }
360
361    /**
362     * @return True if is current expanded including subactions list or
363     * action with {@link GuidedAction#hasEditableActivatorView()} is true.
364     */
365    public boolean isExpanded() {
366        return mActionsStylist.isExpanded();
367    }
368
369    /**
370     * @return True if the sub actions list is expanded, false otherwise.
371     */
372    public boolean isSubActionsExpanded() {
373        return mActionsStylist.isSubActionsExpanded();
374    }
375
376    /**
377     * Expand a given action's sub actions list.
378     * @param action GuidedAction to expand.
379     * @see #expandAction(GuidedAction, boolean)
380     */
381    public void expandSubActions(GuidedAction action) {
382        if (!action.hasSubActions()) {
383            return;
384        }
385        expandAction(action, true);
386    }
387
388    /**
389     * Expand a given action with sub actions list or
390     * {@link GuidedAction#hasEditableActivatorView()} is true. The method must be called after
391     * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} creates fragment view.
392     *
393     * @param action GuidedAction to expand.
394     * @param withTransition True to run transition animation, false otherwise.
395     */
396    public void expandAction(GuidedAction action, boolean withTransition) {
397        mActionsStylist.expandAction(action, withTransition);
398    }
399
400    /**
401     * Collapse sub actions list.
402     * @see GuidedAction#getSubActions()
403     */
404    public void collapseSubActions() {
405        collapseAction(true);
406    }
407
408    /**
409     * Collapse action which either has a sub actions list or action with
410     * {@link GuidedAction#hasEditableActivatorView()} is true.
411     *
412     * @param withTransition True to run transition animation, false otherwise.
413     */
414    public void collapseAction(boolean withTransition) {
415        if (mActionsStylist != null && mActionsStylist.getActionsGridView() != null) {
416            mActionsStylist.collapseAction(withTransition);
417        }
418    }
419
420    /**
421     * Callback invoked when an action is focused (made to be the current selection) by the user.
422     */
423    @Override
424    public void onGuidedActionFocused(GuidedAction action) {
425    }
426
427    /**
428     * Callback invoked when an action's title or description has been edited, this happens either
429     * when user clicks confirm button in IME or user closes IME window by BACK key.
430     * @deprecated Override {@link #onGuidedActionEditedAndProceed(GuidedAction)} and/or
431     *             {@link #onGuidedActionEditCanceled(GuidedAction)}.
432     */
433    @Deprecated
434    public void onGuidedActionEdited(GuidedAction action) {
435    }
436
437    /**
438     * Callback invoked when an action has been canceled editing, for example when user closes
439     * IME window by BACK key.  Default implementation calls deprecated method
440     * {@link #onGuidedActionEdited(GuidedAction)}.
441     * @param action The action which has been canceled editing.
442     */
443    public void onGuidedActionEditCanceled(GuidedAction action) {
444        onGuidedActionEdited(action);
445    }
446
447    /**
448     * Callback invoked when an action has been edited, for example when user clicks confirm button
449     * in IME window.  Default implementation calls deprecated method
450     * {@link #onGuidedActionEdited(GuidedAction)} and returns {@link GuidedAction#ACTION_ID_NEXT}.
451     *
452     * @param action The action that has been edited.
453     * @return ID of the action will be focused or {@link GuidedAction#ACTION_ID_NEXT},
454     * {@link GuidedAction#ACTION_ID_CURRENT}.
455     */
456    public long onGuidedActionEditedAndProceed(GuidedAction action) {
457        onGuidedActionEdited(action);
458        return GuidedAction.ACTION_ID_NEXT;
459    }
460
461    /**
462     * Adds the specified GuidedStepFragment to the fragment stack, replacing any existing
463     * GuidedStepFragments in the stack, and configuring the fragment-to-fragment custom
464     * transitions.  A backstack entry is added, so the fragment will be dismissed when BACK key
465     * is pressed.
466     * <li>If current fragment on stack is GuidedStepFragment: assign {@link #UI_STYLE_REPLACE}
467     * <li>If current fragment on stack is not GuidedStepFragment: assign {@link #UI_STYLE_ENTRANCE}
468     * <p>
469     * Note: currently fragments added using this method must be created programmatically rather
470     * than via XML.
471     * @param fragmentManager The FragmentManager to be used in the transaction.
472     * @param fragment The GuidedStepFragment to be inserted into the fragment stack.
473     * @return The ID returned by the call FragmentTransaction.commit.
474     */
475    public static int add(FragmentManager fragmentManager, GuidedStepFragment fragment) {
476        return add(fragmentManager, fragment, android.R.id.content);
477    }
478
479    /**
480     * Adds the specified GuidedStepFragment to the fragment stack, replacing any existing
481     * GuidedStepFragments in the stack, and configuring the fragment-to-fragment custom
482     * transitions.  A backstack entry is added, so the fragment will be dismissed when BACK key
483     * is pressed.
484     * <li>If current fragment on stack is GuidedStepFragment: assign {@link #UI_STYLE_REPLACE} and
485     * {@link #onAddSharedElementTransition(FragmentTransaction, GuidedStepFragment)} will be called
486     * to perform shared element transition between GuidedStepFragments.
487     * <li>If current fragment on stack is not GuidedStepFragment: assign {@link #UI_STYLE_ENTRANCE}
488     * <p>
489     * Note: currently fragments added using this method must be created programmatically rather
490     * than via XML.
491     * @param fragmentManager The FragmentManager to be used in the transaction.
492     * @param fragment The GuidedStepFragment to be inserted into the fragment stack.
493     * @param id The id of container to add GuidedStepFragment, can be android.R.id.content.
494     * @return The ID returned by the call FragmentTransaction.commit.
495     */
496    public static int add(FragmentManager fragmentManager, GuidedStepFragment fragment, int id) {
497        GuidedStepFragment current = getCurrentGuidedStepFragment(fragmentManager);
498        boolean inGuidedStep = current != null;
499        if (IS_FRAMEWORK_FRAGMENT && Build.VERSION.SDK_INT >= 21 && Build.VERSION.SDK_INT < 23
500                && !inGuidedStep) {
501            // workaround b/22631964 for framework fragment
502            fragmentManager.beginTransaction()
503                .replace(id, new DummyFragment(), TAG_LEAN_BACK_ACTIONS_FRAGMENT)
504                .commit();
505        }
506        FragmentTransaction ft = fragmentManager.beginTransaction();
507
508        fragment.setUiStyle(inGuidedStep ? UI_STYLE_REPLACE : UI_STYLE_ENTRANCE);
509        ft.addToBackStack(fragment.generateStackEntryName());
510        if (current != null) {
511            fragment.onAddSharedElementTransition(ft, current);
512        }
513        return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
514    }
515
516    /**
517     * Called when this fragment is added to FragmentTransaction with {@link #UI_STYLE_REPLACE} (aka
518     * when the GuidedStepFragment replacing an existing GuidedStepFragment). Default implementation
519     * establishes connections between action background views to morph action background bounds
520     * change from disappearing GuidedStepFragment into this GuidedStepFragment. The default
521     * implementation heavily relies on {@link GuidedActionsStylist}'s layout, app may override this
522     * method when modifying the default layout of {@link GuidedActionsStylist}.
523     *
524     * @see GuidedActionsStylist
525     * @see #onProvideFragmentTransitions()
526     * @param ft The FragmentTransaction to add shared element.
527     * @param disappearing The disappearing fragment.
528     */
529    protected void onAddSharedElementTransition(FragmentTransaction ft, GuidedStepFragment
530            disappearing) {
531        View fragmentView = disappearing.getView();
532        addNonNullSharedElementTransition(ft, fragmentView.findViewById(
533                R.id.action_fragment_root), "action_fragment_root");
534        addNonNullSharedElementTransition(ft, fragmentView.findViewById(
535                R.id.action_fragment_background), "action_fragment_background");
536        addNonNullSharedElementTransition(ft, fragmentView.findViewById(
537                R.id.action_fragment), "action_fragment");
538        addNonNullSharedElementTransition(ft, fragmentView.findViewById(
539                R.id.guidedactions_root), "guidedactions_root");
540        addNonNullSharedElementTransition(ft, fragmentView.findViewById(
541                R.id.guidedactions_content), "guidedactions_content");
542        addNonNullSharedElementTransition(ft, fragmentView.findViewById(
543                R.id.guidedactions_list_background), "guidedactions_list_background");
544        addNonNullSharedElementTransition(ft, fragmentView.findViewById(
545                R.id.guidedactions_root2), "guidedactions_root2");
546        addNonNullSharedElementTransition(ft, fragmentView.findViewById(
547                R.id.guidedactions_content2), "guidedactions_content2");
548        addNonNullSharedElementTransition(ft, fragmentView.findViewById(
549                R.id.guidedactions_list_background2), "guidedactions_list_background2");
550    }
551
552    private static void addNonNullSharedElementTransition (FragmentTransaction ft, View subView,
553                                                           String transitionName)
554    {
555        if (subView != null)
556            TransitionHelper.addSharedElement(ft, subView, transitionName);
557    }
558
559    /**
560     * Returns BackStackEntry name for the GuidedStepFragment or empty String if no entry is
561     * associated.  Note {@link #UI_STYLE_ACTIVITY_ROOT} will return empty String.  The method
562     * returns undefined value if the fragment is not in FragmentManager.
563     * @return BackStackEntry name for the GuidedStepFragment or empty String if no entry is
564     * associated.
565     */
566    final String generateStackEntryName() {
567        return generateStackEntryName(getUiStyle(), getClass());
568    }
569
570    /**
571     * Generates BackStackEntry name for GuidedStepFragment class or empty String if no entry is
572     * associated.  Note {@link #UI_STYLE_ACTIVITY_ROOT} is not allowed and returns empty String.
573     * @param uiStyle {@link #UI_STYLE_REPLACE} or {@link #UI_STYLE_ENTRANCE}
574     * @return BackStackEntry name for the GuidedStepFragment or empty String if no entry is
575     * associated.
576     */
577    static String generateStackEntryName(int uiStyle, Class guidedStepFragmentClass) {
578        switch (uiStyle) {
579        case UI_STYLE_REPLACE:
580            return ENTRY_NAME_REPLACE + guidedStepFragmentClass.getName();
581        case UI_STYLE_ENTRANCE:
582            return ENTRY_NAME_ENTRANCE + guidedStepFragmentClass.getName();
583        case UI_STYLE_ACTIVITY_ROOT:
584        default:
585            return "";
586        }
587    }
588
589    /**
590     * Returns true if the backstack entry represents GuidedStepFragment with
591     * {@link #UI_STYLE_ENTRANCE}, i.e. this is the first GuidedStepFragment pushed to stack; false
592     * otherwise.
593     * @see #generateStackEntryName(int, Class)
594     * @param backStackEntryName Name of BackStackEntry.
595     * @return True if the backstack represents GuidedStepFragment with {@link #UI_STYLE_ENTRANCE};
596     * false otherwise.
597     */
598    static boolean isStackEntryUiStyleEntrance(String backStackEntryName) {
599        return backStackEntryName != null && backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE);
600    }
601
602    /**
603     * Extract Class name from BackStackEntry name.
604     * @param backStackEntryName Name of BackStackEntry.
605     * @return Class name of GuidedStepFragment.
606     */
607    static String getGuidedStepFragmentClassName(String backStackEntryName) {
608        if (backStackEntryName.startsWith(ENTRY_NAME_REPLACE)) {
609            return backStackEntryName.substring(ENTRY_NAME_REPLACE.length());
610        } else if (backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE)) {
611            return backStackEntryName.substring(ENTRY_NAME_ENTRANCE.length());
612        } else {
613            return "";
614        }
615    }
616
617    /**
618     * Adds the specified GuidedStepFragment as content of Activity; no backstack entry is added so
619     * the activity will be dismissed when BACK key is pressed.  The method is typically called in
620     * Activity.onCreate() when savedInstanceState is null.  When savedInstanceState is not null,
621     * the Activity is being restored,  do not call addAsRoot() to duplicate the Fragment restored
622     * by FragmentManager.
623     * {@link #UI_STYLE_ACTIVITY_ROOT} is assigned.
624     *
625     * Note: currently fragments added using this method must be created programmatically rather
626     * than via XML.
627     * @param activity The Activity to be used to insert GuidedstepFragment.
628     * @param fragment The GuidedStepFragment to be inserted into the fragment stack.
629     * @param id The id of container to add GuidedStepFragment, can be android.R.id.content.
630     * @return The ID returned by the call FragmentTransaction.commit, or -1 there is already
631     *         GuidedStepFragment.
632     */
633    public static int addAsRoot(Activity activity, GuidedStepFragment fragment, int id) {
634        // Workaround b/23764120: call getDecorView() to force requestFeature of ActivityTransition.
635        activity.getWindow().getDecorView();
636        FragmentManager fragmentManager = activity.getFragmentManager();
637        if (fragmentManager.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT) != null) {
638            Log.w(TAG, "Fragment is already exists, likely calling "
639                    + "addAsRoot() when savedInstanceState is not null in Activity.onCreate().");
640            return -1;
641        }
642        FragmentTransaction ft = fragmentManager.beginTransaction();
643        fragment.setUiStyle(UI_STYLE_ACTIVITY_ROOT);
644        return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
645    }
646
647    /**
648     * Returns the current GuidedStepFragment on the fragment transaction stack.
649     * @return The current GuidedStepFragment, if any, on the fragment transaction stack.
650     */
651    public static GuidedStepFragment getCurrentGuidedStepFragment(FragmentManager fm) {
652        Fragment f = fm.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT);
653        if (f instanceof GuidedStepFragment) {
654            return (GuidedStepFragment) f;
655        }
656        return null;
657    }
658
659    /**
660     * Returns the GuidanceStylist that displays guidance information for the user.
661     * @return The GuidanceStylist for this fragment.
662     */
663    public GuidanceStylist getGuidanceStylist() {
664        return mGuidanceStylist;
665    }
666
667    /**
668     * Returns the GuidedActionsStylist that displays the actions the user may take.
669     * @return The GuidedActionsStylist for this fragment.
670     */
671    public GuidedActionsStylist getGuidedActionsStylist() {
672        return mActionsStylist;
673    }
674
675    /**
676     * Returns the list of button GuidedActions that the user may take in this fragment.
677     * @return The list of button GuidedActions for this fragment.
678     */
679    public List<GuidedAction> getButtonActions() {
680        return mButtonActions;
681    }
682
683    /**
684     * Find button GuidedAction by Id.
685     * @param id  Id of the button action to search.
686     * @return  GuidedAction object or null if not found.
687     */
688    public GuidedAction findButtonActionById(long id) {
689        int index = findButtonActionPositionById(id);
690        return index >= 0 ? mButtonActions.get(index) : null;
691    }
692
693    /**
694     * Find button GuidedAction position in array by Id.
695     * @param id  Id of the button action to search.
696     * @return  position of GuidedAction object in array or -1 if not found.
697     */
698    public int findButtonActionPositionById(long id) {
699        if (mButtonActions != null) {
700            for (int i = 0; i < mButtonActions.size(); i++) {
701                GuidedAction action = mButtonActions.get(i);
702                if (mButtonActions.get(i).getId() == id) {
703                    return i;
704                }
705            }
706        }
707        return -1;
708    }
709
710    /**
711     * Returns the GuidedActionsStylist that displays the button actions the user may take.
712     * @return The GuidedActionsStylist for this fragment.
713     */
714    public GuidedActionsStylist getGuidedButtonActionsStylist() {
715        return mButtonActionsStylist;
716    }
717
718    /**
719     * Sets the list of button GuidedActions that the user may take in this fragment.
720     * @param actions The list of button GuidedActions for this fragment.
721     */
722    public void setButtonActions(List<GuidedAction> actions) {
723        mButtonActions = actions;
724        if (mButtonAdapter != null) {
725            mButtonAdapter.setActions(mButtonActions);
726        }
727    }
728
729    /**
730     * Notify an button action has changed and update its UI.
731     * @param position Position of the button GuidedAction in array.
732     */
733    public void notifyButtonActionChanged(int position) {
734        if (mButtonAdapter != null) {
735            mButtonAdapter.notifyItemChanged(position);
736        }
737    }
738
739    /**
740     * Returns the view corresponding to the button action at the indicated position in the list of
741     * actions for this fragment.
742     * @param position The integer position of the button action of interest.
743     * @return The View corresponding to the button action at the indicated position, or null if
744     * that action is not currently onscreen.
745     */
746    public View getButtonActionItemView(int position) {
747        final RecyclerView.ViewHolder holder = mButtonActionsStylist.getActionsGridView()
748                    .findViewHolderForPosition(position);
749        return holder == null ? null : holder.itemView;
750    }
751
752    /**
753     * Scrolls the action list to the position indicated, selecting that button action's view.
754     * @param position The integer position of the button action of interest.
755     */
756    public void setSelectedButtonActionPosition(int position) {
757        mButtonActionsStylist.getActionsGridView().setSelectedPosition(position);
758    }
759
760    /**
761     * Returns the position if the currently selected button GuidedAction.
762     * @return position The integer position of the currently selected button action.
763     */
764    public int getSelectedButtonActionPosition() {
765        return mButtonActionsStylist.getActionsGridView().getSelectedPosition();
766    }
767
768    /**
769     * Returns the list of GuidedActions that the user may take in this fragment.
770     * @return The list of GuidedActions for this fragment.
771     */
772    public List<GuidedAction> getActions() {
773        return mActions;
774    }
775
776    /**
777     * Find GuidedAction by Id.
778     * @param id  Id of the action to search.
779     * @return  GuidedAction object or null if not found.
780     */
781    public GuidedAction findActionById(long id) {
782        int index = findActionPositionById(id);
783        return index >= 0 ? mActions.get(index) : null;
784    }
785
786    /**
787     * Find GuidedAction position in array by Id.
788     * @param id  Id of the action to search.
789     * @return  position of GuidedAction object in array or -1 if not found.
790     */
791    public int findActionPositionById(long id) {
792        if (mActions != null) {
793            for (int i = 0; i < mActions.size(); i++) {
794                GuidedAction action = mActions.get(i);
795                if (mActions.get(i).getId() == id) {
796                    return i;
797                }
798            }
799        }
800        return -1;
801    }
802
803    /**
804     * Sets the list of GuidedActions that the user may take in this fragment.
805     * @param actions The list of GuidedActions for this fragment.
806     */
807    public void setActions(List<GuidedAction> actions) {
808        mActions = actions;
809        if (mAdapter != null) {
810            mAdapter.setActions(mActions);
811        }
812    }
813
814    /**
815     * Notify an action has changed and update its UI.
816     * @param position Position of the GuidedAction in array.
817     */
818    public void notifyActionChanged(int position) {
819        if (mAdapter != null) {
820            mAdapter.notifyItemChanged(position);
821        }
822    }
823
824    /**
825     * Returns the view corresponding to the action at the indicated position in the list of
826     * actions for this fragment.
827     * @param position The integer position of the action of interest.
828     * @return The View corresponding to the action at the indicated position, or null if that
829     * action is not currently onscreen.
830     */
831    public View getActionItemView(int position) {
832        final RecyclerView.ViewHolder holder = mActionsStylist.getActionsGridView()
833                    .findViewHolderForPosition(position);
834        return holder == null ? null : holder.itemView;
835    }
836
837    /**
838     * Scrolls the action list to the position indicated, selecting that action's view.
839     * @param position The integer position of the action of interest.
840     */
841    public void setSelectedActionPosition(int position) {
842        mActionsStylist.getActionsGridView().setSelectedPosition(position);
843    }
844
845    /**
846     * Returns the position if the currently selected GuidedAction.
847     * @return position The integer position of the currently selected action.
848     */
849    public int getSelectedActionPosition() {
850        return mActionsStylist.getActionsGridView().getSelectedPosition();
851    }
852
853    /**
854     * Called by Constructor to provide fragment transitions.  The default implementation assigns
855     * transitions based on {@link #getUiStyle()}:
856     * <ul>
857     * <li> {@link #UI_STYLE_REPLACE} Slide from/to end(right) for enter transition, slide from/to
858     * start(left) for exit transition, shared element enter transition is set to ChangeBounds.
859     * <li> {@link #UI_STYLE_ENTRANCE} Enter transition is set to slide from both sides, exit
860     * transition is same as {@link #UI_STYLE_REPLACE}, no shared element enter transition.
861     * <li> {@link #UI_STYLE_ACTIVITY_ROOT} Enter transition is set to null and app should rely on
862     * activity transition, exit transition is same as {@link #UI_STYLE_REPLACE}, no shared element
863     * enter transition.
864     * </ul>
865     * <p>
866     * The default implementation heavily relies on {@link GuidedActionsStylist} and
867     * {@link GuidanceStylist} layout, app may override this method when modifying the default
868     * layout of {@link GuidedActionsStylist} or {@link GuidanceStylist}.
869     * <p>
870     * TIP: because the fragment view is removed during fragment transition, in general app cannot
871     * use two Visibility transition together. Workaround is to create your own Visibility
872     * transition that controls multiple animators (e.g. slide and fade animation in one Transition
873     * class).
874     */
875    protected void onProvideFragmentTransitions() {
876        if (Build.VERSION.SDK_INT >= 21) {
877            final int uiStyle = getUiStyle();
878            if (uiStyle == UI_STYLE_REPLACE) {
879                Object enterTransition = TransitionHelper.createFadeAndShortSlide(Gravity.END);
880                TransitionHelper.exclude(enterTransition, R.id.guidedstep_background, true);
881                TransitionHelper.exclude(enterTransition, R.id.guidedactions_sub_list_background,
882                        true);
883                TransitionHelper.setEnterTransition(this, enterTransition);
884
885                Object fade = TransitionHelper.createFadeTransition(
886                        TransitionHelper.FADE_IN | TransitionHelper.FADE_OUT);
887                TransitionHelper.include(fade, R.id.guidedactions_sub_list_background);
888                Object changeBounds = TransitionHelper.createChangeBounds(false);
889                Object sharedElementTransition = TransitionHelper.createTransitionSet(false);
890                TransitionHelper.addTransition(sharedElementTransition, fade);
891                TransitionHelper.addTransition(sharedElementTransition, changeBounds);
892                TransitionHelper.setSharedElementEnterTransition(this, sharedElementTransition);
893            } else if (uiStyle == UI_STYLE_ENTRANCE) {
894                if (entranceTransitionType == SLIDE_FROM_SIDE) {
895                    Object fade = TransitionHelper.createFadeTransition(
896                            TransitionHelper.FADE_IN | TransitionHelper.FADE_OUT);
897                    TransitionHelper.include(fade, R.id.guidedstep_background);
898                    Object slideFromSide = TransitionHelper.createFadeAndShortSlide(
899                            Gravity.END | Gravity.START);
900                    TransitionHelper.include(slideFromSide, R.id.content_fragment);
901                    TransitionHelper.include(slideFromSide, R.id.action_fragment_root);
902                    Object enterTransition = TransitionHelper.createTransitionSet(false);
903                    TransitionHelper.addTransition(enterTransition, fade);
904                    TransitionHelper.addTransition(enterTransition, slideFromSide);
905                    TransitionHelper.setEnterTransition(this, enterTransition);
906                } else {
907                    Object slideFromBottom = TransitionHelper.createFadeAndShortSlide(
908                            Gravity.BOTTOM);
909                    TransitionHelper.include(slideFromBottom, R.id.guidedstep_background_view_root);
910                    Object enterTransition = TransitionHelper.createTransitionSet(false);
911                    TransitionHelper.addTransition(enterTransition, slideFromBottom);
912                    TransitionHelper.setEnterTransition(this, enterTransition);
913                }
914                // No shared element transition
915                TransitionHelper.setSharedElementEnterTransition(this, null);
916            } else if (uiStyle == UI_STYLE_ACTIVITY_ROOT) {
917                // for Activity root, we don't need enter transition, use activity transition
918                TransitionHelper.setEnterTransition(this, null);
919                // No shared element transition
920                TransitionHelper.setSharedElementEnterTransition(this, null);
921            }
922            // exitTransition is same for all style
923            Object exitTransition = TransitionHelper.createFadeAndShortSlide(Gravity.START);
924            TransitionHelper.exclude(exitTransition, R.id.guidedstep_background, true);
925            TransitionHelper.exclude(exitTransition, R.id.guidedactions_sub_list_background,
926                    true);
927            TransitionHelper.setExitTransition(this, exitTransition);
928        }
929    }
930
931    /**
932     * Called by onCreateView to inflate background view.  Default implementation loads view
933     * from {@link R.layout#lb_guidedstep_background} which holds a reference to
934     * guidedStepBackground.
935     * @param inflater LayoutInflater to load background view.
936     * @param container Parent view of background view.
937     * @param savedInstanceState
938     * @return Created background view or null if no background.
939     */
940    public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container,
941            Bundle savedInstanceState) {
942        return inflater.inflate(R.layout.lb_guidedstep_background, container, false);
943    }
944
945    /**
946     * Set UI style to fragment arguments. Default value is {@link #UI_STYLE_ENTRANCE} when fragment
947     * is first initialized. UI style is used to choose different fragment transition animations and
948     * determine if this is the first GuidedStepFragment on backstack. In most cases app does not
949     * directly call this method, app calls helper function
950     * {@link #add(FragmentManager, GuidedStepFragment, int)}. However if the app creates Fragment
951     * transaction and controls backstack by itself, it would need call setUiStyle() to select the
952     * fragment transition to use.
953     *
954     * @param style {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_REPLACE} or
955     *        {@link #UI_STYLE_ENTRANCE}.
956     */
957    public void setUiStyle(int style) {
958        int oldStyle = getUiStyle();
959        Bundle arguments = getArguments();
960        boolean isNew = false;
961        if (arguments == null) {
962            arguments = new Bundle();
963            isNew = true;
964        }
965        arguments.putInt(EXTRA_UI_STYLE, style);
966        // call setArgument() will validate if the fragment is already added.
967        if (isNew) {
968            setArguments(arguments);
969        }
970        if (style != oldStyle) {
971            onProvideFragmentTransitions();
972        }
973    }
974
975    /**
976     * Read UI style from fragment arguments.  Default value is {@link #UI_STYLE_ENTRANCE} when
977     * fragment is first initialized.  UI style is used to choose different fragment transition
978     * animations and determine if this is the first GuidedStepFragment on backstack.
979     *
980     * @return {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_REPLACE} or
981     * {@link #UI_STYLE_ENTRANCE}.
982     * @see #onProvideFragmentTransitions()
983     */
984    public int getUiStyle() {
985        Bundle b = getArguments();
986        if (b == null) return UI_STYLE_ENTRANCE;
987        return b.getInt(EXTRA_UI_STYLE, UI_STYLE_ENTRANCE);
988    }
989
990    /**
991     * {@inheritDoc}
992     */
993    @Override
994    public void onCreate(Bundle savedInstanceState) {
995        super.onCreate(savedInstanceState);
996        if (DEBUG) Log.v(TAG, "onCreate");
997        // Set correct transition from saved arguments.
998        onProvideFragmentTransitions();
999
1000        ArrayList<GuidedAction> actions = new ArrayList<GuidedAction>();
1001        onCreateActions(actions, savedInstanceState);
1002        if (savedInstanceState != null) {
1003            onRestoreActions(actions, savedInstanceState);
1004        }
1005        setActions(actions);
1006        ArrayList<GuidedAction> buttonActions = new ArrayList<GuidedAction>();
1007        onCreateButtonActions(buttonActions, savedInstanceState);
1008        if (savedInstanceState != null) {
1009            onRestoreButtonActions(buttonActions, savedInstanceState);
1010        }
1011        setButtonActions(buttonActions);
1012    }
1013
1014    /**
1015     * {@inheritDoc}
1016     */
1017    @Override
1018    public void onDestroyView() {
1019        mGuidanceStylist.onDestroyView();
1020        mActionsStylist.onDestroyView();
1021        mButtonActionsStylist.onDestroyView();
1022        mAdapter = null;
1023        mSubAdapter =  null;
1024        mButtonAdapter = null;
1025        mAdapterGroup = null;
1026        super.onDestroyView();
1027    }
1028
1029    /**
1030     * {@inheritDoc}
1031     */
1032    @Override
1033    public View onCreateView(LayoutInflater inflater, ViewGroup container,
1034            Bundle savedInstanceState) {
1035        if (DEBUG) Log.v(TAG, "onCreateView");
1036
1037        resolveTheme();
1038        inflater = getThemeInflater(inflater);
1039
1040        GuidedStepRootLayout root = (GuidedStepRootLayout) inflater.inflate(
1041                R.layout.lb_guidedstep_fragment, container, false);
1042
1043        root.setFocusOutStart(isFocusOutStartAllowed());
1044        root.setFocusOutEnd(isFocusOutEndAllowed());
1045
1046        ViewGroup guidanceContainer = (ViewGroup) root.findViewById(R.id.content_fragment);
1047        ViewGroup actionContainer = (ViewGroup) root.findViewById(R.id.action_fragment);
1048
1049        Guidance guidance = onCreateGuidance(savedInstanceState);
1050        View guidanceView = mGuidanceStylist.onCreateView(inflater, guidanceContainer, guidance);
1051        guidanceContainer.addView(guidanceView);
1052
1053        View actionsView = mActionsStylist.onCreateView(inflater, actionContainer);
1054        actionContainer.addView(actionsView);
1055
1056        View buttonActionsView = mButtonActionsStylist.onCreateView(inflater, actionContainer);
1057        actionContainer.addView(buttonActionsView);
1058
1059        GuidedActionAdapter.EditListener editListener = new GuidedActionAdapter.EditListener() {
1060
1061                @Override
1062                public void onImeOpen() {
1063                    runImeAnimations(true);
1064                }
1065
1066                @Override
1067                public void onImeClose() {
1068                    runImeAnimations(false);
1069                }
1070
1071                @Override
1072                public long onGuidedActionEditedAndProceed(GuidedAction action) {
1073                    return GuidedStepFragment.this.onGuidedActionEditedAndProceed(action);
1074                }
1075
1076                @Override
1077                public void onGuidedActionEditCanceled(GuidedAction action) {
1078                    GuidedStepFragment.this.onGuidedActionEditCanceled(action);
1079                }
1080        };
1081
1082        mAdapter = new GuidedActionAdapter(mActions, new GuidedActionAdapter.ClickListener() {
1083            @Override
1084            public void onGuidedActionClicked(GuidedAction action) {
1085                GuidedStepFragment.this.onGuidedActionClicked(action);
1086                if (isExpanded()) {
1087                    collapseAction(true);
1088                } else if (action.hasSubActions() || action.hasEditableActivatorView()) {
1089                    expandAction(action, true);
1090                }
1091            }
1092        }, this, mActionsStylist, false);
1093        mButtonAdapter =
1094                new GuidedActionAdapter(mButtonActions, new GuidedActionAdapter.ClickListener() {
1095                    @Override
1096                    public void onGuidedActionClicked(GuidedAction action) {
1097                        GuidedStepFragment.this.onGuidedActionClicked(action);
1098                    }
1099                }, this, mButtonActionsStylist, false);
1100        mSubAdapter = new GuidedActionAdapter(null, new GuidedActionAdapter.ClickListener() {
1101            @Override
1102            public void onGuidedActionClicked(GuidedAction action) {
1103                if (mActionsStylist.isInExpandTransition()) {
1104                    return;
1105                }
1106                if (GuidedStepFragment.this.onSubGuidedActionClicked(action)) {
1107                    collapseSubActions();
1108                }
1109            }
1110        }, this, mActionsStylist, true);
1111        mAdapterGroup = new GuidedActionAdapterGroup();
1112        mAdapterGroup.addAdpter(mAdapter, mButtonAdapter);
1113        mAdapterGroup.addAdpter(mSubAdapter, null);
1114        mAdapterGroup.setEditListener(editListener);
1115        mActionsStylist.setEditListener(editListener);
1116
1117        mActionsStylist.getActionsGridView().setAdapter(mAdapter);
1118        if (mActionsStylist.getSubActionsGridView() != null) {
1119            mActionsStylist.getSubActionsGridView().setAdapter(mSubAdapter);
1120        }
1121        mButtonActionsStylist.getActionsGridView().setAdapter(mButtonAdapter);
1122        if (mButtonActions.size() == 0) {
1123            // when there is no button actions, we don't need show the second panel, but keep
1124            // the width zero to run ChangeBounds transition.
1125            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
1126                    buttonActionsView.getLayoutParams();
1127            lp.weight = 0;
1128            buttonActionsView.setLayoutParams(lp);
1129        } else {
1130            // when there are two actions panel, we need adjust the weight of action to
1131            // guidedActionContentWidthWeightTwoPanels.
1132            Context ctx = mThemeWrapper != null ? mThemeWrapper : FragmentUtil.getContext(this);
1133            TypedValue typedValue = new TypedValue();
1134            if (ctx.getTheme().resolveAttribute(R.attr.guidedActionContentWidthWeightTwoPanels,
1135                    typedValue, true)) {
1136                View actionsRoot = root.findViewById(R.id.action_fragment_root);
1137                float weight = typedValue.getFloat();
1138                LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) actionsRoot
1139                        .getLayoutParams();
1140                lp.weight = weight;
1141                actionsRoot.setLayoutParams(lp);
1142            }
1143        }
1144
1145        // Add the background view.
1146        View backgroundView = onCreateBackgroundView(inflater, root, savedInstanceState);
1147        if (backgroundView != null) {
1148            FrameLayout backgroundViewRoot = (FrameLayout)root.findViewById(
1149                R.id.guidedstep_background_view_root);
1150            backgroundViewRoot.addView(backgroundView, 0);
1151        }
1152
1153        return root;
1154    }
1155
1156    @Override
1157    public void onResume() {
1158        super.onResume();
1159        getView().findViewById(R.id.action_fragment).requestFocus();
1160    }
1161
1162    /**
1163     * Get the key will be used to save GuidedAction with Fragment.
1164     * @param action GuidedAction to get key.
1165     * @return Key to save the GuidedAction.
1166     */
1167    final String getAutoRestoreKey(GuidedAction action) {
1168        return EXTRA_ACTION_PREFIX + action.getId();
1169    }
1170
1171    /**
1172     * Get the key will be used to save GuidedAction with Fragment.
1173     * @param action GuidedAction to get key.
1174     * @return Key to save the GuidedAction.
1175     */
1176    final String getButtonAutoRestoreKey(GuidedAction action) {
1177        return EXTRA_BUTTON_ACTION_PREFIX + action.getId();
1178    }
1179
1180    final static boolean isSaveEnabled(GuidedAction action) {
1181        return action.isAutoSaveRestoreEnabled() && action.getId() != GuidedAction.NO_ID;
1182    }
1183
1184    final void onRestoreActions(List<GuidedAction> actions, Bundle savedInstanceState) {
1185        for (int i = 0, size = actions.size(); i < size; i++) {
1186            GuidedAction action = actions.get(i);
1187            if (isSaveEnabled(action)) {
1188                action.onRestoreInstanceState(savedInstanceState, getAutoRestoreKey(action));
1189            }
1190        }
1191    }
1192
1193    final void onRestoreButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
1194        for (int i = 0, size = actions.size(); i < size; i++) {
1195            GuidedAction action = actions.get(i);
1196            if (isSaveEnabled(action)) {
1197                action.onRestoreInstanceState(savedInstanceState, getButtonAutoRestoreKey(action));
1198            }
1199        }
1200    }
1201
1202    final void onSaveActions(List<GuidedAction> actions, Bundle outState) {
1203        for (int i = 0, size = actions.size(); i < size; i++) {
1204            GuidedAction action = actions.get(i);
1205            if (isSaveEnabled(action)) {
1206                action.onSaveInstanceState(outState, getAutoRestoreKey(action));
1207            }
1208        }
1209    }
1210
1211    final void onSaveButtonActions(List<GuidedAction> actions, Bundle outState) {
1212        for (int i = 0, size = actions.size(); i < size; i++) {
1213            GuidedAction action = actions.get(i);
1214            if (isSaveEnabled(action)) {
1215                action.onSaveInstanceState(outState, getButtonAutoRestoreKey(action));
1216            }
1217        }
1218    }
1219
1220    /**
1221     * {@inheritDoc}
1222     */
1223    @Override
1224    public void onSaveInstanceState(Bundle outState) {
1225        super.onSaveInstanceState(outState);
1226        onSaveActions(mActions, outState);
1227        onSaveButtonActions(mButtonActions, outState);
1228    }
1229
1230    private static boolean isGuidedStepTheme(Context context) {
1231        int resId = R.attr.guidedStepThemeFlag;
1232        TypedValue typedValue = new TypedValue();
1233        boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
1234        if (DEBUG) Log.v(TAG, "Found guided step theme flag? " + found);
1235        return found && typedValue.type == TypedValue.TYPE_INT_BOOLEAN && typedValue.data != 0;
1236    }
1237
1238    /**
1239     * Convenient method to close GuidedStepFragments on top of other content or finish Activity if
1240     * GuidedStepFragments were started in a separate activity.  Pops all stack entries including
1241     * {@link #UI_STYLE_ENTRANCE}; if {@link #UI_STYLE_ENTRANCE} is not found, finish the activity.
1242     * Note that this method must be paired with {@link #add(FragmentManager, GuidedStepFragment,
1243     * int)} which sets up the stack entry name for finding which fragment we need to pop back to.
1244     */
1245    public void finishGuidedStepFragments() {
1246        final FragmentManager fragmentManager = getFragmentManager();
1247        final int entryCount = fragmentManager.getBackStackEntryCount();
1248        if (entryCount > 0) {
1249            for (int i = entryCount - 1; i >= 0; i--) {
1250                BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
1251                if (isStackEntryUiStyleEntrance(entry.getName())) {
1252                    GuidedStepFragment top = getCurrentGuidedStepFragment(fragmentManager);
1253                    if (top != null) {
1254                        top.setUiStyle(UI_STYLE_ENTRANCE);
1255                    }
1256                    fragmentManager.popBackStackImmediate(entry.getId(),
1257                            FragmentManager.POP_BACK_STACK_INCLUSIVE);
1258                    return;
1259                }
1260            }
1261        }
1262        ActivityCompat.finishAfterTransition(getActivity());
1263    }
1264
1265    /**
1266     * Convenient method to pop to fragment with Given class.
1267     * @param  guidedStepFragmentClass  Name of the Class of GuidedStepFragment to pop to.
1268     * @param flags Either 0 or {@link FragmentManager#POP_BACK_STACK_INCLUSIVE}.
1269     */
1270    public void popBackStackToGuidedStepFragment(Class guidedStepFragmentClass, int flags) {
1271        if (!GuidedStepFragment.class.isAssignableFrom(guidedStepFragmentClass)) {
1272            return;
1273        }
1274        final FragmentManager fragmentManager = getFragmentManager();
1275        final int entryCount = fragmentManager.getBackStackEntryCount();
1276        String className = guidedStepFragmentClass.getName();
1277        if (entryCount > 0) {
1278            for (int i = entryCount - 1; i >= 0; i--) {
1279                BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
1280                String entryClassName = getGuidedStepFragmentClassName(entry.getName());
1281                if (className.equals(entryClassName)) {
1282                    fragmentManager.popBackStackImmediate(entry.getId(), flags);
1283                    return;
1284                }
1285            }
1286        }
1287    }
1288
1289    /**
1290     * Returns true if allows focus out of start edge of GuidedStepFragment, false otherwise.
1291     * Default value is false, the reason is to disable FocusFinder to find focusable views
1292     * beneath content of GuidedStepFragment.  Subclass may override.
1293     * @return True if allows focus out of start edge of GuidedStepFragment.
1294     */
1295    public boolean isFocusOutStartAllowed() {
1296        return false;
1297    }
1298
1299    /**
1300     * Returns true if allows focus out of end edge of GuidedStepFragment, false otherwise.
1301     * Default value is false, the reason is to disable FocusFinder to find focusable views
1302     * beneath content of GuidedStepFragment.  Subclass may override.
1303     * @return True if allows focus out of end edge of GuidedStepFragment.
1304     */
1305    public boolean isFocusOutEndAllowed() {
1306        return false;
1307    }
1308
1309    /**
1310     * Sets the transition type to be used for {@link #UI_STYLE_ENTRANCE} animation.
1311     * Currently we provide 2 different variations for animation - slide in from
1312     * side (default) or bottom.
1313     *
1314     * Ideally we can retrieve the screen mode settings from the theme attribute
1315     * {@code Theme.Leanback.GuidedStep#guidedStepHeightWeight} and use that to
1316     * determine the transition. But the fragment context to retrieve the theme
1317     * isn't available on platform v23 or earlier.
1318     *
1319     * For now clients(subclasses) can call this method inside the constructor.
1320     * @hide
1321     */
1322    @RestrictTo(LIBRARY_GROUP)
1323    public void setEntranceTransitionType(int transitionType) {
1324      this.entranceTransitionType = transitionType;
1325    }
1326
1327    /**
1328     * Opens the provided action in edit mode and raises ime. This can be
1329     * used to programmatically skip the extra click required to go into edit mode. This method
1330     * can be invoked in {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.
1331     */
1332    public void openInEditMode(GuidedAction action) {
1333        mActionsStylist.openInEditMode(action);
1334    }
1335
1336    private void resolveTheme() {
1337        // Look up the guidedStepTheme in the currently specified theme.  If it exists,
1338        // replace the theme with its value.
1339        Context context = FragmentUtil.getContext(this);
1340        int theme = onProvideTheme();
1341        if (theme == -1 && !isGuidedStepTheme(context)) {
1342            // Look up the guidedStepTheme in the activity's currently specified theme.  If it
1343            // exists, replace the theme with its value.
1344            int resId = R.attr.guidedStepTheme;
1345            TypedValue typedValue = new TypedValue();
1346            boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
1347            if (DEBUG) Log.v(TAG, "Found guided step theme reference? " + found);
1348            if (found) {
1349                ContextThemeWrapper themeWrapper =
1350                        new ContextThemeWrapper(context, typedValue.resourceId);
1351                if (isGuidedStepTheme(themeWrapper)) {
1352                    mThemeWrapper = themeWrapper;
1353                } else {
1354                    found = false;
1355                    mThemeWrapper = null;
1356                }
1357            }
1358            if (!found) {
1359                Log.e(TAG, "GuidedStepFragment does not have an appropriate theme set.");
1360            }
1361        } else if (theme != -1) {
1362            mThemeWrapper = new ContextThemeWrapper(context, theme);
1363        }
1364    }
1365
1366    private LayoutInflater getThemeInflater(LayoutInflater inflater) {
1367        if (mThemeWrapper == null) {
1368            return inflater;
1369        } else {
1370            return inflater.cloneInContext(mThemeWrapper);
1371        }
1372    }
1373
1374    private int getFirstCheckedAction() {
1375        for (int i = 0, size = mActions.size(); i < size; i++) {
1376            if (mActions.get(i).isChecked()) {
1377                return i;
1378            }
1379        }
1380        return 0;
1381    }
1382
1383    void runImeAnimations(boolean entering) {
1384        ArrayList<Animator> animators = new ArrayList<Animator>();
1385        if (entering) {
1386            mGuidanceStylist.onImeAppearing(animators);
1387            mActionsStylist.onImeAppearing(animators);
1388            mButtonActionsStylist.onImeAppearing(animators);
1389        } else {
1390            mGuidanceStylist.onImeDisappearing(animators);
1391            mActionsStylist.onImeDisappearing(animators);
1392            mButtonActionsStylist.onImeDisappearing(animators);
1393        }
1394        AnimatorSet set = new AnimatorSet();
1395        set.playTogether(animators);
1396        set.start();
1397    }
1398}
1399