/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.googlecode.android_scripting.facade; import android.app.Service; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Intent; import android.content.res.AssetFileDescriptor; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract; import android.util.Log; import com.googlecode.android_scripting.jsonrpc.RpcReceiver; import com.googlecode.android_scripting.rpc.Rpc; import com.googlecode.android_scripting.rpc.RpcOptional; import com.googlecode.android_scripting.rpc.RpcParameter; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** * Provides access to contacts related functionality. */ public class ContactsFacade extends RpcReceiver { private static final String TAG = "ContactsFacade"; private static final Uri CONTACTS_URI = ContactsContract.Contacts.CONTENT_URI; private static final String ERASE_COMPLETE = "ContactsErased"; private final ContentResolver mContentResolver; private final Service mService; private final CommonIntentsFacade mCommonIntentsFacade; private final ContactsStatusReceiver mContactsStatusReceiver; private final EventFacade mEventFacade; private Uri mPhoneContent = null; private String mContactId; private String mPrimary; private String mPhoneNumber; private String mHasPhoneNumber; public ContactsFacade(FacadeManager manager) { super(manager); mService = manager.getService(); mContentResolver = mService.getContentResolver(); mCommonIntentsFacade = manager.getReceiver(CommonIntentsFacade.class); mContactsStatusReceiver = new ContactsStatusReceiver(); mContentResolver.registerContentObserver( ContactsContract.Contacts.CONTENT_URI, true, mContactsStatusReceiver); mEventFacade = manager.getReceiver(EventFacade.class); try { // Backward compatibility... get contract stuff using reflection Class<?> phone = Class.forName("android.provider.ContactsContract$CommonDataKinds$Phone"); mPhoneContent = (Uri) phone.getField("CONTENT_URI").get(null); mContactId = (String) phone.getField("CONTACT_ID").get(null); mPrimary = (String) phone.getField("IS_PRIMARY").get(null); mPhoneNumber = (String) phone.getField("NUMBER").get(null); mHasPhoneNumber = (String) phone.getField("HAS_PHONE_NUMBER").get(null); } catch (Exception e) { Log.e(TAG, "Unable to get field from Contacts Database"); } } private Uri getUri(Integer id) { return ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id); } @Rpc( description = "Displays a list of contacts to pick from.", returns = "A map of result values." ) public Intent contactsDisplayContactPickList() throws JSONException { return mCommonIntentsFacade.pick(CONTACTS_URI.toString()); } @Rpc( description = "Displays a list of phone numbers to pick from.", returns = "The selected phone number." ) public String contactsDisplayPhonePickList() throws JSONException { String phoneNumber = null; Intent data = mCommonIntentsFacade.pick(CONTACTS_URI.toString()); if (data != null) { Uri phoneData = data.getData(); Cursor cursor = mService.getContentResolver().query(phoneData, null, null, null, null); if (cursor != null) { if (cursor.moveToFirst()) { phoneNumber = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.PhoneLookup.NUMBER)); } cursor.close(); } } return phoneNumber; } @Rpc(description = "Returns a List of all possible attributes for contacts.") public List<String> contactsGetAttributes() { List<String> attributes = new ArrayList<String>(); Cursor cursor = mContentResolver.query(CONTACTS_URI, null, null, null, null); if (cursor != null) { String[] columns = cursor.getColumnNames(); for (int i = 0; i < columns.length; i++) { attributes.add(columns[i]); } cursor.close(); } return attributes; } @Rpc(description = "Returns a List of all contact IDs.") public List<Integer> contactsGetContactIds() { List<Integer> ids = new ArrayList<Integer>(); String[] columns = {"_id"}; Cursor cursor = mContentResolver.query(CONTACTS_URI, columns, null, null, null); if (cursor != null) { while (cursor.moveToNext()) { ids.add(cursor.getInt(0)); } cursor.close(); } return ids; } @Rpc(description = "Returns a List of all contacts.", returns = "a List of contacts as Maps") public List<JSONObject> contactsGetAllContacts( @RpcParameter(name = "attributes") @RpcOptional JSONArray attributes) throws JSONException { List<JSONObject> contacts = new ArrayList<JSONObject>(); String[] columns; if (attributes == null || attributes.length() == 0) { // In case no attributes are specified we set the default ones. columns = new String[] {ContactsContract.Contacts.NAME_RAW_CONTACT_ID, ContactsContract.Contacts.DISPLAY_NAME}; } else { // Convert selected attributes list into usable string list. columns = new String[attributes.length()]; for (int i = 0; i < attributes.length(); i++) { columns[i] = attributes.getString(i); } } List<String> queryList = new ArrayList<String>(); for (String s : columns) { queryList.add(s); } String[] query = queryList.toArray(new String[queryList.size()]); Cursor cursor = mContentResolver.query(CONTACTS_URI, query, null, null, null); if (cursor != null) { int idIndex = cursor.getColumnIndex("_id"); while (cursor.moveToNext()) { String id = cursor.getString(idIndex); JSONObject message = new JSONObject(); for (int i = 0; i < columns.length; i++) { String key = columns[i]; String value = cursor.getString(cursor.getColumnIndex(key)); if (mPhoneNumber != null) { if (key.equals("primary_phone")) { value = findPhone(id); } } message.put(key, value); } contacts.add(message); } cursor.close(); } return contacts; } private String findPhone(String id) { String phoneNumber = null; if (id == null || id.equals("")) { return phoneNumber; } try { if (Integer.parseInt(id) > 0) { Cursor pCur = mContentResolver.query( mPhoneContent, new String[] {mPhoneNumber}, mContactId + " = ? and " + mPrimary + "=1", new String[] {id}, null); if (pCur != null) { pCur.getColumnNames(); while (pCur.moveToNext()) { phoneNumber = pCur.getString(0); break; } } pCur.close(); } } catch (Exception e) { return null; } return phoneNumber; } @Rpc(description = "Returns contacts by ID.") public JSONObject contactsGetContactById( @RpcParameter(name = "id") Integer id, @RpcParameter(name = "attributes") @RpcOptional JSONArray attributes) throws JSONException { JSONObject contact = null; Uri uri = getUri(id); String[] columns; if (attributes == null || attributes.length() == 0) { // In case no attributes are specified we set the default ones. columns = new String[] {"_id", "name", "primary_phone", "primary_email", "type"}; } else { // Convert selected attributes list into usable string list. columns = new String[attributes.length()]; for (int i = 0; i < attributes.length(); i++) { columns[i] = attributes.getString(i); } } Cursor cursor = mContentResolver.query(uri, columns, null, null, null); if (cursor != null) { contact = new JSONObject(); cursor.moveToFirst(); for (int i = 0; i < columns.length; i++) { contact.put(columns[i], cursor.getString(i)); } cursor.close(); } return contact; } @Rpc(description = "Returns the number of contacts.") public Integer contactsGetCount() { Integer count = 0; Cursor cursor = mContentResolver.query(CONTACTS_URI, null, null, null, null); if (cursor != null) { count = cursor.getCount(); cursor.close(); } return count; } private String[] jsonToArray(JSONArray array) throws JSONException { String[] resultingArray = null; if (array != null && array.length() > 0) { resultingArray = new String[array.length()]; for (int i = 0; i < array.length(); i++) { resultingArray[i] = array.getString(i); } } return resultingArray; } private Uri getAllContactsVcardUri() { Cursor cursor = mContentResolver.query( ContactsContract.Contacts.CONTENT_URI, new String[] {ContactsContract.Contacts.LOOKUP_KEY}, null, null, null); if (cursor == null) { return null; } try { StringBuilder uriListBuilder = new StringBuilder(); int index = 0; while (cursor.moveToNext()) { if (index != 0) { uriListBuilder.append(':'); } uriListBuilder.append(cursor.getString(0)); index++; } return Uri.withAppendedPath( ContactsContract.Contacts.CONTENT_MULTI_VCARD_URI, Uri.encode(uriListBuilder.toString())); } finally { cursor.close(); } } @Rpc(description = "Erase all contacts in phone book.") public void contactsEraseAll() { Cursor cursor = mContentResolver.query( ContactsContract.Contacts.CONTENT_URI, new String[] {ContactsContract.Contacts.LOOKUP_KEY}, null, null, null); if (cursor == null) { return; } while (cursor.moveToNext()) { Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, cursor.getString(0)); mContentResolver.delete(uri, null, null); } mEventFacade.postEvent(ERASE_COMPLETE, null); return; } /** * Exactly as per <a href= * "http://developer.android.com/reference/android/content/ContentResolver.html#query%28android.net.Uri,%20java.lang.String[],%20java.lang.String,%20java.lang.String[],%20java.lang.String%29" * >ContentResolver.query</a> */ @Rpc(description = "Content Resolver Query", returns = "result of query as Maps") public List<JSONObject> contactsQueryContent( @RpcParameter( name = "uri", description = "The URI, using the content:// scheme, for the content to retrieve." ) String uri, @RpcParameter( name = "attributes", description = "A list of which columns to return. Passing null will return all columns" ) @RpcOptional JSONArray attributes, @RpcParameter(name = "selection", description = "A filter declaring which rows to return") @RpcOptional String selection, @RpcParameter( name = "selectionArgs", description = "You may include ?s in selection, which will be replaced by the values from selectionArgs" ) @RpcOptional JSONArray selectionArgs, @RpcParameter(name = "order", description = "How to order the rows") @RpcOptional String order) throws JSONException { List<JSONObject> queryResults = new ArrayList<JSONObject>(); String[] columns = jsonToArray(attributes); String[] args = jsonToArray(selectionArgs); Cursor cursor = mContentResolver.query(Uri.parse(uri), columns, selection, args, order); if (cursor != null) { String[] names = cursor.getColumnNames(); while (cursor.moveToNext()) { JSONObject message = new JSONObject(); for (int i = 0; i < cursor.getColumnCount(); i++) { String key = names[i]; String value = cursor.getString(i); message.put(key, value); } queryResults.add(message); } cursor.close(); } return queryResults; } @Rpc( description = "Content Resolver Query Attributes", returns = "a list of available columns for a given content uri" ) public JSONArray queryAttributes( @RpcParameter( name = "uri", description = "The URI, using the content:// scheme, for the content to retrieve." ) String uri) throws JSONException { JSONArray columns = new JSONArray(); Cursor cursor = mContentResolver.query(Uri.parse(uri), null, "1=0", null, null); if (cursor != null) { String[] names = cursor.getColumnNames(); for (String name : names) { columns.put(name); } cursor.close(); } return columns; } @Rpc(description = "Launches VCF import.") public void importVcf( @RpcParameter( name = "uri", description = "The URI, using the file:/// scheme, for the content to retrieve." ) String uri) { Intent intent = new Intent(); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setComponent( new ComponentName( "com.google.android.contacts", "com.google.android.apps.contacts.vcard.ImportVCardActivity")); intent.setData(Uri.parse(uri)); mService.startActivity(intent); } @Rpc(description = "Launches VCF export.") public void exportVcf( @RpcParameter( name = "path", description = "The file path, using the / scheme, for the content to save to." ) String path) { OutputStream out = null; StringBuilder string = new StringBuilder(); try { AssetFileDescriptor fd = mContentResolver.openAssetFileDescriptor(getAllContactsVcardUri(), "r"); FileInputStream inputStream = fd.createInputStream(); PrintWriter writer = new PrintWriter(path, "UTF-8"); int character; while ((character = inputStream.read()) != -1) { if ((char) character != '\r') { string.append((char) character); } } writer.append(string); writer.close(); } catch (IOException e) { Log.w(TAG, "Failed to export VCF."); } } private class ContactsStatusReceiver extends ContentObserver { public ContactsStatusReceiver() { super(null); } public void onChange(boolean updated) { mEventFacade.postEvent("ContactsChanged", null); } } @Override public void shutdown() { mContentResolver.unregisterContentObserver(mContactsStatusReceiver); } }