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.example.android.leanback;
18
19import android.app.Activity;
20import android.app.Fragment;
21import android.app.FragmentManager;
22import android.content.Context;
23import android.content.res.Configuration;
24import android.graphics.drawable.Drawable;
25import android.os.Bundle;
26import android.support.v17.leanback.app.GuidedStepFragment;
27import android.support.v17.leanback.widget.GuidanceStylist;
28import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
29import android.support.v17.leanback.widget.GuidedAction;
30import android.support.v17.leanback.widget.GuidedActionsStylist;
31import android.support.v17.leanback.widget.GuidedDatePickerAction;
32import android.support.v4.content.res.ResourcesCompat;
33import android.text.InputType;
34import android.text.TextUtils;
35import android.util.Log;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.ViewGroup;
39import android.view.inputmethod.EditorInfo;
40
41import java.util.ArrayList;
42import java.util.Calendar;
43import java.util.List;
44
45/**
46 * Activity that showcases different aspects of GuidedStepFragments.
47 */
48public class GuidedStepActivity extends Activity {
49
50    private static final int BACK = 2;
51
52    private static final int FIRST_NAME = 3;
53    private static final int LAST_NAME = 4;
54    private static final int PASSWORD = 5;
55    private static final int PAYMENT = 6;
56    private static final int NEW_PAYMENT = 7;
57    private static final int PAYMENT_EXPIRE = 8;
58
59    private static final long RADIO_ID_BASE = 0;
60    private static final long CHECKBOX_ID_BASE = 100;
61
62    private static final long DEFAULT_OPTION = RADIO_ID_BASE;
63
64    private static final String[] OPTION_NAMES = { "Option A", "Option B", "Option C" };
65    private static final String[] OPTION_DESCRIPTIONS = { "Here's one thing you can do",
66            "Here's another thing you can do", "Here's one more thing you can do" };
67
68    private static final String TAG = GuidedStepActivity.class.getSimpleName();
69
70    @Override
71    protected void onCreate(Bundle savedInstanceState) {
72        Log.v(TAG, "onCreate");
73        super.onCreate(savedInstanceState);
74        setContentView(R.layout.guided_step_activity);
75        if (savedInstanceState == null) {
76            GuidedStepFragment.addAsRoot(this, new FirstStepFragment(), R.id.lb_guidedstep_host);
77        }
78    }
79
80    @Override
81    public void onConfigurationChanged(Configuration newConfig) {
82        Log.v(TAG, "onConfigurationChanged");
83        super.onConfigurationChanged(newConfig);
84    }
85
86    @Override
87    protected void onSaveInstanceState(Bundle outState) {
88        Log.v(TAG, "onSaveInstanceState");
89        super.onSaveInstanceState(outState);
90    }
91
92    @Override
93    protected void onRestoreInstanceState(Bundle savedInstanceState) {
94        Log.v(TAG, "onRestoreInstanceState");
95        super.onRestoreInstanceState(savedInstanceState);
96    }
97
98    private static GuidedAction addAction(List<GuidedAction> actions, long id, String title,
99            String desc) {
100        GuidedAction action;
101        actions.add(action = new GuidedAction.Builder(null)
102                .id(id)
103                .title(title)
104                .description(desc)
105                .build());
106        return action;
107    }
108
109    private static GuidedAction addAction(List<GuidedAction> actions, long id, String title,
110            String desc, List<GuidedAction> subActions) {
111        GuidedAction action;
112        actions.add(action = new GuidedAction.Builder(null)
113                .id(id)
114                .title(title)
115                .description(desc)
116                .subActions(subActions)
117                .build());
118        return action;
119    }
120
121    private static GuidedAction addEditableAction(Context context, List<GuidedAction> actions,
122            long id, String title, String desc) {
123        GuidedAction action;
124        actions.add(action = new GuidedAction.Builder(context)
125                .id(id)
126                .title(title)
127                .description(desc)
128                .editable(true)
129                .icon(R.drawable.lb_ic_search_mic)
130                .build());
131        return action;
132    }
133
134    private static GuidedAction addEditableAction(List<GuidedAction> actions, long id, String title,
135            String editTitle, String desc) {
136        GuidedAction action;
137        actions.add(action = new GuidedAction.Builder(null)
138                .id(id)
139                .title(title)
140                .editTitle(editTitle)
141                .description(desc)
142                .editable(true)
143                .build());
144        return action;
145    }
146
147    private static GuidedAction addEditableAction(List<GuidedAction> actions, long id, String title,
148            String editTitle, int editInputType, String desc, String editDesc) {
149        GuidedAction action;
150        actions.add(action = new GuidedAction.Builder(null)
151                .id(id)
152                .title(title)
153                .editTitle(editTitle)
154                .editInputType(editInputType)
155                .description(desc)
156                .editDescription(editDesc)
157                .editable(true)
158                .build());
159        return action;
160    }
161
162    private static GuidedDatePickerAction addDatePickerAction(List<GuidedAction> actions, long id,
163            String title) {
164        GuidedDatePickerAction action;
165        actions.add(action = new GuidedDatePickerAction.Builder(null)
166                .id(id)
167                .title(title)
168                .datePickerFormat("MY")
169                .build());
170        return action;
171    }
172
173    private static GuidedAction addEditableDescriptionAction(List<GuidedAction> actions, long id,
174            String title, String desc, String editDescription, int descriptionEditInputType) {
175        GuidedAction action;
176        actions.add(action = new GuidedAction.Builder(null)
177                .id(id)
178                .title(title)
179                .description(desc)
180                .editDescription(editDescription)
181                .descriptionEditInputType(descriptionEditInputType)
182                .descriptionEditable(true)
183                .build());
184        return action;
185    }
186
187    private static GuidedAction addCheckedAction(List<GuidedAction> actions, long id,
188            String title, String desc, int checkSetId) {
189        GuidedAction action;
190        actions.add(action = new GuidedAction.Builder(null)
191                .id(id)
192                .title(title)
193                .description(desc)
194                .checkSetId(checkSetId)
195                .build());
196        return action;
197    }
198
199    public static class FirstStepFragment extends GuidedStepFragment {
200
201        @Override
202        public int onProvideTheme() {
203            return R.style.Theme_Example_Leanback_GuidedStep_First;
204        }
205
206        @Override
207        public Guidance onCreateGuidance(Bundle savedInstanceState) {
208            String title = getString(R.string.guidedstep_first_title);
209            String breadcrumb = getString(R.string.guidedstep_first_breadcrumb);
210            String description = getString(R.string.guidedstep_first_description);
211            final Context context = getActivity();
212            Drawable icon = ResourcesCompat.getDrawable(context.getResources(),
213                    R.drawable.ic_main_icon, context.getTheme());
214            return new Guidance(title, description, breadcrumb, icon);
215        }
216
217        @Override
218        public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
219            Context context = getActivity();
220            actions.add(new GuidedAction.Builder(context)
221                    .clickAction(GuidedAction.ACTION_ID_CONTINUE)
222                    .description("Let's do it")
223                    .build());
224            actions.add(new GuidedAction.Builder(context)
225                    .clickAction(GuidedAction.ACTION_ID_CANCEL)
226                    .description("Never mind")
227                    .build());
228        }
229
230        @Override
231        public void onGuidedActionClicked(GuidedAction action) {
232            FragmentManager fm = getFragmentManager();
233            if (action.getId() == GuidedAction.ACTION_ID_CONTINUE) {
234                GuidedStepFragment.add(fm, new SecondStepFragment(), R.id.lb_guidedstep_host);
235            } else if (action.getId() == GuidedAction.ACTION_ID_CANCEL){
236                finishGuidedStepFragments();
237            }
238        }
239    }
240
241    public interface NewPaymentFragmentTarget {
242        void onNewPaymentFragmentStarted();
243        void onNewPaymentAdded(int selection);
244    }
245
246    static ArrayList<String> sCards = new ArrayList<String>();
247    static int sSelectedCard = -1;
248    static {
249        sCards.add("Visa-1234");
250        sCards.add("Master-4321");
251    }
252
253    public static class NewPaymentStepFragment extends GuidedStepFragment {
254
255        NewPaymentFragmentTarget mNewPaymentTarget;
256
257        @Override
258        public void onCreate(Bundle savedInstance) {
259            super.onCreate(savedInstance);
260            Fragment targetFragment = getTargetFragment();
261            if (targetFragment instanceof NewPaymentFragmentTarget) {
262                mNewPaymentTarget = ((NewPaymentFragmentTarget) targetFragment);
263                mNewPaymentTarget.onNewPaymentFragmentStarted();
264            }
265        }
266
267        @Override
268        public Guidance onCreateGuidance(Bundle savedInstanceState) {
269            String title = getString(R.string.guidedstep_newpayment_title);
270            String breadcrumb = getString(R.string.guidedstep_newpayment_breadcrumb);
271            String description = getString(R.string.guidedstep_newpayment_description);
272            final Context context = getActivity();
273            Drawable icon = ResourcesCompat.getDrawable(context.getResources(),
274                    R.drawable.ic_main_icon, context.getTheme());
275            return new Guidance(title, description, breadcrumb, icon);
276        }
277
278        @Override
279        public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
280            addEditableAction(actions, NEW_PAYMENT, "Input credit card number", "",
281                    InputType.TYPE_CLASS_NUMBER,
282                    "Input credit card number", "Input credit card number");
283            addDatePickerAction(actions, PAYMENT_EXPIRE, "Exp:");
284        }
285
286        @Override
287        public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
288            Context context = getActivity();
289            actions.add(new GuidedAction.Builder(context).clickAction(GuidedAction.ACTION_ID_OK)
290                    .build());
291            actions.get(actions.size() - 1).setEnabled(false);
292        }
293
294        @Override
295        public void onGuidedActionClicked(GuidedAction action) {
296            if (action.getId() == GuidedAction.ACTION_ID_OK) {
297                CharSequence desc = findActionById(NEW_PAYMENT).getDescription();
298                String cardNumber = desc.subSequence(desc.length() - 4, desc.length()).toString();
299                String card;
300                if ((Integer.parseInt(cardNumber) & 1) == 0) {
301                    card = "Visa "+cardNumber;
302                } else {
303                    card = "Master "+cardNumber;
304                }
305                int selection = sCards.size();
306                sCards.add(card);
307                if (mNewPaymentTarget != null) {
308                    mNewPaymentTarget.onNewPaymentAdded(selection);
309                }
310                popBackStackToGuidedStepFragment(NewPaymentStepFragment.class,
311                        FragmentManager.POP_BACK_STACK_INCLUSIVE);
312            }
313        }
314
315        @Override
316        public long onGuidedActionEditedAndProceed(GuidedAction action) {
317            if (action.getId() == NEW_PAYMENT) {
318                CharSequence editTitle = action.getEditTitle();
319                if (isCardNumberValid(editTitle)) {
320                    editTitle = editTitle.subSequence(editTitle.length() - 4, editTitle.length());
321                    action.setDescription("Visa XXXX-XXXX-XXXX-" + editTitle);
322                    updateOkButton(isExpDateValid(findActionById(PAYMENT_EXPIRE)));
323                    return GuidedAction.ACTION_ID_NEXT;
324                } else if (editTitle.length() == 0) {
325                    action.setDescription("Input credit card number");
326                    updateOkButton(false);
327                    return GuidedAction.ACTION_ID_CURRENT;
328                } else {
329                    action.setDescription("Error credit card number");
330                    updateOkButton(false);
331                    return GuidedAction.ACTION_ID_CURRENT;
332                }
333            } else if (action.getId() == PAYMENT_EXPIRE) {
334                updateOkButton(isExpDateValid(action) &&
335                        isCardNumberValid(findActionById(NEW_PAYMENT).getEditTitle()));
336            }
337            return GuidedAction.ACTION_ID_NEXT;
338        }
339
340        boolean isCardNumberValid(CharSequence number) {
341            return TextUtils.isDigitsOnly(number) && number.length() == 16;
342        }
343
344        boolean isExpDateValid(GuidedAction action) {
345            long date = ((GuidedDatePickerAction) action).getDate();
346            Calendar c = Calendar.getInstance();
347            c.setTimeInMillis(date);
348            return Calendar.getInstance().before(c);
349        }
350
351        void updateOkButton(boolean enabled) {
352            findButtonActionById(GuidedAction.ACTION_ID_OK).setEnabled(enabled);
353            notifyButtonActionChanged(findButtonActionPositionById(GuidedAction.ACTION_ID_OK));
354        }
355    }
356
357    public static class SecondStepFragment extends GuidedStepFragment
358            implements NewPaymentFragmentTarget {
359
360
361        boolean mExpandPaymentListInOnCreateView;
362
363        @Override
364        public void onNewPaymentAdded(int selection) {
365            // if a new payment is added, we dont need expand the sub actions list.
366            mExpandPaymentListInOnCreateView = false;
367            sSelectedCard = selection;
368            updatePaymentAction(findActionById(PAYMENT));
369            findButtonActionById(GuidedAction.ACTION_ID_CONTINUE).setEnabled(sSelectedCard != -1);
370        }
371
372        @Override
373        public void onNewPaymentFragmentStarted() {
374            // if a new payment fragment is opened, when come back we should expand the payment
375            // sub actions list unless user created a new payment in onNewPaymentAdded
376            mExpandPaymentListInOnCreateView = true;
377        }
378
379        @Override
380        public GuidedActionsStylist onCreateActionsStylist() {
381            return new GuidedActionsStylist() {
382                @Override
383                protected void setupImeOptions(GuidedActionsStylist.ViewHolder vh,
384                        GuidedAction action) {
385                    if (action.getId() == PASSWORD) {
386                        vh.getEditableDescriptionView().setImeActionLabel("Confirm!",
387                                EditorInfo.IME_ACTION_DONE);
388                    } else {
389                        super.setupImeOptions(vh, action);
390                    }
391                }
392            };
393        }
394
395        @Override
396        public Guidance onCreateGuidance(Bundle savedInstanceState) {
397            String title = getString(R.string.guidedstep_second_title);
398            String breadcrumb = getString(R.string.guidedstep_second_breadcrumb);
399            String description = getString(R.string.guidedstep_second_description);
400            final Context context = getActivity();
401            Drawable icon = ResourcesCompat.getDrawable(context.getResources(),
402                    R.drawable.ic_main_icon, context.getTheme());
403            return new Guidance(title, description, breadcrumb, icon);
404        }
405
406        @Override
407        public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
408            addEditableAction(getActivity(), actions, FIRST_NAME, "Pat", "Your first name");
409            addEditableAction(getActivity(), actions, LAST_NAME, "Smith", "Your last name");
410            List<GuidedAction> subActions = new ArrayList<GuidedAction>();
411            updatePaymentAction(addAction(actions, PAYMENT, "Select Payment", "", subActions));
412            addEditableDescriptionAction(actions, PASSWORD, "Password", "", "",
413                    InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
414        }
415
416        @Override
417        public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
418            actions.add(new GuidedAction.Builder(getActivity())
419                    .clickAction(GuidedAction.ACTION_ID_CONTINUE)
420                    .description("Continue")
421                    .enabled(isPasswordValid() && isPaymentValid())
422                    .build());
423        }
424
425        @Override
426        public void onGuidedActionClicked(GuidedAction action) {
427            if (action.getId() == GuidedAction.ACTION_ID_CONTINUE) {
428                FragmentManager fm = getFragmentManager();
429                GuidedStepFragment.add(fm, new ThirdStepFragment(), R.id.lb_guidedstep_host);
430            }
431        }
432
433        void updatePaymentAction(GuidedAction paymentAction) {
434            List<GuidedAction> subActions = paymentAction.getSubActions();
435            subActions.clear();
436            for (int i = 0; i < sCards.size(); i++) {
437                addCheckedAction(subActions, -1, sCards.get(i), "",
438                        GuidedAction.DEFAULT_CHECK_SET_ID);
439                if (i == sSelectedCard) {
440                    subActions.get(i).setChecked(true);
441                }
442            }
443            addAction(subActions, NEW_PAYMENT, "Add New Card", "");
444            paymentAction.setDescription(sSelectedCard == -1 ? "" : sCards.get(sSelectedCard));
445        }
446
447        @Override
448        public long onGuidedActionEditedAndProceed(GuidedAction action) {
449            if (action.getId() == PASSWORD) {
450                CharSequence password = action.getEditDescription();
451                if (password.length() > 0) {
452                    if (isPaymentValid()) {
453                        updateContinue(true);
454                        return GuidedAction.ACTION_ID_NEXT;
455                    } else {
456                        updateContinue(false);
457                        return GuidedAction.ACTION_ID_CURRENT;
458                    }
459                } else {
460                    updateContinue(false);
461                    return GuidedAction.ACTION_ID_CURRENT;
462                }
463            }
464            return GuidedAction.ACTION_ID_NEXT;
465        }
466
467        @Override
468        public boolean onSubGuidedActionClicked(GuidedAction action) {
469            if (action.isChecked()) {
470                String payment = action.getTitle().toString();
471                for (int i = 0; i < sCards.size(); i++) {
472                    if (payment.equals(sCards.get(i))) {
473                        sSelectedCard = i;
474                        findActionById(PAYMENT).setDescription(payment);
475                        notifyActionChanged(findActionPositionById(PAYMENT));
476                        updateContinue(isPasswordValid());
477                        break;
478                    }
479                }
480                return true;
481            } else {
482                FragmentManager fm = getFragmentManager();
483                NewPaymentStepFragment newPaymentFragment = new NewPaymentStepFragment();
484                newPaymentFragment.setTargetFragment(this, 0);
485                GuidedStepFragment.add(fm, newPaymentFragment, R.id.lb_guidedstep_host);
486                return false;
487            }
488        }
489
490        @Override
491        public View onCreateView(LayoutInflater inflater, ViewGroup container,
492                Bundle savedInstanceState) {
493            View view = super.onCreateView(inflater, container, savedInstanceState);
494            if (mExpandPaymentListInOnCreateView) {
495                expandAction(findActionById(PAYMENT), false);
496            }
497            return view;
498        }
499
500        boolean isPaymentValid() {
501            CharSequence paymentType = findActionById(PAYMENT).getDescription();
502            return (paymentType.length() >= 4 &&
503                    paymentType.subSequence(0, 4).toString().equals("Visa")) ||
504                    (paymentType.length() >= 6 &&
505                    paymentType.subSequence(0, 6).toString().equals("Master"));
506        }
507
508        boolean isPasswordValid() {
509            return findActionById(PASSWORD).getEditDescription().length() > 0;
510        }
511
512        void updateContinue(boolean enabled) {
513            findButtonActionById(GuidedAction.ACTION_ID_CONTINUE).setEnabled(enabled);
514            notifyButtonActionChanged(findButtonActionPositionById(
515                    GuidedAction.ACTION_ID_CONTINUE));
516        }
517    }
518
519    public static class ThirdStepFragment extends GuidedStepFragment {
520
521        private long mSelectedOption = DEFAULT_OPTION;
522
523        @Override
524        public Guidance onCreateGuidance(Bundle savedInstanceState) {
525            String title = getString(R.string.guidedstep_third_title);
526            String breadcrumb = getString(R.string.guidedstep_third_breadcrumb);
527            String description = getString(R.string.guidedstep_third_description);
528            final Context context = getActivity();
529            Drawable icon = ResourcesCompat.getDrawable(context.getResources(),
530                    R.drawable.ic_main_icon, context.getTheme());
531            return new Guidance(title, description, breadcrumb, icon);
532        }
533
534        @Override
535        public GuidanceStylist onCreateGuidanceStylist() {
536            return new GuidanceStylist() {
537                @Override
538                public int onProvideLayoutId() {
539                    return R.layout.guidedstep_second_guidance;
540                }
541            };
542        }
543
544        @Override
545        public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
546            String desc = "The description can be quite long as well.  " +
547                    "Just be sure to set multilineDescription to true in the GuidedAction.";
548            actions.add(new GuidedAction.Builder(getActivity())
549                    .title("Note that Guided Actions can have titles that are quite long.")
550                    .description(desc)
551                    .multilineDescription(true)
552                    .infoOnly(true)
553                    .enabled(true)
554                    .focusable(false)
555                    .build());
556            for (int i = 0; i < OPTION_NAMES.length; i++) {
557                addCheckedAction(actions, RADIO_ID_BASE + i, OPTION_NAMES[i],
558                        OPTION_DESCRIPTIONS[i], GuidedAction.DEFAULT_CHECK_SET_ID);
559                if (i == DEFAULT_OPTION) {
560                    actions.get(actions.size() -1).setChecked(true);
561                }
562            }
563            for (int i = 0; i < OPTION_NAMES.length; i++) {
564                addCheckedAction(actions, CHECKBOX_ID_BASE + i, OPTION_NAMES[i],
565                        OPTION_DESCRIPTIONS[i], GuidedAction.CHECKBOX_CHECK_SET_ID);
566            }
567        }
568
569        @Override
570        public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
571            actions.add(new GuidedAction.Builder(getActivity())
572                    .clickAction(GuidedAction.ACTION_ID_CONTINUE)
573                    .build());
574        }
575
576        @Override
577        public void onGuidedActionClicked(GuidedAction action) {
578            if (action.getId() == GuidedAction.ACTION_ID_CONTINUE) {
579                FragmentManager fm = getFragmentManager();
580                FourthStepFragment f = new FourthStepFragment();
581                Bundle arguments = new Bundle();
582                arguments.putLong(FourthStepFragment.EXTRA_OPTION, mSelectedOption);
583                f.setArguments(arguments);
584                GuidedStepFragment.add(fm, f, R.id.lb_guidedstep_host);
585            } else if (action.getCheckSetId() == GuidedAction.DEFAULT_CHECK_SET_ID) {
586                mSelectedOption = action.getId();
587            }
588        }
589
590    }
591
592    public static class FourthStepFragment extends GuidedStepFragment {
593        public static final String EXTRA_OPTION = "extra_option";
594
595        public FourthStepFragment() {
596        }
597
598        public long getOption() {
599            Bundle b = getArguments();
600            if (b == null) return 0;
601            return b.getLong(EXTRA_OPTION, 0);
602        }
603
604        @Override
605        public Guidance onCreateGuidance(Bundle savedInstanceState) {
606            String title = getString(R.string.guidedstep_fourth_title);
607            String breadcrumb = getString(R.string.guidedstep_fourth_breadcrumb);
608            String description = "You chose: " + OPTION_NAMES[(int) getOption()];
609            final Context context = getActivity();
610            Drawable icon = ResourcesCompat.getDrawable(context.getResources(),
611                    R.drawable.ic_main_icon, context.getTheme());
612            return new Guidance(title, description, breadcrumb, icon);
613        }
614
615        @Override
616        public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
617            actions.add(new GuidedAction.Builder(getActivity())
618                    .clickAction(GuidedAction.ACTION_ID_FINISH)
619                    .description("All Done...")
620                    .build());
621            addAction(actions, BACK, "Start Over", "Let's try this again...");
622        }
623
624        @Override
625        public void onGuidedActionClicked(GuidedAction action) {
626            if (action.getId() == GuidedAction.ACTION_ID_FINISH) {
627                finishGuidedStepFragments();
628            } else if (action.getId() == BACK) {
629                // pop 4, 3, 2
630                popBackStackToGuidedStepFragment(SecondStepFragment.class,
631                        FragmentManager.POP_BACK_STACK_INCLUSIVE);
632            }
633        }
634
635    }
636
637}
638