@Nullable
  public JobInvocation.Builder decode(@NonNull Bundle providedBundle) {
    if (providedBundle == null) {
      throw new IllegalArgumentException("Unexpected null Bundle provided");
    }

    Bundle data = new Bundle(providedBundle); // Copy to prevent modification
    boolean recur = data.getBoolean(prefix + BundleProtocol.PACKED_PARAM_RECURRING);
    boolean replaceCur = data.getBoolean(prefix + BundleProtocol.PACKED_PARAM_REPLACE_CURRENT);
    int lifetime = data.getInt(prefix + BundleProtocol.PACKED_PARAM_LIFETIME);
    int[] constraints = uncompact(data.getInt(prefix + BundleProtocol.PACKED_PARAM_CONSTRAINTS));

    JobTrigger trigger = decodeTrigger(data);
    RetryStrategy retryStrategy = decodeRetryStrategy(data);

    String tag = data.getString(prefix + BundleProtocol.PACKED_PARAM_TAG);
    String service = data.getString(prefix + BundleProtocol.PACKED_PARAM_SERVICE);

    if (tag == null || service == null || trigger == null || retryStrategy == null) {
      return null;
    }

    JobInvocation.Builder builder = new JobInvocation.Builder();
    builder.setTag(tag);
    builder.setService(service);
    builder.setTrigger(trigger);
    builder.setRetryStrategy(retryStrategy);
    builder.setRecurring(recur);
    // noinspection WrongConstant
    builder.setLifetime(lifetime);
    // noinspection WrongConstant
    builder.setConstraints(constraints);
    builder.setReplaceCurrent(replaceCur);

    // remove any prefixed keys
    if (!TextUtils.isEmpty(prefix)) {
      Iterator<String> keyIterator = data.keySet().iterator();
      while (keyIterator.hasNext()) {
        String key = keyIterator.next();
        if (key.startsWith(prefix)) {
          keyIterator.remove();
        }
      }
    }

    // Assume anything that wasn't prefixed is a user-supplied extra
    builder.addExtras(data);

    return builder;
  }
JobCoder is a tool to encode and decode JobSpecs from Bundles.
jobdispatcher/src/main/java/com/firebase/jobdispatcher/JobCoder.java