Fix for Job Scheduler Charging test case

This test was previously started when the device was plugged in.
The device goes into charging state some time after it is plugged in.
Now, we wait until ACTION_CHARGING is received and then start the test.

Bug: 23882801
Change-Id: I1aa48857efa6b20ecdab4c008d9b8ab627fd7e86
diff --git a/apps/CtsVerifier/res/layout/js_charging.xml b/apps/CtsVerifier/res/layout/js_charging.xml
index 8d9ed1d..2888714 100644
--- a/apps/CtsVerifier/res/layout/js_charging.xml
+++ b/apps/CtsVerifier/res/layout/js_charging.xml
@@ -29,30 +29,29 @@
                 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:id="@+id/js_waiting_for_charging_text_view"
                 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"/>
+                android:text="@string/js_charging_description_3"
+                android:textStyle="bold"
+                android:visibility="gone"/>
+            <com.android.cts.verifier.TimerProgressBar
+                android:id="@+id/js_waiting_for_charging_progress_bar"
+                style="?android:attr/progressBarStyleHorizontal"
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:layout_margin="@dimen/js_padding"
+                android:visibility="gone"/>
+            <TextView
+                android:id="@+id/js_problem_with_charger_text_view"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_margin="@dimen/js_padding"
+                android:text="@string/js_charging_description_4"
+                android:textStyle="bold"
+                android:visibility="gone"/>
             <LinearLayout
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
@@ -70,6 +69,29 @@
                     android:text="@string/js_charging_on_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_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>
             <include layout="@layout/pass_fail_buttons" />
         </LinearLayout>
     </ScrollView>
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index 874f8cd..1650a28 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -1876,10 +1876,12 @@
 
     <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 device in order to begin.</string>
+    <string name="js_charging_description_1">Plug in the charger if it isn\'t already plugged in.</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_charging_description_2">After the above test has passed, remove the charger to continue. If the above failed, you can simply fail this test.</string>
+    <string name="js_charging_description_3">Device is plugged in. Please wait while it get\s into stable charging state.</string>
+    <string name="js_charging_description_4">There seems to be a problem with your charger. Pleasy try again.</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>
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TimerProgressBar.java b/apps/CtsVerifier/src/com/android/cts/verifier/TimerProgressBar.java
new file mode 100644
index 0000000..4e0f61e
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TimerProgressBar.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2015 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;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.widget.ProgressBar;
+
+/**
+ * Can be used to show time outs for events. A progress bar will be displayed to the user.
+ * On calling start, it will start filling up.
+ */
+public class TimerProgressBar extends ProgressBar {
+  public TimerProgressBar(Context context) {
+    super(context);
+    setHandler(context);
+  }
+
+  public TimerProgressBar(Context context, AttributeSet attrs) {
+    super(context, attrs);
+    setHandler(context);
+  }
+
+  public TimerProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
+    super(context, attrs, defStyleAttr);
+    setHandler(context);
+  }
+
+  @TargetApi(21)
+  public TimerProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+    super(context, attrs, defStyleAttr, defStyleRes);
+    setHandler(context);
+  }
+
+  private void setHandler(Context context) {
+    mHandler = new Handler(context.getMainLooper());
+  }
+
+  private Handler mHandler;
+  private TimerExpiredCallback mTimerExpiredCallback;
+  private long mStartTime;
+  private long mDuration;
+  private long mStepSize;
+  private boolean mForceComplete;
+
+  private Runnable mProgressCallback = new Runnable() {
+    @Override
+    public void run() {
+      if (mForceComplete) {
+        TimerProgressBar.this.setProgress(TimerProgressBar.this.getMax());
+        return;
+      }
+
+      long currentTime = SystemClock.elapsedRealtime();
+      int progress = (int) ((currentTime - mStartTime) / mStepSize);
+      progress = Math.min(progress, TimerProgressBar.this.getMax());
+      TimerProgressBar.this.setProgress(progress);
+
+      if (mStartTime + mDuration > currentTime) {
+        mHandler.postDelayed(this, mStepSize);
+      } else {
+        if (mTimerExpiredCallback != null) {
+          mTimerExpiredCallback.onTimerExpired();
+        }
+      }
+    }
+  };
+
+  public void start(long duration, long stepSize) {
+    start(duration, stepSize, null);
+  }
+
+  /**
+   * Start filling up the progress bar.
+   *
+   * @param duration Time in milliseconds the progress bar takes to fill up completely
+   * @param stepSize Time in milliseconds between consecutive updates to progress bar's progress
+   * @param callback Callback that should be executed after the progress bar is filled completely (i.e. timer expires)
+   */
+  public void start(long duration, long stepSize, TimerExpiredCallback callback) {
+    mDuration = duration;
+    mStepSize = stepSize;
+    mStartTime = SystemClock.elapsedRealtime();
+    mForceComplete = false;
+    mTimerExpiredCallback = callback;
+    this.setMax((int) (duration / stepSize));
+    this.setProgress(0);
+    mHandler.post(mProgressCallback);
+  }
+
+  /**
+   * Fill the progress bar completely. Timer expired callback won't be executed.
+   */
+  public void forceComplete() {
+    mForceComplete = true;
+  }
+
+  public interface TimerExpiredCallback {
+    void onTimerExpired();
+  }
+
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ChargingConstraintTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ChargingConstraintTestActivity.java
index 2a94ace..4b70b89 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ChargingConstraintTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ChargingConstraintTestActivity.java
@@ -9,17 +9,21 @@
 import android.content.IntentFilter;
 import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.BatteryManager;
 import android.widget.Button;
 import android.widget.ImageView;
