package me.tatarka.support.job;

import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;

import me.tatarka.support.internal.IJobServiceCompat;

/**
 *
Entry point for the callback from the {@link android.app.job.JobScheduler}.

 *
This is the base class that handles asynchronous requests that were previously scheduled. You
 * are responsible for overriding {@link JobService#onStartJob(JobParameters)}, which is where
 * you will implement your job logic.

 *
This service executes each incoming job on a {@link android.os.Handler} running on your
 * application's main thread. This means that you must offload your execution logic to
 * another thread/handler/{@link android.os.AsyncTask} of your choosing. Not doing so will result
 * in blocking any future callbacks from the JobManager - specifically
 * {@link #onStopJob(JobParameters)}, which is meant to inform you that the
 * scheduling requirements are no longer being met.

 */
public abstract class JobService extends Service {
    private static final String TAG = "JobServiceCompat";

    /**
     * Identifier for a message that will result in a call to
     * {@link #onStartJob(me.tatarka.support.job.JobParameters)}.
     */
    private final int MSG_EXECUTE_JOB = 0;
    /**
     * Message that will result in a call to {@link #onStopJob(me.tatarka.support.job.JobParameters)}.
     */
    private final int MSG_STOP_JOB = 1;
    /**
     * Message that the client has completed execution of this job.
     */
    private final int MSG_JOB_FINISHED = 2;

    /**
     * Lock object for {@link #mHandler}.
     */
    private final Object mHandlerLock = new Object();

    /**
     * Handler we post jobs to. Responsible for calling into the client logic, and handling the
     * callback to the system.
     */
    JobHandler mHandler;

    /**
     * Binder for this service.
     */
    IJobServiceCompat mBinder = new IJobServiceCompat() {
        @Override
        public void startJob(JobParameters jobParams) {
            ensureHandler();
            Message m = Message.obtain(mHandler, MSG_EXECUTE_JOB, jobParams);
            m.sendToTarget();
        }

        @Override
        public void stopJob(JobParameters jobParams) {
            ensureHandler();
            Message m = Message.obtain(mHandler, MSG_STOP_JOB, jobParams);
            m.sendToTarget();
        }
    };

    /**
     * @hide
     */
    void ensureHandler() {
        synchronized (mHandlerLock) {
            if (mHandler == null) {
                mHandler = new JobHandler(getMainLooper());
            }
        }
    }

    /**
     * Runs on application's main thread - callbacks are meant to offboard work to some other
     * (app-specified) mechanism.
     *
     * @hide
     */
    class JobHandler extends Handler {
        JobHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            final JobParameters params = (JobParameters) msg.obj;
            switch (msg.what) {
                case MSG_EXECUTE_JOB:
                    try {
                        boolean workOngoing = JobService.this.onStartJob(params);
                        ackStartMessage(params, workOngoing);
                    } catch (Exception e) {
                        Log.e(TAG, "Error while executing job: " + params.getJobId());
                        throw new RuntimeException(e);
                    }
                    break;
                case MSG_STOP_JOB:
                    try {
                        boolean ret = JobService.this.onStopJob(params);
                        ackStopMessage(params, ret);
                    } catch (Exception e) {
                        Log.e(TAG, "Application unable to handle onStopJob.", e);
                        throw new RuntimeException(e);
                    }
                    break;
                case MSG_JOB_FINISHED:
                    final boolean needsReschedule = (msg.arg2 == 1);
                    IJobCallback callback = params.getCallback();
                    if (callback != null) {
                        try {
                            callback.jobFinished(params.getJobId(), needsReschedule);
                        } catch (RemoteException e) {
                            Log.e(TAG, "Error reporting job finish to system: binder has gone" +
                                    "away.");
                        }
                    } else {
                        Log.e(TAG, "finishJob() called for a nonexistent job id.");
                    }
                    break;
                default:
                    Log.e(TAG, "Unrecognised message received.");
                    break;
            }
        }

        private void ackStartMessage(JobParameters params, boolean workOngoing) {
            final IJobCallback callback = params.getCallback();
            final int jobId = params.getJobId();
            if (callback != null) {
                try {
                    callback.acknowledgeStartMessage(jobId, workOngoing);
                } catch (RemoteException e) {
                    Log.e(TAG, "System unreachable for starting job.");
                }
            } else {
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "Attempting to ack a job that has already been processed.");
                }
            }
        }

        private void ackStopMessage(JobParameters params, boolean reschedule) {
            final IJobCallback callback = params.getCallback();
            final int jobId = params.getJobId();
            if (callback != null) {
                try {
                    callback.acknowledgeStopMessage(jobId, reschedule);
                } catch (RemoteException e) {
                    Log.e(TAG, "System unreachable for stopping job.");
                }
            } else {
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "Attempting to ack a job that has already been processed.");
                }
            }
        }
    }

    /**
     * @hide
     */
    public final IBinder onBind(Intent intent) {
        return mBinder.asBinder();
    }

    /**
     * Override this method with the callback logic for your job. Any such logic needs to be
     * performed on a separate thread, as this function is executed on your application's main
     * thread.
     *
     * @param params Parameters specifying info about this job, including the extras bundle you
     *               optionally provided at job-creation time.
     * @return True if your service needs to process the work (on a separate thread). False if
     * there's no more work to be done for this job.
     */
    public abstract boolean onStartJob(JobParameters params);

    /**
     * This method is called if the system has determined that you must stop execution of your job
     * even before you've had a chance to call {@link #jobFinished(JobParameters, boolean)}.
     *

     *
This will happen if the requirements specified at schedule time are no longer met. For
     * example you may have requested WiFi with
     * {@link android.app.job.JobInfo.Builder#setRequiredNetworkType(int)}, yet while your
     * job was executing the user toggled WiFi. Another example is if you had specified
     * {@link android.app.job.JobInfo.Builder#setRequiresDeviceIdle(boolean)}, and the phone left its
     * idle maintenance window. You are solely responsible for the behaviour of your application
     * upon receipt of this message; your app will likely start to misbehave if you ignore it. One
     * immediate repercussion is that the system will cease holding a wakelock for you.

     *
     * @param params Parameters specifying info about this job.
     * @return True to indicate to the JobManager whether you'd like to reschedule this job based
     * on the retry criteria provided at job creation-time. False to drop the job. Regardless of
     * the value returned, your job must stop executing.
     */
    public abstract boolean onStopJob(JobParameters params);

    /**
     * Callback to inform the JobManager you've finished executing. This can be called from any
     * thread, as it will ultimately be run on your application's main thread. When the system
     * receives this message it will release the wakelock being held.
     *

     * You can specify post-execution behaviour to the scheduler here with
     * needsReschedule . This will apply a back-off timer to your job based on
     * the default, or what was set with
     * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)}. The original
     * requirements are always honoured even for a backed-off job. Note that a job running in
     * idle mode will not be backed-off. Instead what will happen is the job will be re-added
     * to the queue and re-executed within a future idle maintenance window.
     *

     *
     * @param params          Parameters specifying system-provided info about this job, this was given to
     *                        your application in {@link #onStartJob(JobParameters)}.
     * @param needsReschedule True if this job should be rescheduled according to the back-off
     *                        criteria specified at schedule-time. False otherwise.
     */
    public final void jobFinished(JobParameters params, boolean needsReschedule) {
        ensureHandler();
        Message m = Message.obtain(mHandler, MSG_JOB_FINISHED, params);
        m.arg2 = needsReschedule ? 1 : 0;
        m.sendToTarget();
    }
}


