You are on page 1of 15

ICSpad Codelab Steps Australia/New Zealand 2012

ICSpad Codelab Steps Australia/New Zealand 2012 Step 1. Target ICS Compatibly Step 2. Check out the ActionBarCompat project Step 3. Add ActionBarCompat (aka ABC) code to ICSpad project Step 4. Enable the Action Bar in ICSpad Step 5. Add Sharing support to ICSpad for pre-ICS Step 6. Add Sharing support to ICSpad using ShareActionProvider Step 7. Create a new View Pager UI for ICSpad Step 8. Complete Edit button implementation Step 9. Complete Add action item implementation Step 10. Complete Delete action item implementation Bonus

Import the projects into Eclipse or prepare Android makefiles for the command line. We will start with the ICSpad_Start project. * = bonus optional step

Step 1. Target ICS Compatibly


Change project build target to API level 15 Specify the min sdk version as API level 7, but the target sdk version as API level 15 * Turn on hardware acceleration

Step 2. Check out the ActionBarCompat project


File -> New -> Android Project Select Create project from existing sample and click Next > Select Android 4.0.3 and click Next > Select ActionBarCompat and click Finish

Step 3. Add ActionBarCompat (aka ABC) code to ICSpad project


Create a new package called com.example.android.adl.actionbar and copy the following 7 files to it: ActionBarActivity.java ActionBarHelper.java ActionBarHelperBase.java ActionBarHelperHoneycomb.java ActionBarHelperICS.java SimpleMenu.java SimpleMenuItem.java You should now see some errors in the project but no worries, we will fix them. First, look at ActionBarHelper.java, it complains Build.VERSION_CODES.ICE_CREAM_SANDWICH

is not defined. Fix this by compiling the project with Android 4.0.3.

Next, look at ActionBarHelperBase.java, it has many errors. Seems like we are missing some resources like layouts, drawables, attrs, colors, dimens, ids and styles. For layouts, lets copy the actionbar_compat.xml from ABC/res/layout to ICSpad/res/layout. Now the file itself complains @id/actionbar_compat is missing. We can fix this by copying ids.xml from ABC to ICSpad. If you compare the two projects layout structure, you will find layout-v11 is missing in ICSpad, so lets create that and copy actionbar_indeterminate_progress.xml to it. For drawables, lets copy all the file from ABCs drawable, drawable-hdpi, drawable-mdpi and drawable-xhdpi to ICSpad. For attrs, copy ABC/res/values/attrs.xml to ICSPad/res/values For colors, we already have colors.xml so just copy the missing values For dimens, we also already have dimens.xml so just copy the missing values For ids, we took care of that earlier For styles, we have to update the styles.xml in values, values-v11 and values-v13 (new) For strings, we have to update the strings.xml in values

Step 4. Enable the Action Bar in ICSpad


Open ActionBarActivity.java, extends it with FragmentActivity instead of Activity since we are going to use fragments in this codelab Open NotepadActivity.java, extends it with ActionBarActivity instead of FragmentActivity. Sweet! Now you should see a pretty action bar (not quite functional yet though) Lets add a few more action items from ABC/res/menu/main.xml to the notepad_menu.xml Lets attach some logic to the action items. Lets keep things clean and create a new parent activity class called ActionBarFragmentActivity for NotepadActivity which extends ActionBarActivity.

Snippet:
Update notepad_menu.xml to add a few more action items <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/add_note" android:title="@string/menu_add" android:icon="@drawable/ic_menu_add" android:orderInCategory="0" android:showAsAction="always" /> <item android:id="@+id/menu_refresh" android:title="@string/menu_refresh" android:icon="@drawable/ic_action_refresh" android:orderInCategory="1" android:showAsAction="always" /> <item android:id="@+id/menu_search" android:title="@string/menu_search" android:icon="@drawable/ic_action_search" android:orderInCategory="2" android:showAsAction="never" /> <item android:id="@+id/menu_share" android:title="@string/menu_share" android:icon="@drawable/ic_menu_share" android:orderInCategory="3" android:showAsAction="never" /> </menu> Attach logic to the action items @Override public boolean onOptionsItemSelected(MenuItem item) {

