1/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.car.setupwizardlib;
18
19import android.annotation.CallSuper;
20import android.content.Intent;
21import android.os.Bundle;
22import android.support.annotation.LayoutRes;
23import android.support.annotation.VisibleForTesting;
24import android.support.v4.app.Fragment;
25import android.support.v4.app.FragmentActivity;
26import android.view.View;
27
28import com.android.car.setupwizardlib.util.CarWizardManagerHelper;
29
30
31/**
32 * Base Activity for CarSetupWizard screens that provides a variety of helper functions that make
33 * it easier to work with the CarSetupWizardLayout and moving between Setup Wizard screens.
34 *
35 * <p>This activity sets an instance of {@link CarSetupWizardLayout} as the Content View.
36 * <p>Provides helper methods like {@link #setContentFragment} and {@link #onContentFragmentSet} for
37 * easy updating of the CarSetupWizard layout components based on the current Fragment being
38 * displayed
39 * <p>Provides helper methods like {@link #nextAction} and {@link #finishAction} for properly
40 * moving to the next and previous screens in a Setup Wizard
41 * <p>Provides setters {@link #setBackButtonVisible(boolean)} for setting CarSetupWizardLayout
42 * component attributes
43 */
44public class BaseActivity extends FragmentActivity {
45    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
46    static final String CONTENT_FRAGMENT_TAG = "CONTENT_FRAGMENT_TAG";
47    /**
48     * Wizard Manager does not actually return an activity result, but if we invoke Wizard
49     * Manager without requesting a result, the framework will choose not to issue a call to
50     * onActivityResult with RESULT_CANCELED when navigating backward.
51     */
52    private static final int REQUEST_CODE_NEXT = 10000;
53
54    /**
55     * To implement a specific request code, see the following:
56     *
57     * <pre>{@code
58     * static final int REQUEST_CODE_A = REQUEST_CODE_FIRST_USER;
59     * static final int REQUEST_CODE_B = REQUEST_CODE_FIRST_USER + 1;</pre>
60     * }
61     */
62    protected static final int REQUEST_CODE_FIRST_USER = 10001;
63
64    private int mResultCode = RESULT_CANCELED;
65
66    /**
67     * Whether it is safe to make transactions on the
68     * {@link androidx.fragment.app.FragmentManager}. This variable prevents a possible exception
69     * when calling commit() on the FragmentManager.
70     *
71     * <p>The default value is {@code true} because it is only after
72     * {@link #onSaveInstanceState(Bundle)} that fragment commits are not allowed.
73     */
74    private boolean mAllowFragmentCommits = true;
75    private CarSetupWizardLayout mCarSetupWizardLayout;
76    private Intent mResultData;
77
78    @Override
79    @CallSuper
80    protected void onCreate(Bundle savedInstanceState) {
81        super.onCreate(savedInstanceState);
82        setContentView(R.layout.base_activity);
83
84        mCarSetupWizardLayout = findViewById(R.id.car_setup_wizard_layout);
85
86        mCarSetupWizardLayout.setBackButtonListener(v -> {
87            if (!handleBackButton()) {
88                finish();
89            }
90        });
91
92        /* If this activity has a saved instance and a content fragment, call onContentFragmentSet()
93         * so the appropriate views/events are updated.
94         */
95        if (savedInstanceState != null && getContentFragment() != null) {
96            onContentFragmentSet(getContentFragment());
97        }
98
99        resetPrimaryToolbarButtonOnCLickListener();
100        resetSecondaryToolbarButtonOnCLickListener();
101    }
102
103    @Override
104    @CallSuper
105    protected void onStart() {
106        super.onStart();
107        // Fragment commits are not allowed once the Activity's state has been saved. Once
108        // onStart() has been called, the FragmentManager should now allow commits.
109        mAllowFragmentCommits = true;
110    }
111
112    @Override
113    @CallSuper
114    protected void onSaveInstanceState(Bundle outState) {
115        // A transaction can only be committed with this method prior to its containing activity
116        // saving its state.
117        mAllowFragmentCommits = false;
118        super.onSaveInstanceState(outState);
119    }
120
121    // Content Fragment accessors
122
123    /**
124     * Sets the content fragment and adds it to the fragment backstack.
125     */
126    @CallSuper
127    protected void setContentFragmentWithBackstack(Fragment fragment) {
128        if (mAllowFragmentCommits) {
129            getSupportFragmentManager().beginTransaction()
130                    .replace(R.id.car_setup_wizard_layout, fragment, CONTENT_FRAGMENT_TAG)
131                    .addToBackStack(null)
132                    .commit();
133            getSupportFragmentManager().executePendingTransactions();
134            onContentFragmentSet(getContentFragment());
135        }
136    }
137
138    /**
139     * Returns the fragment that is currently being displayed as the content view.
140     */
141    @CallSuper
142    protected Fragment getContentFragment() {
143        return getSupportFragmentManager().findFragmentByTag(CONTENT_FRAGMENT_TAG);
144    }
145
146    /**
147     * Sets the fragment that will be shown as the main content of this Activity.
148     */
149    @CallSuper
150    protected void setContentFragment(Fragment fragment) {
151        if (mAllowFragmentCommits) {
152            getSupportFragmentManager().beginTransaction()
153                    .setCustomAnimations(
154                            android.R.animator.fade_in,
155                            android.R.animator.fade_out,
156                            android.R.animator.fade_in,
157                            android.R.animator.fade_out)
158                    .replace(R.id.car_setup_wizard_layout, fragment, CONTENT_FRAGMENT_TAG)
159                    .commitNow();
160            onContentFragmentSet(getContentFragment());
161        }
162    }
163
164    /**
165     * Pops the top Fragment from the Fragment backstack (immediately executing the transaction) and
166     * then updates the CarSetupWizardLayout toolbar for the current fragment.
167     *
168     * @return {@code true} if a fragment was popped.
169     */
170    @CallSuper
171    protected boolean popBackStackImmediate() {
172        if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
173            getSupportFragmentManager().popBackStackImmediate();
174            onContentFragmentSet(getContentFragment());
175            return true;
176        }
177        return false;
178    }
179
180    /**
181     * Method to be overwritten by subclasses wanting to perform any additional actions when the
182     * content fragment is set (usually via adding or popping from fragment backstack). For example,
183     * the CarSetupWizardLayout toolbar usually needs to be changed when the fragment changes.
184     */
185    protected void onContentFragmentSet(Fragment fragment) {
186
187    }
188
189    /**
190     * Sets the layout view that will be shown as the main content of this Activity. Should be used
191     * when the activity does not hold a fragment.
192     */
193    @CallSuper
194    protected View setContentLayout(@LayoutRes int id) {
195        return getLayoutInflater().inflate(id, mCarSetupWizardLayout);
196    }
197
198    /**
199     * Method to be overwritten by subclasses wanting to implement their own back behavior.
200     * Default behavior is to pop a fragment off of the backstack if one exists, otherwise call
201     * finish()
202     *
203     * @return {@code true} whether to call finish()
204     */
205    protected boolean handleBackButton() {
206        return popBackStackImmediate();
207    }
208
209    /**
210     * Called when nextAction has been invoked, should be overridden on derived class when it is
211     * needed perform work when nextAction has been invoked.
212     */
213    protected void onNextActionInvoked() {
214    }
215
216    /**
217     * Moves to the next Activity in the SetupWizard flow.
218     */
219    protected void nextAction(int resultCode) {
220        nextAction(resultCode, null);
221    }
222
223    /**
224     * Moves to the next Activity in the SetupWizard flow, and save the intent data.
225     */
226    protected void nextAction(int resultCode, Intent data) {
227        if (resultCode == RESULT_CANCELED) {
228            throw new IllegalArgumentException("Cannot call nextAction with RESULT_CANCELED");
229        }
230        onNextActionInvoked();
231        setResultCode(resultCode, data);
232        Intent nextIntent =
233                CarWizardManagerHelper.getNextIntent(getIntent(), mResultCode, mResultData);
234        startActivityForResult(nextIntent, REQUEST_CODE_NEXT);
235    }
236
237    /**
238     * Method for finishing an action. The default behavior is to close out the screen and
239     * go back to the previous one.
240     */
241    protected void finishAction() {
242        finishAction(RESULT_CANCELED);
243    }
244
245    /**
246     * Method for finishing an action with a non-default result code. This is a convenience
247     * method to replace nextAction(resultCode); finish();
248     */
249    protected void finishAction(int resultCode) {
250        finishAction(resultCode, null);
251    }
252
253    /**
254     * Convenience method for nextAction(resultCode, data); finish();
255     */
256    protected void finishAction(int resultCode, Intent data) {
257        if (resultCode != RESULT_CANCELED) {
258            nextAction(resultCode, data);
259        }
260        finish();
261    }
262
263    /**
264     * Method to retrieve resultCode saved via {@link #setResultCode(int, Intent)}
265     */
266    protected int getResultCode() {
267        return mResultCode;
268    }
269
270    /**
271     * Use instead of {@link #setResult(int, Intent)} so that the resultCode
272     * and data can be referenced later
273     */
274    protected void setResultCode(int resultCode, Intent data) {
275        mResultCode = resultCode;
276        mResultData = data;
277        setResult(resultCode, data);
278    }
279
280    /**
281     * Use instead of {@link #setResult(int)} so that the resultCode can be referenced later
282     */
283    protected void setResultCode(int resultCode) {
284        setResultCode(resultCode, getResultData());
285    }
286
287    /**
288     * Method to retrieve intent data saved via {@link #setResultCode(int, Intent)}
289     */
290    protected Intent getResultData() {
291        return mResultData;
292    }
293
294
295    // CarSetupWizardLayout Accessors
296
297    /**
298     * Sets whether the back button is visible. If this value is {@code true}, clicking the
299     * button will take the user back to the previous screen in the setup flow.
300     */
301    protected void setBackButtonVisible(boolean visible) {
302        mCarSetupWizardLayout.setBackButtonVisible(visible);
303    }
304
305    /**
306     * Sets whether the toolbar title is visible.
307     */
308    protected void setToolbarTitleVisible(boolean visible) {
309        mCarSetupWizardLayout.setToolbarTitleVisible(visible);
310    }
311
312    /**
313     * Sets the text for the toolbar title.
314     */
315    protected void setToolbarTitleText(String text) {
316        mCarSetupWizardLayout.setToolbarTitleText(text);
317    }
318
319    /**
320     * Sets whether the primary continue button is visible.
321     */
322    protected void setPrimaryToolbarButtonVisible(boolean visible) {
323        mCarSetupWizardLayout.setPrimaryToolbarButtonVisible(visible);
324    }
325
326    /**
327     * Sets whether the primary continue button is enabled. If this value is {@code true},
328     * clicking the button will take the user to the next screen in the setup flow.
329     */
330    protected void setPrimaryToolbarButtonEnabled(boolean enabled) {
331        mCarSetupWizardLayout.setPrimaryToolbarButtonEnabled(enabled);
332    }
333
334    /**
335     * Sets the text of the primary continue button.
336     */
337    protected void setPrimaryToolbarButtonText(String text) {
338        mCarSetupWizardLayout.setPrimaryToolbarButtonText(text);
339    }
340
341    /**
342     * Sets whether the primary button is displayed as a flat or raised button.
343     */
344    protected void setPrimaryToolbarButtonFlat(boolean flat) {
345        mCarSetupWizardLayout.setPrimaryToolbarButtonFlat(flat);
346    }
347
348    /**
349     * Sets the primary button onClick behavior to a custom method.
350     *
351     * <p>NOTE: This will overwrite the primary tool bar button's default action to call
352     * {@link #nextAction} with RESULT_OK.
353     */
354    protected void setPrimaryToolbarButtonOnClickListener(View.OnClickListener listener) {
355        mCarSetupWizardLayout.setPrimaryToolbarButtonListener(listener);
356    }
357
358    /**
359     * Reset's the primary toolbar button's on click listener to call {@link #nextAction} with
360     * RESULT_OK
361     */
362    protected void resetPrimaryToolbarButtonOnCLickListener() {
363        setPrimaryToolbarButtonOnClickListener(v -> nextAction(RESULT_OK));
364    }
365
366
367    /**
368     * Sets whether the secondary continue button is visible.
369     */
370    protected void setSecondaryToolbarButtonVisible(boolean visible) {
371        mCarSetupWizardLayout.setSecondaryToolbarButtonVisible(visible);
372    }
373
374    /**
375     * Sets whether the secondary continue button is enabled. If this value is {@code true},
376     * clicking the button will take the user to the next screen in the setup flow.
377     */
378    protected void setSecondaryToolbarButtonEnabled(boolean enabled) {
379        mCarSetupWizardLayout.setSecondaryToolbarButtonEnabled(enabled);
380    }
381
382    /**
383     * Sets the text of the secondary continue button.
384     */
385    protected void setSecondaryToolbarButtonText(String text) {
386        mCarSetupWizardLayout.setSecondaryToolbarButtonText(text);
387    }
388
389    /**
390     * Sets the secondary button onClick behavior to a custom method.
391     *
392     * <p>NOTE: This will overwrite the secondary tool bar button's default action to call
393     * {@link #nextAction} with RESULT_OK.
394     */
395    protected void setSecondaryToolbarButtonOnClickListener(View.OnClickListener listener) {
396        mCarSetupWizardLayout.setSecondaryToolbarButtonListener(listener);
397    }
398
399    /**
400     * Reset's the secondary toolbar button's on click listener to call {@link #nextAction} with
401     * RESULT_OK
402     */
403    protected void resetSecondaryToolbarButtonOnCLickListener() {
404        setSecondaryToolbarButtonOnClickListener(v -> nextAction(RESULT_OK));
405    }
406
407    /**
408     * Adds elevation to the title bar in order to produce a drop shadow.
409     */
410    protected void addElevationToTitleBar(boolean animate) {
411        mCarSetupWizardLayout.addElevationToTitleBar(animate);
412    }
413
414    /**
415     * Removes the elevation from the title bar using an animation.
416     */
417    protected void removeElevationFromTitleBar(boolean animate) {
418        mCarSetupWizardLayout.removeElevationFromTitleBar(animate);
419    }
420
421    /**
422     * Sets whether the progress bar is visible.
423     */
424    protected void setProgressBarVisible(boolean visible) {
425        mCarSetupWizardLayout.setProgressBarVisible(visible);
426    }
427
428    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
429    boolean getAllowFragmentCommits() {
430        return mAllowFragmentCommits;
431    }
432
433    protected CarSetupWizardLayout getCarSetupWizardLayout() {
434        return mCarSetupWizardLayout;
435    }
436}
437