package me.tatarka.support.os;

import android.os.Parcel;
import android.os.Parcelable;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import me.tatarka.support.internal.util.XmlUtils;

/**
 * A mapping from String values to various types that can be saved to persistent and later
 * restored.
 */
public final class PersistableBundle implements Parcelable, Cloneable, XmlUtils.WriteMapCallback {
    private static final String TAG_PERSISTABLEMAP = "pbundle_as_map";
    public static final PersistableBundle EMPTY = new PersistableBundle(PersistableBundleCompat.EMPTY);

    private Object bundle;

    public PersistableBundle() {
        bundle = PersistableBundleCompat.newInstance();
    }

    public PersistableBundle(int capacity) {
        bundle = PersistableBundleCompat.newInstance(capacity);
    }

    public PersistableBundle(PersistableBundle extras) {
        bundle = PersistableBundleCompat.newInstance(extras.bundle);
    }

    /**
     * @hide
     */
    public PersistableBundle(Object extras) {
        bundle = extras;
    }

    /**
     * Constructs a PersistableBundle containing the mappings passed in.
     *
     * @param map a Map containing only those items that can be persisted.
     * @throws IllegalArgumentException if any element of #map cannot be persisted.
     */
    private PersistableBundle(Map<String, Object> map) {
        bundle = PersistableBundleCompat.newInstance();
        putAll(map);
    }

    /**
     * Returns the number of mappings contained in this Bundle.
     *
     * @return the number of mappings as an int.
     */
    public int size() {
        return PersistableBundleCompat.size(bundle);
    }

    /**
     * Returns true if the mapping of this Bundle is empty, false otherwise.
     */
    public boolean isEmpty() {
        return PersistableBundleCompat.isEmpty(bundle);
    }

    /**
     * Removes all elements from the mapping of this Bundle.
     */
    public void clear() {
        PersistableBundleCompat.clear(bundle);
    }

    /**
     * Returns true if the given key is contained in the mapping of this Bundle.
     *
     * @param key a String key
     * @return true if the key is part of the mapping, false otherwise
     */
    public boolean containsKey(String key) {
        return PersistableBundleCompat.containsKey(bundle, key);
    }

    /**
     * Returns the entry with the given key as an object.
     *
     * @param key a String key
     * @return an Object, or null
     */
    public Object get(String key) {
        return PersistableBundleCompat.get(bundle, key);
    }

    /**
     * Removes any entry with the given key from the mapping of this Bundle.
     *
     * @param key a String key
     */
    public void remove(String key) {
        PersistableBundleCompat.remove(bundle, key);
    }

    /**
     * Inserts all mappings from the given PersistableBundle into this BaseBundle.
     *
     * @param bundle a PersistableBundle
     */
    public void putAll(PersistableBundle bundle) {
        PersistableBundleCompat.putAll(this.bundle, bundle.bundle);
    }

    /**
     * Inserts all mappings from the given Map into this BaseBundle.
     *
     * @param map a Map
     */
    void putAll(Map map) {
        PersistableBundleCompat.putAll(bundle, map);
    }

    /**
     * Returns a Set containing the Strings used as keys in this Bundle.
     *
     * @return a Set of String keys
     */
    public Set<String> keySet() {
        return PersistableBundleCompat.keySet(bundle);
    }

    /**
     * Inserts an int value into the mapping of this Bundle, replacing any existing value for the
     * given key.
     *
     * @param key   a String, or null
     * @param value an int, or null
     */
    public void putInt(String key, int value) {
        PersistableBundleCompat.putInt(bundle, key, value);
    }

    /**
     * Inserts a long value into the mapping of this Bundle, replacing any existing value for the
     * given key.
     *
     * @param key   a String, or null
     * @param value a long
     */
    public void putLong(String key, long value) {
        PersistableBundleCompat.putLong(bundle, key, value);
    }

    /**
     * Inserts a double value into the mapping of this Bundle, replacing any existing value for the
     * given key.
     *
     * @param key   a String, or null
     * @param value a double
     */
    public void putDouble(String key, double value) {
        PersistableBundleCompat.putDouble(bundle, key, value);
    }

    /**
     * Inserts a String value into the mapping of this Bundle, replacing any existing value for the
     * given key.  Either key or value may be null.
     *
     * @param key   a String, or null
     * @param value a String, or null
     */
    public void putString(String key, String value) {
        PersistableBundleCompat.putString(bundle, key, value);
    }

    /**
     * Inserts an int array value into the mapping of this Bundle, replacing any existing value for
     * the given key.  Either key or value may be null.
     *
     * @param key   a String, or null
     * @param value an int array object, or null
     */
    public void putIntArray(String key, int[] value) {
        PersistableBundleCompat.putIntArray(bundle, key, value);
    }

    /**
     * Inserts a long array value into the mapping of this Bundle, replacing any existing value for
     * the given key.  Either key or value may be null.
     *
     * @param key   a String, or null
     * @param value a long array object, or null
     */
    public void putLongArray(String key, long[] value) {
        PersistableBundleCompat.putLongArray(bundle, key, value);
    }

    /**
     * Inserts a double array value into the mapping of this Bundle, replacing any existing value
     * for the given key.  Either key or value may be null.
     *
     * @param key   a String, or null
     * @param value a double array object, or null
     */
    public void putDoubleArray(String key, double[] value) {
        PersistableBundleCompat.putDoubleArray(bundle, key, value);
    }

    /**
     * Inserts a String array value into the mapping of this Bundle, replacing any existing value
     * for the given key.  Either key or value may be null.
     *
     * @param key   a String, or null
     * @param value a String array object, or null
     */
    public void putStringArray(String key, String[] value) {
        PersistableBundleCompat.putStringArray(bundle, key, value);
    }

    /**
     * Returns the value associated with the given key, or 0 if no mapping of the desired type
     * exists for the given key.
     *
     * @param key a String
     * @return an int value
     */
    public int getInt(String key) {
        return PersistableBundleCompat.getInt(bundle, key);
    }

    /**
     * Returns the value associated with the given key, or defaultValue if no mapping of the desired
     * type exists for the given key.
     *
     * @param key          a String
     * @param defaultValue Value to return if key does not exist
     * @return an int value
     */
    public int getInt(String key, int defaultValue) {
        return PersistableBundleCompat.getInt(bundle, key, defaultValue);
    }

    /**
     * Returns the value associated with the given key, or 0L if no mapping of the desired type
     * exists for the given key.
     *
     * @param key a String
     * @return a long value
     */
    public long getLong(String key) {
        return PersistableBundleCompat.getLong(bundle, key);
    }

