1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.googlecode.android_scripting.facade; 18 19 import android.app.Service; 20 import android.content.ComponentName; 21 import android.content.ContentResolver; 22 import android.content.ContentUris; 23 import android.content.Intent; 24 import android.content.res.AssetFileDescriptor; 25 import android.database.ContentObserver; 26 import android.database.Cursor; 27 import android.net.Uri; 28 import android.provider.ContactsContract; 29 import android.util.Log; 30 31 import com.googlecode.android_scripting.jsonrpc.RpcReceiver; 32 import com.googlecode.android_scripting.rpc.Rpc; 33 import com.googlecode.android_scripting.rpc.RpcOptional; 34 import com.googlecode.android_scripting.rpc.RpcParameter; 35 36 import java.io.FileInputStream; 37 import java.io.IOException; 38 import java.io.OutputStream; 39 import java.io.PrintWriter; 40 import java.util.ArrayList; 41 import java.util.List; 42 43 import org.json.JSONArray; 44 import org.json.JSONException; 45 import org.json.JSONObject; 46 47 /** 48 * Provides access to contacts related functionality. 49 */ 50 public class ContactsFacade extends RpcReceiver { 51 private static final String TAG = "ContactsFacade"; 52 private static final Uri CONTACTS_URI = ContactsContract.Contacts.CONTENT_URI; 53 private static final String ERASE_COMPLETE = "ContactsErased"; 54 private final ContentResolver mContentResolver; 55 private final Service mService; 56 private final CommonIntentsFacade mCommonIntentsFacade; 57 private final ContactsStatusReceiver mContactsStatusReceiver; 58 private final EventFacade mEventFacade; 59 60 private Uri mPhoneContent = null; 61 private String mContactId; 62 private String mPrimary; 63 private String mPhoneNumber; 64 private String mHasPhoneNumber; 65 ContactsFacade(FacadeManager manager)66 public ContactsFacade(FacadeManager manager) { 67 super(manager); 68 mService = manager.getService(); 69 mContentResolver = mService.getContentResolver(); 70 mCommonIntentsFacade = manager.getReceiver(CommonIntentsFacade.class); 71 mContactsStatusReceiver = new ContactsStatusReceiver(); 72 mContentResolver.registerContentObserver( 73 ContactsContract.Contacts.CONTENT_URI, true, mContactsStatusReceiver); 74 mEventFacade = manager.getReceiver(EventFacade.class); 75 try { 76 // Backward compatibility... get contract stuff using reflection 77 Class<?> phone = Class.forName("android.provider.ContactsContract$CommonDataKinds$Phone"); 78 mPhoneContent = (Uri) phone.getField("CONTENT_URI").get(null); 79 mContactId = (String) phone.getField("CONTACT_ID").get(null); 80 mPrimary = (String) phone.getField("IS_PRIMARY").get(null); 81 mPhoneNumber = (String) phone.getField("NUMBER").get(null); 82 mHasPhoneNumber = (String) phone.getField("HAS_PHONE_NUMBER").get(null); 83 } catch (Exception e) { 84 Log.e(TAG, "Unable to get field from Contacts Database"); 85 } 86 } 87 getUri(Integer id)88 private Uri getUri(Integer id) { 89 return ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id); 90 } 91 92 @Rpc( 93 description = "Displays a list of contacts to pick from.", 94 returns = "A map of result values." 95 ) contactsDisplayContactPickList()96 public Intent contactsDisplayContactPickList() throws JSONException { 97 return mCommonIntentsFacade.pick(CONTACTS_URI.toString()); 98 } 99 100 @Rpc( 101 description = "Displays a list of phone numbers to pick from.", 102 returns = "The selected phone number." 103 ) contactsDisplayPhonePickList()104 public String contactsDisplayPhonePickList() throws JSONException { 105 String phoneNumber = null; 106 Intent data = mCommonIntentsFacade.pick(CONTACTS_URI.toString()); 107 if (data != null) { 108 Uri phoneData = data.getData(); 109 Cursor cursor = mService.getContentResolver().query(phoneData, null, null, null, null); 110 if (cursor != null) { 111 if (cursor.moveToFirst()) { 112 phoneNumber = 113 cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.PhoneLookup.NUMBER)); 114 } 115 cursor.close(); 116 } 117 } 118 return phoneNumber; 119 } 120 121 @Rpc(description = "Returns a List of all possible attributes for contacts.") contactsGetAttributes()122 public List<String> contactsGetAttributes() { 123 List<String> attributes = new ArrayList<String>(); 124 Cursor cursor = mContentResolver.query(CONTACTS_URI, null, null, null, null); 125 if (cursor != null) { 126 String[] columns = cursor.getColumnNames(); 127 for (int i = 0; i < columns.length; i++) { 128 attributes.add(columns[i]); 129 } 130 cursor.close(); 131 } 132 return attributes; 133 } 134 135 @Rpc(description = "Returns a List of all contact IDs.") contactsGetContactIds()136 public List<Integer> contactsGetContactIds() { 137 List<Integer> ids = new ArrayList<Integer>(); 138 String[] columns = {"_id"}; 139 Cursor cursor = mContentResolver.query(CONTACTS_URI, columns, null, null, null); 140 if (cursor != null) { 141 while (cursor.moveToNext()) { 142 ids.add(cursor.getInt(0)); 143 } 144 cursor.close(); 145 } 146 return ids; 147 } 148 149 @Rpc(description = "Returns a List of all contacts.", returns = "a List of contacts as Maps") contactsGetAllContacts( @pcParametername = "attributes") @pcOptional JSONArray attributes)150 public List<JSONObject> contactsGetAllContacts( 151 @RpcParameter(name = "attributes") @RpcOptional JSONArray attributes) throws JSONException { 152 List<JSONObject> contacts = new ArrayList<JSONObject>(); 153 String[] columns; 154 if (attributes == null || attributes.length() == 0) { 155 // In case no attributes are specified we set the default ones. 156 columns = new String[] {ContactsContract.Contacts.NAME_RAW_CONTACT_ID, 157 ContactsContract.Contacts.DISPLAY_NAME}; 158 } else { 159 // Convert selected attributes list into usable string list. 160 columns = new String[attributes.length()]; 161 for (int i = 0; i < attributes.length(); i++) { 162 columns[i] = attributes.getString(i); 163 } 164 } 165 List<String> queryList = new ArrayList<String>(); 166 for (String s : columns) { 167 queryList.add(s); 168 } 169 170 String[] query = queryList.toArray(new String[queryList.size()]); 171 Cursor cursor = mContentResolver.query(CONTACTS_URI, query, null, null, null); 172 if (cursor != null) { 173 int idIndex = cursor.getColumnIndex("_id"); 174 while (cursor.moveToNext()) { 175 String id = cursor.getString(idIndex); 176 JSONObject message = new JSONObject(); 177 for (int i = 0; i < columns.length; i++) { 178 String key = columns[i]; 179 String value = cursor.getString(cursor.getColumnIndex(key)); 180 if (mPhoneNumber != null) { 181 if (key.equals("primary_phone")) { 182 value = findPhone(id); 183 } 184 } 185 message.put(key, value); 186 } 187 contacts.add(message); 188 } 189 cursor.close(); 190 } 191 return contacts; 192 } 193 findPhone(String id)194 private String findPhone(String id) { 195 String phoneNumber = null; 196 if (id == null || id.equals("")) { 197 return phoneNumber; 198 } 199 try { 200 if (Integer.parseInt(id) > 0) { 201 Cursor pCur = 202 mContentResolver.query( 203 mPhoneContent, 204 new String[] {mPhoneNumber}, 205 mContactId + " = ? and " + mPrimary + "=1", 206 new String[] {id}, 207 null); 208 if (pCur != null) { 209 pCur.getColumnNames(); 210 while (pCur.moveToNext()) { 211 phoneNumber = pCur.getString(0); 212 break; 213 } 214 } 215 pCur.close(); 216 } 217 } catch (Exception e) { 218 return null; 219 } 220 return phoneNumber; 221 } 222 223 @Rpc(description = "Returns contacts by ID.") contactsGetContactById( @pcParametername = "id") Integer id, @RpcParameter(name = "attributes") @RpcOptional JSONArray attributes)224 public JSONObject contactsGetContactById( 225 @RpcParameter(name = "id") Integer id, 226 @RpcParameter(name = "attributes") @RpcOptional JSONArray attributes) 227 throws JSONException { 228 JSONObject contact = null; 229 Uri uri = getUri(id); 230 String[] columns; 231 if (attributes == null || attributes.length() == 0) { 232 // In case no attributes are specified we set the default ones. 233 columns = new String[] {"_id", "name", "primary_phone", "primary_email", "type"}; 234 } else { 235 // Convert selected attributes list into usable string list. 236 columns = new String[attributes.length()]; 237 for (int i = 0; i < attributes.length(); i++) { 238 columns[i] = attributes.getString(i); 239 } 240 } 241 Cursor cursor = mContentResolver.query(uri, columns, null, null, null); 242 if (cursor != null) { 243 contact = new JSONObject(); 244 cursor.moveToFirst(); 245 for (int i = 0; i < columns.length; i++) { 246 contact.put(columns[i], cursor.getString(i)); 247 } 248 cursor.close(); 249 } 250 return contact; 251 } 252 253 @Rpc(description = "Returns the number of contacts.") contactsGetCount()254 public Integer contactsGetCount() { 255 Integer count = 0; 256 Cursor cursor = mContentResolver.query(CONTACTS_URI, null, null, null, null); 257 if (cursor != null) { 258 count = cursor.getCount(); 259 cursor.close(); 260 } 261 return count; 262 } 263 jsonToArray(JSONArray array)264 private String[] jsonToArray(JSONArray array) throws JSONException { 265 String[] resultingArray = null; 266 if (array != null && array.length() > 0) { 267 resultingArray = new String[array.length()]; 268 for (int i = 0; i < array.length(); i++) { 269 resultingArray[i] = array.getString(i); 270 } 271 } 272 return resultingArray; 273 } 274 getAllContactsVcardUri()275 private Uri getAllContactsVcardUri() { 276 Cursor cursor = 277 mContentResolver.query( 278 ContactsContract.Contacts.CONTENT_URI, 279 new String[] {ContactsContract.Contacts.LOOKUP_KEY}, 280 null, 281 null, 282 null); 283 if (cursor == null) { 284 return null; 285 } 286 try { 287 StringBuilder uriListBuilder = new StringBuilder(); 288 int index = 0; 289 while (cursor.moveToNext()) { 290 if (index != 0) { 291 uriListBuilder.append(':'); 292 } 293 uriListBuilder.append(cursor.getString(0)); 294 index++; 295 } 296 return Uri.withAppendedPath( 297 ContactsContract.Contacts.CONTENT_MULTI_VCARD_URI, Uri.encode(uriListBuilder.toString())); 298 } finally { 299 cursor.close(); 300 } 301 } 302 303 @Rpc(description = "Erase all contacts in phone book.") contactsEraseAll()304 public void contactsEraseAll() { 305 Cursor cursor = 306 mContentResolver.query( 307 ContactsContract.Contacts.CONTENT_URI, 308 new String[] {ContactsContract.Contacts.LOOKUP_KEY}, 309 null, 310 null, 311 null); 312 if (cursor == null) { 313 return; 314 } 315 while (cursor.moveToNext()) { 316 Uri uri = 317 Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, cursor.getString(0)); 318 mContentResolver.delete(uri, null, null); 319 } 320 mEventFacade.postEvent(ERASE_COMPLETE, null); 321 return; 322 } 323 324 /** 325 * Exactly as per <a href= 326 * "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" 327 * >ContentResolver.query</a> 328 */ 329 @Rpc(description = "Content Resolver Query", returns = "result of query as Maps") contactsQueryContent( @pcParameter 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)330 public List<JSONObject> contactsQueryContent( 331 @RpcParameter( 332 name = "uri", 333 description = "The URI, using the content:// scheme, for the content to retrieve." 334 ) 335 String uri, 336 @RpcParameter( 337 name = "attributes", 338 description = "A list of which columns to return. Passing null will return all columns" 339 ) 340 @RpcOptional 341 JSONArray attributes, 342 @RpcParameter(name = "selection", description = "A filter declaring which rows to return") 343 @RpcOptional 344 String selection, 345 @RpcParameter( 346 name = "selectionArgs", 347 description = 348 "You may include ?s in selection, which will be replaced by the values from selectionArgs" 349 ) 350 @RpcOptional 351 JSONArray selectionArgs, 352 @RpcParameter(name = "order", description = "How to order the rows") @RpcOptional 353 String order) 354 throws JSONException { 355 List<JSONObject> queryResults = new ArrayList<JSONObject>(); 356 String[] columns = jsonToArray(attributes); 357 String[] args = jsonToArray(selectionArgs); 358 Cursor cursor = mContentResolver.query(Uri.parse(uri), columns, selection, args, order); 359 if (cursor != null) { 360 String[] names = cursor.getColumnNames(); 361 while (cursor.moveToNext()) { 362 JSONObject message = new JSONObject(); 363 for (int i = 0; i < cursor.getColumnCount(); i++) { 364 String key = names[i]; 365 String value = cursor.getString(i); 366 message.put(key, value); 367 } 368 queryResults.add(message); 369 } 370 cursor.close(); 371 } 372 return queryResults; 373 } 374 375 @Rpc( 376 description = "Content Resolver Query Attributes", 377 returns = "a list of available columns for a given content uri" 378 ) queryAttributes( @pcParameter name = "uri", description = "The URI, using the content:// scheme, for the content to retrieve." ) String uri)379 public JSONArray queryAttributes( 380 @RpcParameter( 381 name = "uri", 382 description = "The URI, using the content:// scheme, for the content to retrieve." 383 ) 384 String uri) 385 throws JSONException { 386 JSONArray columns = new JSONArray(); 387 Cursor cursor = mContentResolver.query(Uri.parse(uri), null, "1=0", null, null); 388 if (cursor != null) { 389 String[] names = cursor.getColumnNames(); 390 for (String name : names) { 391 columns.put(name); 392 } 393 cursor.close(); 394 } 395 return columns; 396 } 397 398 @Rpc(description = "Launches VCF import.") importVcf( @pcParameter name = "uri", description = "The URI, using the file:/// scheme, for the content to retrieve." ) String uri)399 public void importVcf( 400 @RpcParameter( 401 name = "uri", 402 description = "The URI, using the file:/// scheme, for the content to retrieve." 403 ) 404 String uri) { 405 Intent intent = new Intent(); 406 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 407 intent.setComponent( 408 new ComponentName( 409 "com.google.android.contacts", 410 "com.google.android.apps.contacts.vcard.ImportVCardActivity")); 411 intent.setData(Uri.parse(uri)); 412 mService.startActivity(intent); 413 } 414 415 @Rpc(description = "Launches VCF export.") exportVcf( @pcParameter name = "path", description = "The file path, using the / scheme, for the content to save to." ) String path)416 public void exportVcf( 417 @RpcParameter( 418 name = "path", 419 description = "The file path, using the / scheme, for the content to save to." 420 ) 421 String path) { 422 OutputStream out = null; 423 StringBuilder string = new StringBuilder(); 424 try { 425 AssetFileDescriptor fd = 426 mContentResolver.openAssetFileDescriptor(getAllContactsVcardUri(), "r"); 427 FileInputStream inputStream = fd.createInputStream(); 428 PrintWriter writer = new PrintWriter(path, "UTF-8"); 429 int character; 430 while ((character = inputStream.read()) != -1) { 431 if ((char) character != '\r') { 432 string.append((char) character); 433 } 434 } 435 writer.append(string); 436 writer.close(); 437 } catch (IOException e) { 438 Log.w(TAG, "Failed to export VCF."); 439 } 440 } 441 442 private class ContactsStatusReceiver extends ContentObserver { ContactsStatusReceiver()443 public ContactsStatusReceiver() { 444 super(null); 445 } 446 onChange(boolean updated)447 public void onChange(boolean updated) { 448 mEventFacade.postEvent("ContactsChanged", null); 449 } 450 } 451 452 @Override shutdown()453 public void shutdown() { 454 mContentResolver.unregisterContentObserver(mContactsStatusReceiver); 455 } 456 } 457