switch (item.getItemId()) { // Home icon has special ID from the framework case android.R.id.home: Toast.makeText(this, "Tapped home", Toast.LENGTH_SHORT).show(); break; case R.id.menu_refresh: Toast.makeText(this, "Fake refreshing...", Toast.LENGTH_SHORT).show(); getActionBarHelper().setRefreshActionItemState(true); getWindow().getDecorView().postDelayed( new Runnable() { @Override public void run() { getActionBarHelper().setRefreshActionItemState(false); } }, 1000); break; case R.id.menu_search: Toast.makeText(this, "Tapped search", Toast.LENGTH_SHORT).show(); break; } return super.onOptionsItemSelected(item); }

** Checkpoint: ICSpad_checkpoint_1

Step 5. Add Sharing support to ICSpad for pre-ICS


Add the share logic for the Share action item in onOptionsItemSelected method in ActionBarFragmentActivity.java

Snippet:
Add share logic : case R.id.menu_share: Intent shareIntent = new Intent(android.content.Intent.ACTION_SEND); shareIntent.setType("text/plain"); String shareBody = "Body text"; shareIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "Subject Here"); shareIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareBody); startActivity(Intent.createChooser(shareIntent, "Share via")); break;

Step 6. Add Sharing support to ICSpad using ShareActionProvider


Update the Share item in the notepad_menu.xml. To enable ShareActionProvider, we need to add the android:actionProviderClass attribute with value android.widget.ShareActionProvider. Ok, now the ICS Share icon is there but its not tied to any logic. Lets attach some logic to the button. Since ShareActionProvider is newly introduced in ICS only, we can should add the logic in ActionBarHelperICS.java by overriding the onCreateOptionMenu method. Basically, we need to get a hold of the ShareActionProvider and set its ShareIntent. For sample purpose, lets add a createShareIntent method in ActionBarFragmentActivity.java

Snippet:
Update the Share item in the note_menu.xml <item android:id="@+id/menu_share" android:title="@string/menu_share" android:icon="@drawable/ic_menu_share"