    /**
     * Returns the value associated with the given key, or defaultValue if no mapping of the desired
     * type exists for the given key.
     *
     * @param key          a String
     * @param defaultValue Value to return if key does not exist
     * @return a long value
     */
    public long getLong(String key, long defaultValue) {
        return PersistableBundleCompat.getLong(bundle, key, defaultValue);
    }

    /**
     * Returns the value associated with the given key, or 0.0 if no mapping of the desired type
     * exists for the given key.
     *
     * @param key a String
     * @return a double value
     */
    public double getDouble(String key) {
        return PersistableBundleCompat.getDouble(bundle, key);
    }

    /**
     * Returns the value associated with the given key, or defaultValue if no mapping of the desired
     * type exists for the given key.
     *
     * @param key          a String
     * @param defaultValue Value to return if key does not exist
     * @return a double value
     */
    public double getDouble(String key, double defaultValue) {
        return PersistableBundleCompat.getDouble(bundle, key, defaultValue);
    }

    /**
     * Returns the value associated with the given key, or null if no mapping of the desired type
     * exists for the given key or a null value is explicitly associated with the key.
     *
     * @param key a String, or null
     * @return a String value, or null
     */
    public String getString(String key) {
        return PersistableBundleCompat.getString(bundle, key);
    }

    /**
     * Returns the value associated with the given key, or defaultValue if no mapping of the desired
     * type exists for the given key or if a null value is explicitly associated with the given
     * key.
     *
     * @param key          a String, or null
     * @param defaultValue Value to return if key does not exist or if a null value is associated
     *                     with the given key.
     * @return the String value associated with the given key, or defaultValue if no valid String
     * object is currently mapped to that key.
     */
    public String getString(String key, String defaultValue) {
        return PersistableBundleCompat.getString(bundle, key, defaultValue);
    }

    /**
     * Returns the value associated with the given key, or null if no mapping of the desired type
     * exists for the given key or a null value is explicitly associated with the key.
     *
     * @param key a String, or null
     * @return an int[] value, or null
     */
    public int[] getIntArray(String key) {
        return PersistableBundleCompat.getIntArray(bundle, key);
    }

    /**
     * Returns the value associated with the given key, or null if no mapping of the desired type
     * exists for the given key or a null value is explicitly associated with the key.
     *
     * @param key a String, or null
     * @return a long[] value, or null
     */
    public long[] getLongArray(String key) {
        return PersistableBundleCompat.getLongArray(bundle, key);
    }

    /**
     * Returns the value associated with the given key, or null if no mapping of the desired type
     * exists for the given key or a null value is explicitly associated with the key.
     *
     * @param key a String, or null
     * @return a double[] value, or null
     */
    public double[] getDoubleArray(String key) {
        return PersistableBundleCompat.getDoubleArray(bundle, key);
    }

    /**
     * Returns the value associated with the given key, or null if no mapping of the desired type
     * exists for the given key or a null value is explicitly associated with the key.
     *
     * @param key a String, or null
     * @return a String[] value, or null
     */
    public String[] getStringArray(String key) {
        return PersistableBundleCompat.getStringArray(bundle, key);
    }

    /**
     * Inserts a PersistableBundle value into the mapping of this Bundle, replacing any existing
     * value for the given key.  Either key or value may be null.
     *
     * @param key   a String, or null
     * @param value a Bundle object, or null
     */
    public void putPersistableBundle(String key, PersistableBundle value) {
        PersistableBundleCompat.putPersistableBundle(bundle, key, value.bundle);
    }

    /**
     * Returns the value associated with the given key, or null if no mapping of the desired type
     * exists for the given key or a null value is explicitly associated with the key.
     *
     * @param key a String, or null
     * @return a Bundle value, or null
     */
    public PersistableBundle getPersistableBundle(String key) {
        return new PersistableBundle(PersistableBundleCompat.getPersistableBundle(bundle, key));
    }

    /**
     * Clones the current PersistableBundle. The internal map is cloned, but the keys and values to
     * which it refers are copied by reference.
     */
    @Override
    public Object clone() {
        return new PersistableBundle(this);
    }

    /**
     * Gets the underlying bundle. This is a {@link
     * android.os.PersistableBundle} on api 21+ and {@link android.os.Bundle} on lower apis.
     *
     * @return the underlying bundle
     */
    public Object getRealBundle() {
        return bundle;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        PersistableBundleCompat.write(dest, bundle);
    }

    private PersistableBundle(Parcel in) {
        this.bundle = PersistableBundleCompat.read(in);
    }

    public static final Creator<PersistableBundle> CREATOR = new Creator<PersistableBundle>() {
        public PersistableBundle createFromParcel(Parcel source) {
            return new PersistableBundle(source);
        }

        public PersistableBundle[] newArray(int size) {
            return new PersistableBundle[size];
        }
    };

    @Override
    public String toString() {
        return bundle.toString();
    }

    public static void writePersitableBundle(PersistableBundle bundle, Parcel parcel) {
        PersistableBundleCompat.write(parcel, bundle.bundle);
    }

    public static PersistableBundle readPersistableBundle(Parcel parcel) {
        return new PersistableBundle(PersistableBundleCompat.read(parcel));
    }

    /**
     * @hide
     */
    public static class MyReadMapCallback implements XmlUtils.ReadMapCallback {
        @Override
        public Object readThisUnknownObjectXml(XmlPullParser in, String tag)
                throws XmlPullParserException, IOException {
            if (TAG_PERSISTABLEMAP.equals(tag)) {
                return restoreFromXml(in);
            }
            throw new XmlPullParserException("Unknown tag=" + tag);
        }
    }

    /**
     * @hide
     */
    @Override
    public void writeUnknownObject(Object v, String name, XmlSerializer out) throws XmlPullParserException, IOException {
        if (v instanceof PersistableBundle) {
            out.startTag(null, TAG_PERSISTABLEMAP);
            out.attribute(null, "name", name);
            ((PersistableBundle) v).saveToXml(out);
            out.endTag(null, TAG_PERSISTABLEMAP);
        } else {
            throw new XmlPullParserException("Unknown Object o=" + v);
        }
    }

    /**
     * @hide
     */
    public void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException {
        Map map = new HashMap();
        for (String key : keySet()) {
            map.put(key, get(key));
        }
        XmlUtils.writeMapXml(map, out, this);
    }

    /**
     * @hide
     */
    public static PersistableBundle restoreFromXml(XmlPullParser in) throws IOException,
            XmlPullParserException {
        final int outerDepth = in.getDepth();
        final String startTag = in.getName();
        final String[] tagName = new String[1];
        int event;
        while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
                (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
            if (event == XmlPullParser.START_TAG) {
                return new PersistableBundle((Map<String, Object>)
                        XmlUtils.readThisMapXml(in, startTag, tagName, new MyReadMapCallback()));
            }
        }
        return EMPTY;
    }
}