+import android.widget.TextView;
+import android.view.View;
 
 import com.android.cts.verifier.R;
+import com.android.cts.verifier.TimerProgressBar;
 
 /**
  *  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.
+ *     - Ask the tester to unplug the phone, and verify that jobs with charging constraints will
+ *      not run.
  */
 @TargetApi(21)
 public class ChargingConstraintTestActivity extends ConstraintTestActivity {
@@ -29,6 +33,19 @@
     private static final int OFF_CHARGING_JOB_ID =
             ChargingConstraintTestActivity.class.hashCode() + 1;
 
+    // Time in milliseconds to wait after power is connected for the phone
+    // to get into charging mode.
+    private static final long WAIT_FOR_CHARGING_DURATION = 3 * 60 * 1000;
+
+    private static final int STATE_NOT_RUNNING = 0;
+    private static final int STATE_WAITING_TO_START_ON_CHARGING_TEST = 1;
+    private static final int STATE_ON_CHARGING_TEST_PASSED = 2;
+
+    private int mTestState;
+    TimerProgressBar mWaitingForChargingProgressBar;
+    TextView mWaitingForChargingTextView;
+    TextView mProblemWithChargerTextView;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -38,6 +55,20 @@
         setPassFailButtonClickListeners();
         setInfoResources(R.string.js_charging_test, R.string.js_charging_instructions, -1);
         mStartButton = (Button) findViewById(R.id.js_charging_start_test_button);
+        mWaitingForChargingProgressBar = (TimerProgressBar) findViewById(
+            R.id.js_waiting_for_charging_progress_bar);
+        mWaitingForChargingTextView = (TextView) findViewById(
+            R.id.js_waiting_for_charging_text_view);
+        mProblemWithChargerTextView = (TextView) findViewById(
+            R.id.js_problem_with_charger_text_view);
+
+        if (isDevicePluggedIn()){
+            mStartButton.setEnabled(true);
+        }
+
+        hideWaitingForStableChargingViews();
+
+        mTestState = STATE_NOT_RUNNING;
 
         mJobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
 
@@ -45,6 +76,8 @@
         IntentFilter intentFilter = new IntentFilter();
         intentFilter.addAction(Intent.ACTION_POWER_CONNECTED);
         intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);
+        intentFilter.addAction(BatteryManager.ACTION_CHARGING);
+        intentFilter.addAction(BatteryManager.ACTION_DISCHARGING);
 
         registerReceiver(mChargingChangedReceiver, intentFilter);
     }
@@ -55,22 +88,47 @@
         unregisterReceiver(mChargingChangedReceiver);
     }
 
+    private boolean isDevicePluggedIn() {
+        IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+        Intent batteryStatus = registerReceiver(null, ifilter);
+        int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
+        // 0 indicates device is on battery power
+        return status != 0;
+    }
+
     @Override
     public void startTestImpl() {
-        new TestDeviceUnpluggedConstraint().execute();
+        BatteryManager bm = (BatteryManager) getSystemService(BATTERY_SERVICE);
+        if (bm.isCharging()) {
+            new TestDevicePluggedInConstraint().execute();
+        } else if (isDevicePluggedIn()) {
+            mTestState = STATE_WAITING_TO_START_ON_CHARGING_TEST;
+            showWaitingForStableChargingViews();
+        }
     }
 
     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();
