Merge "CTS tests for android.app.job" into lmp-dev
diff --git a/CtsTestCaseList.mk b/CtsTestCaseList.mk
index a31014d..721b9d4 100644
--- a/CtsTestCaseList.mk
+++ b/CtsTestCaseList.mk
@@ -122,6 +122,7 @@
     CtsGraphicsTestCases \
     CtsGraphics2TestCases \
     CtsHardwareTestCases \
+    CtsJobSchedulerDeviceTestCases \
     CtsJniTestCases \
     CtsKeystoreTestCases \
     CtsLocationTestCases \
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index bba42ad..fc899b1 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -48,6 +48,7 @@
     <uses-permission android:name="android.permission.READ_PHONE_STATE" />
     <uses-permission android:name="android.permission.READ_CONTACTS"/>
     <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
 
     <!-- Needed by the Audio Quality Verifier to store the sound samples that will be mailed. -->
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@@ -1272,6 +1273,33 @@
             </intent-filter>
         </receiver>
 
+        <activity android:name=".jobscheduler.IdleConstraintTestActivity" android:label="@string/js_idle_test">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.cts.intent.category.MANUAL_TEST" />
+            </intent-filter>
+            <meta-data android:name="test_category" android:value="@string/test_category_jobscheduler" />
+        </activity>
+
+        <activity android:name=".jobscheduler.ChargingConstraintTestActivity" android:label="@string/js_charging_test">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.cts.intent.category.MANUAL_TEST" />
+            </intent-filter>
+            <meta-data android:name="test_category" android:value="@string/test_category_jobscheduler" />
+        </activity>
+
+        <activity android:name=".jobscheduler.ConnectivityConstraintTestActivity" android:label="@string/js_connectivity_test">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.cts.intent.category.MANUAL_TEST" />
+            </intent-filter>
+            <meta-data android:name="test_category" android:value="@string/test_category_jobscheduler" />
+        </activity>
+
+        <service android:name=".jobscheduler.MockJobService"
+            android:permission="android.permission.BIND_JOB_SERVICE"/>
+
     </application>
 
 </manifest>
diff --git a/apps/CtsVerifier/res/layout/js_charging.xml b/apps/CtsVerifier/res/layout/js_charging.xml
new file mode 100644
index 0000000..4c0e552
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/js_charging.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical" android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/js_test_description"
+        android:layout_margin="@dimen/js_padding"/>
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/js_padding"
+        android:text="@string/js_charging_description_1"
+        android:textStyle="bold"/>
+    <Button
+        android:id="@+id/js_charging_start_test_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:text="@string/js_start_test_text"
+        android:onClick="startTest"
+        android:enabled="false"/>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/js_padding"
+        android:layout_marginBottom="@dimen/js_padding">
+        <ImageView
+            android:id="@+id/charging_off_test_image"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/fs_indeterminate"
+            android:layout_marginRight="@dimen/js_padding"/>
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/js_charging_off_test"
+            android:textSize="16dp"/>
+    </LinearLayout>
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/js_padding"
+        android:text="@string/js_charging_description_2"
+        android:textStyle="bold"/>
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/js_padding"
+        android:layout_marginBottom="@dimen/js_padding">
+        <ImageView
+            android:id="@+id/charging_on_test_image"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/fs_indeterminate"
+            android:layout_marginRight="@dimen/js_padding"/>
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/js_charging_on_test"
+            android:textSize="16dp"/>
+    </LinearLayout>
+    <include layout="@layout/pass_fail_buttons" />
+</LinearLayout>
\ No newline at end of file
diff --git a/apps/CtsVerifier/res/layout/js_connectivity.xml b/apps/CtsVerifier/res/layout/js_connectivity.xml
new file mode 100644
index 0000000..5208c18
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/js_connectivity.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical" android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/js_test_description"
+        android:layout_margin="@dimen/js_padding"/>
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/js_connectivity_description_1"
+        android:layout_margin="@dimen/js_padding"
+        android:textStyle="bold"/>
+
+    <Button
+        android:id="@+id/js_connectivity_start_test_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:text="@string/js_start_test_text"
+        android:onClick="startTest"
+        android:enabled="false"/>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/js_padding"
+        android:layout_marginBottom="@dimen/js_padding">
+        <ImageView
+            android:id="@+id/connectivity_off_test_unmetered_image"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/fs_indeterminate"
+            android:layout_marginRight="@dimen/js_padding"/>
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/js_unmetered_connectivity_test"
+            android:textSize="16dp"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/js_padding"
+        android:layout_marginBottom="@dimen/js_padding">
+        <ImageView
+            android:id="@+id/connectivity_off_test_any_connectivity_image"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/fs_indeterminate"
+            android:layout_marginRight="@dimen/js_padding"/>
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/js_any_connectivity_test"
+            android:textSize="16dp"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/js_padding"
+        android:layout_marginBottom="@dimen/js_padding">
+        <ImageView
+            android:id="@+id/connectivity_off_test_no_connectivity_image"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/fs_indeterminate"
+            android:layout_marginRight="@dimen/js_padding"/>
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/js_no_connectivity_test"
+            android:textSize="16dp"/>
+    </LinearLayout>
+
+    <include layout="@layout/pass_fail_buttons" />
+</LinearLayout>
\ No newline at end of file
diff --git a/apps/CtsVerifier/res/layout/js_idle.xml b/apps/CtsVerifier/res/layout/js_idle.xml
new file mode 100644
index 0000000..90e55ec
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/js_idle.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical" android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/js_test_description"
+        android:layout_margin="@dimen/js_padding"/>
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/js_idle_description_1"
+        android:layout_margin="@dimen/js_padding"
+        android:textStyle="bold"/>
+
+    <Button
+        android:id="@+id/js_idle_start_test_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:text="@string/js_start_test_text"
+        android:onClick="startTest"
+        android:enabled="false"/>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/js_padding"
+        android:layout_marginBottom="@dimen/js_padding">
+        <ImageView
+            android:id="@+id/idle_off_test_image"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/fs_indeterminate"
+            android:layout_marginRight="@dimen/js_padding"/>
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/js_idle_item_idle_off"
+            android:textSize="16dp"/>
+    </LinearLayout>
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/js_padding"
+        android:layout_marginBottom="@dimen/js_padding">
+        <ImageView
+            android:id="@+id/idle_on_test_image"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/fs_indeterminate"
+            android:layout_marginRight="@dimen/js_padding"/>
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/js_idle_item_idle_on"
+            android:textSize="16dp"/>
+    </LinearLayout>
+    <include layout="@layout/pass_fail_buttons" />
+</LinearLayout>
\ No newline at end of file
diff --git a/apps/CtsVerifier/res/values/dimens.xml b/apps/CtsVerifier/res/values/dimens.xml
index b1367f7..8df5b35 100644
--- a/apps/CtsVerifier/res/values/dimens.xml
+++ b/apps/CtsVerifier/res/values/dimens.xml
@@ -36,4 +36,6 @@
     <dimen name="snsr_view_padding_left">8dp</dimen>
     <dimen name="snsr_view_padding_right">8dp</dimen>
 