/*
 * 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 me.tatarka.support.internal.receivers;

import android.content.ComponentName;
import android.os.SystemClock;
import android.text.format.DateUtils;

import java.io.PrintWriter;
import java.util.concurrent.atomic.AtomicBoolean;

import me.tatarka.support.job.JobInfo;
import me.tatarka.support.os.PersistableBundle;

/**
 * Uniquely identifies a job internally.
 * Created from the public {@link android.app.job.JobInfo} object when it lands on the scheduler.
 * Contains current state of the requirements of the job, as well as a function to evaluate
 * whether it's ready to run.
 * This object is shared among the various controllers - hence why the different fields are atomic.
 * This isn't strictly necessary because each controller is only interested in a specific field,
 * and the receivers that are listening for global state change will all run on the main looper,
 * but we don't enforce that so this is safer.
 *
 * @hide
 */
public class JobStatus {
    public static final long NO_LATEST_RUNTIME = Long.MAX_VALUE;
    public static final long NO_EARLIEST_RUNTIME = 0L;

    final JobInfo job;
    final String name;
    final String tag;

    // Constraints.
    public final AtomicBoolean chargingConstraintSatisfied = new AtomicBoolean();
    public final AtomicBoolean timeDelayConstraintSatisfied = new AtomicBoolean();
    public final AtomicBoolean deadlineConstraintSatisfied = new AtomicBoolean();
    public final AtomicBoolean idleConstraintSatisfied = new AtomicBoolean();
    public final AtomicBoolean unmeteredConstraintSatisfied = new AtomicBoolean();
    public final AtomicBoolean connectivityConstraintSatisfied = new AtomicBoolean();

    /**
     * Earliest point in the future at which this job will be eligible to run. A value of 0
     * indicates there is no delay constraint. See {@link #hasTimingDelayConstraint()}.
     */
    private long earliestRunTimeElapsedMillis;
    /**
     * Latest point in the future at which this job must be run. A value of {@link Long#MAX_VALUE}
     * indicates there is no deadline constraint. See {@link #hasDeadlineConstraint()}.
     */
    private long latestRunTimeElapsedMillis;
    /**
     * How many times this job has failed, used to compute back-off.
     */
    private final int numFailures;

    private JobStatus(JobInfo job, int numFailures) {
        this.job = job;
        this.name = job.getService().flattenToShortString();
        this.tag = "*job*/" + this.name;
        this.numFailures = numFailures;
    }

    /**
     * Create a newly scheduled job.
     */
    public JobStatus(JobInfo job) {
        this(job, 0);

        final long elapsedNow = SystemClock.elapsedRealtime();

        if (job.isPeriodic()) {
            earliestRunTimeElapsedMillis = elapsedNow;
            latestRunTimeElapsedMillis = elapsedNow + job.getIntervalMillis();
        } else {
            earliestRunTimeElapsedMillis = job.hasEarlyConstraint() ?
                    elapsedNow + job.getMinLatencyMillis() : NO_EARLIEST_RUNTIME;
            latestRunTimeElapsedMillis = job.hasLateConstraint() ?
                    elapsedNow + job.getMaxExecutionDelayMillis() : NO_LATEST_RUNTIME;
        }
    }

    /**
     * Create a new JobStatus that was loaded from disk. We ignore the provided
     * {@link JobInfo} time criteria because we can load a persisted periodic job
     * from the {@link me.tatarka.support.internal.job.JobStore} and still want to respect its
     * wallclock runtime rather than resetting it on every boot.
     * We consider a freshly loaded job to no longer be in back-off.
     */
    public JobStatus(JobInfo job, long earliestRunTimeElapsedMillis,
                     long latestRunTimeElapsedMillis) {
        this(job, 0);

        this.earliestRunTimeElapsedMillis = earliestRunTimeElapsedMillis;
        this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
    }

    /**
     * Create a new job to be rescheduled with the provided parameters.
     */
    public JobStatus(JobStatus rescheduling, long newEarliestRuntimeElapsedMillis,
                     long newLatestRuntimeElapsedMillis, int backoffAttempt) {
        this(rescheduling.job, backoffAttempt);

        earliestRunTimeElapsedMillis = newEarliestRuntimeElapsedMillis;
        latestRunTimeElapsedMillis = newLatestRuntimeElapsedMillis;
    }

    public JobInfo getJob() {
        return job;
    }

    public int getJobId() {
        return job.getId();
    }

    public int getNumFailures() {
        return numFailures;
    }

    public ComponentName getServiceComponent() {
        return job.getService();
    }

    public String getName() {
        return name;
    }

    public String getTag() {
        return tag;
    }

    public PersistableBundle getExtras() {
        return job.getExtras();
    }

    public boolean hasConnectivityConstraint() {
        return job.getNetworkType() == JobInfo.NETWORK_TYPE_ANY;
    }

    public boolean hasUnmeteredConstraint() {
        return job.getNetworkType() == JobInfo.NETWORK_TYPE_UNMETERED;
    }

    public boolean hasChargingConstraint() {
        return job.isRequireCharging();
    }

    public boolean hasTimingDelayConstraint() {
        return earliestRunTimeElapsedMillis != NO_EARLIEST_RUNTIME;
    }

    public boolean hasDeadlineConstraint() {
        return latestRunTimeElapsedMillis != NO_LATEST_RUNTIME;
    }

    public boolean hasIdleConstraint() {
        return job.isRequireDeviceIdle();
    }

    public boolean isPersisted() {
        return job.isPersisted();
    }

    public long getEarliestRunTime() {
        return earliestRunTimeElapsedMillis;
    }

    public long getLatestRunTimeElapsed() {
        return latestRunTimeElapsedMillis;
    }

    /**
     * @return Whether or not this job is ready to run, based on its requirements. This is true if
     * the constraints are satisfied or the deadline on the job has expired.
     */
    public synchronized boolean isReady() {
        return isConstraintsSatisfied()
                || (hasDeadlineConstraint() && deadlineConstraintSatisfied.get());
    }

    /**
     * @return Whether the constraints set on this job are satisfied.
     */
    public synchronized boolean isConstraintsSatisfied() {
        return (!hasChargingConstraint() || chargingConstraintSatisfied.get())
                && (!hasTimingDelayConstraint() || timeDelayConstraintSatisfied.get())
                && (!hasConnectivityConstraint() || connectivityConstraintSatisfied.get())
                && (!hasUnmeteredConstraint() || unmeteredConstraintSatisfied.get())
                && (!hasIdleConstraint() || idleConstraintSatisfied.get());
    }

    public boolean matches(int jobId) {
        return this.job.getId() == jobId;
    }

    @Override
    public String toString() {
        return String.valueOf(hashCode()).substring(0, 3) + ".."
                + ":[" + job.getService()
                + ",jId=" + job.getId()
                + ",R=(" + formatRunTime(earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME)
                + "," + formatRunTime(latestRunTimeElapsedMillis, NO_LATEST_RUNTIME) + ")"
                + ",N=" + job.getNetworkType() + ",C=" + job.isRequireCharging()
                + ",I=" + job.isRequireDeviceIdle() + ",F=" + numFailures
                + ",P=" + job.isPersisted()
                + (isReady() ? "(READY)" : "")
                + "]";
    }