+            if (BatteryManager.ACTION_CHARGING.equals(intent.getAction())) {
+                if (mTestState == STATE_WAITING_TO_START_ON_CHARGING_TEST) {
+                    mWaitingForChargingProgressBar.forceComplete();
+                    hideWaitingForStableChargingViews();
+                    new TestDevicePluggedInConstraint().execute();
                 }
+            } else if (BatteryManager.ACTION_DISCHARGING.equals(intent.getAction())) {
+                if (mTestState == STATE_ON_CHARGING_TEST_PASSED) {
+                    new TestDeviceUnpluggedConstraint().execute();
+                }
+            } else if (Intent.ACTION_POWER_CONNECTED.equals(intent.getAction())) {
+                if (mTestState == STATE_WAITING_TO_START_ON_CHARGING_TEST) {
+                    showWaitingForStableChargingViews();
+                } else if (mTestState == STATE_NOT_RUNNING) {
+                    mStartButton.setEnabled(true);
+                }
+                mStartButton.setEnabled(true);
+            } else if (Intent.ACTION_POWER_DISCONNECTED.equals(intent.getAction())) {
+                hideWaitingForStableChargingViews();
             }
         }
     };
@@ -79,15 +137,6 @@
     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() {
@@ -99,17 +148,12 @@
                 .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));
     }
 
@@ -127,15 +171,14 @@
         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;
         }
+
+        mTestState = testPassed ? STATE_ON_CHARGING_TEST_PASSED : STATE_NOT_RUNNING;
         runOnUiThread(new ChargingConstraintTestResultRunner(ON_CHARGING_JOB_ID, testPassed));
     }
 
@@ -144,11 +187,15 @@
         @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.
+            notifyTestCompleted();
             return null;
         }
+
+        @Override
+        protected void onPostExecute(Void res) {
+            mTestState = STATE_NOT_RUNNING;
+            mStartButton.setEnabled(true);
+        }
     }
 
     /** Run test for when the <bold>device is connected to power.</bold> */
@@ -156,8 +203,6 @@
         @Override
         protected Void doInBackground(Void... voids) {
             testChargingConstraintExecutes_onCharging();
-
-            notifyTestCompleted();
             return null;
         }
     }
@@ -181,4 +226,25 @@
             view.setImageResource(mTestPassed ? R.drawable.fs_good : R.drawable.fs_error);
         }
     }
+
+    private void showWaitingForStableChargingViews() {
+        mWaitingForChargingProgressBar.start(WAIT_FOR_CHARGING_DURATION, 1000,
+            new TimerProgressBar.TimerExpiredCallback(){
+                @Override
+                public void onTimerExpired() {
+                    mProblemWithChargerTextView.setVisibility(View.VISIBLE);
+                }
+            }
+        );
+        mWaitingForChargingProgressBar.setVisibility(View.VISIBLE);
+        mWaitingForChargingTextView.setVisibility(View.VISIBLE);
+        mProblemWithChargerTextView.setVisibility(View.GONE);
+    }
+
+    private void hideWaitingForStableChargingViews() {
+        mWaitingForChargingProgressBar.forceComplete();
+        mWaitingForChargingProgressBar.setVisibility(View.GONE);
+        mWaitingForChargingTextView.setVisibility(View.GONE);
+        mProblemWithChargerTextView.setVisibility(View.GONE);
+    }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ConnectivityConstraintTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ConnectivityConstraintTestActivity.java
index 8d10bda..aaf68e6 100755
--- a/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ConnectivityConstraintTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ConnectivityConstraintTestActivity.java
@@ -106,11 +106,6 @@
         mJobScheduler.schedule(testJob1);
         mJobScheduler.schedule(testJob2);
 
-        /*
-        // 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();
@@ -132,9 +127,6 @@
 
         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();
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ConstraintTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ConstraintTestActivity.java
index da0862a..0d8d13f 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ConstraintTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/jobscheduler/ConstraintTestActivity.java
@@ -18,13 +18,7 @@
 
 @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;