Day 8: Dealing with Lists and ListViews

  • 446 views
Uploaded on

 

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
    Be the first to like this
No Downloads

Views

Total Views
446
On Slideshare
0
From Embeds
0
Number of Embeds
0

Actions

Shares
Downloads
43
Comments
0
Likes
0

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. 1. Android and Lists1.1. ListViewAndroid provides the view "ListView" which is capable of displaying a scrollable list of items. "ListView"gets the data to display via an adapter. An adapter which must extend "BaseAdapter" and is responsiblefor providing the data model for the list and for converting the data into the fields of the list.Android has two standard adapters, ArrayAdapter and CursorAdapter . "ArrayAdapter" can handle databased on Arrays or Lists while "SimpleCursorAdapter" handle database related data. You can developyour own Adapter by extending these classes or the BaseAdapter class.The following description will focus on ArrayAdapter and how to develop your own adapter. It will alsogive a small example for "CursorAdapter". For a larger example which uses CursorAdapters pleasesee Android SQLite Database - Tutorial .1.2. ListActivityYou can directly use the "ListView" in your layout as any other UI component. In case your Activity isprimary showing a list you can extend the activity "ListActivity" which simplifies the handling of a"ListView". "ListActivity" extends "Activity" and provides simplified handling of lists. For example you havea predefine method if someone clicks on a list element."ListActivity" contains a "ListAdapter" which is responsible for managing the data. This adapter must beset in the onCreate() method of your Activity via the method setListAdapter().If the user select in the list a list entry the method onListItemClick() will be called. This method allows toaccess the selected element.Android provides already some default layouts which you can use in your Adapter, e.g."android.R.layout.simple_list_item1". In case you dont want to use one of the pre-defined layouts yourown layout must have an element with the id "@android:id/list" which is the ListView. You can also use aview with the id "@android:id/empty". This view is displayed if the list is empty. For example you coulddisplay here an error message.1.3. ListViews and performanceDisplaying a large dataset must be efficiently implemented on a mobile device. Therefore the ListViewonly creates views (widget) if needed and attach them to the view hierarchy. The default Adapterimplementation for a ListView will recycle views, e.g. if a row is not displayed anymore it will be recycledand only its content will change. If you implement your own adapter for a view you also should do this toavoid performance problems.This technique will be demonstrated in this tutorial.2. ListActivity with ArrayAdapter and Android standard layoutCreate a new Android project "com.basistraining.listactivity" with the activity "MyList". You do not need tochange the default layout "main.xml". Create the following activity.
  • 2. package com.basistraining.listactivity;import android.app.ListActivity;import android.os.Bundle;import android.view.View;import android.widget.ArrayAdapter;import android.widget.ListView;import android.widget.Toast;public class MyList extends ListActivity { /** Called when the activity is first created. */ public void onCreate(Bundle icicle) { super.onCreate(icicle); // Create an array of Strings, that will be put to our ListActivity String[] names = new String[] { "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone"}; // Create an ArrayAdapter, that will actually make the Strings above // appear in the ListView this.setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, names)); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); // Get the item that was clicked Object o = this.getListAdapter().getItem(position); String keyword = o.toString(); Toast.makeText(this, "You selected: " + keyword, Toast.LENGTH_LONG) .show(); }}
  • 3. 3. ListActivity with own layoutYou can also define your own layout for the rows and assign this layout to your row adapter. We will adda graphic to each list entry.Create the following layout file "rowlayout.xml" in the res/layout folder of your project"com.basistraining.listactivity".
  • 4. <?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"> <ImageView android:id="@+id/icon" android:layout_height="wrap_content" android:src="@drawable/icon" android:layout_width="22px" android:layout_marginTop="4px" android:layout_marginRight="4px" android:layout_marginLeft="4px"> </ImageView> <TextView android:text="@+id/TextView01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/label" android:textSize="30px"></TextView></LinearLayout>Change your activity "MyList" to the following. This is almost the same coding as in the previous example,the only difference is that we are using our own layout in the ArrayAdapter and telling the adapter whichUI element should contains the text.package com.basistraining.listactivity;import android.app.ListActivity;import android.os.Bundle;import android.view.View;import android.widget.ArrayAdapter;import android.widget.ListView;import android.widget.Toast;public class MyList extends ListActivity { /** Called when the activity is first created. */ public void onCreate(Bundle icicle) { super.onCreate(icicle); // Create an array of Strings, that will be put to our ListActivity String[] names = new String[] { "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone" }; // Use your own layout and point the adapter to the UI elements which // contains the label this.setListAdapter(new ArrayAdapter<String>(this, R.layout.rowlayout, R.id.label, names)); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); // Get the item that was clicked Object o = this.getListAdapter().getItem(position); String keyword = o.toString(); Toast.makeText(this, "You selected: " + keyword, Toast.LENGTH_LONG) .show();
  • 5. }}4. ListActivity Extended4.1. ListActivities with flexible layoutThe following uses an image "no.png". I placed it in the "res/drawable-mdpi" folder. You must maintainyour own icon. In the easiest case just copy "icon.png" to "no.png" and use a drawing program tochange it a little bit.The above example uses one layout for all rows. If you want to influence the display of the different rowsyou can define your own adapter and override the getView() method. This method is responsible forcreating the individual rows of your "ListView". getView() need to return a View (containing severalothers) for each row. For this read the pre-defined layout via the class "LayoutInflator" and return oneindividual view per row. We extend ArrayAdapter but we could also directly implement "BaseAdapter"If "convertView" is not null we re-used this view. Android recycles rows (views) which are not displayedanymore. Using exsting rows saves memory and CPU consumption.
  • 6. Our implementation will also use the so-called "ViewHolder" pattern. The method findViewById() is aexpensive operation, therefore we should avoid doing this operation if not necessary.The ViewHolder stores a reference to the required views in a row. This ViewHolder is then attached tothe row via the method setTag(). Every view can get a tag assigned. If the row is recycled we can getthe ViewHolder via getTag() method. This seems like a lot of overhead but is much faster then therepetitive call of findViewById().Both techniques (re-used of existing views and the ViewHolder pattern) improve the performance of aListView by approx. 175%, especially for larger data sets.We still using the project "com.basistraining.listactivity". Create the following class"MyArrayAdapter.java".package com.basistraining.listactivity;import android.app.Activity;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.ArrayAdapter;import android.widget.ImageView;import android.widget.TextView;public class MyArrayAdapter extends ArrayAdapter<String> { private final Activity context; private final String[] names; public MyArrayAdapter(Activity context, String[] names) { super(context, R.layout.rowlayout, names); this.context = context; this.names = names; } // static to save the reference to the outer class and to avoid access to // any members of the containing class static class ViewHolder { public ImageView imageView; public TextView textView; } @Override public View getView(int position, View convertView, ViewGroup parent) { // ViewHolder will buffer the assess to the individual fields of the row // layout ViewHolder holder; // Recycle existing view if passed as parameter // This will save memory and time on Android // This only works if the base layout for all classes are the same View rowView = convertView; if (rowView == null) {
  • 7. LayoutInflater inflater = context.getLayoutInflater(); rowView = inflater.inflate(R.layout.rowlayout, null, true); holder = new ViewHolder(); holder.textView = (TextView) rowView.findViewById(R.id.label); holder.imageView = (ImageView) rowView.findViewById(R.id.icon); rowView.setTag(holder); } else { holder = (ViewHolder) rowView.getTag(); } holder.textView.setText(names[position]); // Change the icon for Windows and iPhone String s = names[position]; if (s.startsWith("Windows7") || s.startsWith("iPhone") || s.startsWith("Solaris")) { holder.imageView.setImageResource(R.drawable.no); } else { holder.imageView.setImageResource(R.drawable.ok); } return rowView; }}package com.basistraining.listactivity;import android.app.ListActivity;import android.os.Bundle;import android.view.View;import android.widget.ListView;import android.widget.Toast;public class MyList extends ListActivity { /** Called when the activity is first created. */ public void onCreate(Bundle icicle) { super.onCreate(icicle); // Create an array of Strings, that will be put to our ListActivity String[] names = new String[] { "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu","Solaris", "Android", "iPhone"}; this.setListAdapter(new MyArrayAdapter(this, names)); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); // Get the item that was clicked Object o = this.getListAdapter().getItem(position); String keyword = o.toString(); Toast.makeText(this, "You selected: " + keyword, Toast.LENGTH_LONG)
  • 8. .show(); }}4.2. ListActivities with different layout for different elementsYou can also have different (row) layouts. For example you may want to use different row layout for evenand odd line numbers or you have different objects in your list and want to use different layouts for then.You define the number of available (row) layouts in the method getViewTypeCount() and the view typefor a certain element via getItemViewType(int position).We still using the project "com.basistraining.listactivity". Create two new layouts, "row_even.xml" and"row_odd.xml".<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  • 9. android:layout_width="fill_parent" android:orientation="horizontal" android:layout_height="fill_parent"><TextView android:background="#BFEFFF" android:layout_width="match_parent"android:text="@+id/TextView01" android:layout_height="60dip" android:id="@+id/TextView01"android:layout_weight="1" android:textColor="#000000" android:textSize="22sp"></TextView><ImageView android:id="@+id/ImageView01" android:layout_height="60dip"android:background="#1874CD" android:layout_width="60dip"></ImageView></LinearLayout><?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="horizontal"><ImageView android:id="@+id/ImageView01" android:layout_width="60dip"android:layout_height="60dip" android:background="#1874CD"></ImageView><TextView android:text="@+id/TextView01" android:id="@+id/TextView01"android:layout_width="match_parent" android:layout_height="60dip" android:background="#BFEFFF"android:textColor="#000000" android:textSize="22sp"></TextView></LinearLayout>Create the following class "TwoLayoutsArrayAdapter"package com.basistraining.listactivity;import android.content.Context;import android.view.LayoutInflater;
  • 10. import android.view.View;import android.view.ViewGroup;import android.widget.ArrayAdapter;import android.widget.ImageView;import android.widget.TextView;public class TwoLayoutsArrayAdapter extends ArrayAdapter<String> { private final String[] values; private LayoutInflater inflator; static class ViewHolder { public TextView text; public ImageView image; } public TwoLayoutsArrayAdapter(Context context, String[] values) { super(context, R.id.TextView01, values); this.values = values; inflator = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @Override public int getViewTypeCount() { return 2; } @Override public int getItemViewType(int position) { return (position % 2 == 0) ? 0 : 1; } @Override public View getView(int position, View convertView, ViewGroup parent) { View rowView = convertView; if (rowView == null) { if (getItemViewType(position) == 0) { rowView = inflator.inflate(R.layout.row_even, null); } else { rowView = inflator.inflate(R.layout.row_odd, null); } ViewHolder viewHolder = new ViewHolder(); viewHolder.text = (TextView) rowView.findViewById(R.id.TextView01); viewHolder.image = (ImageView) rowView .findViewById(R.id.ImageView01); rowView.setTag(viewHolder); } ViewHolder holder = (ViewHolder) rowView.getTag(); holder.text.setText(values[position]); return rowView; }}
  • 11. Also adjust your "MyList" class to use the new adapter.package com.basistraining.listactivity;import android.app.ListActivity;import android.os.Bundle;import android.view.View;import android.widget.ArrayAdapter;import android.widget.ListView;import android.widget.Toast;public class MyList extends ListActivity { /** Called when the activity is first created. */ public void onCreate(Bundle icicle) { super.onCreate(icicle); // Create an array of Strings, that will be put to our ListActivity String[] names = new String[] { "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone", "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone" }; ArrayAdapter<String> adapter = new TwoLayoutsArrayAdapter(this, names); setListAdapter(adapter); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); // Get the item that was clicked Object o = this.getListAdapter().getItem(position); String keyword = o.toString(); Toast.makeText(this, "You selected: " + keyword, Toast.LENGTH_SHORT) .show(); }}This should result is a screen similar to the following.
  • 12. 4.3. Adding a longclick listener to the list itemsYou can also add a LongItemClickListener to the view. For this receive the ListView via the methodgetListVIew() and set the long click listener via the method setOnItemLongClickListener().package com.basistraining.listactivity;import android.app.ListActivity;import android.os.Bundle;import android.view.View;import android.widget.AdapterView;import android.widget.AdapterView.OnItemLongClickListener;import android.widget.ArrayAdapter;import android.widget.ListView;import android.widget.Toast;public class MyList extends ListActivity { /** Called when the activity is first created. */ public void onCreate(Bundle icicle) {
  • 13. super.onCreate(icicle); // Create an array of Strings, that will be put to our ListActivity String[] names = new String[] { "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone", "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone" }; ArrayAdapter<String> adapter = new TwoLayoutsArrayAdapter(this, names); setListAdapter(adapter); ListView list = getListView(); list.setOnItemLongClickListener(new OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(MyList.this, "Item in position " + position + " clicked", Toast.LENGTH_LONG).show(); // Return true to consume the click event. In this case the // onListItemClick listener is not called anymore. return true; } }); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); // Get the item that was clicked Object o = this.getListAdapter().getItem(position); String keyword = o.toString(); Toast.makeText(this, "You selected: " + keyword, Toast.LENGTH_SHORT) .show(); }}4.4. Rows interacting with the data modelYour row layout can also contain views which interact with the underlying data model. For example youcan have a "Checkbox" view in your row and if the checkbox is selected you change the data which isdisplayed in the row.We still use the same project. Create a new row layout "rowbuttonlayout.xml"<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:text="@+id/label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/label" android:textSize="30px"></TextView>
  • 14. <CheckBox android:id="@+id/check" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="4px" android:layout_marginRight="10px" android:layout_alignParentRight="true" ></CheckBox></RelativeLayout>create for this example the class "Model" which hold the name and the information if this element iscurrently selected.package com.basistraining.listactivity;public class Model { private String name; private boolean selected; public Model(String name) { this.name = name; selected = false; } public String getName() { return name; } public void setName(String name) { this.name = name; } public boolean isSelected() { return selected; } public void setSelected(boolean selected) { this.selected = selected; }}Create the following Adapter. This adapter will add a listener on the Checkbox. If the checkbox isselected the underlying data of the model is also changed. Search Checkbox gets its model elementassigned via the setTag() method.package com.basistraining.listactivity;
  • 15. import java.util.List;import android.app.Activity;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.ArrayAdapter;import android.widget.CheckBox;import android.widget.CompoundButton;import android.widget.TextView;public class InteractiveArrayAdapter extends ArrayAdapter<Model> { private final List<Model> list; private final Activity context; public InteractiveArrayAdapter(Activity context, List<Model> list) { super(context, R.layout.rowbuttonlayout, list); this.context = context; this.list = list; } static class ViewHolder { protected TextView text; protected CheckBox checkbox; } @Override public View getView(int position, View convertView, ViewGroup parent) { View view = null; if (convertView == null) { LayoutInflater inflator = context.getLayoutInflater(); view = inflator.inflate(R.layout.rowbuttonlayout, null); final ViewHolder viewHolder = new ViewHolder(); viewHolder.text = (TextView) view.findViewById(R.id.label); viewHolder.checkbox = (CheckBox) view.findViewById(R.id.check); viewHolder.checkbox .setOnCheckedChangeListener(newCompoundButton.OnCheckedChangeListener() { @Override public voidonCheckedChanged(CompoundButton buttonView, boolean isChecked) { Model element = (Model)viewHolder.checkbox .getTag(); element.setSelected(buttonView.isChecked()); } }); view.setTag(viewHolder); viewHolder.checkbox.setTag(list.get(position)); } else { view = convertView; ((ViewHolder) view.getTag()).checkbox.setTag(list.get(position)); } ViewHolder holder = (ViewHolder) view.getTag();
  • 16. holder.text.setText(list.get(position).getName()); holder.checkbox.setChecked(list.get(position).isSelected()); return view; }}Finally change your "ListView" to the following.package com.basistraining.listactivity;import java.util.ArrayList;import java.util.List;import android.app.ListActivity;import android.os.Bundle;import android.widget.ArrayAdapter;public class MyList extends ListActivity { /** Called when the activity is first created. */ public void onCreate(Bundle icicle) { super.onCreate(icicle); // Create an array of Strings, that will be put to our ListActivity ArrayAdapter<Model> adapter = new InteractiveArrayAdapter(this, getModel()); setListAdapter(adapter); } private List<Model> getModel() { List<Model> list = new ArrayList<Model>(); list.add(get("Linux")); list.add(get("Windows7")); list.add(get("Suse")); list.add(get("Eclipse")); list.add(get("Ubuntu")); list.add(get("Solaris")); list.add(get("Android")); list.add(get("iPhone")); // Initially select one of the items list.get(1).setSelected(true); return list; } private Model get(String s) { return new Model(s); }}
  • 17. If you start your app you should be able to flag items. These changes will be reflected in your model.5. Single vrs. MultiselectionYou can also support single and multi selection. See the following snippets for examples. To get theselected item(s) use listView.getCheckedItemPosition() or listView.getCheckedItemPositions(). If youhave stable ID you could also use listView.getCheckedItemIds() to get the selected ids.package com.basistraining.listactivity;import android.app.ListActivity;import android.os.Bundle;import android.widget.ArrayAdapter;import android.widget.ListView;public class MyList extends ListActivity { /** Called when the activity is first created. */ public void onCreate(Bundle icicle) { super.onCreate(icicle);
  • 18. // Create an array of Strings, that will be put to our ListActivity String[] names = new String[] { "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone", "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone" }; setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_multiple_choice, android.R.id.text1, names)); ListView listView = getListView(); listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); }}package com.basistraining.listactivity;import android.app.ListActivity;import android.os.Bundle;import android.widget.ArrayAdapter;import android.widget.ListView;public class MyList extends ListActivity { /** Called when the activity is first created. */ public void onCreate(Bundle icicle) { super.onCreate(icicle); // Create an array of Strings, that will be put to our ListActivity String[] names = new String[] { "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone", "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone" }; setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_single_choice, android.R.id.text1, names)); ListView listView = getListView(); listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); }}6. Header and FooterYou can of course put arbitray elements around your ListView. For example you can define a layout withtwo TextViews and a ListView between them. If you do this, you must assign the id "@android:id/list" tothe ListView, as the ListActivity searches for a view with this id. If you do this then one TextView willalways be visible above the List (header) and the other will be visible below the ListView. If you want todisplay the header / footer view only if see the beginning / end of the list you can useview.setHeaderView() or view.setFooterView(). For example:package com.basistraining.listactivity;
  • 19. import android.app.ListActivity;import android.os.Bundle;import android.view.View;import android.widget.ArrayAdapter;import android.widget.ListView;public class MyList extends ListActivity { /** Called when the activity is first created. */ public void onCreate(Bundle icicle) { super.onCreate(icicle); // Create an array of Strings, that will be put to our ListActivity String[] names = new String[] { "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone", "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone" }; View header = getLayoutInflater().inflate(R.layout.header, null); View footer = getLayoutInflater().inflate(R.layout.footer, null); ListView listView = getListView(); listView.addHeaderView(header); listView.addFooterView(footer); setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_single_choice, android.R.id.text1, names)); }}7. SimpleCursorAdapterIf you work with content providern or directly with database you can use the SimpleCursorAdapter todefine the data for your ListView. For more information on Android and SQLight please see the SQLiteand Lists Tutorial .Create a new Android project "com.basistraining.listactivity.cursor" with the activity "MyListActivity".Create the following activity.package com.basistraining.listactivity.cursor;import android.app.ListActivity;import android.database.Cursor;import android.net.Uri;import android.os.Bundle;import android.provider.ContactsContract;import android.widget.ListAdapter;import android.widget.SimpleCursorAdapter;public class MyListActivity extends ListActivity { /** Called when the activity is first created. */
  • 20. @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Cursor mCursor = getContacts(); startManagingCursor(mCursor); // Now create a new list adapter bound to the cursor. // SimpleListAdapter is designed for binding to a Cursor. ListAdapter adapter = new SimpleCursorAdapter(this, // Context. android.R.layout.two_line_list_item, // Specify the row template mCursor, // Pass in the cursor to bind to. // Array of cursor columns to bind to. new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME}, // Parallel array of which template objects to bind to those // columns. new int[] { android.R.id.text1, android.R.id.text2 }); // Bind to our new adapter. setListAdapter(adapter); } private Cursor getContacts() { // Run query Uri uri = ContactsContract.Contacts.CONTENT_URI; String[] projection = new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME }; String selection = ContactsContract.Contacts.IN_VISIBLE_GROUP + " = " + ("1") + ""; String[] selectionArgs = null; String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; return managedQuery(uri, projection, selection, selectionArgs, sortOrder); }}Make sure you give your application the permission to read the contacts. (Uses Permissions"android.permission.READ_CONTACTS" in AndroidManifest.xml)