    private String formatRunTime(long runtime, long defaultValue) {
        if (runtime == defaultValue) {
            return "none";
        } else {
            long elapsedNow = SystemClock.elapsedRealtime();
            long nextRuntime = runtime - elapsedNow;
            if (nextRuntime > 0) {
                return DateUtils.formatElapsedTime(nextRuntime / 1000);
            } else {
                return "-" + DateUtils.formatElapsedTime(nextRuntime / -1000);
            }
        }
    }

    /**
     * Convenience function to identify a job uniquely without pulling all the data that
     * {@link #toString()} returns.
     */
    public String toShortString() {
        return job.getService().flattenToShortString() + " jId=" + job.getId();
    }

    // Dumpsys infrastructure
    public void dump(PrintWriter pw, String prefix) {
        pw.print(prefix);
        pw.println(this.toString());
    }
}


package me.tatarka.support.job;

import android.content.Context;
import android.os.Build;

import java.util.List;

import me.tatarka.support.internal.JobSchedulerCompat;
import me.tatarka.support.internal.JobSchedulerLollipopDelegate;

/**
 * Created by evantatarka on 10/21/14.
 */
public abstract class JobScheduler {
    /**
     * Returned from {@link #schedule(JobInfo)} when an invalid parameter was supplied. This can
     * occur if the run-time for your job is too short, or perhaps the system can't resolve the
     * requisite {@link JobService} in your package.
     */
    public static final int RESULT_FAILURE = 0;

    /**
     * Returned from {@link #schedule(JobInfo)} if this application has made too many requests for
     * work over too short a time.
     */
    // TODO: Determine if this is necessary.
    public static final int RESULT_SUCCESS = 1;

    /**
     * @param job The job you wish scheduled. See {@link JobInfo.Builder JobInfo.Builder} for more
     *            detail on the sorts of jobs you can schedule.
     * @return If >0, this int returns the jobId of the successfully scheduled job. Otherwise you
     * have to compare the return value to the error codes defined in this class.
     */
    public abstract int schedule(JobInfo job);

    /**
     * Cancel a job that is pending in the JobScheduler.
     *
     * @param jobId unique identifier for this job. Obtain this value from the jobs returned by
     *              {@link #getAllPendingJobs()}.
     */
    public abstract void cancel(int jobId);

    /**
     * Cancel all jobs that have been registered with the JobScheduler by this package.
     */
    public abstract void cancelAll();

    /**
     * @return a list of all the jobs registered by this package that have not yet been executed.
     */
    public abstract List<JobInfo> getAllPendingJobs();

    /**
     * Get an instance of a the {@link JobScheduler}, which will delegate to the android one in api
     * 21+ and use a backported version on older apis.
     *
     * @param context the context
     * @return a JobScheduler instance
     */
    public static JobScheduler getInstance(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            return JobSchedulerLollipopDelegate.getLollipopInstance(context);
        } else {
            return JobSchedulerCompat.getCompatInstance(context);
        }
    }
}


package me.tatarka.support.internal.receivers;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.SystemClock;
import android.support.v4.content.WakefulBroadcastReceiver;

import me.tatarka.support.internal.util.ArraySet;
import me.tatarka.support.internal.job.JobServiceCompat;
import me.tatarka.support.internal.job.JobStore;

/**
 * @hide
 */
public class PowerReceiver extends WakefulBroadcastReceiver {
    private static final String ACTION_CHARGING_STABLE =
            "me.tatarka.support.server.job.controllers.PowerReceiver.ACTION_CHARGING_STABLE";
    /** Wait this long after phone is plugged in before doing any work. */
    private static final long STABLE_CHARGING_THRESHOLD_MILLIS = 2 * 60 * 1000; // 2 minutes.
//    private static final long STABLE_CHARGING_THRESHOLD_MILLIS = 1000; // 1 second.

    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        if (Intent.ACTION_BATTERY_LOW.equals(action)) {
            // If we get this action, the battery is discharging => it isn't plugged in so
            // there's no work to cancel. We track this variable for the case where it is
            // charging, but hasn't been for long enough to be healthy.
            ControllerPrefs.getInstance(context).edit().setBatteryLow(true).apply();
        } else if (Intent.ACTION_BATTERY_OKAY.equals(action)) {
            ControllerPrefs.getInstance(context).edit().setBatteryLow(false).apply();
            maybeReportNewChargingState(context, isCharging(context));
        } else if (Intent.ACTION_POWER_CONNECTED.equals(action)) {
            // Set up an alarm for ACTION_CHARGING_STABLE - we don't want to kick off tasks
            // here if the user unplugs the phone immediately.
            setStableChargingAlarm(context);
        } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) {
            // If an alarm is set, breathe a sigh of relief and cancel it - crisis averted.
            cancelStableChargingAlarm(context);
            maybeReportNewChargingState(context, false);
        } else if (ACTION_CHARGING_STABLE.equals(action)) {
            // Here's where we actually do the notify for a task being ready.
            maybeReportNewChargingState(context, !isBatteryLow(context));
        }
    }

    public static void setPowerForJob(Context context, JobStatus job) {
        if (job.hasChargingConstraint()) {
            boolean stablePower = isCharging(context) && !isBatteryLow(context);
            job.chargingConstraintSatisfied.set(stablePower);
        }
    }

    private static void maybeReportNewChargingState(Context context, boolean stablePower) {
        boolean reportChange = false;
        final JobStore jobStore = JobStore.initAndGet(context);
        synchronized (jobStore) {
            ArraySet<JobStatus> jobs = jobStore.getJobs();
            for (int i = 0; i < jobs.size(); i++) {
                JobStatus ts = jobs.valueAt(i);
                boolean previous = ts.chargingConstraintSatisfied.getAndSet(stablePower);
                if (previous != stablePower) {
                    reportChange = true;
                }
            }
        }
        // Let the scheduler know that state has changed. This may or may not result in an
        // execution.
        if (reportChange) {
            startWakefulService(context, JobServiceCompat.maybeRunJobs(context));
        }
    }

    private void setStableChargingAlarm(Context context) {
        AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        Intent intent = new Intent(context, PowerReceiver.class);
        intent.setAction(ACTION_CHARGING_STABLE);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

        final long alarmTriggerElapsed = SystemClock.elapsedRealtime() + STABLE_CHARGING_THRESHOLD_MILLIS;
        am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTriggerElapsed, pendingIntent);
    }

    private void cancelStableChargingAlarm(Context context) {
        Intent intent = new Intent(context, PowerReceiver.class);
        intent.setAction(ACTION_CHARGING_STABLE);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_NO_CREATE);
        if (pendingIntent != null) {
            AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
            am.cancel(pendingIntent);
        }
    }

    private static boolean isCharging(Context context) {
        Intent i = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
        int plugged = i.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
        return plugged == BatteryManager.BATTERY_PLUGGED_AC || plugged == BatteryManager.BATTERY_PLUGGED_USB;
    }

    private static boolean isBatteryLow(Context context) {
        return ControllerPrefs.getInstance(context).isBatteryLow();
    }
}


