// Copyright 2016 Google, Inc. // // 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.firebase.jobdispatcher; import static com.firebase.jobdispatcher.RetryStrategy.RETRY_POLICY_EXPONENTIAL; import static com.firebase.jobdispatcher.RetryStrategy.RETRY_POLICY_LINEAR; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; import android.os.Parcel; import android.support.annotation.CallSuper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; /** * Validates Jobs according to some safe standards. * <p/> * Custom JobValidators should typically extend from this. */ public class DefaultJobValidator implements JobValidator { /** * The maximum length of a tag, in characters (i.e. String.length()). Strings longer than this * will cause validation to fail. */ public static final int MAX_TAG_LENGTH = 100; /** * The maximum size, in bytes, that the provided extras bundle can be. Corresponds to * {@link Parcel#dataSize()}. */ public final static int MAX_EXTRAS_SIZE_BYTES = 10 * 1024; /** Private ref to the Context. Necessary to check that the manifest is configured correctly. */ private final Context context; public DefaultJobValidator(Context context) { this.context = context; } /** @see {@link #MAX_EXTRAS_SIZE_BYTES}. */ private static int measureBundleSize(Bundle extras) { Parcel p = Parcel.obtain(); extras.writeToParcel(p, 0); int sizeInBytes = p.dataSize(); p.recycle(); return sizeInBytes; } /** Combines two {@literal List<String>s} together. */ @Nullable private static List<String> mergeErrorLists(@Nullable List<String> errors, @Nullable List<String> newErrors) { if (errors == null) { return newErrors; } if (newErrors == null) { return errors; } errors.addAll(newErrors); return errors; } @Nullable private static List<String> addError(@Nullable List<String> errors, String newError) { if (newError == null) { return errors; } if (errors == null) { return getMutableSingletonList(newError); } Collections.addAll(errors, newError); return errors; } @Nullable private static List<String> addErrorsIf(boolean condition, List<String> errors, String newErr) { if (condition) { return addError(errors, newErr); } return errors; } /** * Attempts to validate the provided {@code JobParameters}. If the JobParameters is valid, null will be * returned. If the JobParameters has errors, a list of those errors will be returned. */ @Nullable @Override @CallSuper public List<String> validate(JobParameters job) { List<String> errors = null; errors = mergeErrorLists(errors, validate(job.getTrigger())); errors = mergeErrorLists(errors, validate(job.getRetryStrategy())); if (job.isRecurring() && job.getTrigger() == Trigger.NOW) { errors = addError(errors, "ImmediateTriggers can't be used with recurring jobs"); } errors = mergeErrorLists(errors, validateForTransport(job.getExtras())); if (job.getLifetime() > Lifetime.UNTIL_NEXT_BOOT) { //noinspection ConstantConditions errors = mergeErrorLists(errors, validateForPersistence(job.getExtras())); } errors = mergeErrorLists(errors, validateTag(job.getTag())); errors = mergeErrorLists(errors, validateService(job.getService())); return errors; } /** * Attempts to validate the provided Trigger. If valid, null is returned. Otherwise a list of * errors will be returned. * <p/> * Note that a Trigger that passes validation here is not necessarily valid in all permutations * of a JobParameters. For example, an Immediate is never valid for a recurring job. * @param trigger */ @Nullable @Override @CallSuper public List<String> validate(JobTrigger trigger) { if (trigger != Trigger.NOW && !(trigger instanceof JobTrigger.ExecutionWindowTrigger)) { return getMutableSingletonList("Unknown trigger provided"); } return null; } /** * Attempts to validate the provided RetryStrategy. If valid, null is returned. Otherwise a list * of errors will be returned. */ @Nullable @Override @CallSuper public List<String> validate(RetryStrategy retryStrategy) { List<String> errors = null; int policy = retryStrategy.getPolicy(); int initial = retryStrategy.getInitialBackoff(); int maximum = retryStrategy.getMaximumBackoff(); errors = addErrorsIf(policy != RETRY_POLICY_EXPONENTIAL && policy != RETRY_POLICY_LINEAR, errors, "Unknown retry policy provided"); errors = addErrorsIf(maximum < initial, errors, "Maximum backoff must be greater than or equal to initial backoff"); errors = addErrorsIf(300 > maximum, errors, "Maximum backoff must be greater than 300s (5 minutes)"); errors = addErrorsIf(initial < 30, errors, "Initial backoff must be at least 30s"); return errors; } @Nullable private List<String> validateForPersistence(Bundle extras) { List<String> errors = null; if (extras != null) { // check the types to make sure they're persistable for (String k : extras.keySet()) { errors = addError(errors, validateExtrasType(extras, k)); } } return errors; } @Nullable private List<String> validateForTransport(Bundle extras) { if (extras == null) { return null; } int bundleSizeInBytes = measureBundleSize(extras); if (bundleSizeInBytes > MAX_EXTRAS_SIZE_BYTES) { return getMutableSingletonList(String.format(Locale.US, "Extras too large: %d bytes is > the max (%d bytes)", bundleSizeInBytes, MAX_EXTRAS_SIZE_BYTES)); } return null; } @Nullable private String validateExtrasType(Bundle extras, String key) { Object o = extras.get(key); if (o == null || o instanceof Integer || o instanceof Long || o instanceof Double || o instanceof String || o instanceof Boolean) { return null; } return String.format(Locale.US, "Received value of type '%s' for key '%s', but only the" + " following extra parameter types are supported:" + " Integer, Long, Double, String, and Boolean", o == null ? null : o.getClass(), key); } private List<String> validateService(String service) { if (service == null || service.isEmpty()) { return getMutableSingletonList("Service can't be empty"); } if (context == null) { return getMutableSingletonList("Context is null, can't query PackageManager"); } PackageManager pm = context.getPackageManager(); if (pm == null) { return getMutableSingletonList("PackageManager is null, can't validate service"); } final String msg = "Couldn't find a registered service with the name " + service + ". Is it declared in the manifest with the right intent-filter?"; Intent executeIntent = new Intent(JobService.ACTION_EXECUTE); executeIntent.setClassName(context, service); List<ResolveInfo> intentServices = pm.queryIntentServices(executeIntent, 0); if (intentServices == null || intentServices.isEmpty()) { return getMutableSingletonList(msg); } for (ResolveInfo info : intentServices) { if (info.serviceInfo != null && info.serviceInfo.enabled) { // found a match! return null; } } return getMutableSingletonList(msg); } private List<String> validateTag(String tag) { if (tag == null) { return getMutableSingletonList("Tag can't be null"); } if (tag.length() > MAX_TAG_LENGTH) { return getMutableSingletonList("Tag must be shorter than " + MAX_TAG_LENGTH); } return null; } @NonNull private static List<String> getMutableSingletonList(String msg) { ArrayList<String> strings = new ArrayList<>(); strings.add(msg); return strings; } }