Java tutorial
/* * Copyright (C) 2006 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.coreform.open.android.formidablevalidation; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map.Entry; import java.util.Set; import android.content.Context; import android.os.Build; import android.os.Handler; import android.support.v4.view.accessibility.AccessibilityEventCompat; import android.text.SpannableStringBuilder; import android.util.Log; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.widget.CheckBox; import android.widget.EditText; import android.widget.ScrollView; import android.widget.TextView; public class ValidationManager { private static final boolean DEBUG = true; private static final String TAG = "ValidationManager"; private static final long ACCESSIBILITY_ANNOUNCE_DELAY = 1 * 1000; private static Context mContext; private Handler mHandler; /** * The accessibility manager for this context. This is used to check the * accessibility enabled state, as well as to send raw accessibility events. */ private static AccessibilityManager mAccessibilityManager; private LinkedHashMap<String, List<ValueValidatorInterface>> valueValidatorMap; private LinkedHashMap<String, List<DependencyValidatorInterface>> dependencyValidatorMap; public ValidationManager(Context context) { mContext = context; valueValidatorMap = new LinkedHashMap<String, List<ValueValidatorInterface>>(); dependencyValidatorMap = new LinkedHashMap<String, List<DependencyValidatorInterface>>(); mHandler = new Handler(); // Keep a handle to the accessibility manager. mAccessibilityManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); } /* * METHODS */ /** * note: add the key to both maps (if not already created) * * @param key */ public ValidationManager add(String key) { if (valueValidatorMap.containsKey(key)) { //field has already been added, thus no need to re-add it! //do nada } else { //field has NOT already been added, so create it's List<ValueValidatorInterface>... //...and add the key and the empty List<ValueValidatorInterface> to the valueValidatorMap List<ValueValidatorInterface> fieldValueValidatorList = new ArrayList<ValueValidatorInterface>(); valueValidatorMap.put(key, fieldValueValidatorList); } if (dependencyValidatorMap.containsKey(key)) { //field has already been added, thus no need to re-add it! //do nada } else { //field has NOT already been added, so create it's List<DependencyValidatorInterface>... //...and add the key and the empty List<DependencyValidatorInterface> to the dependencyValidatorMap List<DependencyValidatorInterface> fieldDependencyValidatorList = new ArrayList<DependencyValidatorInterface>(); dependencyValidatorMap.put(key, fieldDependencyValidatorList); } return this; } public ValidationManager add(String key, ValueValidatorInterface valueValidator) { if (valueValidatorMap.containsKey(key)) { //field has already been added, thus it's List<ValueValidatorInterface> has already been created, so just add to that List<ValueValidatorInterface> fieldValueValidatorList = valueValidatorMap.get(key); fieldValueValidatorList.add(valueValidator); } else { //field has NOT already been added, so create it's List<ValueValidatorInterface> and add the first valueValidator to that... //...then add the key and the List<ValueValidatorInterface> to the valueValidatorMap List<ValueValidatorInterface> fieldValueValidatorList = new ArrayList<ValueValidatorInterface>(); fieldValueValidatorList.add(valueValidator); valueValidatorMap.put(key, fieldValueValidatorList); } return this; } public ValidationManager add(String key, DependencyValidatorInterface dependencyValidator) { if (dependencyValidatorMap.containsKey(key)) { //field has already been added, thus it's List<DependencyValidatorInterface> has already been created, so just add to that List<DependencyValidatorInterface> fieldDependencyValidatorList = dependencyValidatorMap.get(key); fieldDependencyValidatorList.add(dependencyValidator); } else { //field has NOT already been added, so create it's List<DependencyValidatorInterface> and add the first dependencyValidator to that... //...then add the key and the List<DependencyValidatorInterface> to the dependencyValidatorMap List<DependencyValidatorInterface> fieldDependencyValidatorList = new ArrayList<DependencyValidatorInterface>(); fieldDependencyValidatorList.add(dependencyValidator); dependencyValidatorMap.put(key, fieldDependencyValidatorList); } return this; } public HashMap<String, List<ValueValidationResult>> validateValues() { if (DEBUG) Log.d(TAG, ".validateValues()..."); HashMap<String, List<ValueValidationResult>> valueValidationResultsMap = new HashMap<String, List<ValueValidationResult>>(); for (Entry<String, List<ValueValidatorInterface>> entry : valueValidatorMap.entrySet()) { List<ValueValidatorInterface> entryValidatorList = entry.getValue(); if (DEBUG) Log.d(TAG, "...validating value for field: " + entry.getKey() + " (field has " + entryValidatorList.size() + " valueValidators)"); if (entryValidatorList.size() > 0) { //only bother creating a result entry for this field if it actually has at least one ValueValidator List<ValueValidationResult> fieldValueValidationResultsList = new ArrayList<ValueValidationResult>(); //now run validation for each ValueValidator and put its Result into the ResultsList for (ValueValidatorInterface valueValidatorInterface : entryValidatorList) { if (DEBUG) Log.d(TAG, "......value validation expression: " + valueValidatorInterface.getExpression()); ValueValidationResult aValueValidationResult = valueValidatorInterface.validateValue(); //only add a FinalValidationResult if ValueValidationResult is invalid WHETHER OR NOT we care if its invalid if (!aValueValidationResult.isValid()) { fieldValueValidationResultsList.add(aValueValidationResult); } if (DEBUG && !aValueValidationResult.isValid()) { Log.d(TAG, ".........a ValueValidationResult is INVALID..."); } if (DEBUG && aValueValidationResult.isValid()) { Log.d(TAG, ".........a ValueValidationResult is VALID..."); } } //add ResultsList to ResultsMap for this field valueValidationResultsMap.put(entry.getKey(), fieldValueValidationResultsList); } } return valueValidationResultsMap; } public HashMap<String, List<DependencyValidationResult>> validateDependencies( HashMap<String, List<ValueValidationResult>> valueValidationResultsMap) { if (DEBUG) Log.d(TAG, ".validateDependencies()..."); HashMap<String, List<DependencyValidationResult>> dependencyValidationResultsMap = new HashMap<String, List<DependencyValidationResult>>(); for (Entry<String, List<DependencyValidatorInterface>> entry : dependencyValidatorMap.entrySet()) { List<DependencyValidatorInterface> entryValidatorList = entry.getValue(); //get List<ValueValidationResult> for this field (note: might be null if no ValueValidators were set for this field) //List<ValueValidationResult> fieldValueValidationResultsList = valueValidationResultsMap.get(entry.getKey()); //actually, we want the dependent field's ValueValidators, derrr //if(DEBUG) Log.d(TAG, "...validating dependency for field:"+entry.getKey()+" (field has "+entryValidatorList.size()+" dependencyValidators and "+fieldValueValidationResultsList.size()+" valueValidationResults)"); List<ValueValidationResult> thisFieldValueValidationResultsList = valueValidationResultsMap .get(entry.getKey()); int thisFieldValueValidationAmount = (thisFieldValueValidationResultsList != null) ? thisFieldValueValidationResultsList.size() : 0; if (DEBUG) Log.d(TAG, "...validating dependency for field:" + entry.getKey() + " (field has " + entryValidatorList.size() + " dependencyValidators and " + thisFieldValueValidationAmount + " valueValidators)"); //only bother creating a result entry for this field if it actually has at least one DependencyValidator if (entryValidatorList.size() > 0) { //determine whether this field's value is itself valid according to its ValueValidationResults String thisFieldInvalidMessage = null; //override if any invalid results found (using first result found) if (!thisFieldValueValidationResultsList.isEmpty()) { thisFieldInvalidMessage = thisFieldValueValidationResultsList.get(0).getMessage(); } //get dependent field's ValidationResults List<DependencyValidationResult> fieldCruxValidationResultsList = new ArrayList<DependencyValidationResult>(); //now run validation for each DependencyValidator and put its Result into the ResultsList for (DependencyValidatorInterface dependencyValidatorInterface : entryValidatorList) { List<ValueValidationResult> cruxFieldValueValidationResultsList = valueValidationResultsMap .get(dependencyValidatorInterface.getCruxFieldKey()); int cruxValueValidationResultAmount = (cruxFieldValueValidationResultsList != null) ? cruxFieldValueValidationResultsList.size() : 0; if (DEBUG) Log.d(TAG, "......crux field's number of ValueValidationResults: " + cruxValueValidationResultAmount); DependencyValidationResult aDependencyValidationResult = dependencyValidatorInterface .validateDependency(thisFieldInvalidMessage, cruxFieldValueValidationResultsList); //null check is a bit of a quick hack to alleviate a bigger problem...which it doesn't fix...TODO fix the actual problem! //NullPointerException when accessing FinalValidationResult.getSource() in submit button's onClickListener WHEN nameFirst is invalid (so its due to the dependency check logic) if (!aDependencyValidationResult.isValid()) fieldCruxValidationResultsList.add(aDependencyValidationResult); if (DEBUG && !aDependencyValidationResult.isValid()) { Log.d(TAG, ".........a DependencyValidationResult is INVALID..."); } if (DEBUG && aDependencyValidationResult.isValid()) { Log.d(TAG, ".........a DependencyValidationResult is VALID..."); } if (DEBUG && dependencyValidatorInterface.getSource() == null) { Log.d(TAG, ".........a DependencyValidator's source is NULL..."); } } //add ResultsList to ResultsMap for this field dependencyValidationResultsMap.put(entry.getKey(), fieldCruxValidationResultsList); } } return dependencyValidationResultsMap; } private void removePreviouslySetErrors() { for (Entry<String, List<ValueValidatorInterface>> entry : valueValidatorMap.entrySet()) { List<ValueValidatorInterface> entryValidatorList = entry.getValue(); for (ValueValidatorInterface valueValidatorInterface : entryValidatorList) { if (valueValidatorInterface.getSource() instanceof SetErrorAble) { ((SetErrorAble) valueValidatorInterface.getSource()).betterSetError(null); } else if (valueValidatorInterface.getSource() instanceof TextView) { ((TextView) valueValidatorInterface.getSource()).setError(null); } } } for (Entry<String, List<DependencyValidatorInterface>> entry : dependencyValidatorMap.entrySet()) { List<DependencyValidatorInterface> entryValidatorList = entry.getValue(); for (DependencyValidatorInterface dependencyValidatorInterface : entryValidatorList) { if (dependencyValidatorInterface.getSource() instanceof SetErrorAble) { ((SetErrorAble) dependencyValidatorInterface.getSource()).betterSetError(null); } else if (dependencyValidatorInterface.getSource() instanceof TextView) { ((TextView) dependencyValidatorInterface.getSource()).setError(null); } } } } public HashMap<String, FinalValidationResult> validateAll() { return validateAll(false); } public HashMap<String, FinalValidationResult> validateAll(boolean removePreviouslySetErrors) { if (DEBUG) Log.d(TAG, ".validateAll()..."); if (removePreviouslySetErrors) { removePreviouslySetErrors(); } HashMap<String, FinalValidationResult> finalValidationResultMap = new HashMap<String, FinalValidationResult>(); HashMap<String, List<ValueValidationResult>> valueValidationResultsMap = validateValues(); HashMap<String, List<DependencyValidationResult>> dependencyValidationResultsMap = validateDependencies( valueValidationResultsMap); Object fieldSource = null; //get a full set of fields for which validations are applicable (since ValueValidators and DependencyValidators don't have to be added 1-for-1) //...note: if a field was added to either ValueValidator list or DependencyValidator list or both but never had a validator associated with it, it won't be in this set. Set<String> valueValidatorFieldsSet = valueValidationResultsMap.keySet(); Set<String> dependencyValidatorFieldsSet = dependencyValidationResultsMap.keySet(); Set<String> validatedFieldsSet = new LinkedHashSet<String>(); validatedFieldsSet.addAll(dependencyValidatorFieldsSet); validatedFieldsSet.addAll(valueValidatorFieldsSet); //now aggregate the ValueValidationResults with DependencyValidationResults into a FinalValidationResult for each field for (String fieldRefName : validatedFieldsSet) { if (DEBUG) Log.d(TAG, "...validateAll for field: " + fieldRefName + "..."); boolean fieldValueOk = true; //will be overridden to false if a validation found to have failed boolean fieldDependencyOk = true; //will be overridden to false if a validation found to have failed String fieldValueValidationResultMessage = "Default field value validation result message"; String fieldDependencyValidationResultMessage = "Default field dependency validation result message"; //get List<DependencyValidationResult> for fieldRefName List<DependencyValidationResult> aFieldDependencyValidationResultsList = dependencyValidationResultsMap .get(fieldRefName); if (aFieldDependencyValidationResultsList != null) { if (DEBUG) Log.d(TAG, "...field has unsatisfied dependencies..."); //find the first (if any) DependencyValidation failure for this field for (DependencyValidationResult aDependencyValidationResult : aFieldDependencyValidationResultsList) { if (!aDependencyValidationResult.isValid()) { fieldDependencyOk = false; fieldDependencyValidationResultMessage = aDependencyValidationResult.getMessage(); } } } else { if (DEBUG) Log.d(TAG, "...field has satisfied dependencies..."); //field has no DependencyValidators so definitely check its ValueValidators //get List<ValueValidationResult> for fieldRefName List<ValueValidationResult> aFieldValueValidationResultsList = valueValidationResultsMap .get(fieldRefName); if (aFieldValueValidationResultsList != null) { if (DEBUG) Log.d(TAG, "......field has invalid value..."); //find the first (if any) ValueValidation failure for this field if (!aFieldValueValidationResultsList.isEmpty()) { for (ValueValidationResult aValueValidationResult : aFieldValueValidationResultsList) { fieldSource = aValueValidationResult.getSource(); if (!aValueValidationResult.isValid()) { fieldValueOk = false; fieldValueValidationResultMessage = aValueValidationResult.getMessage(); } } } else { if (DEBUG) Log.d(TAG, "...field's value is invalid, however no ValueValidationResults exists. You may need to implement the validate() method in your AbstractValueValidator implementation."); } } } //populate field source via DependencyValidationResults OR ValueValidationResults if (fieldSource == null) { List<ValueValidationResult> aFieldValueValidationResultsList = valueValidationResultsMap .get(fieldRefName); if (!aFieldValueValidationResultsList.isEmpty()) { fieldSource = aFieldValueValidationResultsList.get(0).getSource(); } else { if (DEBUG) Log.d(TAG, "...field has no source! (no DependencyValidators and no ValueValidators)..."); } } //create and add the FinalValidationResult for this field to the finalValidationResultMap ONLY if a value/dependency fail occurred if (!fieldValueOk || !fieldDependencyOk) { FinalValidationResult aFinalValidationResult = new FinalValidationResult(fieldSource, fieldValueOk, fieldDependencyOk, fieldValueValidationResultMessage, fieldDependencyValidationResultMessage); finalValidationResultMap.put(fieldRefName, aFinalValidationResult); } } return finalValidationResultMap; } public boolean validateAllAndSetError() { //validateAll HashMap<String, FinalValidationResult> finalValidationResultMap = validateAll(true); if (finalValidationResultMap.isEmpty()) { //all valid return true; } else { //something invalid if (DEBUG) Log.d(TAG, "...at least one field is invalid, perhaps more."); if (DEBUG) Log.d(TAG, "...number of invalid fields: " + Integer.toString(finalValidationResultMap.size())); boolean setErrorAlreadyShown = false; View firstInvalidView = null; SpannableStringBuilder firstInvalidErrorText = null; validationLoop: for (Entry<String, FinalValidationResult> entry : finalValidationResultMap.entrySet()) { if (DEBUG) Log.d(TAG, "...validation result failed for field: " + entry.getKey()); FinalValidationResult aFinalValidationResult = entry.getValue(); boolean invalidDependencyTakesPrecedence = false; boolean invalidFieldHasAlreadyBeenFocused = false; //used to force focus to the first invalid field in List instead of last if (!aFinalValidationResult.isDependencyValid()) { if (DEBUG) Log.d(TAG, "...dependency validation failed, with message: " + aFinalValidationResult.getDependencyInvalidMessage()); invalidDependencyTakesPrecedence = true; //if(DEBUG) Log.d(TAG, "aFinalValidationResult source's class: "+aFinalValidationResult.getSource().getClass().getSimpleName()); if ("EditText".equals(aFinalValidationResult.getSource().getClass().getSimpleName())) { if (!invalidFieldHasAlreadyBeenFocused) { invalidFieldHasAlreadyBeenFocused = true; } //setError SpannableStringBuilder errorText = new SpannableStringBuilder( aFinalValidationResult.getDependencyInvalidMessage()); ((EditText) aFinalValidationResult.getSource()).setError(errorText); setErrorAlreadyShown = true; if (firstInvalidView == null) { firstInvalidView = (View) aFinalValidationResult.getSource(); firstInvalidErrorText = errorText; } } } if (!aFinalValidationResult.isValueValid()) { //note: ValueValidationResults for which careInvalid = false should NOT have caused a submission of FinalValidationResult to this List if (DEBUG) Log.d(TAG, "...value validation failed, with message: " + aFinalValidationResult.getValueInvalidMessage()); //only display value invalid message if dependency(ies) satisfied for this field if (!invalidDependencyTakesPrecedence) { if (aFinalValidationResult.getSource() == null) { if (DEBUG) Log.d(TAG, "...cannot display error balloon because source is null!"); //do nothing } else if (aFinalValidationResult.getSource() instanceof SetErrorAble) { //only set (don't show) error for a SetErrorAble View if no other Views have had their error set. //...to avoid having multiple ErrorPopups displayed at same time (at least until User moves focus) SpannableStringBuilder errorText = new SpannableStringBuilder( aFinalValidationResult.getValueInvalidMessage()); if (DEBUG) Log.d(TAG, "...set and show custom error balloon..."); if (true || !invalidFieldHasAlreadyBeenFocused) { invalidFieldHasAlreadyBeenFocused = true; } //setError (for a button, this won't requestFocus() and won't show the message in a popup...it only sets the exclamation inner drawable) ((SetErrorAble) aFinalValidationResult.getSource()).betterSetError(errorText, false); setErrorAlreadyShown = true; if (firstInvalidView == null) { firstInvalidView = (View) aFinalValidationResult.getSource(); firstInvalidErrorText = errorText; } } else if (aFinalValidationResult.getSource() instanceof TextView) { if (DEBUG) Log.d(TAG, "...set and show native error balloon..."); if (!invalidFieldHasAlreadyBeenFocused) { invalidFieldHasAlreadyBeenFocused = true; } //setError SpannableStringBuilder errorText = new SpannableStringBuilder( aFinalValidationResult.getValueInvalidMessage()); ((TextView) aFinalValidationResult.getSource()).setError(errorText); setErrorAlreadyShown = true; if (firstInvalidView == null) { firstInvalidView = (View) aFinalValidationResult.getSource(); firstInvalidErrorText = errorText; } } else { if (DEBUG) Log.d(TAG, "...field does not support .setError()!"); } } } } //need to delay accessibility announcement so its the last View to steal focus if (DEBUG) Log.d(TAG, "firstInvalidErrorText: " + firstInvalidErrorText); AnnounceForAccessibilityRunnable announceForAccessibilityRunnable = new AnnounceForAccessibilityRunnable( firstInvalidErrorText); mHandler.postDelayed(announceForAccessibilityRunnable, ACCESSIBILITY_ANNOUNCE_DELAY); if (((View) firstInvalidView).isFocusable() && ((View) firstInvalidView).isFocusableInTouchMode()) { ((View) firstInvalidView).requestFocus(); } else if (firstInvalidView instanceof SetErrorAble) { ((SetErrorAble) firstInvalidView).betterSetError(firstInvalidErrorText, true); ((View) firstInvalidView).requestFocusFromTouch(); //probably won't do much } return false; } } /** * Generates and dispatches an SDK-specific spoken announcement. * <p> * For backwards compatibility, we're constructing an event from scratch * using the appropriate event type. If your application only targets SDK * 16+, you can just call View.announceForAccessibility(CharSequence). * </p> * * Adapted from https://http://eyes-free.googlecode.com/files/accessibility_codelab_demos_v2_src.zip * * @param text The text to announce. */ public static void announceForAccessibilityCompat(CharSequence text) { if (!mAccessibilityManager.isEnabled()) { return; } // Prior to SDK 16, announcements could only be made through FOCUSED // events. Jelly Bean (SDK 16) added support for speaking text verbatim // using the ANNOUNCEMENT event type. final int eventType; if (Build.VERSION.SDK_INT < 16) { eventType = AccessibilityEvent.TYPE_VIEW_FOCUSED; } else { eventType = AccessibilityEventCompat.TYPE_ANNOUNCEMENT; } // Construct an accessibility event with the minimum recommended // attributes. An event without a class name or package may be dropped. final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); event.getText().add(text); event.setClassName(SetErrorHandler.class.getName()); event.setPackageName(mContext.getPackageName()); // Sends the event directly through the accessibility manager. If your // application only targets SDK 14+, you should just call // getParent().requestSendAccessibilityEvent(this, event); mAccessibilityManager.sendAccessibilityEvent(event); } /* * INNER CLASSES */ private static class AnnounceForAccessibilityRunnable implements Runnable { private final CharSequence mText; public AnnounceForAccessibilityRunnable(CharSequence text) { mText = text; } @Override public void run() { announceForAccessibilityCompat(mText); } } }