package com.github.mrengineer13.snackbar;

import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.TextView;

import com.github.mrengineer13.snackbar.SnackBar.OnVisibilityChangeListener;

import java.util.LinkedList;
import java.util.Queue;

class SnackContainer extends FrameLayout {

    private static final int ANIMATION_DURATION = 300;

    private static final String SAVED_MSGS = "SAVED_MSGS";

    private Queue<SnackHolder> mSnacks = new LinkedList<SnackHolder>();

    private AnimationSet mOutAnimationSet;
    private AnimationSet mInAnimationSet;

    private float mPreviousY;

    public SnackContainer(Context context) {
        super(context);
        init();
    }

    public SnackContainer(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    SnackContainer(ViewGroup container) {
        super(container.getContext());

        container.addView(this, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        setVisibility(View.GONE);
        setId(R.id.snackContainer);
        init();
    }

    private void init() {
        mInAnimationSet = new AnimationSet(false);

        TranslateAnimation mSlideInAnimation = new TranslateAnimation(
                TranslateAnimation.RELATIVE_TO_PARENT, 0.0f,
                TranslateAnimation.RELATIVE_TO_PARENT, 0.0f,
                TranslateAnimation.RELATIVE_TO_SELF, 1.0f,
                TranslateAnimation.RELATIVE_TO_SELF, 0.0f);

        AlphaAnimation mFadeInAnimation = new AlphaAnimation(0.0f, 1.0f);

        mInAnimationSet.addAnimation(mSlideInAnimation);
        mInAnimationSet.addAnimation(mFadeInAnimation);

        mOutAnimationSet = new AnimationSet(false);

        TranslateAnimation mSlideOutAnimation = new TranslateAnimation(
                TranslateAnimation.RELATIVE_TO_PARENT, 0.0f,
                TranslateAnimation.RELATIVE_TO_PARENT, 0.0f,
                TranslateAnimation.RELATIVE_TO_SELF, 0.0f,
                TranslateAnimation.RELATIVE_TO_SELF, 1.0f);

        AlphaAnimation mFadeOutAnimation = new AlphaAnimation(1.0f, 0.0f);

        mOutAnimationSet.addAnimation(mSlideOutAnimation);
        mOutAnimationSet.addAnimation(mFadeOutAnimation);

        mOutAnimationSet.setDuration(ANIMATION_DURATION);
        mOutAnimationSet.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                removeAllViews();

                if (!mSnacks.isEmpty()) {
                    sendOnHide(mSnacks.poll());
                }

                if (!isEmpty()) {
                    showSnack(mSnacks.peek());
                } else {
                    setVisibility(View.GONE);
                }
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mInAnimationSet.cancel();
        mOutAnimationSet.cancel();
        removeCallbacks(mHideRunnable);
        mSnacks.clear();
    }

    /*
     * Q Management *
     */

    public boolean isEmpty() {
        return mSnacks.isEmpty();
    }

    public Snack peek() {
        return mSnacks.peek().snack;
    }

    public Snack pollSnack() {
        return mSnacks.poll().snack;
    }

    public void clearSnacks(boolean animate) {
        mSnacks.clear();
        removeCallbacks(mHideRunnable);
        if (animate) mHideRunnable.run();
    }

    /*
     * Showing Logic *
     */

    public boolean isShowing() {
        return !mSnacks.isEmpty();
    }

    public void hide() {
        removeCallbacks(mHideRunnable);
        mHideRunnable.run();
    }

    public void showSnack(Snack snack, View snackView, OnVisibilityChangeListener listener) {
        showSnack(snack, snackView, listener, false);
    }

    public void showSnack(Snack snack, View snackView, OnVisibilityChangeListener listener, boolean immediately) {
        if (snackView.getParent() != null && snackView.getParent() != this) {
            ((ViewGroup) snackView.getParent()).removeView(snackView);
        }

        SnackHolder holder = new SnackHolder(snack, snackView, listener);
        mSnacks.offer(holder);
        if (mSnacks.size() == 1) showSnack(holder, immediately);
    }

    private void showSnack(final SnackHolder holder) {
        showSnack(holder, false);
    }

    private void showSnack(final SnackHolder holder, boolean showImmediately) {

        setVisibility(View.VISIBLE);

        sendOnShow(holder);

        addView(holder.snackView);
        holder.messageView.setText(holder.snack.mMessage);
        if (holder.snack.mActionMessage != null) {
            holder.button.setVisibility(View.VISIBLE);
            holder.button.setText(holder.snack.mActionMessage);
            holder.button.setCompoundDrawablesWithIntrinsicBounds(holder.snack.mActionIcon, 0, 0, 0);
        } else {
            holder.button.setVisibility(View.GONE);
        }

        holder.button.setTypeface(holder.snack.mTypeface);
        holder.messageView.setTypeface(holder.snack.mTypeface);

        holder.button.setTextColor(holder.snack.mBtnTextColor);
        holder.snackView.setBackgroundColor(holder.snack.mBackgroundColor.getDefaultColor());
        if(holder.snack.mHeight > 0)
            holder.snackView.getLayoutParams().height = this.getPxFromDp(holder.snack.mHeight);

        if (showImmediately) {
            mInAnimationSet.setDuration(0);
        } else {
            mInAnimationSet.setDuration(ANIMATION_DURATION);
        }
        startAnimation(mInAnimationSet);

        if (holder.snack.mDuration > 0) {
            postDelayed(mHideRunnable, holder.snack.mDuration);
        }

        holder.snackView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                float y = event.getY();

                switch (event.getAction()) {
                    case MotionEvent.ACTION_MOVE:
                        int[] location = new int[2];
                        holder.snackView.getLocationInWindow(location);
                        if (y > mPreviousY) {
                            float dy = y - mPreviousY;
                            holder.snackView.offsetTopAndBottom(Math.round(4 * dy));

                            if ((getResources().getDisplayMetrics().heightPixels - location[1]) - 100 <= 0) {
                                removeCallbacks(mHideRunnable);
                                sendOnHide(holder);
                                startAnimation(mOutAnimationSet);

                                if (!mSnacks.isEmpty()) {
                                    mSnacks.clear();
                                }
                            }
                        }
                }

                mPreviousY = y;

                return true;
            }
        });
    }

    private void sendOnHide(SnackHolder snackHolder) {
        if (snackHolder.visListener != null) {
            snackHolder.visListener.onHide(mSnacks.size());
        }
    }

    private void sendOnShow(SnackHolder snackHolder) {
        if (snackHolder.visListener != null) {
            snackHolder.visListener.onShow(mSnacks.size());
        }
    }

    /*
     * Runnable stuff
     */

    private final Runnable mHideRunnable = new Runnable() {
        @Override
        public void run() {
            if (View.VISIBLE == getVisibility()) {
                startAnimation(mOutAnimationSet);
            }
        }
    };

    /*
     * Restoration *
     */

    public void restoreState(Bundle state, View v) {
        Parcelable[] messages = state.getParcelableArray(SAVED_MSGS);
        boolean showImmediately = true;

        for (Parcelable message : messages) {
            showSnack((Snack) message, v, null, showImmediately);
            showImmediately = false;
        }
    }

    public Bundle saveState() {
        Bundle outState = new Bundle();

        final int count = mSnacks.size();
        final Snack[] snacks = new Snack[count];
        int i = 0;
        for (SnackHolder holder : mSnacks) {
            snacks[i++] = holder.snack;
        }

        outState.putParcelableArray(SAVED_MSGS, snacks);
        return outState;
    }

    private static class SnackHolder {
        final View snackView;
        final TextView messageView;
        final TextView button;

        final Snack snack;
        final OnVisibilityChangeListener visListener;

        private SnackHolder(Snack snack, View snackView, OnVisibilityChangeListener listener) {
            this.snackView = snackView;
            button = (TextView) snackView.findViewById(R.id.snackButton);
            messageView = (TextView) snackView.findViewById(R.id.snackMessage);

            this.snack = snack;
            visListener = listener;
        }
    }

    /*
     * Helpers
     */
    private int getPxFromDp(int dp) {
        Resources rs = getResources();
        int pxConverter = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, rs.getDisplayMetrics());
        int px = pxConverter * dp;
        return px;
    }

}


