diff --git a/build/sdk.atree b/build/sdk.atree index 89761167b..be7097482 100644 --- a/build/sdk.atree +++ b/build/sdk.atree @@ -98,6 +98,8 @@ development/samples/Snake samples/${PLATFORM_NAME}/Snake development/samples/SoftKeyboard samples/${PLATFORM_NAME}/SoftKeyboard development/samples/JetBoy samples/${PLATFORM_NAME}/JetBoy development/samples/SearchableDictionary samples/${PLATFORM_NAME}/SearchableDictionary +development/samples/Spinner samples/${PlATFORM_NAME}/Spinner +development/samples/SpinnerTest samples/${PLATFORM_NAME}/SpinnerTest development/samples/ContactManager samples/${PLATFORM_NAME}/ContactManager development/samples/MultiResolution samples/${PLATFORM_NAME}/MultiResolution development/samples/Wiktionary samples/${PLATFORM_NAME}/Wiktionary diff --git a/samples/Spinner/AndroidManifest.xml b/samples/Spinner/AndroidManifest.xml new file mode 100644 index 000000000..f1accf8fe --- /dev/null +++ b/samples/Spinner/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Spinner/_index.html b/samples/Spinner/_index.html new file mode 100644 index 000000000..2e50d9d88 --- /dev/null +++ b/samples/Spinner/_index.html @@ -0,0 +1,22 @@ +

+This sample is the application under test for the +Activity + Testing Tutorial. It contains a single Activity that displays a +Spinner widget backed by an array of strings containing the names of the planets +in the Solar System. When an entry is selected from the Spinner, the entry is +displayed in a text box. +

+

+An important part of this application is state management. When the application +is first run, the spinner widget displays a default selection of +"Earth". Thereafter, the application saves a selection as soon as it +is made. The application remembers the selection from invocation to invocation, even +if the device reboots. +

+

+For more information about this application, see the Activity Testing Tutorial. +The test application for this application is in the SpinnerTest sample application. +