+    <dimen name="js_padding">10dp</dimen>
+
 </resources>
\ No newline at end of file
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index 2a236d1..558a86d 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -35,6 +35,7 @@
     <string name="test_category_streaming">Streaming</string>
     <string name="test_category_features">Features</string>
     <string name="test_category_deskclock">Clock</string>
+    <string name="test_category_jobscheduler">Job Scheduler</string>
     <string name="test_category_other">Other</string>
     <string name="clear">Clear</string>
     <string name="test_results_cleared">Test results cleared.</string>
@@ -1264,4 +1265,28 @@
     <string name="device_owner_negative_test">Device owner negative test</string>
     <string name="device_owner_negative_test_info">Device owner provisioning should only work on new or factory reset devices. Please click on the "Start provisioning" button and verify that you get a warning dialog telling you that the device is already set up. If that is the case, this test has passed.</string>
     <string name="start_device_owner_provisioning_button">Start provisioning</string>
+
+    <!-- Strings for JobScheduler Tests -->
+    <string name="js_test_description">This test is mostly automated, but requires some user interaction. You can pass this test once the list items below are checked.</string>
+
+    <string name="js_idle_test">Idle Mode Constraints</string>
+    <string name="js_start_test_text">Start test</string>
+    <string name="js_idle_instructions">Verify the behaviour of the JobScheduler API for when the device is in idle mode. Simply follow the on-screen instructions.</string>
+    <string name="js_idle_description_1">Turn the screen off and then back on in order to begin.</string>
+    <string name="js_idle_item_idle_off">Idle job does not execute when device is not idle.</string>
+    <string name="js_idle_item_idle_on">Idle job does execute when device is forced into idle.</string>
+
+    <string name="js_charging_test">Charging Constraints</string>
+    <string name="js_charging_instructions">Verify the behaviour of the JobScheduler API for when the device is on power and unplugged from power. Simply follow the on-screen instructions.</string>
+    <string name="js_charging_description_1">Unplug the phone in order to begin.</string>
+    <string name="js_charging_off_test">Device not charging will not execute a job with a charging constraint.</string>
+    <string name="js_charging_on_test">Device when charging will execute a job with a charging constraint.</string>
+    <string name="js_charging_description_2">After the above test has passed, plug the device back in to continue. If the above failed, you can simply fail this test.</string>
+
+    <string name="js_connectivity_test">Connectivity Constraints</string>
+    <string name="js_connectivity_instructions">Verify the behaviour of the JobScheduler API for when the device has no access to data connectivity. Simply follow the on-screen instructions.</string>
+    <string name="js_connectivity_description_1">Disable WiFi and Cellular data to begin.</string>
+    <string name="js_unmetered_connectivity_test">Device with no connectivity will not execute a job with an unmetered connectivity constraint.</string>
+    <string name="js_any_connectivity_test">Device with no connectivity will not execute a job with an unmetered connectivity constraint.</string>
+    <string name="js_no_connectivity_test">Device with no connectivity will still execute a job with no connectivity constraints.</string>
 </resources>
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ChargingConstraintTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ChargingConstraintTestActivity.java
new file mode 100644
index 0000000..2a94ace
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ChargingConstraintTestActivity.java
@@ -0,0 +1,184 @@
+package com.android.cts.verifier.jobscheduler;
+
+import android.annotation.TargetApi;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.widget.Button;
+import android.widget.ImageView;
+
+import com.android.cts.verifier.R;
+
+/**
+ *  This activity runs the following tests:
+ *     - Ask the tester to unplug the phone, and verify that jobs with charging constraints will
+ *      not run.
+ *     - Ask the tester to ensure the phone is plugged in, and verify that jobs with charging
+ *      constraints are run.
+ */
+@TargetApi(21)
+public class ChargingConstraintTestActivity extends ConstraintTestActivity {
+
+    private static final int ON_CHARGING_JOB_ID =
+            ChargingConstraintTestActivity.class.hashCode() + 0;
+    private static final int OFF_CHARGING_JOB_ID =
+            ChargingConstraintTestActivity.class.hashCode() + 1;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Set up the UI.
+        setContentView(R.layout.js_charging);
+        setPassFailButtonClickListeners();
+        setInfoResources(R.string.js_charging_test, R.string.js_charging_instructions, -1);
+        mStartButton = (Button) findViewById(R.id.js_charging_start_test_button);
+
+        mJobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
+
+        // Register receiver for connected/disconnected power events.
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_POWER_CONNECTED);
+        intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);
+
+        registerReceiver(mChargingChangedReceiver, intentFilter);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        unregisterReceiver(mChargingChangedReceiver);
+    }
+
+    @Override
+    public void startTestImpl() {
+        new TestDeviceUnpluggedConstraint().execute();
+    }
+
+    private BroadcastReceiver mChargingChangedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_POWER_DISCONNECTED.equals(intent.getAction())) {
+                mDeviceUnpluggedTestPassed = false;
+                mStartButton.setEnabled(true);
+            } else if (Intent.ACTION_POWER_CONNECTED.equals(intent.getAction())) {
+                mStartButton.setEnabled(false);
+                if (mDeviceUnpluggedTestPassed) {
+                    continueTest();
+                }
+            }
+        }
+    };
+
+    /** Simple state boolean we use to determine whether to continue with the second test. */
+    private boolean mDeviceUnpluggedTestPassed = false;
+
+    /**
+     * After the first test has passed, and preconditions are met, this will kick off the second
+     * test.
+     * See {@link #startTest(android.view.View)}.
+     */
+    private void continueTest() {
+        new TestDevicePluggedInConstraint().execute();
+    }
+
+    /**
+     * Test blocks and can't be run on the main thread.
+     */
+    private void testChargingConstraintFails_notCharging() {
+        mTestEnvironment.setUp();
+
+        mTestEnvironment.setExpectedExecutions(0);
+        JobInfo runOnCharge = new JobInfo.Builder(OFF_CHARGING_JOB_ID, mMockComponent)
+                .setRequiresCharging(true)
+                .build();
+        mJobScheduler.schedule(runOnCharge);
+
+        // Send intent to kick off any jobs. This will be a no-op as the device is not plugged in;
+        // the JobScheduler tracks charging state independently.
+        sendBroadcastAndBlockForResult(EXPEDITE_STABLE_CHARGING);
+
+        boolean testPassed;
+        try {
+            testPassed = mTestEnvironment.awaitTimeout();
+        } catch (InterruptedException e) {
+            testPassed = false;
+        }
+        mDeviceUnpluggedTestPassed = testPassed;
+        runOnUiThread(new ChargingConstraintTestResultRunner(OFF_CHARGING_JOB_ID, testPassed));
+    }
+
+    /**
+     * Test blocks and can't be run on the main thread.
+     */
+    private void testChargingConstraintExecutes_onCharging() {
+        mTestEnvironment.setUp();
+
+        JobInfo delayConstraintAndUnexpiredDeadline =
+                new JobInfo.Builder(ON_CHARGING_JOB_ID, mMockComponent)
+                        .setRequiresCharging(true)
+                        .build();
+
+        mTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(delayConstraintAndUnexpiredDeadline);
+
+        // Force the JobScheduler to consider any jobs that have charging constraints.
+        sendBroadcast(EXPEDITE_STABLE_CHARGING);
+
+        boolean testPassed;
+        try {
+            testPassed = mTestEnvironment.awaitExecution();
+        } catch (InterruptedException e) {
+            testPassed = false;
+        }
+        runOnUiThread(new ChargingConstraintTestResultRunner(ON_CHARGING_JOB_ID, testPassed));
+    }
+
+    /** Run test for when the <bold>device is not connected to power.</bold>. */
+    private class TestDeviceUnpluggedConstraint extends AsyncTask<Void, Void, Void> {
+        @Override
+        protected Void doInBackground(Void... voids) {
+            testChargingConstraintFails_notCharging();
+
+            // Do not call notifyTestCompleted here, as we're still waiting for the user to put
+            // the device back on charge to continue with TestDevicePluggedInConstraint.
+            return null;
+        }
+    }
+
+    /** Run test for when the <bold>device is connected to power.</bold> */
+    private class TestDevicePluggedInConstraint extends AsyncTask<Void, Void, Void> {
+        @Override
+        protected Void doInBackground(Void... voids) {
+            testChargingConstraintExecutes_onCharging();
+
+            notifyTestCompleted();
+            return null;
+        }
+    }
+
+    private class ChargingConstraintTestResultRunner extends TestResultRunner {
+        ChargingConstraintTestResultRunner(int jobId, boolean testPassed) {
+            super(jobId, testPassed);
+        }
+
+        @Override
+        public void run() {
+            ImageView view;
+            if (mJobId == OFF_CHARGING_JOB_ID) {
+                view = (ImageView) findViewById(R.id.charging_off_test_image);
+            } else if (mJobId == ON_CHARGING_JOB_ID) {
+                view = (ImageView) findViewById(R.id.charging_on_test_image);
+            } else {
+                noteInvalidTest();
+                return;
+            }
+            view.setImageResource(mTestPassed ? R.drawable.fs_good : R.drawable.fs_error);
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ConnectivityConstraintTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ConnectivityConstraintTestActivity.java
new file mode 100644
index 0000000..e97539d
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ConnectivityConstraintTestActivity.java
@@ -0,0 +1,184 @@
+package com.android.cts.verifier.jobscheduler;
+
+import android.annotation.TargetApi;
+import android.app.job.JobInfo;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Button;
+import android.widget.ImageView;
+
+import com.android.cts.verifier.R;
+
+/**
+ * The majority of the connectivity constraints are done in the device-side test app
+ * android.jobscheduler.cts.deviceside. However a manual tester is required to completely turn off
+ * connectivity on the device in order to verify that jobs with connectivity constraints will not
+ * run in the absence of an internet connection.
+ */
+@TargetApi(21)
+public class ConnectivityConstraintTestActivity extends ConstraintTestActivity {
+    private static final String TAG = "ConnectivityConstraintTestActivity";
+    private static final int ANY_CONNECTIVITY_JOB_ID =
+            ConnectivityConstraintTestActivity.class.hashCode() + 0;
+    private static final int UNMETERED_CONNECTIVITY_JOB_ID =
+            ConnectivityConstraintTestActivity.class.hashCode() + 1;
+    private static final int NO_CONNECTIVITY_JOB_ID =
+            ConnectivityConstraintTestActivity.class.hashCode() + 2;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Set up the UI.
+        setContentView(R.layout.js_connectivity);
+        setPassFailButtonClickListeners();
+        setInfoResources(R.string.js_connectivity_test, R.string.js_connectivity_instructions, -1);
+        mStartButton = (Button) findViewById(R.id.js_connectivity_start_test_button);
+
+        // Disable test start if there is data connectivity.
+        mStartButton.setEnabled(isDataUnavailable());
+        // Register receiver to listen for connectivity changes.
+        IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+        registerReceiver(mConnectivityChangedReceiver, intentFilter);
+
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        unregisterReceiver(mConnectivityChangedReceiver);
+    }
+
+    @Override
+    protected void startTestImpl() {
+        new TestConnectivityConstraint().execute();
+    }
+
+    /** Ensure that there's no connectivity before we allow the test to start. */
+    BroadcastReceiver mConnectivityChangedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Log.d(TAG, "received: " + intent);
+            String extras = "";
+            for (String name : intent.getExtras().keySet()) {
+                extras += " |" + name + " " + intent.getExtras().get(name) + "|";
+
+            }
+            Log.d(TAG, "extras: " + extras);
+            if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
+                // Only enable the test when we know there is no connectivity.
+                mStartButton.setEnabled(isDataUnavailable());
+            }
+        }
+    };
+
+    private void testUnmeteredConstraintFails_noConnectivity() {
+        testConnectivityConstraintFailsImpl(
+                JobInfo.NETWORK_TYPE_UNMETERED, UNMETERED_CONNECTIVITY_JOB_ID);
+    }
+
+    private void testAnyConnectivityConstraintFails_noConnectivity() {
+        testConnectivityConstraintFailsImpl(JobInfo.NETWORK_TYPE_ANY, ANY_CONNECTIVITY_JOB_ID);
+    }
+
+    private void testNoConnectivityConstraintExecutes_noConnectivity() {
+        JobInfo testJob = new JobInfo.Builder(NO_CONNECTIVITY_JOB_ID, mMockComponent)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_NONE)
+                .setOverrideDeadline(100000L)  // Will not expire.
+                .build();
+
+        mTestEnvironment.setUp();
+        mTestEnvironment.setExpectedExecutions(1);
+
+        mJobScheduler.schedule(testJob);
+
+        // Send intent to kick off ready jobs that the JobScheduler might be lazily holding on to.
+        sendBroadcastAndBlockForResult(EXPEDITE_STABLE_CHARGING);
+
+        boolean testPassed;
+        try {
+            testPassed = mTestEnvironment.awaitExecution();
+        } catch (InterruptedException e) {
+            testPassed = false;
+        }
+        runOnUiThread(
+                new ConnectivityConstraintTestResultRunner(NO_CONNECTIVITY_JOB_ID, testPassed));
+    }
+
+    private void testConnectivityConstraintFailsImpl(int requiredNetworkType, int jobId) {
+        // Use arguments provided to construct job with required connectivity constraint.
+        JobInfo testJob = new JobInfo.Builder(jobId, mMockComponent)
+                .setRequiredNetworkType(requiredNetworkType)
+                .build();
+
+        mTestEnvironment.setUp();
+        mTestEnvironment.setExpectedExecutions(0);
+
+        mJobScheduler.schedule(testJob);
+
+        // Send intent to kick off ready jobs that the JobScheduler might be lazily holding on to.
+        sendBroadcastAndBlockForResult(EXPEDITE_STABLE_CHARGING);
+
+        boolean testPassed;
+        try {
+            testPassed = mTestEnvironment.awaitTimeout();
+        } catch (InterruptedException e) {
+            testPassed = false;
+        }
+        runOnUiThread(
+                new ConnectivityConstraintTestResultRunner(jobId, testPassed));
+    }
+
+    /** Query the active network connection and return if there is no data connection. */
+    private boolean isDataUnavailable() {
+        final ConnectivityManager cm =
+                (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+        final NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
+
+        return (activeNetwork == null) ||
+                !activeNetwork.isConnectedOrConnecting();
+    }
+
+    private class TestConnectivityConstraint extends AsyncTask<Void, Void, Void> {
+
+        @Override
+        protected Void doInBackground(Void... voids) {
+            testUnmeteredConstraintFails_noConnectivity();
+            testAnyConnectivityConstraintFails_noConnectivity();
+            testNoConnectivityConstraintExecutes_noConnectivity();
+
+            notifyTestCompleted();
+            return null;
+        }
+    }
+
+    private class ConnectivityConstraintTestResultRunner extends TestResultRunner {
+        ConnectivityConstraintTestResultRunner(int jobId, boolean testPassed) {
+            super(jobId, testPassed);
+        }
+
+        @Override
+        public void run() {
+            ImageView view;
+            if (mJobId == ANY_CONNECTIVITY_JOB_ID) {
+                view = (ImageView) findViewById(R.id.connectivity_off_test_any_connectivity_image);
+            } else if (mJobId == UNMETERED_CONNECTIVITY_JOB_ID) {
+                view = (ImageView) findViewById(R.id.connectivity_off_test_unmetered_image);
+            } else if (mJobId == NO_CONNECTIVITY_JOB_ID) {
+                view = (ImageView) findViewById(R.id.connectivity_off_test_no_connectivity_image);
+            } else {
+                noteInvalidTest();
+                return;
+            }
+            view.setImageResource(mTestPassed ? R.drawable.fs_good : R.drawable.fs_error);
+        }
+    }
+
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ConstraintTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ConstraintTestActivity.java
new file mode 100644
index 0000000..da0862a
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ConstraintTestActivity.java
@@ -0,0 +1,115 @@
+package com.android.cts.verifier.jobscheduler;
+
+import android.annotation.TargetApi;
+import android.app.job.JobScheduler;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.Toast;
+
+import com.android.cts.verifier.PassFailButtons;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@TargetApi(21)
+public abstract class ConstraintTestActivity extends PassFailButtons.Activity {
+    /**
+     * Intent we use to force the job scheduler to consider any ready jobs that otherwise it may
+     * have decided to be lazy about.
+     */
+    protected static final Intent EXPEDITE_STABLE_CHARGING =
+            new Intent("com.android.server.task.controllers.BatteryController.ACTION_CHARGING_STABLE");
+
+    protected ComponentName mMockComponent;
+
+    protected MockJobService.TestEnvironment mTestEnvironment;
+    protected JobScheduler mJobScheduler;
+
+    /** Avoid cases where user might press "start test" more than once. */
+    private boolean mTestInProgress;
+    /**
+     * Starts the test - set up by subclass, which also controls the logic for how/when the test
+     * can be started.
+     */
+    protected Button mStartButton;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mJobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
+        mMockComponent = new ComponentName(this, MockJobService.class);
+        mTestEnvironment = MockJobService.TestEnvironment.getTestEnvironment();
+    }
+
+    /** OnClickListener for the "Start Test" ({@link #mStartButton}) button */
+    public final void startTest(View v) {
+        if (mTestInProgress) {
+            Toast toast =
+                    Toast.makeText(
+                            ConstraintTestActivity.this,
+                            "Test already in progress",
+                            Toast.LENGTH_SHORT);
+            toast.show();
+            return;
+        } else {
+            mTestInProgress = true;
+            startTestImpl();
+        }
+    }
+
+    /** Called by subclasses to allow the user to rerun the test if necessary. */
+    protected final void notifyTestCompleted() {
+        mTestInProgress = false;
+    }
+
+    /** Implemented by subclasses to determine logic for running the test. */
+    protected abstract void startTestImpl();
+
+    /**
+     * Broadcast the provided intent, and register a receiver to notify us after the broadcast has
+     * been processed.
+     * This function will block until the broadcast comes back, and <bold>cannot</bold> be called
+     * on the main thread.
+     * @return True if we received the callback, false if not.
+     */
+    protected boolean sendBroadcastAndBlockForResult(Intent intent) {
+        final CountDownLatch latch = new CountDownLatch(1);
+        sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                latch.countDown();
+            }
+        }, null, -1, null, null);
+        try {
+            return latch.await(5, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            return false;
+        }
+    }
+
+    /** Extended by test activities to report results of a test. */
+    protected abstract class TestResultRunner implements Runnable {
+        final int mJobId;
+        final boolean mTestPassed;
+
+        TestResultRunner(int jobId, boolean testPassed) {
+            mJobId = jobId;
+            mTestPassed = testPassed;
+        }
+        protected void noteInvalidTest() {
+            final Toast toast =
+                    Toast.makeText(
+                            ConstraintTestActivity.this,
+                            "Invalid result returned from test thread: job=" + mJobId + ", res="
+                                    + mTestPassed,
+                            Toast.LENGTH_SHORT);
+            toast.show();
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/IdleConstraintTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/IdleConstraintTestActivity.java
new file mode 100644
index 0000000..a8bd993
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/IdleConstraintTestActivity.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier.jobscheduler;
+
+import com.android.cts.verifier.R;
+
+import android.annotation.TargetApi;
+import android.app.job.JobInfo;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Button;
+import android.widget.ImageView;
+
+/**
+ *  Idle constraints:
+ *      The framework doesn't support turning idle mode off. Use the manual tester to ensure that
+ *      the device is not in idle mode (by turning the screen off and then back on) before running
+ *      the tests.
+ */
+@TargetApi(21)
+public class IdleConstraintTestActivity extends ConstraintTestActivity {
+    private static final String TAG = "IdleModeTestActivity";
+    /**
+     * It takes >1hr for idle mode to be triggered. We'll use this secret broadcast to force the
+     * scheduler into idle. It's not a protected broadcast so that's alright.
+     */
+    private static final String ACTION_EXPEDITE_IDLE_MODE =
+            "com.android.server.task.controllers.IdleController.ACTION_TRIGGER_IDLE";
+
+    /**
+     * Id for the job that we schedule when the device is not in idle mode. This job is expected
+     * to not execute. Executing means that the verifier test should fail.
+     */
+    private static final int IDLE_OFF_JOB_ID = IdleConstraintTestActivity.class.hashCode() + 0;
+    /**
+     * Id for the job that we schedule when the device *is* in idle mode. This job is expected to
+     * execute. Not executing means that the verifier test should fail.
+     */
+    private static final int IDLE_ON_JOB_ID = IdleConstraintTestActivity.class.hashCode() + 1;
+
+    /**
+     * Listens for idle mode off/on events, namely {@link #ACTION_EXPEDITE_IDLE_MODE} and
+     * {@link Intent#ACTION_SCREEN_ON}.
+     * On ACTION_EXPEDITE_IDLE_MODE, we will disable the {@link #mStartButton}, and on
+     * ACTION_SCREEN_ON we enable it. This is to avoid the start button being clicked when the
+     * device is in idle mode.
+     */
+    private BroadcastReceiver mIdleChangedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
+                mStartButton.setEnabled(true);
+            } else if (ACTION_EXPEDITE_IDLE_MODE.equals(intent.getAction())) {
+                mStartButton.setEnabled(false);
+            } else {
+                Log.e(TAG, "Invalid broadcast received, was expecting SCREEN_ON");
+            }
+        }
+    };
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Set up the UI.
+        setContentView(R.layout.js_idle);
+        setPassFailButtonClickListeners();
+        setInfoResources(R.string.js_idle_test, R.string.js_idle_instructions, -1);
+        mStartButton = (Button) findViewById(R.id.js_idle_start_test_button);
+
+        // Register receiver for idle off/on events.
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_SCREEN_ON);
+        intentFilter.addAction(ACTION_EXPEDITE_IDLE_MODE);
+
+        registerReceiver(mIdleChangedReceiver, intentFilter);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        unregisterReceiver(mIdleChangedReceiver);
+    }
+
+    @Override
+    protected void startTestImpl() {
+        new TestIdleModeTask().execute();
+    }
+
+    /** Background task that will run the actual test. */
+    private class TestIdleModeTask extends AsyncTask<Void, Void, Void> {
+
+        @Override
+        protected Void doInBackground(Void... voids) {
+            testIdleConstraintFails_notIdle();
+
+
+            // Send the {@link #ACTION_EXPEDITE_IDLE_MODE} broadcast as an ordered broadcast, this
+            // function will block until all receivers have processed the broadcast.
+            if (!sendBroadcastAndBlockForResult(new Intent(ACTION_EXPEDITE_IDLE_MODE))) {
+                // Fail the test if the broadcast wasn't processed.
+                runOnUiThread(new IdleTestResultRunner(IDLE_ON_JOB_ID, false));
+            }
+
+            testIdleConstraintExecutes_onIdle();
+
+            notifyTestCompleted();
+            return null;
+        }
+
+    }
+
+    /**
+     * The user has just pressed the "Start Test" button, so we know that the device can't be idle.
+     * Schedule a job with an idle constraint and verify that it doesn't execute.
+     */
+    private void testIdleConstraintFails_notIdle() {
+        mTestEnvironment.setUp();
+        mJobScheduler.cancelAll();
+
+        mTestEnvironment.setExpectedExecutions(0);
+
+        mJobScheduler.schedule(
+                new JobInfo.Builder(IDLE_OFF_JOB_ID, mMockComponent)
+                        .setRequiresDeviceIdle(true)
+                        .build());
+
+        boolean testPassed;
+        try {
+            testPassed = mTestEnvironment.awaitTimeout();
+        } catch (InterruptedException e) {
+            // We'll just indicate that it failed, not why.
+            testPassed = false;
+        }
+        runOnUiThread(new IdleTestResultRunner(IDLE_OFF_JOB_ID, testPassed));
+    }
+
+    private void testIdleConstraintExecutes_onIdle() {
+        mTestEnvironment.setUp();
+        mJobScheduler.cancelAll();
+
+        mTestEnvironment.setExpectedExecutions(1);
+
+        mJobScheduler.schedule(
+                new JobInfo.Builder(IDLE_ON_JOB_ID, mMockComponent)
+                .setRequiresDeviceIdle(true)
+                .build());
+
+        boolean testPassed;
+        try {
+            testPassed = mTestEnvironment.awaitExecution();
+        } catch (InterruptedException e) {
+            // We'll just indicate that it failed, not why.
+            testPassed = false;
+        }
+        runOnUiThread(new IdleTestResultRunner(IDLE_ON_JOB_ID, testPassed));
+    }
+
+    /**
+     * Runnable to update the UI with the outcome of the test. This class only runs two tests, so
+     * the argument passed into the constructor will indicate which of the tests we are reporting
+     * for.
+     */
+    protected class IdleTestResultRunner extends TestResultRunner {
+
+        IdleTestResultRunner(int jobId, boolean testPassed) {
+            super(jobId, testPassed);
+        }
+
+        @Override
+        public void run() {
+            ImageView view;
+            if (mJobId == IDLE_OFF_JOB_ID) {
+                view = (ImageView) findViewById(R.id.idle_off_test_image);
+            } else if (mJobId == IDLE_ON_JOB_ID) {
+                view = (ImageView) findViewById(R.id.idle_on_test_image);
+            } else {
+                noteInvalidTest();
+                return;
+            }
+            view.setImageResource(mTestPassed ? R.drawable.fs_good : R.drawable.fs_error);
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/MockJobService.java b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/MockJobService.java
new file mode 100644
index 0000000..9595a6a
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/MockJobService.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier.jobscheduler;
+
+import android.annotation.TargetApi;
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Handles callback from the framework {@link android.app.job.JobScheduler}. The behaviour of this
+ * class is configured through the static
+ * {@link TestEnvironment}.
+ */
+@TargetApi(21)
+public class MockJobService extends JobService {
+    private static final String TAG = "MockJobService";
+
+    /** Wait this long before timing out the test. */
+    private static final long DEFAULT_TIMEOUT_MILLIS = 5000L; // 5 seconds.
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        Log.e(TAG, "Created test service.");
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        Log.i(TAG, "Test job executing: " + params.getJobId());
+
+        TestEnvironment.getTestEnvironment().notifyExecution(params.getJobId());
+        return false;  // No work to do.
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        return false;
+    }
+
+    /**
+     * Configures the expected behaviour for each test. This object is shared across consecutive
+     * tests, so to clear state each test is responsible for calling
+     * {@link TestEnvironment#setUp()}.
+     */
+    public static final class TestEnvironment {
+
+        private static TestEnvironment kTestEnvironment;
+        public static final int INVALID_JOB_ID = -1;
+
+        private CountDownLatch mLatch;
+        private int mExecutedJobId;
+
+        public static TestEnvironment getTestEnvironment() {
+            if (kTestEnvironment == null) {
+                kTestEnvironment = new TestEnvironment();
+            }
+            return kTestEnvironment;
+        }
+
+        /**
+         * Block the test thread, waiting on the JobScheduler to execute some previously scheduled
+         * job on this service.
+         */
+        public boolean awaitExecution() throws InterruptedException {
+            final boolean executed = mLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+            return executed;
+        }
+
+        /**
+         * Block the test thread, expecting to timeout but still listening to ensure that no jobs
+         * land in the interim.
+         * @return True if the latch timed out waiting on an execution.
+         */
+        public boolean awaitTimeout() throws InterruptedException {
+            return !mLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+        }
+
+        private void notifyExecution(int jobId) {
+            Log.d(TAG, "Job executed:" + jobId);
+            mExecutedJobId = jobId;
+            mLatch.countDown();
+        }
+
+        public void setExpectedExecutions(int numExecutions) {
+            // For no executions expected, set count to 1 so we can still block for the timeout.
+            if (numExecutions == 0) {
+                mLatch = new CountDownLatch(1);
+            } else {
+                mLatch = new CountDownLatch(numExecutions);
+            }
+        }
+
+        /** Called in each testCase#setup */
+        public void setUp() {
+            mLatch = null;
+            mExecutedJobId = INVALID_JOB_ID;
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/tests/JobScheduler/Android.mk b/tests/JobScheduler/Android.mk
new file mode 100755
index 0000000..499abde
--- /dev/null
+++ b/tests/JobScheduler/Android.mk
@@ -0,0 +1,34 @@
+# 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# Don't include this package in any target.
+LOCAL_MODULE_TAGS := optional
+
+# When built, explicitly put it in the data partition.
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := ctsdeviceutil ctstestrunner
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+# Must match the package name in CtsTestCaseList.mk
+LOCAL_PACKAGE_NAME := CtsJobSchedulerDeviceTestCases
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_CTS_PACKAGE)
diff --git a/tests/JobScheduler/AndroidManifest.xml b/tests/JobScheduler/AndroidManifest.xml
new file mode 100755
index 0000000..17cf399
--- /dev/null
+++ b/tests/JobScheduler/AndroidManifest.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.jobscheduler.cts.deviceside">
+
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+
+        <service android:name="android.jobscheduler.MockJobService"
+            android:permission="android.permission.BIND_JOB_SERVICE" />
+    </application>
+
+    <!--  self-instrumenting test package. -->
+    <instrumentation
+        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:label="JobScheduler device-side tests"
+        android:targetPackage="android.jobscheduler.cts.deviceside" >
+        <meta-data
+            android:name="listener"
+            android:value="com.android.cts.runner.CtsTestRunListener" />
+    </instrumentation>
+</manifest>
+
diff --git a/tests/JobScheduler/src/android/jobscheduler/MockJobService.java b/tests/JobScheduler/src/android/jobscheduler/MockJobService.java
new file mode 100644
index 0000000..a0177e2
--- /dev/null
+++ b/tests/JobScheduler/src/android/jobscheduler/MockJobService.java
@@ -0,0 +1,118 @@
+/*
+ * 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 android.jobscheduler;
+
+import android.annotation.TargetApi;
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Handles callback from the framework {@link android.app.job.JobScheduler}. The behaviour of this
+ * class is configured through the static
+ * {@link TestEnvironment}.
+ */
+@TargetApi(21)
+public class MockJobService extends JobService {
+    private static final String TAG = "MockJobService";
+
+    /** Wait this long before timing out the test. */
+    private static final long DEFAULT_TIMEOUT_MILLIS = 5000L; // 5 seconds.
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        Log.e(TAG, "Created test service.");
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        Log.i(TAG, "Test job executing: " + params.getJobId());
+
+        TestEnvironment.getTestEnvironment().notifyExecution(params.getJobId());
+        return false;  // No work to do.
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        return false;
+    }
+
+    /**
+     * Configures the expected behaviour for each test. This object is shared across consecutive
+     * tests, so to clear state each test is responsible for calling
+     * {@link TestEnvironment#setUp()}.
+     */
+    public static final class TestEnvironment {
+
+        private static TestEnvironment kTestEnvironment;
+        public static final int INVALID_JOB_ID = -1;
+
+        private CountDownLatch mLatch;
+        private int mExecutedJobId;
+
+        public static TestEnvironment getTestEnvironment() {
+            if (kTestEnvironment == null) {
+                kTestEnvironment = new TestEnvironment();
+            }
+            return kTestEnvironment;
+        }
+
+        /**
+         * Block the test thread, waiting on the JobScheduler to execute some previously scheduled
+         * job on this service.
+         */
+        public boolean awaitExecution() throws InterruptedException {
+            final boolean executed = mLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+            return executed;
+        }
+
+        /**
+         * Block the test thread, expecting to timeout but still listening to ensure that no jobs
+         * land in the interim.
+         * @return True if the latch timed out waiting on an execution.
+         */
+        public boolean awaitTimeout() throws InterruptedException {
+            return !mLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+        }
+
+        private void notifyExecution(int jobId) {
+            Log.d(TAG, "Job executed:" + jobId);
+            mExecutedJobId = jobId;
+            mLatch.countDown();
+        }
+
+        public void setExpectedExecutions(int numExecutions) {
+            // For no executions expected, set count to 1 so we can still block for the timeout.
+            if (numExecutions == 0) {
+                mLatch = new CountDownLatch(1);
+            } else {
+                mLatch = new CountDownLatch(numExecutions);
+            }
+        }
+
+        /** Called in each testCase#setup */
+        public void setUp() {
+            mLatch = null;
+            mExecutedJobId = INVALID_JOB_ID;
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/ConnectivityConstraintTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/ConnectivityConstraintTest.java
new file mode 100644
index 0000000..a83f7a9
--- /dev/null
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/ConnectivityConstraintTest.java
@@ -0,0 +1,287 @@
+/*
+ * 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 android.jobscheduler.cts;
+
+
+import android.annotation.TargetApi;
+import android.app.job.JobInfo;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiManager;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Schedules jobs with the {@link android.app.job.JobScheduler} that have network connectivity
+ * constraints.
+ * Requires manipulating the {@link android.net.wifi.WifiManager} to ensure an unmetered network.
+ * Similarly, requires that the phone be connected to a wifi hotspot, or else the test will fail.
+ */
+@TargetApi(21)
+public class ConnectivityConstraintTest extends ConstraintTest {
+    private static final String TAG = "ConnectivityConstraintTest";
+
+    /** Unique identifier for the job scheduled by this suite of tests. */
+    public static final int CONNECTIVITY_JOB_ID = ConnectivityConstraintTest.class.hashCode();
+
+    private WifiManager mWifiManager;
+    private ConnectivityManager mCm;
+
+    /** Whether the device running these tests supports WiFi. */
+    private boolean mHasWifi;
+    /** Whether the device running these tests supports telephony. */
+    private boolean mHasTelephony;
+
+    private JobInfo.Builder mBuilder;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mWifiManager = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
+        mCm =
+                (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+
+        PackageManager packageManager = mContext.getPackageManager();
+        mHasWifi = packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI);
+        mHasTelephony = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
+        mBuilder =
+                new JobInfo.Builder(CONNECTIVITY_JOB_ID, kJobServiceComponent);
+    }
+
+    // --------------------------------------------------------------------------------------------
+    // Positives - schedule jobs under conditions that require them to pass.
+    // --------------------------------------------------------------------------------------------
+
+    /**
+     * Schedule a job that requires a WiFi connection, and assert that it executes when the device
+     * is connected to WiFi. This will fail if a wifi connection is unavailable.
+     */
+    public void testUnmeteredConstraintExecutes_withWifi() throws Exception {
+        if (!mHasWifi) {
+            Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
+            return;
+        }
+        connectToWiFi();
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(
+                mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
+                        .build());
+
+        sendExpediteStableChargingBroadcast();
+
+        assertTrue("Job with unmetered constraint did not fire on WiFi.",
+                kTestEnvironment.awaitExecution());
+    }
+
+    /**
+     * Schedule a job with a connectivity constraint, and ensure that it executes on WiFi.
+     */
+    public void testConnectivityConstraintExecutes_withWifi() throws Exception {
+        if (!mHasWifi) {
+            Log.d(TAG, "Skipping test that requires the device be WiFi enabled.");
+            return;
+        }
+        connectToWiFi();
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(
+                mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+                        .build());
+
+        sendExpediteStableChargingBroadcast();
+
+        assertTrue("Job with connectivity constraint did not fire on WiFi.",
+                kTestEnvironment.awaitExecution());
+    }
+
+    /**
+     * Schedule a job with a connectivity constraint, and ensure that it executes on on a mobile
+     * data connection.
+     */
+    public void testConnectivityConstraintExecutes_withMobile() throws Exception {
+        if (!checkDeviceSupportsMobileData()) {
+            return;
+        }
+        disconnectWifiToConnectToMobile();
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(
+                mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+                        .build());
+
+        sendExpediteStableChargingBroadcast();
+
+        assertTrue("Job with connectivity constraint did not fire on mobile.",
+                kTestEnvironment.awaitExecution());
+    }
+
+    // --------------------------------------------------------------------------------------------
+    // Negatives - schedule jobs under conditions that require that they fail.
+    // --------------------------------------------------------------------------------------------
+
+    /**
+     * Schedule a job that requires a WiFi connection, and assert that it fails when the device is
+     * connected to a cellular provider.
+     * This test assumes that if the device supports a mobile data connection, then this connection
+     * will be available.
+     */
+    public void testUnmeteredConstraintFails_withMobile() throws Exception {
+        if (!checkDeviceSupportsMobileData()) {
+            return;
+        }
+        disconnectWifiToConnectToMobile();
+
+        kTestEnvironment.setExpectedExecutions(0);
+        mJobScheduler.schedule(
+                mBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
+                        .build());
+        sendExpediteStableChargingBroadcast();
+
+        assertTrue("Job requiring unmetered connectivity still executed on mobile.",
+                kTestEnvironment.awaitTimeout());
+    }
+
+    /**
+     * Determine whether the device running these CTS tests should be subject to tests involving
+     * mobile data.
+     * @return True if this device will support a mobile data connection.
+     */
+    private boolean checkDeviceSupportsMobileData() {
+        if (!mHasTelephony) {
+            Log.d(TAG, "Skipping test that requires telephony features, not supported by this" +
+                    " device");
+            return false;
+        }
+        if (mCm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE) == null) {
+            Log.d(TAG, "Skipping test that requires ConnectivityManager.TYPE_MOBILE");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Ensure WiFi is enabled, and block until we've verified that we are in fact connected.
+     * Taken from {@link android.net.http.cts.ApacheHttpClientTest}.
+     */
+    private void connectToWiFi() throws InterruptedException {
+        if (!mWifiManager.isWifiEnabled()) {
+            ConnectivityActionReceiver receiver =
+                    new ConnectivityActionReceiver(ConnectivityManager.TYPE_WIFI,
+                            NetworkInfo.State.CONNECTED);
+            IntentFilter filter = new IntentFilter();
+            filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+            mContext.registerReceiver(receiver, filter);
+
+            assertTrue(mWifiManager.setWifiEnabled(true));
+            assertTrue("Wifi must be configured to connect to an access point for this test.",
+                    receiver.waitForStateChange());
+
+            mContext.unregisterReceiver(receiver);
+        }
+    }
+
+    private void disconnectWifiToConnectToMobile() throws InterruptedException {
+        if (mHasWifi && mWifiManager.isWifiEnabled()) {
+            ConnectivityActionReceiver connectMobileReceiver =
+                    new ConnectivityActionReceiver(ConnectivityManager.TYPE_MOBILE,
+                            NetworkInfo.State.CONNECTED);
+            ConnectivityActionReceiver disconnectWifiReceiver =
+                    new ConnectivityActionReceiver(ConnectivityManager.TYPE_WIFI,
+                            NetworkInfo.State.DISCONNECTED);
+            IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+            mContext.registerReceiver(connectMobileReceiver, filter);
+            mContext.registerReceiver(disconnectWifiReceiver, filter);
+
+            assertTrue(mWifiManager.setWifiEnabled(false));
+            assertTrue("Failure disconnecting from WiFi.",
+                    disconnectWifiReceiver.waitForStateChange());
+            assertTrue("Device must have access to a metered network for this test.",
+                    connectMobileReceiver.waitForStateChange());
+
+            mContext.unregisterReceiver(connectMobileReceiver);
+            mContext.unregisterReceiver(disconnectWifiReceiver);
+        }
+    }
+
+    /** Capture the last connectivity change's network type and state. */
+    private class ConnectivityActionReceiver extends BroadcastReceiver {
+
+        private final CountDownLatch mReceiveLatch = new CountDownLatch(1);
+
+        private final int mNetworkType;
+
+        private final NetworkInfo.State mExpectedState;
+
+        ConnectivityActionReceiver(int networkType, NetworkInfo.State expectedState) {
+            mNetworkType = networkType;
+            mExpectedState = expectedState;
+        }
+
+        public void onReceive(Context context, Intent intent) {
+            // Dealing with a connectivity changed event for this network type.
+            final int networkTypeChanged =
+                    intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, -1);
+            if (networkTypeChanged == -1) {
+                Log.e(TAG, "No network type provided in intent");
+                return;
+            }
+
+            if (networkTypeChanged != mNetworkType) {
+                // Only track changes for the connectivity event that we are interested in.
+                return;
+            }
+            // Pull out the NetworkState object that we're interested in. Necessary because
+            // the ConnectivityManager will filter on uid for background connectivity.
+            NetworkInfo[] allNetworkInfo = mCm.getAllNetworkInfo();
+            NetworkInfo networkInfo = null;
+            for (int i=0; i<allNetworkInfo.length; i++) {
+                NetworkInfo ni = allNetworkInfo[i];
+                if (ni.getType() == mNetworkType) {
+                    networkInfo =  ni;
+                    break;
+                }
+            }
+            if (networkInfo == null) {
+                Log.e(TAG, "Could not find correct network type.");
+                return;
+            }
+
+            NetworkInfo.State networkState = networkInfo.getState();
+            Log.i(TAG, "Network type: " + mNetworkType + " State: " + networkState);
+            if (networkState == mExpectedState) {
+                mReceiveLatch.countDown();
+            }
+        }
+
+        public boolean waitForStateChange() throws InterruptedException {
+            return mReceiveLatch.await(30, TimeUnit.SECONDS) || hasExpectedState();
+        }
+
+        private boolean hasExpectedState() {
+            return mExpectedState == mCm.getNetworkInfo(mNetworkType).getState();
+        }
+    }
+
+}
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/ConstraintTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/ConstraintTest.java
new file mode 100644
index 0000000..b9a498f
--- /dev/null
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/ConstraintTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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 android.jobscheduler.cts;
+
+import android.annotation.TargetApi;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.jobscheduler.MockJobService;
+import android.test.AndroidTestCase;
+
+/**
+ * Common functionality from which the other test case classes derive.
+ */
+@TargetApi(21)
+public abstract class ConstraintTest extends AndroidTestCase {
+    /** Force the scheduler to consider the device to be on stable charging. */
+    private static final Intent EXPEDITE_STABLE_CHARGING =
+            new Intent("com.android.server.task.controllers.BatteryController.ACTION_CHARGING_STABLE");
+
+    /** Environment that notifies of JobScheduler callbacks. */
+    static MockJobService.TestEnvironment kTestEnvironment =
+            MockJobService.TestEnvironment.getTestEnvironment();
+    /** Handle for the service which receives the execution callbacks from the JobScheduler. */
+    static ComponentName kJobServiceComponent;
+    JobScheduler mJobScheduler;
+
+    @Override
+    public void setUp() throws Exception {
+        kTestEnvironment.setUp();
+        kJobServiceComponent = new ComponentName(getContext(), MockJobService.class);
+        mJobScheduler = (JobScheduler) getContext().getSystemService(Context.JOB_SCHEDULER_SERVICE);
+        mJobScheduler.cancelAll();
+    }
+
+    /**
+     * The scheduler will usually only flush its queue of unexpired jobs when the device is
+     * considered to be on stable power - that is, plugged in for a period of 2 minutes.
+     * Rather than wait for this to happen, we cheat and send this broadcast instead.
+     */
+    protected void sendExpediteStableChargingBroadcast() {
+        getContext().sendBroadcast(EXPEDITE_STABLE_CHARGING);
+    }
+}
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/TimingConstraintsTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/TimingConstraintsTest.java
new file mode 100644
index 0000000..36f44ef
--- /dev/null
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/TimingConstraintsTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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 android.jobscheduler.cts;
+
+import android.annotation.TargetApi;
+import android.app.job.JobInfo;
+
+/**
+ * Schedules jobs with various timing constraints and ensures that they are executed when
+ * appropriate.
+ */
+@TargetApi(21)
+public class TimingConstraintsTest extends ConstraintTest {
+    private static final int TIMING_JOB_ID = TimingConstraintsTest.class.hashCode() + 0;
+    private static final int CANCEL_JOB_ID = TimingConstraintsTest.class.hashCode() + 1;
+
+    public void testScheduleOnce() throws Exception {
+        JobInfo oneTimeJob = new JobInfo.Builder(TIMING_JOB_ID, kJobServiceComponent)
+                        .setOverrideDeadline(1000)  // 1 secs
+                        .build();
+
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(oneTimeJob);
+        final boolean executed = kTestEnvironment.awaitExecution();
+        assertTrue("Timed out waiting for override deadline.", executed);
+    }
+
+    public void testSchedulePeriodic() throws Exception {
+        JobInfo periodicJob =
+                new JobInfo.Builder(TIMING_JOB_ID, kJobServiceComponent)
+                        .setPeriodic(1000L)  // 1 second period.
+                        .build();
+
+        kTestEnvironment.setExpectedExecutions(3);
+        mJobScheduler.schedule(periodicJob);
+        final boolean countedDown = kTestEnvironment.awaitExecution();
+        assertTrue("Timed out waiting for periodic jobs to execute", countedDown);
+    }
+
+    public void testCancel() throws Exception {
+        JobInfo cancelJob = new JobInfo.Builder(CANCEL_JOB_ID, kJobServiceComponent)
+                .setOverrideDeadline(2000L)
+                .build();
+
+        kTestEnvironment.setExpectedExecutions(0);
+        mJobScheduler.schedule(cancelJob);
+        // Now cancel it.
+        mJobScheduler.cancel(CANCEL_JOB_ID);
+        assertTrue("Cancel failed: job executed when it shouldn't have.",
+                kTestEnvironment.awaitTimeout());
+    }
+}
\ No newline at end of file
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/build/CtsBuildHelper.java b/tools/tradefed-host/src/com/android/cts/tradefed/build/CtsBuildHelper.java
index 61b4b43..0940355 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/build/CtsBuildHelper.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/build/CtsBuildHelper.java
@@ -33,7 +33,7 @@
     private final String mSuiteName = "CTS";
     /** The root location of the extracted CTS package */
     private final File mRootDir;
-    /** the {@link CTS_DIR_NAME} directory */
+    /** the {@link #CTS_DIR_NAME} directory */
     private final File mCtsDir;
 
     /**