package com.github.mrengineer13.snackbar;

import android.content.res.ColorStateList;
import android.graphics.Typeface;
import android.os.Parcel;
import android.os.Parcelable;

class Snack implements Parcelable {

    final String mMessage;

    final String mActionMessage;

    final int mActionIcon;

    final Parcelable mToken;

    final short mDuration;

    final ColorStateList mBtnTextColor;

    final ColorStateList mBackgroundColor;

    final int mHeight;

    Typeface mTypeface;

    Snack(String message, String actionMessage, int actionIcon,
                 Parcelable token, short duration, ColorStateList textColor,
                 ColorStateList backgroundColor, int height, Typeface typeFace) {

        mMessage = message;
        mActionMessage = actionMessage;
        mActionIcon = actionIcon;
        mToken = token;
        mDuration = duration;
        mBtnTextColor = textColor;
        mBackgroundColor = backgroundColor;
        mHeight = height;
        mTypeface = typeFace;
    }
    // reads data from parcel
    Snack(Parcel p) {
        mMessage = p.readString();
        mActionMessage = p.readString();
        mActionIcon = p.readInt();
        mToken = p.readParcelable(p.getClass().getClassLoader());
        mDuration = (short) p.readInt();
        mBtnTextColor = p.readParcelable(p.getClass().getClassLoader());
        mBackgroundColor = p.readParcelable(p.getClass().getClassLoader());
        mHeight = p.readInt();
        mTypeface = (Typeface) p.readValue(p.getClass().getClassLoader());
    }

    // writes data to parcel
    public void writeToParcel(Parcel out, int flags) {
        out.writeString(mMessage);
        out.writeString(mActionMessage);
        out.writeInt(mActionIcon);
        out.writeParcelable(mToken, 0);
        out.writeInt((int) mDuration);
        out.writeParcelable(mBtnTextColor, 0);
        out.writeParcelable(mBackgroundColor, 0);
        out.writeInt(mHeight);
        out.writeValue(mTypeface);
    }

    public int describeContents() {
        return 0;
    }

    // creates snack array
    public static final Parcelable.Creator<Snack> CREATOR = new Parcelable.Creator<Snack>() {
        public Snack createFromParcel(Parcel in) {
            return new Snack(in);
        }

        public Snack[] newArray(int size) {
            return new Snack[size];
        }
    };
}