+ +The Spinner application diff --git a/samples/Spinner/res/layout/main.xml b/samples/Spinner/res/layout/main.xml new file mode 100644 index 000000000..3765b3a69 --- /dev/null +++ b/samples/Spinner/res/layout/main.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + diff --git a/samples/Spinner/res/values/strings.xml b/samples/Spinner/res/values/strings.xml new file mode 100644 index 000000000..ae445f3f3 --- /dev/null +++ b/samples/Spinner/res/values/strings.xml @@ -0,0 +1,37 @@ + + + + + + Spinner + + Mercury + Venus + Earth + Mars + Jupiter + Saturn + Uranus + Neptune + Pluto + + Select a planet + \ No newline at end of file diff --git a/samples/Spinner/src/com/android/example/spinner/SpinnerActivity.java b/samples/Spinner/src/com/android/example/spinner/SpinnerActivity.java new file mode 100644 index 000000000..fa4967c22 --- /dev/null +++ b/samples/Spinner/src/com/android/example/spinner/SpinnerActivity.java @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2010 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.example.spinner; + +import com.android.example.spinner.R; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.AdapterView.OnItemSelectedListener; + +/** + * Displays an Android spinner widget backed by data in an array. The + * array is loaded from the strings.xml resources file. + */ +public class SpinnerActivity extends Activity { + + /** + * Fields to contain the current position and display contents of the spinner + */ + protected int mPos; + protected String mSelection; + + /** + * ArrayAdapter connects the spinner widget to array-based data. + */ + protected ArrayAdapter mAdapter; + + /** + * The initial position of the spinner when it is first installed. + */ + public static final int DEFAULT_POSITION = 2; + + /** + * The name of a properties file that stores the position and + * selection when the activity is not loaded. + */ + public static final String PREFERENCES_FILE = "SpinnerPrefs"; + + /** + * These values are used to read and write the properties file. + * PROPERTY_DELIMITER delimits the key and value in a Java properties file. + * The "marker" strings are used to write the properties into the file + */ + public static final String PROPERTY_DELIMITER = "="; + + /** + * The key or label for "position" in the preferences file + */ + public static final String POSITION_KEY = "Position"; + + /** + * The key or label for "selection" in the preferences file + */ + public static final String SELECTION_KEY = "Selection"; + + public static final String POSITION_MARKER = + POSITION_KEY + PROPERTY_DELIMITER; + + public static final String SELECTION_MARKER = + SELECTION_KEY + PROPERTY_DELIMITER; + + /** + * Initializes the application and the activity. + * 1) Sets the view + * 2) Reads the spinner's backing data from the string resources file + * 3) Instantiates a callback listener for handling selection from the + * spinner + * Notice that this method includes code that can be uncommented to force + * tests to fail. + * + * This method overrides the default onCreate() method for an Activity. + * + * @see android.app.Activity#onCreate(android.os.Bundle) + */ + @Override + public void onCreate(Bundle savedInstanceState) { + + /** + * derived classes that use onCreate() overrides must always call the super constructor + */ + super.onCreate(savedInstanceState); + + setContentView(R.layout.main); + + Spinner spinner = (Spinner) findViewById(R.id.Spinner01); + + /* + * Create a backing mLocalAdapter for the Spinner from a list of the + * planets. The list is defined by XML in the strings.xml file. + */ + + this.mAdapter = ArrayAdapter.createFromResource(this, R.array.Planets, + android.R.layout.simple_spinner_dropdown_item); + + /* + * Attach the mLocalAdapter to the spinner. + */ + + spinner.setAdapter(this.mAdapter); + + /* + * Create a listener that is triggered when Android detects the + * user has selected an item in the Spinner. + */ + + OnItemSelectedListener spinnerListener = new myOnItemSelectedListener(this,this.mAdapter); + + /* + * Attach the listener to the Spinner. + */ + + spinner.setOnItemSelectedListener(spinnerListener); + + + /* + * To demonstrate a failure in the preConditions test, + * uncomment the following line. + * The test will fail because the selection listener for the + * Spinner is not set. + */ + // spinner.setOnItemSelectedListener(null); + + } + + + /** + * A callback listener that implements the + * {@link android.widget.AdapterView.OnItemSelectedListener} interface + * For views based on adapters, this interface defines the methods available + * when the user selects an item from the View. + * + */ + public class myOnItemSelectedListener implements OnItemSelectedListener { + + /* + * provide local instances of the mLocalAdapter and the mLocalContext + */ + + ArrayAdapter mLocalAdapter; + Activity mLocalContext; + + /** + * Constructor + * @param c - The activity that displays the Spinner. + * @param ad - The Adapter view that + * controls the Spinner. + * Instantiate a new listener object. + */ + public myOnItemSelectedListener(Activity c, ArrayAdapter ad) { + + this.mLocalContext = c; + this.mLocalAdapter = ad; + + } + + /** + * When the user selects an item in the spinner, this method is invoked by the callback + * chain. Android calls the item selected listener for the spinner, which invokes the + * onItemSelected method. + * + * @see android.widget.AdapterView.OnItemSelectedListener#onItemSelected( + * android.widget.AdapterView, android.view.View, int, long) + * @param parent - the AdapterView for this listener + * @param v - the View for this listener + * @param pos - the 0-based position of the selection in the mLocalAdapter + * @param row - the 0-based row number of the selection in the View + */ + public void onItemSelected(AdapterView parent, View v, int pos, long row) { + + SpinnerActivity.this.mPos = pos; + SpinnerActivity.this.mSelection = parent.getItemAtPosition(pos).toString(); + /* + * Set the value of the text field in the UI + */ + TextView resultText = (TextView)findViewById(R.id.SpinnerResult); + resultText.setText(SpinnerActivity.this.mSelection); + } + + /** + * The definition of OnItemSelectedListener requires an override + * of onNothingSelected(), even though this implementation does not use it. + * @param parent - The View for this Listener + */ + public void onNothingSelected(AdapterView parent) { + + // do nothing + + } + } + + /** + * Restores the current state of the spinner (which item is selected, and the value + * of that item). + * Since onResume() is always called when an Activity is starting, even if it is re-displaying + * after being hidden, it is the best place to restore state. + * + * Attempts to read the state from a preferences file. If this read fails, + * assume it was just installed, so do an initialization. Regardless, change the + * state of the spinner to be the previous position. + * + * @see android.app.Activity#onResume() + */ + @Override + public void onResume() { + + /* + * an override to onResume() must call the super constructor first. + */ + + super.onResume(); + + /* + * Try to read the preferences file. If not found, set the state to the desired initial + * values. + */ + + if (!readInstanceState(this)) setInitialState(); + + /* + * Set the spinner to the current state. + */ + + Spinner restoreSpinner = (Spinner)findViewById(R.id.Spinner01); + restoreSpinner.setSelection(getSpinnerPosition()); + + } + + /** + * Store the current state of the spinner (which item is selected, and the value of that item). + * Since onPause() is always called when an Activity is about to be hidden, even if it is about + * to be destroyed, it is the best place to save state. + * + * Attempt to write the state to the preferences file. If this fails, notify the user. + * + * @see android.app.Activity#onPause() + */ + @Override + public void onPause() { + + /* + * an override to onPause() must call the super constructor first. + */ + + super.onPause(); + + /* + * Save the state to the preferences file. If it fails, display a Toast, noting the failure. + */ + + if (!writeInstanceState(this)) { + Toast.makeText(this, + "Failed to write state!", Toast.LENGTH_LONG).show(); + } + } + + /** + * Sets the initial state of the spinner when the application is first run. + */ + public void setInitialState() { + + this.mPos = DEFAULT_POSITION; + + } + + /** + * Read the previous state of the spinner from the preferences file + * @param c - The Activity's Context + */ + public boolean readInstanceState(Context c) { + + /* + * The preferences are stored in a SharedPreferences file. The abstract implementation of + * SharedPreferences is a "file" containing a hashmap. All instances of an application + * share the same instance of this file, which means that all instances of an application + * share the same preference settings. + */ + + /* + * Get the SharedPreferences object for this application + */ + + SharedPreferences p = c.getSharedPreferences(PREFERENCES_FILE, MODE_WORLD_READABLE); + /* + * Get the position and value of the spinner from the file, or a default value if the + * key-value pair does not exist. + */ + this.mPos = p.getInt(POSITION_KEY, SpinnerActivity.DEFAULT_POSITION); + this.mSelection = p.getString(SELECTION_KEY, ""); + + /* + * SharedPreferences doesn't fail if the code tries to get a non-existent key. The + * most straightforward way to indicate success is to return the results of a test that + * SharedPreferences contained the position key. + */ + + return (p.contains(POSITION_KEY)); + + } + + /** + * Write the application's current state to a properties repository. + * @param c - The Activity's Context + * + */ + public boolean writeInstanceState(Context c) { + + /* + * Get the SharedPreferences object for this application + */ + + SharedPreferences p = + c.getSharedPreferences(SpinnerActivity.PREFERENCES_FILE, MODE_WORLD_READABLE); + + /* + * Get the editor for this object. The editor interface abstracts the implementation of + * updating the SharedPreferences object. + */ + + SharedPreferences.Editor e = p.edit(); + + /* + * Write the keys and values to the Editor + */ + + e.putInt(POSITION_KEY, this.mPos); + e.putString(SELECTION_KEY, this.mSelection); + + /* + * Commit the changes. Return the result of the commit. The commit fails if Android + * failed to commit the changes to persistent storage. + */ + + return (e.commit()); + + } + + public int getSpinnerPosition() { + return this.mPos; + } + + public void setSpinnerPosition(int pos) { + this.mPos = pos; + } + + public String getSpinnerSelection() { + return this.mSelection; + } + + public void setSpinnerSelection(String selection) { + this.mSelection = selection; + } +} diff --git a/samples/SpinnerTest/AndroidManifest.xml b/samples/SpinnerTest/AndroidManifest.xml new file mode 100644 index 000000000..e151d6e94 --- /dev/null +++ b/samples/SpinnerTest/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + diff --git a/samples/SpinnerTest/_index.html b/samples/SpinnerTest/_index.html new file mode 100644 index 000000000..7a176fd1e --- /dev/null +++ b/samples/SpinnerTest/_index.html @@ -0,0 +1,9 @@ +

+This sample is the test application for the +Activity + Testing Tutorial. Refer to the tutorial text for a full explanation +of this sample code. +

+

+ This application does not have an Android GUI. +

diff --git a/samples/SpinnerTest/res/values/strings.xml b/samples/SpinnerTest/res/values/strings.xml new file mode 100644 index 000000000..9721572ec --- /dev/null +++ b/samples/SpinnerTest/res/values/strings.xml @@ -0,0 +1,22 @@ + + + + + + SpinnerTest + diff --git a/samples/SpinnerTest/src/com/android/example/spinner/test/SpinnerActivityTest.java b/samples/SpinnerTest/src/com/android/example/spinner/test/SpinnerActivityTest.java new file mode 100644 index 000000000..4c233174d --- /dev/null +++ b/samples/SpinnerTest/src/com/android/example/spinner/test/SpinnerActivityTest.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2010 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.example.spinner.test; + +import com.android.example.spinner.SpinnerActivity; + +import android.app.Instrumentation; +import android.test.ActivityInstrumentationTestCase2; +import android.test.UiThreadTest; +import android.view.KeyEvent; +import android.widget.Spinner; +import android.widget.SpinnerAdapter; +import android.widget.TextView; + +/** + * Tests the example application Spinner. Uses the instrumentation test class + * {@link ActivityInstrumentationTestCase2} as its base class. The tests include + *
    + *
  1. test initial conditions
  2. + *
  3. test the UI
  4. + *
  5. state management - preserving state after the app is shut down and restarted, preserving + * state after the app is hidden (paused) and re-displayed (resumed)
  6. + *
+ * + * Demonstrates the use of JUnit setUp() and assert() methods. + */ +public class SpinnerActivityTest extends ActivityInstrumentationTestCase2 { + + // Number of items in the spinner's backing mLocalAdapter + + public static final int ADAPTER_COUNT = 9; + + // The location of Saturn in the backing mLocalAdapter array (0-based) + + public static final int TEST_POSITION = 5; + + // Set the initial position of the spinner to zero + + public static final int INITIAL_POSITION = 0; + + // The initial position corresponds to Mercury + + public static final String INITIAL_SELECTION = "Mercury"; + + // Test values of position and selection for the testStateDestroy test + + public static final int TEST_STATE_DESTROY_POSITION = 2; + public static final String TEST_STATE_DESTROY_SELECTION = "Earth"; + + // Test values of position and selection for the testStatePause test + + public static final int TEST_STATE_PAUSE_POSITION = 4; + public static final String TEST_STATE_PAUSE_SELECTION = "Jupiter"; + + // The Application object for the application under test + + private SpinnerActivity mActivity; + + // String displayed in the spinner in the app under test + + private String mSelection; + + // The currently selected position in the spinner in the app under test + + private int mPos; + + /* + * The Spinner object in the app under test. Used with instrumentation to control the + * app under test. + */ + + private Spinner mSpinner; + + /* + * The data backing the Spinner in the app under test. + */ + + private SpinnerAdapter mPlanetData; + + /** + * Constructor for the test class. Required by Android test classes. The constructor + * must call the super constructor, providing the Android package name of the app under test + * and the Java class name of the activity in that application that handles the MAIN intent. + */ + public SpinnerActivityTest() { + + super("com.android.example.spinner", SpinnerActivity.class); + } + + /** + * Sets up the test environment before each test. + * @see android.test.ActivityInstrumentationTestCase2#setUp() + */ + @Override + protected void setUp() throws Exception { + + /* + * Call the super constructor (required by JUnit) + */ + + super.setUp(); + + /* + * prepare to send key events to the app under test by turning off touch mode. + * Must be done before the first call to getActivity() + */ + + setActivityInitialTouchMode(false); + + /* + * Start the app under test by starting its main activity. The test runner already knows + * which activity this is from the call to the super constructor, as mentioned + * previously. The tests can now use instrumentation to directly access the main + * activity through mActivity. + */ + mActivity = getActivity(); + + /* + * Get references to objects in the application under test. These are + * tested to ensure that the app under test has initialized correctly. + */ + + mSpinner = (Spinner)mActivity.findViewById(com.android.example.spinner.R.id.Spinner01); + + mPlanetData = mSpinner.getAdapter(); + + } + + /** + * Tests the initial values of key objects in the app under test, to ensure the initial + * conditions make sense. If one of these is not initialized correctly, then subsequent + * tests are suspect and should be ignored. + */ + + public void testPreconditions() { + + /* + * An example of an initialization test. Assert that the item select listener in + * the main Activity is not null (has been set to a valid callback) + */ + assertTrue(mSpinner.getOnItemSelectedListener() != null); + + /* + * Test that the spinner's backing mLocalAdapter was initialized correctly. + */ + + assertTrue(mPlanetData != null); + + /* + * Also ensure that the backing mLocalAdapter has the correct number of entries. + */ + + assertEquals(mPlanetData.getCount(), ADAPTER_COUNT); + } + + /* + * Tests the UI of the main activity. Sends key events (keystrokes) to the UI, then checks + * if the resulting spinner state is consistent with the attempted selection. + */ + public void testSpinnerUI() { + + /* + * Request focus for the spinner widget in the application under test, + * and set its initial position. This code interacts with the app's View + * so it has to run on the app's thread not the test's thread. + * + * To do this, pass the necessary code to the application with + * runOnUiThread(). The parameter is an anonymous Runnable object that + * contains the Java statements put in it by its run() method. + */ + mActivity.runOnUiThread( + new Runnable() { + public void run() { + mSpinner.requestFocus(); + mSpinner.setSelection(INITIAL_POSITION); + } + } + ); + + // Activate the spinner by clicking the center keypad key + + this.sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); + + // send 5 down arrow keys to the spinner + + for (int i = 1; i <= TEST_POSITION; i++) { + + this.sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); + } + + // select the item at the current spinner position + + this.sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); + + // get the position of the selected item + + mPos = mSpinner.getSelectedItemPosition(); + + /* + * from the spinner's data mLocalAdapter, get the object at the selected position + * (this is a String value) + */ + + mSelection = (String)mSpinner.getItemAtPosition(mPos); + + /* + * Get the TextView widget that displays the result of selecting an item from the spinner + */ + + TextView resultView = + (TextView) mActivity.findViewById(com.android.example.spinner.R.id.SpinnerResult); + + // Get the String value in the EditText object + + String resultText = (String) resultView.getText(); + + /* + * Confirm that the EditText contains the same value as the data in the mLocalAdapter + */ + + assertEquals(resultText,mSelection); + } + + /* + * Tests that the activity under test maintains the spinner state when the activity halts + * and then restarts (for example, if the device reboots). Sets the spinner to a + * certain state, calls finish() on the activity, restarts the activity, and then + * checks that the spinner has the same state. + * + */ + + public void testStateDestroy() { + + /* + * Set the position and value of the spinner in the Activity. The test runner's + * instrumentation enables this by running the test app and the main app in the same + * process. + */ + + + mActivity.setSpinnerPosition(TEST_STATE_DESTROY_POSITION); + + mActivity.setSpinnerSelection(TEST_STATE_DESTROY_SELECTION); + + // Halt the Activity by calling Activity.finish() on it + + mActivity.finish(); + + // Restart the activity by calling ActivityInstrumentationTestCase2.getActivity() + + mActivity = this.getActivity(); + + /* + * Get the current position and selection from the activity. + */ + + int currentPosition = mActivity.getSpinnerPosition(); + String currentSelection = mActivity.getSpinnerSelection(); + + // test that they are the same. + + assertEquals(TEST_STATE_DESTROY_POSITION, currentPosition); + + assertEquals(TEST_STATE_DESTROY_SELECTION, currentSelection); + } + + /* + * Tests that the activity under test maintains the spinner's state when the activity is + * paused and then resumed. + * + * Calls the activity's onResume() method. Changes the spinner's state by + * altering the activity's View. This means the test must run + * on the UI Thread. All the statements in the test method may be run on + * that thread, so instead of using the runOnUiThread() method, the + * @UiThreadTest is used. + */ + @UiThreadTest + + public void testStatePause() { + + /* + * Get the instrumentation object for this application. This object + * does all the instrumentation work for the test runner + */ + + Instrumentation instr = this.getInstrumentation(); + + /* + * Set the activity's fields for the position and value of the spinner + */ + + mActivity.setSpinnerPosition(TEST_STATE_PAUSE_POSITION); + + mActivity.setSpinnerSelection(TEST_STATE_PAUSE_SELECTION); + + /* + * Use the instrumentation to onPause() on the currently running Activity. + * This analogous to calling finish() in the testStateDestroy() method. + * This way demonstrates using the test class' instrumentation. + */ + + instr.callActivityOnPause(mActivity); + + /* + * Set the spinner to a test position + */ + + mActivity.setSpinnerPosition(0); + + mActivity.setSpinnerSelection(""); + + /* + * Call the activity's onResume() method. This forces the activity + * to restore its state. + */ + + instr.callActivityOnResume(mActivity); + + /* + * Get the current state of the spinner + */ + + int currentPosition = mActivity.getSpinnerPosition(); + + String currentSelection = mActivity.getSpinnerSelection(); + + assertEquals(TEST_STATE_PAUSE_POSITION,currentPosition); + assertEquals(TEST_STATE_PAUSE_SELECTION,currentSelection); + } + +}