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 17 package com.android.car.setupwizardlib; 18 19 import android.annotation.CallSuper; 20 import android.content.Intent; 21 import android.os.Bundle; 22 import android.support.annotation.LayoutRes; 23 import android.support.annotation.VisibleForTesting; 24 import android.support.v4.app.Fragment; 25 import android.support.v4.app.FragmentActivity; 26 import android.view.View; 27 28 import 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 */ 44 public 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