This project illustrates how to query the Contacts database for a device. Once the contact information is read, we shall use the methods discussed in Accessing the File System to write it to a file on the SD card that can be copied off to a computer.
Following the general procedure in Creating a New Project, either choose Start a new Android Studio project from the Android Studio homepage, or from the Android Studio interface choose File > New > New Project. Fill out the fields in the resulting screens as follows,
Application Name:
ReadContacts
Company Domain:< YourNamespace > Package Name: <YourNamespace> . readcontacts Project Location: <ProjectPath> ReadContacts Target Devices: Phone and Tablet; Min SDK API 15 Add an Activity: Empty Activity Activity Name: MainActivity (check the Generate Layout File box) Layout Name: activity_main |
where you should substitute your namespace for <YourNamespace> (com.lightcone in my case) and <ProjectPath> is the path to the directory where you will store this Android Studio Project (/home/guidry/StudioProjects/ in my case). If you have chosen to use version control for your projects, go ahead and commit this project to version control.
Edit res/values/strings.xml to give
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">ReadContacts</string> <string name="action_settings">Settings</string> <string name="hello">Read and Processed Contacts.\n</string> </resources>
and res/values/styles.xml so that it reads
<resources> <!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style> <!--A material design style for indeterminate spinner--> <style name="indeterminateMaterialProgress" parent="Theme.AppCompat.Light"> </style> </resources>
Now open res/layout/activity_main.xml and edit it so that it reads
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:id="@+id/RelativeLayout1" android:orientation="vertical" tools:context="com.lightcone.readcontacts.MainActivity"> <ProgressBar android:id="@+id/progress_bar" style="@style/indeterminateMaterialProgress" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" /> <ScrollView android:id="@+id/ScrollView01" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:id="@+id/TextView01" android:layout_width="fill_parent" android:layout_height="wrap_content" android:padding="5sp" android:text="@string/hello" /> </ScrollView> </RelativeLayout>
In this project we are going to write to a file on the SD card, as described in Accessing the File System, and are going to read from the Contacts. So an explicit permission must be added to the manifest file for each. Open AndroidManifest.xml and edit it to give
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="<YourNamespace>.readcontacts"> <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
where the added lines are indicated in red. Finally, edit MainActivity.java to give
package <YourNamespace>.readcontacts; import android.os.AsyncTask; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.View; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import android.content.ContentResolver; import android.database.Cursor; import android.net.Uri; import android.os.Environment; import android.provider.ContactsContract; import android.util.Log; import android.widget.ProgressBar; import android.widget.TextView; public class MainActivity extends AppCompatActivity { // To suppress notational clutter and make structure clearer, define some shorthand constants. private static final Uri URI = ContactsContract.Contacts.CONTENT_URI; private static final Uri PURI = ContactsContract.CommonDataKinds.Phone.CONTENT_URI; private static final Uri EURI = ContactsContract.CommonDataKinds.Email.CONTENT_URI; private static final Uri AURI = ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI; private static final String ID = ContactsContract.Contacts._ID; private static final String DNAME = ContactsContract.Contacts.DISPLAY_NAME; private static final String HPN = ContactsContract.Contacts.HAS_PHONE_NUMBER; private static final String LOOKY = ContactsContract.Contacts.LOOKUP_KEY; private static final String CID = ContactsContract.CommonDataKinds.Phone.CONTACT_ID; private static final String EID = ContactsContract.CommonDataKinds.Email.CONTACT_ID; private static final String AID = ContactsContract.CommonDataKinds.StructuredPostal.CONTACT_ID; private static final String PNUM = ContactsContract.CommonDataKinds.Phone.NUMBER; private static final String PHONETYPE = ContactsContract.CommonDataKinds.Phone.TYPE; private static final String EMAIL = ContactsContract.CommonDataKinds.Email.DATA; private static final String EMAILTYPE = ContactsContract.CommonDataKinds.Email.TYPE; private static final String STREET = ContactsContract.CommonDataKinds.StructuredPostal.STREET; private static final String CITY = ContactsContract.CommonDataKinds.StructuredPostal.CITY; private static final String STATE = ContactsContract.CommonDataKinds.StructuredPostal.REGION; private static final String POSTCODE = ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE; private static final String COUNTRY = ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY; private static final int MAX_NUMBER_ENTRIES = 5; private String id; private String lookupKey; private String name; private String street; private String city; private String state; private String postcode; private String country; private String ph[]; private String phType[]; private String em[]; private String emType[]; private File root; private int emcounter; private int phcounter; private int addcounter; private TextView tv; private ProgressBar progressBar; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = (TextView) findViewById(R.id.TextView01); // Allow for up to MAX_NUMBER_ENTRIES email and phone entries for a contact em = new String[MAX_NUMBER_ENTRIES]; emType = new String[MAX_NUMBER_ENTRIES]; ph = new String[MAX_NUMBER_ENTRIES]; phType = new String[MAX_NUMBER_ENTRIES]; progressBar = (ProgressBar) findViewById(R.id.progress_bar); // Call a check for runtime permissions here // Check that external media available and writable checkExternalMedia(); // Read the contacts and output to a file. Process this on a background // thread defined by an instance of AsyncTask because it will typically // take several seconds to process a few hundred contacts. We will display // an indeterminate progress bar to the user while the contacts are being // processed. new BackgroundProcessor().execute(); } // Method to process contacts (reads them and writes formatted output to a file on device, in // addition to concatenating a string listing the contacts in the list that will be displayed // on the phone screen). This method will be invoked from a background thread since it will // take some time to execute. private String processContacts() { /** Open a PrintWriter wrapping a FileOutputStream so that we can send output from a query of the Contacts database to a file on the SD card. Must wrap the whole thing in a try-catch to catch file not found and i/o exceptions. Note that since we are writing to external media we must add a WRITE_EXTERNAL_STORAGE permission to the manifest file. Otherwise a FileNotFoundException will be thrown. */ // Create a StringBuilder for efficient concatenation of contact list into single string. // (We cannot append directly to the views from here because this will be run on background // thread and we cannot touch views on main thread from here. We will concatenate the // string here and return it, and then update the view from the onPostExecute method of // AsyncTask (which can interact with views on the main thread). StringBuilder stringBuilder = new StringBuilder(); // This will set up output to /sdcard/download/phoneData.txt if /sdcard is the root of // the external storage. See the project WriteSDCard for more information about // writing to a file on the SD card. File dir = new File(root.getAbsolutePath()); dir.mkdirs(); File file = new File(dir, "phoneData.txt"); stringBuilder.append("Wrote " + file + "\nfor following contacts:\n"); try { FileOutputStream f = new FileOutputStream(file); PrintWriter pw = new PrintWriter(f); // Main loop to query the contacts database, extracting the information. See // http://www.higherpass.com/Android/Tutorials/Working-With-Android-Contacts/ ContentResolver cr = getContentResolver(); Cursor cu = cr.query(URI, null, null, null, null); if (cu.getCount() > 0) { // Loop over all contacts while (cu.moveToNext()) { // Initialize storage variables for the new contact street = ""; city = ""; state = ""; postcode = ""; country = ""; // Get ID information (id, name and lookup key) for this contact. id is an identifier // number, name is the name associated with this row in the database, and // lookupKey is an opaque value that contains hints on how to find the contact // if its row id changed as a result of a sync or aggregation. id = cu.getString(cu.getColumnIndex(ID)); name = cu.getString(cu.getColumnIndex(DNAME)); lookupKey = cu.getString(cu.getColumnIndex(LOOKY)); // Append list of contacts to the StringBuilder object stringBuilder.append("\n" + id + " " + name); // Query phone numbers for this contact (may be more than one), so use a // while-loop to move the cursor to the next row until moveToNext() returns // false, indicating no more rows. Store the results in arrays since there may // be more than one phone number stored per contact. The if-statement // enclosing everything ensures that the contact has at least one phone // number stored in the Contacts database. phcounter = 0; if (Integer.parseInt(cu.getString(cu.getColumnIndex(HPN))) > 0) { Cursor pCur = cr.query(PURI, null, CID + " = ?", new String[]{id}, null); while (pCur.moveToNext()) { ph[phcounter] = pCur.getString(pCur.getColumnIndex(PNUM)); phType[phcounter] = pCur.getString(pCur.getColumnIndex(PHONETYPE)); phcounter++; } pCur.close(); } // Query email addresses for this contact (may be more than one), so use a // while-loop to move the cursor to the next row until moveToNext() returns // false, indicating no more rows. Store the results in arrays since there may // be more than one email address stored per contact. emcounter = 0; Cursor emailCur = cr.query(EURI, null, EID + " = ?", new String[]{id}, null); while (emailCur.moveToNext()) { em[emcounter] = emailCur.getString(emailCur.getColumnIndex(EMAIL)); emType[emcounter] = emailCur.getString(emailCur.getColumnIndex(EMAILTYPE)); emcounter++; } emailCur.close(); // Query Address (assume only one address stored for simplicity). If there is // more than one address we loop through all with the while-loop but keep // only the last one. addcounter = 0; Cursor addCur = cr.query(AURI, null, AID + " = ?", new String[]{id}, null); while (addCur.moveToNext()) { street = addCur.getString(addCur.getColumnIndex(STREET)); city = addCur.getString(addCur.getColumnIndex(CITY)); state = addCur.getString(addCur.getColumnIndex(STATE)); postcode = addCur.getString(addCur.getColumnIndex(POSTCODE)); country = addCur.getString(addCur.getColumnIndex(COUNTRY)); addcounter++; } addCur.close(); // Write identifiers for this contact to the SD card file pw.println(name + " ID=" + id + " LOOKUP_KEY=" + lookupKey); // Write list of phone numbers for this contact to SD card file for (int i = 0; i < phcounter; i++) { pw.println(" phone=" + ph[i] + " type=" + phType[i] + " (" + getPhoneType(phType[i]) + ") "); } // Write list of email addresses for this contact to SD card file for (int i = 0; i < emcounter; i++) { pw.println(" email=" + em[i] + " type=" + emType[i] + " (" + getEmailType(emType[i]) + ") "); } // If street address is stored for contact, write it to SD card file if (addcounter > 0) { if (street != null) pw.println(" street=" + street); if (city != null) pw.println(" city=" + city); if (state != null) pw.println(" state/region=" + state); if (postcode != null) pw.println(" postcode=" + postcode); if (country != null) pw.println(" country=" + country); } } } // Flush the PrintWriter to ensure everything pending is output before closing pw.flush(); pw.close(); f.close(); } catch (FileNotFoundException e) { e.printStackTrace(); Log.i("MEDIA", "File not found. Did you" + " add a WRITE_EXTERNAL_STORAGE permission to the manifest file? "); } catch (IOException e) { e.printStackTrace(); } // Return the string built by the StringBuilder return stringBuilder.toString(); } /** * Method to check whether external media available and writable and to find the * root of the external file system. */ private void checkExternalMedia() { // Check external media availability. This is adapted from // http://developer.android.com/guide/topics/data/data-storage.html#filesExternal boolean mExternalStorageAvailable = false; boolean mExternalStorageWriteable = false; String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { // We can read and write the media mExternalStorageAvailable = mExternalStorageWriteable = true; } else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { // We can only read the media mExternalStorageAvailable = true; mExternalStorageWriteable = false; } else { // Can't read or write mExternalStorageAvailable = mExternalStorageWriteable = false; } // Find the root of the external storage for this app and output external storage info to screen root = this.getExternalFilesDir(null); tv.append("External storage: Exists=" + mExternalStorageAvailable + ", Writable=" + mExternalStorageWriteable + "\nRoot=" + root + "\n"); } /** * Method to return label corresponding to phone type code. Data for correspondence from * http://developer.android.com/reference/android/provider/ContactsContract.CommonDataKinds.Phone.html */ private String getPhoneType(String index) { if (index.trim().equals("1")) { return "home"; } else if (index.trim().equals("2")) { return "mobile"; } else if (index.trim().equals("3")) { return "work"; } else if (index.trim().equals("7")) { return "other"; } else { return "?"; } } /** * Method to return label corresponding to email type code. Data for correspondence from * http://developer.android.com/reference/android/provider/ContactsContract. * CommonDataKinds.Email.html */ private String getEmailType(String index) { if (index.trim().equals("1")) { return "home"; } else if (index.trim().equals("2")) { return "work"; } else if (index.trim().equals("3")) { return "other"; } else if (index.trim().equals("4")) { return "mobile"; } else { return "?"; } } // Subclass AsyncTask to perform the parsing of the contacts list on a background thread. The three // argument types inside the < > are (1) a type for the input parameters (Void in this case), // (2) a type for any published progress during the background task (Void in this case, because // we aren't going to publish progress), and (3) a type for the object returned from the background // task (in this case it is type String). private class BackgroundProcessor extends AsyncTask<Void, Void, String> { // This method executes the task on a background thread @Override protected String doInBackground(Void... params) { // Execute the processContacts() method on this background thread and return from // it a string containing the list of contacts on the phone. The method processContacts // will also write an output file containing a list of information (phone, email, ...) // for each contact. The string returned will be the argument s in the method onPostExecute(s) // below, and in that method we shall update the screen view to display the list of // contacts. return processContacts(); } // This method executed before the thread run by doInBackground. It runs on the main UI thread, // so we can touch the UI views from here. @Override protected void onPreExecute() { // Hide the textview and display the progress bar while thread running tv.setVisibility(TextView.INVISIBLE); progressBar.setVisibility(ProgressBar.VISIBLE); } // This method executed after the thread run by doInBackground has returned. The variable s // passed is the string value returned by doInBackground. This method executes on // the main UI thread, so we can update the view tv and the ProgressBar progressBar // from it. @Override protected void onPostExecute(String s) { // Append the list of contacts to the TextView tv.append(s); // Stop the progress bar and make the TextView visible progressBar.setVisibility(View.GONE); tv.setVisibility(TextView.VISIBLE); } } }
This layout and Java coding will permit us to read the contacts information from an Android device (or a simulated contacts list for an emulator), display the list of contact names in a scrolling textfield on the main display, and output the detailed contact information for each contact to a file on the external storage (SD card or equivalent).
Let us test this app on a phone, tablet, or emulator running Android 6 or later. This will force us to deal with the new runtime permissions for "dangerous" permissions implemented with Android 6 (API 23), which have been discussed and implemented already in the Map Example project for Location permissions. The Android API versions used in the build process are specified in the Gradle Scripts/build.gradle(Module: app) file and will look something like this:
apply plugin: 'com.android.application' android { compileSdkVersion 23 buildToolsVersion "23.0.3" defaultConfig { applicationId "com.lightcone.readcontacts" minSdkVersion 16 targetSdkVersion 23 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:23.4.0' }
In earlier versions of Android developed using the Eclipse IDE these constraints on SDK versions were specified directly in the AndroidManifest.xml file. Now under Android Studio the proper place to specify them is in the Gradle build file for the module. When you create a new project under Android Studio this file is created and filled out with default numbers, but you can edit it to modify the build, as we will do below. |
First run the app on a phone or emulator running Android 6 or later with the targetSdkVersion and compileSdkVersion each set to at least 23 in the Gradle Scripts/build.gradle(Module: app) file. This should give an error when the installed app automatically attempts to execute, as illustrated in the left screen shot below from a phone running Android 6.0.1 Marshmallow. Next, change the targetSdkVersion to 22 (Android Studio will prompt for a re-sync of Gradle when you make this change; do it). Now the app executed on the same phone gives the screenshot below-right, for which the app is functioning correctly and displays a scrollable list of contacts (as we shall discuss further below).
Why this difference? A look at the logcat output identifies the reason for the forced close when running API 23 as a Java security exception:
07-10 20:42:28.063 E/AndroidRuntime(14295): Caused by: java.lang.SecurityException: Permission Denial: opening provider com.android.providers.contacts.ContactsProvider2 from ProcessRecord{5da8a54 14295:com.lightcone.readcontacts/u0a318} (pid=14295, uid=10318) requires android.permission.READ_CONTACTS or android.permission.WRITE_CONTACTS 07-10 20:42:28.063 E/AndroidRuntime(14295): at com.lightcone.readcontacts.MainActivity.processContacts(MainActivity.java:138) 07-10 20:42:28.063 E/AndroidRuntime(14295): at com.lightcone.readcontacts.MainActivity.access$100(MainActivity.java:23) 07-10 20:42:28.063 E/AndroidRuntime(14295): at com.lightcone.readcontacts.MainActivity$BackgroundProcessor.doInBackground(MainActivity.java:343) 07-10 20:42:28.063 E/AndroidRuntime(14295): at com.lightcone.readcontacts.MainActivity$BackgroundProcessor.doInBackground(MainActivity.java:329) 07-10 20:42:28.072 W/ActivityManager( 917): Force finishing activity com.lightcone.readcontacts/.MainActivity
The READ_CONTACTS permission is "dangerous" and therefore API 23 and later requires that it not only be declared in the Manifest file (which we have done), but it must also be given (once) at runtime by the user. No provision has been made for that thus far, so our app fails because it tries to use a permission that has not been given by the user. You can confirm this. Although the app compiled under API 23 failed when executed, it should have been installed. Go to Settings > Apps > ReadContacts > Permissions. There you will find that two permissions have not been given: Contacts, and Storage. Use the button on that screen to turn the Contacts permission ON manually. Now if you open the app you should find that the app functions correctly, giving the rightmost screenshot displayed above!
We will deal later with the other permission shown on that screen (Storage). For Android 6 it will generally not be required as long as we write only into the app-specific external storage area described in Accessing the File System (which will be the case here). It, and the READ_CONTACTS permission, must be included in the Manifest file for backward compatibility with the Android permissions model before API 23. |
This workaround confirms the source of the problem, but having the user turn on the required permissions manually is not a very elegant solution! As in the Map Example project, we need to implement a programmatic solution that checks for the permission before it is needed, prompts the user to give the permission through a system-generated dialog on the screen, and then deals with the response of the user to that dialog. We have already implemented this in Map Example for a different "dangerous" permission (Location), so the same procedure can be transcribed to here for the READ_CONTACTS permission.
The following listing shows the file MainActivity.java after it is modified to include the runtime permissions queries transcribed from the implementation in Map Example for Location services runtime permissions (see the files ShowMap.java and MapMe.java in that project).
package <YourNamespace>.readcontacts; import android.Manifest; import android.content.Context; import android.content.DialogInterface; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.ActivityCompat; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.view.View; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import android.content.ContentResolver; import android.database.Cursor; import android.net.Uri; import android.os.Environment; import android.provider.ContactsContract; import android.util.Log; import android.widget.ProgressBar; import android.widget.TextView; public class MainActivity extends AppCompatActivity { // To suppress notational clutter and make structure clearer, define some shorthand constants. private static final Uri URI = ContactsContract.Contacts.CONTENT_URI; private static final Uri PURI = ContactsContract.CommonDataKinds.Phone.CONTENT_URI; private static final Uri EURI = ContactsContract.CommonDataKinds.Email.CONTENT_URI; private static final Uri AURI = ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI; private static final String ID = ContactsContract.Contacts._ID; private static final String DNAME = ContactsContract.Contacts.DISPLAY_NAME; private static final String HPN = ContactsContract.Contacts.HAS_PHONE_NUMBER; private static final String LOOKY = ContactsContract.Contacts.LOOKUP_KEY; private static final String CID = ContactsContract.CommonDataKinds.Phone.CONTACT_ID; private static final String EID = ContactsContract.CommonDataKinds.Email.CONTACT_ID; private static final String AID = ContactsContract.CommonDataKinds.StructuredPostal.CONTACT_ID; private static final String PNUM = ContactsContract.CommonDataKinds.Phone.NUMBER; private static final String PHONETYPE = ContactsContract.CommonDataKinds.Phone.TYPE; private static final String EMAIL = ContactsContract.CommonDataKinds.Email.DATA; private static final String EMAILTYPE = ContactsContract.CommonDataKinds.Email.TYPE; private static final String STREET = ContactsContract.CommonDataKinds.StructuredPostal.STREET; private static final String CITY = ContactsContract.CommonDataKinds.StructuredPostal.CITY; private static final String STATE = ContactsContract.CommonDataKinds.StructuredPostal.REGION; private static final String POSTCODE = ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE; private static final String COUNTRY = ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY; private static final int MAX_NUMBER_ENTRIES = 5; private String id; private String lookupKey; private String name; private String street; private String city; private String state; private String postcode; private String country; private String ph[]; private String phType[]; private String em[]; private String emType[]; private File root; private int emcounter; private int phcounter; private int addcounter; private TextView tv; private ProgressBar progressBar; // User defines value of REQUEST_CONTACTS. It will identify a permission request // specifically for READ_CONTACTS. Define a different integer for each // "dangerous" permission that you will request at runtime (in this example there // is only one). final private int REQUEST_CONTACTS = 1; // User-defined integer private static final String TAG = "RCONTACTS"; private static final int dialogIcon = R.mipmap.ic_launcher; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = (TextView) findViewById(R.id.TextView01); // Allow for up to MAX_NUMBER_ENTRIES email and phone entries for a contact em = new String[MAX_NUMBER_ENTRIES]; emType = new String[MAX_NUMBER_ENTRIES]; ph = new String[MAX_NUMBER_ENTRIES]; phType = new String[MAX_NUMBER_ENTRIES]; progressBar = (ProgressBar) findViewById(R.id.progress_bar); // Call a check for runtime permissions checkRuntimePermissions(); } // Method executed to run app if permission has been granted public void doTheStuff() { // Check that external media available and writable checkExternalMedia(); // Read the contacts and output to a file. Process this on a background // thread defined by an instance of AsyncTask because it will typically // take several seconds to process a few hundred contacts. We will display // an indeterminate progress bar to the user while the contacts are being // processed. new BackgroundProcessor().execute(); } /* Method to check runtime permissions. For Android 6 (API 23) and beyond, we must check for the "dangerous" permission READ_CONTACTS at runtime (in addition to declaring it in the manifest file). The following code checks for this permission. If it has already been granted, it proceeds as normal. If it has not yet been granted by the user, the user is presented with an opportunity to grant it. In general, the rest of this class will not execute until the user has granted such permission. */ public void checkRuntimePermissions() { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { /* Permission has not been granted by user previously. Request it now. The system will present a dialog to the user requesting the permission, with options "accept", "deny", and a box to check "don't ask again". When the user chooses, the system will then fire the onRequestPermissionsResult() callback, passing in the user-defined integer defining the type of permission request (REQUEST_CONTACTS in this case) and the "accept" or "deny" user response. We deal appropriately with the user response in our override of onRequestPermissionsResult() below.*/ ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_CONTACTS); } else { Log.i(TAG, "Permission has been granted"); doTheStuff(); } } /*Following method invoked by the system after the user response to a runtime permission request (Android 6, API 23 and beyond implement such runtime permissions). The system passes to this method the user's response, which you then should act upon in this method. This method can respond to more than one type permission. The user-defined integer requestCode (passed in the call to ActivityCompat.requestPermissions) distinguishes which permission is being processed. In the present example there will be only one type of permission checked. */ @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { // Since this method may handle more than one type of permission, distinguish which one by a // switch on the requestCode that you defined that is passed back to you by the system. switch (requestCode) { // The permission response was for fine location case REQUEST_CONTACTS: Log.i(TAG, "Read contacts permission granted: requestCode=" + requestCode); // If the request was canceled by user, the results arrays are empty if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Permission was granted. Carry on as we would without the permission request doTheStuff(); } else { Log.i(TAG, "onRequestPermissionsResult - permission denied: requestCode=" + requestCode); // The permission was denied. Warn the user of the consequences and give // them one last time to enable the permission. showTaskDialog(1, "Warning!", "This app will not function without this permission!", dialogIcon, this, "OK, Do Over", "Refuse Permission"); } return; } } /** * Method showTaskDialog() creates a custom alert dialog. This dialog presents text defining * a choice to the user and has buttons for a binary choice. Pressing the rightmost button * will execute the method positiveTask(id) and pressing the leftmost button will execute the * method negativeTask(id). You should define appropriate actions in each. (If the * negativeTask(id) method is empty the default action is just to close the dialog window.) * The argument id is a user-defined integer distinguishing multiple uses of this method in * the same class. The programmer should switch on id in the response methods * positiveTask(id) and negativeTask(id) to decide which alert dialog to respond to. * No theme argument is given to the AlertDialog.Builder below so the default dialog theme * will be used. To supply your own theme, add a second (theme) argument to the AlertDialog.Builder * constructor referencing your custom dialog theme defined in styles.xml. */ private void showTaskDialog(int id, String title, String message, int icon, Context context, String positiveButtonText, String negativeButtonText) { final int fid = id; // Must be final to access from anonymous inner class below AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setMessage(message).setTitle(title).setIcon(icon); // Add the right button builder.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { positiveTask(fid); } }); // Add the left button builder.setNegativeButton(negativeButtonText, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { negativeTask(fid); } }); AlertDialog dialog = builder.create(); dialog.show(); } // Method to be executed if user chooses negative button in dialog. private void negativeTask(int id) { // Use id to distinguish if more than one usage of the alert dialog switch (id) { case 1: // Warning that this part of app not enabled String warn = "Exiting the app. It is installed but not enabled. To enable this "; warn += "app you may manually enable Contacts permission in "; warn += " Settings > App > ReadContacts > Permissions."; // New single-button dialog showTaskDialog(2, "Task not enabled!", warn, dialogIcon, this, "", "OK"); break; case 2: // Exit the app since permission was denied finish(); break; } } // Method to execute if user chooses positive button ("OK, I'll Do It"). This starts the check // of runtime permissions again. private void positiveTask(int id) { // Use id to distinguish if more than one usage of the alert dialog switch (id) { case 1: // User agreed to enable location checkRuntimePermissions(); break; case 2: break; } } // Method to process contacts (reads them and writes formatted output to a file on device, in // addition to concatenating a string listing the contacts in the list that will be displayed // on the phone screen). This method will be invoked from a background thread since it may // take some time to execute if the contacts list is long and we want the UI to remain // responsive. private String processContacts() { /** Open a PrintWriter wrapping a FileOutputStream so that we can send output from a query of the Contacts database to a file on the SD card. Must wrap the whole thing in a try-catch to catch file not found and i/o exceptions. Note that since we are writing to external media we must add a WRITE_EXTERNAL_STORAGE permission to the manifest file prior to Android 4.4. Otherwise a FileNotFoundException will be thrown. Since we are going to write to the external storage specifically for this app, this permission is not required after Android 4.4, but it is included in the manifest for backward compatibility with earlier versions of Android. Because in Android 6 and later this permission is not required for the write we are going to do, it is not necessary to worry about runtime permissions for it as we did above for READ_CONTACTS. */ // Create a StringBuilder for efficient concatenation of contact list into single string. // (We cannot append directly to the views from here because this will be run on background // thread and we cannot touch views on main thread from here. We will concatenate the // string here and return it, and then update the view from the onPostExecute method of // AsyncTask (which can interact with views on the main thread). StringBuilder stringBuilder = new StringBuilder(); // This will set up output to the root of external storage allocated for this app. As noted // above, for Android 4.4 and later this no longer requires WRITE_EXTERNAL_STORAGE permission. // See the project WriteSDCard for more information about writing to a file on the SD card. File dir = new File(root.getAbsolutePath()); dir.mkdirs(); File file = new File(dir, "phoneData.txt"); stringBuilder.append("Wrote " + file + "\nfor following contacts:\n"); try { FileOutputStream f = new FileOutputStream(file); PrintWriter pw = new PrintWriter(f); // Main loop to query the contacts database, extracting the information. See // http://www.higherpass.com/Android/Tutorials/Working-With-Android-Contacts/ ContentResolver cr = getContentResolver(); Cursor cu = cr.query(URI, null, null, null, null); if (cu.getCount() > 0) { // Loop over all contacts while (cu.moveToNext()) { // Initialize storage variables for the new contact street = ""; city = ""; state = ""; postcode = ""; country = ""; // Get ID information (id, name and lookup key) for this contact. id is an identifier // number, name is the name associated with this row in the database, and // lookupKey is an opaque value that contains hints on how to find the contact // if its row id changed as a result of a sync or aggregation. id = cu.getString(cu.getColumnIndex(ID)); name = cu.getString(cu.getColumnIndex(DNAME)); lookupKey = cu.getString(cu.getColumnIndex(LOOKY)); // Append list of contacts to the StringBuilder object stringBuilder.append("\n" + id + " " + name); // Query phone numbers for this contact (may be more than one), so use a // while-loop to move the cursor to the next row until moveToNext() returns // false, indicating no more rows. Store the results in arrays since there may // be more than one phone number stored per contact. The if-statement // enclosing everything ensures that the contact has at least one phone // number stored in the Contacts database. phcounter = 0; if (Integer.parseInt(cu.getString(cu.getColumnIndex(HPN))) > 0) { Cursor pCur = cr.query(PURI, null, CID + " = ?", new String[]{id}, null); while (pCur.moveToNext()) { ph[phcounter] = pCur.getString(pCur.getColumnIndex(PNUM)); phType[phcounter] = pCur.getString(pCur.getColumnIndex(PHONETYPE)); phcounter++; } pCur.close(); } // Query email addresses for this contact (may be more than one), so use a // while-loop to move the cursor to the next row until moveToNext() returns // false, indicating no more rows. Store the results in arrays since there may // be more than one email address stored per contact. emcounter = 0; Cursor emailCur = cr.query(EURI, null, EID + " = ?", new String[]{id}, null); while (emailCur.moveToNext()) { em[emcounter] = emailCur.getString(emailCur.getColumnIndex(EMAIL)); emType[emcounter] = emailCur.getString(emailCur.getColumnIndex(EMAILTYPE)); emcounter++; } emailCur.close(); // Query Address (assume only one address stored for simplicity). If there is // more than one address we loop through all with the while-loop but keep // only the last one. In a realistic application one might want to handle the // choice among different addresses for the same user in a more sophisticated // way. addcounter = 0; Cursor addCur = cr.query(AURI, null, AID + " = ?", new String[]{id}, null); while (addCur.moveToNext()) { street = addCur.getString(addCur.getColumnIndex(STREET)); city = addCur.getString(addCur.getColumnIndex(CITY)); state = addCur.getString(addCur.getColumnIndex(STATE)); postcode = addCur.getString(addCur.getColumnIndex(POSTCODE)); country = addCur.getString(addCur.getColumnIndex(COUNTRY)); addcounter++; } addCur.close(); // Write identifiers for this contact to the SD card file pw.println(name + " ID=" + id + " LOOKUP_KEY=" + lookupKey); // Write list of phone numbers for this contact to SD card file for (int i = 0; i < phcounter; i++) { pw.println(" phone=" + ph[i] + " type=" + phType[i] + " (" + getPhoneType(phType[i]) + ") "); } // Write list of email addresses for this contact to SD card file for (int i = 0; i < emcounter; i++) { pw.println(" email=" + em[i] + " type=" + emType[i] + " (" + getEmailType(emType[i]) + ") "); } // If street address is stored for contact, write it to SD card file if (addcounter > 0) { if (street != null) pw.println(" street=" + street); if (city != null) pw.println(" city=" + city); if (state != null) pw.println(" state/region=" + state); if (postcode != null) pw.println(" postcode=" + postcode); if (country != null) pw.println(" country=" + country); } } } // Flush the PrintWriter to ensure everything pending is output before closing pw.flush(); pw.close(); f.close(); } catch (FileNotFoundException e) { e.printStackTrace(); Log.i("MEDIA", "File not found. Did you" + " add a WRITE_EXTERNAL_STORAGE permission to the manifest file? "); } catch (IOException e) { e.printStackTrace(); } // Return the string built by the StringBuilder return stringBuilder.toString(); } /** * Method to check whether external media available and writable and to find the * root of the external file system. */ private void checkExternalMedia() { // Check external media availability. This is adapted from // http://developer.android.com/guide/topics/data/data-storage.html#filesExternal boolean mExternalStorageAvailable = false; boolean mExternalStorageWriteable = false; String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { // We can read and write the media mExternalStorageAvailable = mExternalStorageWriteable = true; } else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { // We can only read the media mExternalStorageAvailable = true; mExternalStorageWriteable = false; } else { // Can't read or write mExternalStorageAvailable = mExternalStorageWriteable = false; } // Find the root of the external storage for this app and output external storage info to screen root = this.getExternalFilesDir(null); tv.append("External storage: Exists=" + mExternalStorageAvailable + ", Writable=" + mExternalStorageWriteable + "\nRoot=" + root + "\n"); } /** * Method to return label corresponding to phone type code. Data for correspondence from * http://developer.android.com/reference/android/provider/ContactsContract.CommonDataKinds.Phone.html */ private String getPhoneType(String index) { if (index.trim().equals("1")) { return "home"; } else if (index.trim().equals("2")) { return "mobile"; } else if (index.trim().equals("3")) { return "work"; } else if (index.trim().equals("7")) { return "other"; } else { return "?"; } } /** * Method to return label corresponding to email type code. Data for correspondence from * http://developer.android.com/reference/android/provider/ContactsContract. * CommonDataKinds.Email.html */ private String getEmailType(String index) { if (index.trim().equals("1")) { return "home"; } else if (index.trim().equals("2")) { return "work"; } else if (index.trim().equals("3")) { return "other"; } else if (index.trim().equals("4")) { return "mobile"; } else { return "?"; } } // Subclass AsyncTask to perform the parsing of the contacts list on a background thread. The three // argument types inside the < > are (1) a type for the input parameters (Void in this case), // (2) a type for any published progress during the background task (Void in this case, because // we aren't going to publish progress), and (3) a type for the object returned from the background // task (in this case it is type String). private class BackgroundProcessor extends AsyncTask<Void, Void, String> { // This method executes the task on a background thread @Override protected String doInBackground(Void... params) { // Execute the processContacts() method on this background thread and return from // it a string containing the list of contacts on the phone. The method processContacts // will also write an output file containing a list of information (phone, email, ...) // for each contact. The string returned will be the argument s in the method onPostExecute(s) // below, and in that method we shall update the screen view to display the list of // contacts. return processContacts(); } // This method is executed before the thread run by doInBackground. It runs on the main UI thread, // so we can touch the UI views from here. @Override protected void onPreExecute() { // Hide the textview and display the progress bar while thread running. The progress bar is // displayed inline on the screen that the text output will go to. See the format defined // in activity_main.xml and in the Progress Bars project. tv.setVisibility(TextView.INVISIBLE); progressBar.setVisibility(ProgressBar.VISIBLE); } // This method executed after the thread run by doInBackground has returned. The variable s // passed is the string value returned by doInBackground. This method executes on // the main UI thread, so we can update the view tv and the ProgressBar progressBar // from it. @Override protected void onPostExecute(String s) { // Append the list of contacts to the TextView tv.append(s); // Stop the progress bar and make the TextView visible progressBar.setVisibility(View.GONE); tv.setVisibility(TextView.VISIBLE); } } }
The primary additions to the original version are marked in red (we have not bothered to mark some additional elaborations in the comments statements). In addition, the methods checkExternalMedia() and new BackgroundProcessor().execute() have been moved from the onCreate method to the new checkRuntimePermissions() method to prevent them from executing before runtime permissions have been given. Now if this app is executed on a device running Android 6 or later, you should be presented with options to accept the runtime permissions (analogous to those for Map Example) and, if accepted, a screen like the rightmost screenshot displayed above should result.
The code implementing the runtime permissions is almost identical to the code implementing runtime permissions in the Map Example project, except the permission in this case is READ_CONTACTS instead of FINE_LOCATION. Likewise the explanation of the functionality is essentially the same. Hence the reader is referred to the earlier discussion in Map Example and the abundant comments in the present code for an explanation of how the runtime permissions work in this example.
We have already shown above a screenshot of the app in action once the permissions issues discussed in the preceding section are taken care of. Note that the TextView is scrollable, as indicated by the scrollbar on the upper right side (there are another 800 or so contacts that are accessible by scrolling in this example). The following listing illustrates a portion of the corresponding output to the file phoneData.txt on the SD card, for two dummy contact entries in the above list, Test1 Contact and Test2 Contact (not visible in the portion of the contacts list displayed above).
Test1 Contact ID=198 LOOKUP_KEY=0n4F314D4F267C2D45434F292D4F phone=000-000-0000 type=1 (home) phone=333-333-3333 type=2 (mobile) phone=222-222-2222 type=3 (work) email=contact1@home.com type=1 (home) email=contact1@work.com type=2 (work) street=1111 MyStreet city=MyFairCity state/region=MyState postcode=000000 country=USA Test2 Contact ID=202 LOOKUP_KEY=0n4F314D4F267E2D45434F292D4F phone=888-888-8888 type=2 (mobile) phone=999-999-9999 type=1 (home) phone=777-777-7777 type=3 (work) email=contact2@home.com type=1 (home) email=contact2@work.com type=2 (work) street=2222 MyStreet city=MyCity state/region=MyState postcode=111111 country=USA
(See Transferring Files for methods to move the file that we have just written from the SD card on the phone to a computer, or to read it directly on the device.)
As indicated in the screen output displayed above, and explained in more detail in Accessing the File System,the code that we have implemented causes the output file to be written to
/storage/emulated/0/android/data/com.lightcone.readcontacts/files/phoneData.txt
on my phone (which does not require a WRITE_EXTERNAL runtime permission for Android 4.4 and later because it is in the external storage area allocated for the present app). You can check this using WiFi Explorer:
Double-clicking on the file name phoneData.txt will display the text in the browser, or WiFi Explorer can be used to transfer the file from the device to your computer for further manipulation.
Now we shall explain how this code accomplishes the tasks demonstrated above. Those things that should be familiar from earlier projects will be glossed over and we shall concentrate on those aspects of Android programming that we have not yet encountered, in particular issues associated with retrieval of information from a SQL database. (A brief review of SQL concepts and terminology may be found in this SQL tutorial.)
The layout in activity_main.xml is fairly standard except that we use a a ScrollView wrapping a TextView, which provides a vertically scrolling container for the TextView. (It is also possible to set attributes to get TextView to handle its own scrolling.) In the code below we will append text to the TextView using the method append(CharSequence text). In addition, we add a ProgressBar to the layout that will be displayed while the app is retrieving the contacts list for display and output, as for examples discussed in the Progress Bars project. The manifest file is the default one, except for the added permission statements
<uses-permission android:name="android.permission.READ_CONTACTS"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
which are required for backward compatibility with earlier versions of Android so that we can read the contacts and write to the SD card.
In the contacts database queries we are going to need a number of constants from classes such as
and other nested classes of the ContactsContract.CommonDataKinds class as arguments for various methods. Because these constants written out are quite long strings (since in Java specifying a class constant requires that the class be specified), we first define a number of shorthand references to these constants, such as
private static final Uri URI = ContactsContract.Contacts.CONTENT_URI;
to make the subsequent code more transparent, and then define a number of variables and arrays that we will need to store the results of the database queries.
We set up the output to the SD card, as described in Accessing the File System.
Note that since we are writing to external media we must add a WRITE_EXTERNAL_STORAGE permission to the manifest file (as we did above). Otherwise a FileNotFoundException will be thrown.
Accessing, processing, and writing out the Contacts entries can take 10 seconds or more even a fast phone phone with hundreds of entries (this app takes about 10 seconds to process ~800 contacts on a Huawei Nexus 6P phone). If this were run on the main UI thread it would lock up the UI for that period. Thus, we launch a background thread for this task by instantiating the inner class BackgroundProcessor, which extends AsyncTask. The background thread calls the method processContacts() to do most of its work, and while it is working we display an indeterminate progress bar (a spinning circle) to the user to indicate that patience is required while a task is being executed in the background. As soon as the background thread processing the Contacts completes, we hide the progress bar and display the textfield showing the Contacts list on the screen.
We query the contacts database using processContacts() executed on the background thread.
ContentResolver is the class that provides access to the content model for an application's package. |
The query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) method that cr inherits from ContentResolver queries the Uri, returning a Cursor object cu over the whole set positioned before the first entry (or null if the object does not exist). The method query has the following arguments:
Android uses a
Cursor
as a return value for database queries. The Cursor provides random read and write access to the results set of the database query and you may think of it as a pointer to the set of results from such a query. Important methods of
Cursor that we shall employ include
|
Now that we have the Cursor cu, the work is all done inside the
while (cu.moveToNext()) { ... }
loop. Since cu.moveToNext() returns false when positioned past the last row, the while-loop will loop over all rows of the Cursor object cu. In the contacts database, these rows correspond to individual contacts.
More precisely, the rows that we shall retrieve correspond to some aggregation of the contacts information that Android believes to correspond to the same contact. This aggregation becomes an issue if there are multiple entries for the same contact stored in the database, as often happens either for deliberate reasons or because of contact information being copied from one phone to another. For purposes of illustration we shall keep things simple and assume that this aggregation has been done correctly under the hood and consider a row to correspond to a single raw entry for a single contact in the database. A more detailed discussion of aggregation and sync issues for the contacts database may be found in ContactsContract. |
We first assign to some String variables identification information for this contact, using
id = cu.getString(cu.getColumnIndex(ID)); name = cu.getString(cu.getColumnIndex(DNAME)); lookupKey = cu.getString(cu.getColumnIndex(LOOKY));
where
In these compound expressions we first use the getColumnIndex(columnName) method of Cursor to get the column index for particular entries of contact information, and then use that as an argument for the getString(columnIndex) method, which returns the value of the column entry referenced by columnIndex for this contact as a String. The arguments for getColumnIndex() in these statements correspond to
Now, for each contact we will illustrate how to extract phone number, email, and mailing (postal) address information. For the case of phone numbers and email we shall allow explicitly for the possibility that a contact has more than one of each. However, to keep our discussion and coding example simple we will assume that each contact has only one set of postal address information stored in the contacts database (we will read all, but keep only the last if there is more than one address set stored).
There are additional information tables in the contacts database that can be accessed by methods similar to the ones illustrated here for phone numbers, email, and postal address (see Exercise 1).
Some examples include
|
For extracting phone numbers, another while-loop is placed inside the outer while-loop that is iterating over contacts. For a given contact we
This logic gets the ColumnIndex of ContactsContract.Contacts.HAS_PHONE_NUMBER (which is the value of the MainActivity variable HPN), and then uses that to extract the String corresponding to that column for this contact. This returns "1" if the contact has at least one phone number and "0" otherwise. The method parseInt(String s) parses the specified string as a signed integer value. Thus the argument of the if-statement will be true only if there is at least one phone number stored for the contact, in which case we process the phone number data; otherwise we go on to look at email data.if (Integer.parseInt(cu.getString(cu.getColumnIndex(HPN))) > 0) { ... }
where the arguments of the query() method areCursor pCur = cr.query(PURI, null, CID + " = ?", new String[]{id}, null);
It is recommended to use question mark parameter markers such as 'phone=?' instead of explicit values in the selection parameter for reasons of efficiency. See the documentation for query(). |
if (Integer.parseInt(cu.getString(cu.getColumnIndex(HPN))) > 0) { Cursor pCur = cr.query(PURI, null, CID + " = ?", new String[]{id}, null); while (pCur.moveToNext()) { ph[phcounter] = pCur.getString(pCur.getColumnIndex(PNUM)); phType[phcounter] = pCur.getString(pCur.getColumnIndex(PHONETYPE)); phcounter ++; } pCur.close(); }
Next a completely analogous while-loop is executed over the email addresses for the contact.
with the uri for the query specified by the constant relevant for email: EURI = ContactsContract.CommonDataKinds.Email.CONTENT_URI.Cursor emailCur = cr.query(EURI, null, EID + " = ?", new String[]{id}, null);
with the variableswhile (emailCur.moveToNext()) { em[emcounter] = emailCur.getString(emailCur.getColumnIndex(EMAIL)); emType[emcounter] = emailCur.getString(emailCur.getColumnIndex(EMAILTYPE)); emcounter ++; } emailCur.close();
As a last example of retrieving contact information, we extract the street address information for this contact in a way similar to that for phone numbers and email.
with the uri for the query specified by the constant relevant for street addresses: AURI = ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI.Cursor addCur = cr.query(AURI, null, AID + " = ?", new String[]{id}, null);
where the getColumnIndex(columnName) arguments arewhile (addCur.moveToNext()) { street = addCur.getString(addCur.getColumnIndex(STREET)); city = addCur.getString(addCur.getColumnIndex(CITY)); state = addCur.getString(addCur.getColumnIndex(STATE)); postcode = addCur.getString(addCur.getColumnIndex(POSTCODE)); country = addCur.getString(addCur.getColumnIndex(COUNTRY)); addcounter ++; } addCur.close();
To avoid cluttering up our example with a number of new arrays, we have assumed that only one street address is stored for the contact. If there is more than one, we see from the while-loop above that only the last one will be retrieved for the contact's street address.
The types for phone numbers and email that have been retrieved from the Contacts database are strings representing integer codes. The methods getEmailType(String index) and getPhonetype(index) accept an index number string for an email type or phone type and return a more human-readable string such as "Home" or "Work", according to the correspondences in the following table
Type | Phone | |
Home | 1 | 1 |
Mobile | 4 | 2 |
Work | 2 | 3 |
Other | 3 | 7 |
which is inferred from documentation in CommonDataKinds.Phone and CommonDataKinds.Email. Important methods used in getEmailType() and getPhoneType() include
Now all the contact information that we sought is stored in arrays for this contact. It is then an easy matter to write it to a file on the SD card using the methods described in Accessing the File System. We use the println() method of PrintWriter to write in successive lines the ID information, phone numbers, email addresses, and street address for this contact to the file <root>/phoneData.txt, where .<root> is the root of the part of the external file system allocated for this app (or a symbolic link to it).
The complete project for the application described above is archived on GitHub at the link ReadContacts. Instructions for installing it in Android Studio may be found in Packages for All Projects. |
Last modified: July 25, 2016