/*
 * Copyright (c) 2014 MrEngineer13
 * 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.github.mrengineer13.snackbar;

import android.app.Activity;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Typeface;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class SnackBar {

    public static final short LONG_SNACK = 5000;

    public static final short MED_SNACK = 3500;

    public static final short SHORT_SNACK = 2000;

    public static final short PERMANENT_SNACK = 0;

    private SnackContainer mSnackContainer;

    private View mParentView;

    private OnMessageClickListener mClickListener;

    private OnVisibilityChangeListener mVisibilityChangeListener;

    public interface OnMessageClickListener {

        void onMessageClick(Parcelable token);
    }

    public interface OnVisibilityChangeListener {

        /**
         * Gets called when a message is shown
         *
         * @param stackSize the number of messages left to show
         */
        void onShow(int stackSize);

        /**
         * Gets called when a message is hidden
         *
         * @param stackSize the number of messages left to show
         */
        void onHide(int stackSize);
    }

    public SnackBar(Activity activity) {
        ViewGroup container = (ViewGroup) activity.findViewById(android.R.id.content);
        View v = activity.getLayoutInflater().inflate(R.layout.sb__snack, container, false);
        init(container, v);
    }

    public SnackBar(Context context, View v) {
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.sb__snack_container, ((ViewGroup) v));
        View snackLayout = inflater.inflate(R.layout.sb__snack, ((ViewGroup) v), false);
        init((ViewGroup) v, snackLayout);
    }

    private void init(ViewGroup container, View v) {
        mSnackContainer = (SnackContainer) container.findViewById(R.id.snackContainer);
        if (mSnackContainer == null) {
            mSnackContainer = new SnackContainer(container);
        }

        mParentView = v;
        TextView snackBtn = (TextView) v.findViewById(R.id.snackButton);
        snackBtn.setOnClickListener(mButtonListener);
    }

    public static class Builder {

        private SnackBar mSnackBar;

        private Context mContext;
        private String mMessage;
        private String mActionMessage;
        private int mActionIcon = 0;
        private Parcelable mToken;
        private short mDuration = MED_SNACK;
        private ColorStateList mTextColor;
        private ColorStateList mBackgroundColor;
        private int mHeight;
        private boolean mClear;
        private boolean mAnimateClear;
        private Typeface mTypeFace;

        /**
         * Constructs a new SnackBar
         *
         * @param activity the activity to inflate into
         */
        public Builder(Activity activity) {
            mContext = activity.getApplicationContext();
            mSnackBar = new SnackBar(activity);
        }

        /**
         * Constructs a new SnackBar
         *
         * @param context the context used to obtain resources
         * @param v       the view to inflate the SnackBar into
         */
        public Builder(Context context, View v) {
            mContext = context;
            mSnackBar = new SnackBar(context, v);
        }

        /**
         * Sets the message to display on the SnackBar
         *
         * @param message the literal string to display
         * @return this builder
         */
        public Builder withMessage(String message) {
            mMessage = message;
            return this;
        }

        /**
         * Sets the message to display on the SnackBar
         *
         * @param messageId the resource id of the string to display
         * @return this builder
         */
        public Builder withMessageId(int messageId) {
            mMessage = mContext.getString(messageId);
            return this;
        }

        /**
         * Sets the message to display as the action message
         *
         * @param actionMessage the literal string to display
         * @return this builder
         */
        public Builder withActionMessage(String actionMessage) {
            mActionMessage = actionMessage;
            return this;
        }

        /**
         * Sets the message to display as the action message
         *
         * @param actionMessageResId the resource id of the string to display
         * @return this builder
         */
        public Builder withActionMessageId(int actionMessageResId) {
            if (actionMessageResId > 0) {
                mActionMessage = mContext.getString(actionMessageResId);
            }

            return this;
        }

        /**
         * Sets the action icon
         *
         * @param id the resource id of the icon to display
         * @return this builder
         */
        public Builder withActionIconId(int id) {
            mActionIcon = id;
            return this;
        }

        /**
         * Sets the {@link com.github.mrengineer13.snackbar.SnackBar.Style} for the action message
         *
         * @param style the {@link com.github.mrengineer13.snackbar.SnackBar.Style} to use
         * @return this builder
         */
        public Builder withStyle(Style style) {
            mTextColor = getActionTextColor(style);
            return this;
        }

        /**
         * The token used to restore the SnackBar state
         *
         * @param token the parcelable containing the saved SnackBar
         * @return this builder
         */
        public Builder withToken(Parcelable token) {
            mToken = token;
            return this;
        }

        /**
         * Sets the duration to show the message
         *
         * @param duration the number of milliseconds to show the message
         * @return this builder
         */
        public Builder withDuration(Short duration) {
            mDuration = duration;
            return this;
        }

        /**
         * Sets the {@link android.content.res.ColorStateList} for the action message
         *
         * @param colorId the
         * @return this builder
         */
        public Builder withTextColorId(int colorId) {
            ColorStateList color = mContext.getResources().getColorStateList(colorId);
            mTextColor = color;
            return this;
        }

        /**
         * Sets the {@link android.content.res.ColorStateList} for the SnackBar background
         *
         * @param colorId the SnackBar Background color
         * @return this builder
         */
        public Builder withBackgroundColorId(int colorId) {
            ColorStateList color = mContext.getResources().getColorStateList(colorId);
            mBackgroundColor = color;
            return this;
        }

        /**
         * Sets the height for SnackBar
         *
         * @param height the height of SnackBar
         * @return this builder
         */
        public Builder withSnackBarHeight(int height) {
            mHeight = height;
            return this;
        }

        /**
         * Sets the OnClickListener for the action button
         *
         * @param onClickListener the listener to inform of click events
         * @return this builder
         */
        public Builder withOnClickListener(OnMessageClickListener onClickListener) {
            mSnackBar.setOnClickListener(onClickListener);
            return this;
        }

        /**
         * Sets the visibilityChangeListener for the SnackBar
         *
         * @param visibilityChangeListener the listener to inform of visibility changes
         * @return this builder
         */
        public Builder withVisibilityChangeListener(OnVisibilityChangeListener visibilityChangeListener) {
            mSnackBar.setOnVisibilityChangeListener(visibilityChangeListener);
            return this;
        }

        /**
         * Clears all of the queued SnackBars, animates the message being hidden
         *
         * @return this builder
         */
        public Builder withClearQueued() {
            return withClearQueued(true);
        }

        /**
         * Clears all of the queued SnackBars
         *
         * @param animate whether or not to animate the messages being hidden
         * @return this builder
         */
        public Builder withClearQueued(boolean animate) {
            mAnimateClear = animate;
            mClear = true;
            return this;
        }

        /**
         * Sets the Typeface for the SnackBar
         *
         * @param typeFace the typeface to apply to the SnackBar
         * @return this builder
         */
        public Builder withTypeFace(Typeface typeFace) {
            mTypeFace = typeFace;
            return this;
        }

        /**
         * Shows the first message in the SnackBar
         *
         * @return the SnackBar
         */
        public SnackBar show() {
            Snack message = new Snack(mMessage,
                    (mActionMessage != null ? mActionMessage.toUpperCase() : null),
                    mActionIcon,
                    mToken,
                    mDuration,
                    mTextColor != null ? mTextColor : getActionTextColor(Style.DEFAULT),
                    mBackgroundColor != null ? mBackgroundColor : mContext.getResources().getColorStateList(R.color.sb__snack_bkgnd),
                    mHeight != 0 ? mHeight : 0,
                    mTypeFace);

            if (mClear) {
                mSnackBar.clear(mAnimateClear);
            }

            mSnackBar.showMessage(message);

            return mSnackBar;
        }

        private ColorStateList getActionTextColor(Style style) {
            switch (style) {
                case ALERT:
                    return mContext.getResources().getColorStateList(R.color.sb__button_text_color_red);
                case INFO:
                    return mContext.getResources().getColorStateList(R.color.sb__button_text_color_yellow);
                case CONFIRM:
                    return mContext.getResources().getColorStateList(R.color.sb__button_text_color_green);
                case DEFAULT:
                    return mContext.getResources().getColorStateList(R.color.sb__default_button_text_color);
                default:
                    return mContext.getResources().getColorStateList(R.color.sb__default_button_text_color);
            }
        }
    }

    private void showMessage(Snack message) {
        mSnackContainer.showSnack(message, mParentView, mVisibilityChangeListener);
    }

    /**
     * Calculates the height of the SnackBar
     *
     * @return the height of the SnackBar
     */
    public int getHeight() {
        mParentView.measure(View.MeasureSpec.makeMeasureSpec(mParentView.getWidth(), View.MeasureSpec.EXACTLY),
                View.MeasureSpec.makeMeasureSpec(mParentView.getHeight(), View.MeasureSpec.AT_MOST));
        return mParentView.getMeasuredHeight();
    }

    /**
     * Getter for the SnackBars parent view
     *
     * @return the parent view
     */
    public View getContainerView() {
        return mParentView;
    }

    private final View.OnClickListener mButtonListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (mClickListener != null && mSnackContainer.isShowing()) {
                mClickListener.onMessageClick(mSnackContainer.peek().mToken);
            }
            mSnackContainer.hide();
        }
    };

    private SnackBar setOnClickListener(OnMessageClickListener listener) {
        mClickListener = listener;
        return this;
    }

    private SnackBar setOnVisibilityChangeListener(OnVisibilityChangeListener listener) {
        mVisibilityChangeListener = listener;
        return this;
    }

    /**
     * Clears all of the queued messages
     *
     * @param animate whether or not to animate the messages being hidden
     */
    public void clear(boolean animate) {
        mSnackContainer.clearSnacks(animate);
    }

    /**
     * Clears all of the queued messages
     */
    public void clear() {
        clear(true);
    }

    /**
     * Hides all snacks
     */
    public void hide() {
        mSnackContainer.hide();
        clear();
    }

    /**
     * All snacks will be restored using the view from this Snackbar
     */
    public void onRestoreInstanceState(Bundle state) {
        mSnackContainer.restoreState(state, mParentView);
    }

    public Bundle onSaveInstanceState() {
        return mSnackContainer.saveState();
    }

    public enum Style {
        DEFAULT,
        ALERT,
        CONFIRM,
        INFO
    }
}