android:orderInCategory="3" android:showAsAction="ifRoom" android:actionProviderClass="android.widget.ShareActionProvider" /> Override the onCreateOptionMenu method in ActionBarHelperICS.java @Override public boolean onCreateOptionsMenu(Menu menu) { // Set file with share history to the provider and set the share intent. MenuItem actionItem = menu.findItem(R.id.menu_share); if (actionItem != null) { ShareActionProvider actionProvider = (ShareActionProvider) actionItem.getActionProvider(); // Note that you can set/change the intent any time, // say when the user has selected an image. actionProvider.setShareIntent(((ActionBarFragmentActivity) mActivity).createShareIntent()); } return super.onCreateOptionsMenu(menu); } Add a createShareIntent method in ActionBarFragmentActivity.java protected Intent createShareIntent() { Intent shareIntent = new Intent(Intent.ACTION_SEND); //TODO: Get note String shareBody = "Note Body"; shareIntent.setType("text/plain"); shareIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "Note Title"); shareIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareBody); return shareIntent; }

** Checkpoint: ICSpad_checkpoint_2

Step 7. Create a new View Pager UI for ICSpad


Here, we want to use View Pager to display all the notes we have. For that, we need to have an AsyncTask to get all the note IDs. A ViewPager to display all the notes through an adapter. For the sake of learning, lets represent each note as a Fragment so we will need a FragmentPagerAdapter. Create a new launcher Activity called NotepadViewPagerActivity which extends ActionBarFragmentActivity. We also need to define some member variables. Update AndroidManifest.xml to use this as launcher instead of NotepadActivity. Run the app again. Oh! Where are all the action items? We must have forgotten to inflate the right menu. Since we are going to use the same action items, lets push the onCreateOptionsMenu method from NotepadActivity up to ActionBarFragmentActivity. Great! We have all the action items back but there is nothing in the content area. Thats expected but lets create a layout for this activity called fragment_pager.xml for use later. We need a fragment to represent a note in our View Pager. Lets create a fragment called NoteViewFragment. Before we do that, lets think about this a little bit. Actually we can reuse most of NoteEditFragment. The main difference between the two is that NoteEditFragment needs a SaveNote method and has a layout with EditText fields. Lets move most of the methods in NoteViewFragment and have NoteEditFragment extends NoteViewFragment. The quickest way to do this is: Make a copy of NoteEditFragment and name it as NoteViewFragment Keep onCreateView and saveNote in NoteEditFragment Fix the scope of variables mTitleText, mBodyText and mIsAfterGB to default. Change mTitleText and mBodyText to type TextView

Fix the scope of method populateFields to default Add a static method to create new NoteViewFragment instance which takes a note ID as an argument Obtain the noteId argument in the onCreate method to set the mCurrentNote string

We need a new layout for NoteViewFragment. Lets call it note_view.xml. Lets create an AsyncTask to query all the note IDs. Ok, next we need an FragmentPagerAdapter. Lets call it MyAdapter and put it within NotepadViewPagerActivity. Lets put everything together in the onCreate method. We need to add a new constant in the NotepadActivity to indicate whether a call is from the new View Pager UI.

Snippet:
Create a new launcher Activity called NotepadViewPagerActivity.java public class NotepadViewPagerActivity extends ActionBarFragmentActivity { // Query projection for loading notes private static final String[] DEFAULT_PROJECTION = new String[] { NotesProvider.KEY_ID }; // Need a view pager and an adapter private FragmentPagerAdapter mAdapter; private ViewPager mPager; // Need an "Edit" button and a "Delete" button private Button editButton; private Button deleteButton; // Need a list to keep track of all the note IDs private List<Long> noteIds = new ArrayList<Long>(); // Need to keep track of the current Note ID and position private long currentNoteId = -1; private int currentNotePosition = 0; } Update AndroidManifest.xml to use NotepadViewPagerActivity as the launcher <activity android:name=".NotepadActivity" android:label="@string/app_name"> <intent-filter> <!-- <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> --> </intent-filter> </activity> <activity android:name=".NotepadViewPagerActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> Lets push the onCreateOptionsMenu method from NotepadActivity up to ActionBarFragmentActivity @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.notepad_menu, menu); return super.onCreateOptionsMenu(menu); } Create a layout for the NotepadViewPagerActivity called fragment_pager.xml <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:padding="4dip" android:gravity="center_horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v4.view.ViewPager android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="0px"

android:layout_weight="1"> </android.support.v4.view.ViewPager> <LinearLayout android:orientation="horizontal" android:gravity="center" android:measureWithLargestChild="true" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="0"> <Button android:id="@+id/edit_note" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/edit_note"> </Button> <Button android:id="@+id/delete_note" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/delete_note"> </Button> </LinearLayout> </LinearLayout> Add a static method to create new NoteViewFragment instance public static NoteViewFragment newInstance(long noteId) { NoteViewFragment f = new NoteViewFragment(); // Supply noteId as an argument. Bundle args = new Bundle(); args.putLong("noteId", noteId); f.setArguments(args); return f; } Obtain the noteId argument in the onCreate method to get the current note @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); long noteId = getArguments().getLong("noteId", -1); if (noteId != -1) { mCurrentNote = ContentUris.withAppendedId(NotesProvider.CONTENT_URI, noteId); } } Layout note_view.xml for NoteViewFragment.java <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="@dimen/margin" android:padding="@dimen/padding" android:background="@drawable/border"> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/title" android:layout_marginRight="@dimen/margin" android:textAppearance="?android:attr/textAppearanceMedium" /> <TextView android:id="@+id/title" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:textAppearance="?android:attr/textAppearanceMedium" android:inputType="textCapWords" android:contentDescription="@string/title_text" /> </LinearLayout> <TextView android:layout_width="wrap_content"

android:layout_height="wrap_content" android:text="@string/body" android:textAppearance="?android:attr/textAppearanceMedium" /> <TextView android:id="@+id/body" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:scrollbars="vertical" android:inputType="textCapSentences|textMultiLine" android:textAppearance="?android:attr/textAppearanceMedium" android:gravity="top" android:contentDescription="@string/body_text" /> </LinearLayout> Create an AsyncTask to query all the note IDs. private static class MyAsyncTask extends AsyncTask<Void, Void, Void> { private List<Long> noteIds; private NotepadViewPagerActivity activity; public MyAsyncTask(NotepadViewPagerActivity activity, List<Long> noteIds) { this.noteIds = noteIds; this.activity = activity; } @Override protected Void doInBackground(Void... params) { Cursor cursor = activity.getContentResolver().query(NotesProvider.CONTENT_URI, DEFAULT_PROJECTION, null, null, null); // Update the noteIds list if (cursor.moveToFirst()) { noteIds.clear(); do { noteIds.add(cursor.getLong(0)); } while (cursor.moveToNext()); } // Close the cursor cursor.close(); return null; } @Override protected void onPostExecute(Void result) { super.onPostExecute(result); //activity.updateNoteIds(); } }

Create an adapter called MyAdapter and put it within NotepadViewPagerActivity public static class MyAdapter extends FragmentPagerAdapter { private List<Long> noteIds; public MyAdapter(FragmentManager fm, List<Long> noteIds) { super(fm); this.noteIds = noteIds; } @Override public int getCount() { return noteIds.size(); } @Override public Object instantiateItem(View container, int position) { NoteViewFragment noteViewFrag = (NoteViewFragment) super.instantiateItem(container, position); return noteViewFrag;

} @Override public Fragment getItem(int position) { return new NoteViewFragment(noteIds.get(position)); } @Override public int getItemPosition(Object object) { // This essentially clears the adapter return POSITION_NONE; } } Create the onCreate method for NotepadViewPagerActivity @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.fragment_pager); mAdapter = new MyAdapter(getSupportFragmentManager(), noteIds); mPager = (ViewPager)findViewById(R.id.pager); mPager.setAdapter(mAdapter); // Add a new note editButton = (Button)findViewById(R.id.edit_note); editButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { Intent viewNoteIntent = new Intent(getBaseContext(), NotepadActivity.class); viewNoteIntent.setAction(NotepadActivity.ACTION_VIEW_NOTE); viewNoteIntent.putExtra(NotepadActivity.EXTRA_NOTE_ID, currentNoteId); viewNoteIntent.putExtra(NotepadActivity.EXTRA_FROM_VIEW_PAGER, true); startActivityForResult(viewNoteIntent, 0); } }); // Delete a note by removing the row in the DB deleteButton = (Button)findViewById(R.id.delete_note); deleteButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { getContentResolver().delete(ContentUris.withAppendedId(NotesProvider.CONTENT_URI, currentNoteId), null, null); new MyAsyncTask(NotepadViewPagerActivity.this, noteIds).execute(); } }); new MyAsyncTask(NotepadViewPagerActivity.this, noteIds).execute(); } Add an Extra constant to indicate whether a call to the NotepadActivity is from the View Pager UI public static final String EXTRA_FROM_VIEW_PAGER = "fromViewPager";

** Checkpoint: ICSpad_checkpoint_3

Step 8. Complete Edit button implementation


Add a SimpleOnPageChangeListener to update the currentNoteId and currentNotePosition variables Cool. Now the Edit button will pull up the right note for editing. However, it still goes back to NotepadActivity. To fix this, we need to update some logic in NotepadActivity and NoteEditFragment. For NotepadActivity, we need to update the else clause of the showNote method. We need to add a

new flag private boolean mFromViewPager in the class and update the viewNote method.

For NoteEditFragment, we need to update the saveNote method in two places

Snippet
Add a SimpleOnPageChangeListener // Keep track of the current note for the Edit and Delete note buttons mPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { currentNoteId = noteIds.get(position); currentNotePosition = position; } }); Update the showNote methods else clause with the following code // add the NoteEditFragment to the container FragmentTransaction ft = fm.beginTransaction(); if (edit != null) { ft.remove(edit); // Remove old edit fragment } edit = new NoteEditFragment(); if (mUseMultiplePanes) { // Add a note fragment in the note detail container ft.add(R.id.note_detail_container, edit, NOTE_EDIT_TAG); } else { // Single pane layout // Replace the list fragment with the edit fragment ft.replace(R.id.list, edit, NOTE_EDIT_TAG); // We have two ways to get here. One is from the view pager. // Another one is from the list fragment. For the latter, we // need to pop the list fragment back so added a flag to // indicate this. Bundle b = new Bundle(); if (mFromViewPager) { b.putBoolean(NoteEditFragment.ARGUMENT_POP_ON_SAVE, false); } else { b.putBoolean(NoteEditFragment.ARGUMENT_POP_ON_SAVE, true); ft.addToBackStack(null); } edit.setArguments(b); } // Commit and load the note ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); ft.commit(); edit.loadNote(noteUri); Add a new flag called mFromViewPager in NotepadActivity private boolean mIsAfterGB = Build.VERSION.SDK_INT >= 11; private boolean mFromViewPager; Update the viewNote method with the following line final long noteId = launchIntent.getLongExtra(EXTRA_NOTE_ID, -1); mFromViewPager = launchIntent.getBooleanExtra(EXTRA_FROM_VIEW_PAGER, false); showNote(ContentUris.withAppendedId(NotesProvider.CONTENT_URI, noteId)); Update the saveNote method in two places #1 Activity activity = getActivity();

// Update if (updating) { activity.getContentResolver().update(mCurrentNote, values, null, null); activity.setResult(RESULT_CODE_EDIT); // Insert } else { Uri newNote = activity.getContentResolver().insert( NotesProvider.CONTENT_URI, values); if (newNote != null) { mCurrentNote = newNote; activity.setResult(RESULT_CODE_ADD); } } #2 Bundle b = getArguments(); // Check to see whether this fragment is in a single-pane layout if ( null != b) { boolean popOnSave = b.getBoolean(ARGUMENT_POP_ON_SAVE); if (popOnSave) { getFragmentManager().popBackStack(); } else { getActivity().finish(); } } else { // multi-pane layout // Do nothing, stay in the current activity }

Step 9. Complete Add action item implementation


In order to implement Add (or Delete later in Step 10), we need to have a method to update the noteIds list and notify the adapter Create a method called updateNoteIds Added two missing flags: isNewNoteAdded - if a new note is added, we want to move the view pager to that new page onSaveInstanceStateCalled - we dont want to do any fragment commit after onSaveInstanceState is called. We need to update this flag in onRestart and onSaveInstanceState. Call updateNoteIds in the onPostExecute method of the AsyncTask we created earlier We need to add logic to start an intent from the action bar. This can be done by overriding the onMenuItemSelected method Update the logic in the viewNote method to show us a blank entry when adding a new note Finally, we need to override the onActivityResult method to set the isNewNoteAdded flag. Snippet:
Create updateNoteIds() method private void updateNoteIds() { // Initialize currentNoteId if (noteIds.size() > 0 && currentNoteId == -1) { currentNoteId = noteIds.get(0); } // Disable delete button if there is only one note if (noteIds.size() > 1) { deleteButton.setEnabled(true); } else { deleteButton.setEnabled(false);

} // // // if

Make sure onSaveInstanceState is not called before updating fragments. A fragment transaction can only be created/committed prior to an activity saving its state. (!onSaveInstanceStateCalled) { mAdapter.notifyDataSetChanged(); if (isNewNoteAdded) { currentNotePosition = mAdapter.getCount() - 1; isNewNoteAdded = false; } mPager.setCurrentItem(currentNotePosition);

} } Added two missing flags - isNewNoteAdded and onSaveInstanceStateCalled // Flag to indicate a new note is added private boolean isNewNoteAdded; // Flag to keep track of whether onSaveInstanceState is called private boolean onSaveInstanceStateCalled = false; Update flag onSaveInstanceStateCalled in onRestart and onSaveInstanceState @Override protected void onRestart() { super.onRestart(); onSaveInstanceStateCalled = false; new MyAsyncTask(NotepadViewPagerActivity.this, noteIds).execute(); } @Override protected void onSaveInstanceState(Bundle outState) { // Keep track of whether this method is called. Will reset in onRestart. onSaveInstanceStateCalled = true; // Remember the current note position outState.putInt(CURRENT_NOTE_POSITION, currentNotePosition); super.onSaveInstanceState(outState); } Call updateNoteIds in the onPostExecute method of the AsyncTask @Override protected void onPostExecute(Void result) { super.onPostExecute(result); activity.updateNoteIds(); } Override the onMenuItemSelected method in the NotepadViewPagerActivity @Override public boolean onMenuItemSelected(int featureId, MenuItem item) { switch (item.getItemId()) { case R.id.add_note: Intent viewNoteIntent = new Intent(getBaseContext(), NotepadActivity.class); viewNoteIntent.setAction(NotepadActivity.ACTION_VIEW_NOTE); viewNoteIntent.putExtra(NotepadActivity.EXTRA_FROM_VIEW_PAGER, true); startActivityForResult(viewNoteIntent, 0); return true; } return super.onMenuItemSelected(featureId, item); } Update viewNote method even more to show a blank entry if (noteId == -1) { showNote(null); // Show a blank entry for adding new note

} else { showNote(ContentUris.withAppendedId(NotesProvider.CONTENT_URI, noteId)); } Override the onActivityResult method in the NotepadViewPagerActivity to set the isNewNoteAdded flag @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == 0) { if (resultCode == NoteEditFragment.RESULT_CODE_ADD) { isNewNoteAdded = true; } } }

Step 10. Complete Delete action item implementation


The fragments in the adapter are reused so we need to add methods to allow a fragment to update its view Create a setNoteId method in NoteViewFragment to update the note ID and its view Update the adapters instantiateItem method to call the setNoteId method Snippet:
Create a setNoteId method in NoteViewFragment to update the note ID and its view public void setNoteId(long noteId) { mCurrentNote = ContentUris.withAppendedId(NotesProvider.CONTENT_URI, noteId); if (isAdded()) { populateFields(); } } Update the adapters instantiateItem method @Override public Object instantiateItem(View container, int position) { NoteViewFragment noteViewFrag = (NoteViewFragment) super.instantiateItem(container, position); // Since fragments can be reused and tied to a position, // we need to update the note id so the fragment will pull // the right data to display noteViewFrag.setNoteId(noteIds.get(position)); return noteViewFrag; }

** Checkpoint: ICSpad_checkpoint_4

Bonus
Create a multi-pane layout for landscape in GB Do the same for 3.2+ device with width larger or equal to 580dp Find a way to point to the same layout without duplication

** Checkpoint: ICSpad_complete

You might also like