/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.settings.form;
import com.android.tv.settings.dialog.old.Action;
import com.android.tv.settings.dialog.old.ActionAdapter;
import com.android.tv.settings.dialog.old.ActionFragment;
import com.android.tv.settings.dialog.old.ContentFragment;
import com.android.tv.settings.dialog.old.DialogActivity;
import com.android.tv.settings.dialog.old.EditTextFragment;
import com.android.tv.settings.R;
import android.app.Fragment;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Stack;
/**
* Implements a MultiPagedForm.
*
* This is a multi-paged form that can be used for fragment transitions used in
* such as setup, add network, and add credit cards
*/
public abstract class MultiPagedForm extends DialogActivity implements ActionAdapter.Listener,
FormPageResultListener, FormResultListener {
private static final int INTENT_FORM_PAGE_DATA_REQUEST = 1;
private static final String TAG = "MultiPagedForm";
private enum Key {
DONE, CANCEL
}
protected final ArrayList mFormPages = new ArrayList();
private final Stack mFlowStack = new Stack();
private ActionAdapter.Listener mListener = null;
@Override
public void onActionClicked(Action action) {
if (mListener != null) {
mListener.onActionClicked(action);
}
}
@Override
public void onBackPressed() {
// If we don't have a page to go back to, finish as cancelled.
if (mFlowStack.size() < 1) {
setResult(RESULT_CANCELED);
finish();
return;
}
// Pop the current location off the stack.
mFlowStack.pop();
// Peek at the previous location on the stack.
Object lastLocation = mFlowStack.isEmpty() ? null : mFlowStack.peek();
if (lastLocation instanceof FormPage && !mFormPages.contains(lastLocation)) {
onBackPressed();
} else {
displayCurrentStep(false);
if (mFlowStack.isEmpty()) {
setResult(RESULT_CANCELED);
finish();
}
}
}
@Override
public void onBundlePageResult(FormPage page, Bundle bundleResults) {
// Complete the form with the results.
page.complete(bundleResults);
// Indicate that we've completed a page. If we get back false it means
// the data was invalid and the page must be filled out again.
// Otherwise, we move on to the next page.
if (!onPageComplete(page)) {
displayCurrentStep(false);
} else {
performNextStep();
}
}
@Override
public void onFormComplete() {
onComplete(mFormPages);
}
@Override
public void onFormCancelled() {
onCancel(mFormPages);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
performNextStep();
super.onCreate(savedInstanceState);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == INTENT_FORM_PAGE_DATA_REQUEST) {
if (resultCode == RESULT_OK) {
overridePendingTransition(
R.anim.wps_activity_open_in, R.anim.wps_activity_open_out);
Object currentLocation = mFlowStack.peek();
if (currentLocation instanceof FormPage) {
FormPage page = (FormPage) currentLocation;
Bundle results = data == null ? null : data.getExtras();
if (data == null) {
Log.w(TAG, "Intent result was null!");
} else if (results == null) {
Log.w(TAG, "Intent result extras were null!");
} else if (!results.containsKey(FormPage.DATA_KEY_SUMMARY_STRING)) {
Log.w(TAG, "Intent result extras didn't have the result summary key!");
}
onBundlePageResult(page, results);
} else {
Log.e(TAG, "Our current location wasn't on the top of the stack!");
}
} else {
overridePendingTransition(
R.anim.wps_activity_close_in, R.anim.wps_activity_close_out);
onBackPressed();
}
}
}
/**
* Called when a form page completes. If necessary, add or remove any pages
* from the form before this call completes. If all pages are complete when
* onPageComplete returns, the form will be considered finished and the form
* results will be displayed for confirmation.
*
* @param formPage the page that was completed.
* @return true if the form can continue to the next incomplete page, or
* false if the data input is invalid and the form page must be
* completed again.
*/
protected abstract boolean onPageComplete(FormPage formPage);
/**
* Called when all form pages have been completed and the user has accepted
* them.
*
* @param formPages the pages that were completed. Any pages removed during
* the completion of the form are not included.
*/
protected abstract void onComplete(ArrayList formPages);
/**
* Called when all form pages have been completed but the user wants to
* cancel the form and discard the results.
*
* @param formPages the pages that were completed. Any pages removed during
* the completion of the form are not included.
*/
protected abstract void onCancel(ArrayList formPages);
/**
* Override this to fully customize the display of the page.
*
* @param formPage the page that should be displayed.
* @param listener the listener to notify when the page is complete.
*/
protected void displayPage(FormPage formPage, FormPageResultListener listener,
boolean forward) {
switch (formPage.getType()) {
case PASSWORD_INPUT:
setContentAndActionFragments(getContentFragment(formPage),
createPasswordEditTextFragment(formPage));
break;
case TEXT_INPUT:
setContentAndActionFragments(getContentFragment(formPage),
createEditTextFragment(formPage));
break;
case MULTIPLE_CHOICE:
setContentAndActionFragments(getContentFragment(formPage),
createActionFragment(formPage));
break;
case INTENT:
default:
break;
}
}
/**
* Override this to fully customize the display of the form results.
*
* @param formPages the pages that were whose results should be displayed.
* @param listener the listener to notify when the form is complete or has been cancelled.
*/
protected void displayFormResults(ArrayList formPages, FormResultListener listener) {
setContentAndActionFragments(createResultContentFragment(),
createResultActionFragment(formPages, listener));
}
/**
* @return the main title for this multipage form.
*/
protected String getMainTitle() {
return "";
}
/**
* @return the action title to indicate the form is correct.
*/
protected String getFormIsCorrectActionTitle() {
return "";
}
/**
* @return the action title to indicate the form should be canceled and its
* results discarded.
*/
protected String getFormCancelActionTitle() {
return "";
}
/**
* Override this to provide a custom Fragment for displaying the content
* portion of the page.
*
* @param formPage the page the Fragment should display.
* @return a Fragment for identifying the current step.
*/
protected Fragment getContentFragment(FormPage formPage) {
return ContentFragment.newInstance(formPage.getTitle());
}
/**
* Override this to provide a custom Fragment for displaying the content
* portion of the form results.
*
* @return a Fragment for giving context to the result page.
*/
protected Fragment getResultContentFragment() {
return ContentFragment.newInstance(getMainTitle());
}
/**
* Override this to provide a custom EditTextFragment for displaying a form
* page for password input. Warning: the OnEditorActionListener of this
* fragment will be overridden.
*
* @param initialText initial text that should be displayed in the edit
* field.
* @return an EditTextFragment for password input.
*/
protected EditTextFragment getPasswordEditTextFragment(String initialText) {
return EditTextFragment.newInstance(null, initialText, true /* password */);
}
/**
* Override this to provide a custom EditTextFragment for displaying a form
* page for text input. Warning: the OnEditorActionListener of this fragment
* will be overridden.
*
* @param initialText initial text that should be displayed in the edit
* field.
* @return an EditTextFragment for custom input.
*/
protected EditTextFragment getEditTextFragment(String initialText) {
return EditTextFragment.newInstance(null, initialText);
}
/**
* Override this to provide a custom ActionFragment for displaying a form
* page for a list of choices.
*
* @param formPage the page the ActionFragment is for.
* @param actions the actions the ActionFragment should display.
* @param selectedAction the action in actions that is currently selected,
* or null if none are selected.
* @return an ActionFragment displaying the given actions.
*/
protected ActionFragment getActionFragment(FormPage formPage, ArrayList actions,
Action selectedAction) {
ActionFragment actionFragment = ActionFragment.newInstance(actions);
if (selectedAction != null) {
int indexOfSelection = actions.indexOf(selectedAction);
if (indexOfSelection >= 0) {
// TODO: Set initial focus action:
// actionFragment.setSelection(indexOfSelection);
}
}
return actionFragment;
}
/**
* Override this to provide a custom ActionFragment for displaying the list
* of page results.
*
* @param actions the actions the ActionFragment should display.
* @return an ActionFragment displaying the given form results.
*/
protected ActionFragment getResultActionFragment(ArrayList actions) {
return ActionFragment.newInstance(actions);
}
/**
* Adds the page to the end of the form. Only call this before onCreate or
* during onPageComplete.
*
* @param formPage the page to add to the end of the form.
*/
protected void addPage(FormPage formPage) {
mFormPages.add(formPage);
}
/**
* Removes the page from the form. Only call this before onCreate or during
* onPageComplete.
*
* @param formPage the page to remove from the form.
*/
protected void removePage(FormPage formPage) {
mFormPages.remove(formPage);
}
/**
* Clears all pages from the form. Only call this before onCreate or during
* onPageComplete.
*/
protected void clear() {
mFormPages.clear();
}
/**
* Clears all pages after the given page from the form. Only call this
* before onCreate or during onPageComplete.
*
* @param formPage all pages after this page in the form will be removed
* from the form.
*/
protected void clearAfter(FormPage formPage) {
int indexOfPage = mFormPages.indexOf(formPage);
if (indexOfPage >= 0) {
for (int i = mFormPages.size() - 1; i > indexOfPage; i--) {
mFormPages.remove(i);
}
}
}
/**
* Stop display the currently displayed page. Note that this does not
* remove the form page from the set of form pages for this form, it is just
* no longer displayed and no replacement is provided, the screen should be
* empty after this method.
*/
protected void undisplayCurrentPage() {
}
private void performNextStep() {
// First see if there are any incomplete form pages.
FormPage nextIncompleteStep = findNextIncompleteStep();
// If all the pages we have are complete, display the results.
if (nextIncompleteStep == null) {
mFlowStack.push(this);
} else {
mFlowStack.push(nextIncompleteStep);
}
displayCurrentStep(true);
}
private FormPage findNextIncompleteStep() {
for (int i = 0, size = mFormPages.size(); i < size; i++) {
FormPage formPage = mFormPages.get(i);
if (!formPage.isComplete()) {
return formPage;
}
}
return null;
}
private void displayCurrentStep(boolean forward) {
if (!mFlowStack.isEmpty()) {
Object currentLocation = mFlowStack.peek();
if (currentLocation instanceof MultiPagedForm) {
displayFormResults(mFormPages, this);
} else if (currentLocation instanceof FormPage) {
FormPage page = (FormPage) currentLocation;
if (page.getType() == FormPage.Type.INTENT) {
startActivityForResult(page.getIntent(), INTENT_FORM_PAGE_DATA_REQUEST);
}
displayPage(page, this, forward);
} else {
// If this is an unexpected type, something went wrong, finish as
// cancelled.
setResult(RESULT_CANCELED);
finish();
}
} else {
undisplayCurrentPage();
}
}
private Fragment createResultContentFragment() {
return getResultContentFragment();
}
private Fragment createResultActionFragment(final ArrayList formPages,
final FormResultListener listener) {
mListener = new ActionAdapter.Listener() {
@Override
public void onActionClicked(Action action) {
Key key = getKeyFromKey(action.getKey());
if (key != null) {
switch (key) {
case DONE:
listener.onFormComplete();
break;
case CANCEL:
listener.onFormCancelled();
break;
default:
break;
}
} else {
String formPageKey = action.getKey();
for (int i = 0, size = formPages.size(); i < size; i++) {
FormPage formPage = formPages.get(i);
if (formPageKey.equals(formPage.getTitle())) {
mFlowStack.push(formPage);
displayCurrentStep(true);
break;
}
}
}
}
};
return getResultActionFragment(getResultActions());
}
private Key getKeyFromKey(String key) {
try {
return Key.valueOf(key);
} catch (IllegalArgumentException iae) {
return null;
}
}
private ArrayList getActions(FormPage formPage) {
ArrayList actions = new ArrayList();
for (String choice : formPage.getChoices()) {
actions.add(new Action.Builder().key(choice).title(choice).build());
}
return actions;
}
private ArrayList getResultActions() {
ArrayList actions = new ArrayList();
for (int i = 0, size = mFormPages.size(); i < size; i++) {
FormPage formPage = mFormPages.get(i);
actions.add(new Action.Builder().key(formPage.getTitle())
.title(formPage.getDataSummary()).description(formPage.getTitle()).build());
}
actions.add(new Action.Builder().key(Key.CANCEL.name())
.title(getFormCancelActionTitle()).build());
actions.add(new Action.Builder().key(Key.DONE.name())
.title(getFormIsCorrectActionTitle()).build());
return actions;
}
private Fragment createActionFragment(final FormPage formPage) {
mListener = new ActionAdapter.Listener() {
@Override
public void onActionClicked(Action action) {
handleStringPageResult(formPage, action.getKey());
}
};
ArrayList actions = getActions(formPage);
Action selectedAction = null;
String choice = formPage.getDataSummary();
for (int i = 0, size = actions.size(); i < size; i++) {
Action action = actions.get(i);
if (action.getKey().equals(choice)) {
selectedAction = action;
break;
}
}
return getActionFragment(formPage, actions, selectedAction);
}
private Fragment createPasswordEditTextFragment(final FormPage formPage) {
EditTextFragment editTextFragment = getPasswordEditTextFragment(formPage.getDataSummary());
attachListeners(editTextFragment, formPage);
return editTextFragment;
}
private Fragment createEditTextFragment(final FormPage formPage) {
EditTextFragment editTextFragment = getEditTextFragment(formPage.getDataSummary());
attachListeners(editTextFragment, formPage);
return editTextFragment;
}
private void attachListeners(EditTextFragment editTextFragment, final FormPage formPage) {
editTextFragment.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
handleStringPageResult(formPage, v.getText().toString());
return true;
}
});
}
private void handleStringPageResult(FormPage page, String stringResults) {
Bundle bundleResults = new Bundle();
bundleResults.putString(FormPage.DATA_KEY_SUMMARY_STRING, stringResults);
onBundlePageResult(page, bundleResults);
}
}