1 /* Copyright (C) 2010 The Android Open Source Project. 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 package com.android.exchange.provider; 17 18 import com.android.email.Email; 19 import com.android.email.EmailAddressAdapter; 20 import com.android.email.R; 21 import com.android.email.provider.EmailContent.Account; 22 import com.android.email.provider.EmailContent.HostAuth; 23 24 import android.app.Activity; 25 import android.content.Context; 26 import android.database.Cursor; 27 import android.database.MatrixCursor; 28 import android.database.MergeCursor; 29 import android.net.Uri; 30 import android.util.Log; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.widget.ListView; 35 import android.widget.TextView; 36 37 /** 38 * Email Address adapter that performs asynchronous GAL lookups. 39 */ 40 public class GalEmailAddressAdapter extends EmailAddressAdapter { 41 // DO NOT CHECK IN SET TO TRUE 42 private static final boolean DEBUG_GAL_LOG = false; 43 44 // Don't run GAL query until there are 3 characters typed 45 private static final int MINIMUM_GAL_CONSTRAINT_LENGTH = 3; 46 47 private Activity mActivity; 48 private Account mAccount; 49 private boolean mAccountHasGal; 50 private String mAccountEmailDomain; 51 private LayoutInflater mInflater; 52 53 // Local variables to track status of the search 54 private int mSeparatorDisplayCount; 55 private int mSeparatorTotalCount; 56 57 public GalEmailAddressAdapter(Activity activity) { 58 super(activity); 59 mActivity = activity; 60 mAccount = null; 61 mAccountHasGal = false; 62 mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 63 } 64 65 /** 66 * Set the account ID when known. Not used for generic contacts lookup; Use when 67 * linking lookup to specific account. 68 */ 69 @Override 70 public void setAccount(Account account) { 71 mAccount = account; 72 mAccountHasGal = false; 73 int finalSplit = mAccount.mEmailAddress.lastIndexOf('@'); 74 mAccountEmailDomain = mAccount.mEmailAddress.substring(finalSplit + 1); 75 } 76 77 /** 78 * Sniff the provided account and if it's EAS, record "mAccounthHasGal". If not, 79 * clear mAccount so we just ignore it. 80 */ 81 private void checkGalAccount(Account account) { 82 HostAuth ha = HostAuth.restoreHostAuthWithId(mActivity, account.mHostAuthKeyRecv); 83 if (ha != null) { 84 if ("eas".equalsIgnoreCase(ha.mProtocol)) { 85 mAccountHasGal = true; 86 return; 87 } 88 } 89 // for any reason, we could not identify a GAL account, so clear mAccount 90 // and we'll never check this again 91 mAccount = null; 92 mAccountHasGal = false; 93 } 94 95 @Override 96 public Cursor runQueryOnBackgroundThread(final CharSequence constraint) { 97 // One time (and not in the UI thread) - check the account and see if it support GAL 98 // If not, clear it so we never bother again 99 if (mAccount != null && mAccountHasGal == false) { 100 checkGalAccount(mAccount); 101 } 102 103 // Get the cursor from ContactsProvider, and set up to exit immediately, returning it 104 Cursor contactsCursor = super.runQueryOnBackgroundThread(constraint); 105 // If we don't have a GAL account or we don't have a constraint that's long enough, 106 // just return the raw contactsCursor 107 if (!mAccountHasGal || constraint == null) { 108 return contactsCursor; 109 } 110 final String constraintString = constraint.toString().trim(); 111 if (constraintString.length() < MINIMUM_GAL_CONSTRAINT_LENGTH) { 112 return contactsCursor; 113 } 114 115 // Strategy for handling dynamic GAL lookup. 116 // 1. Create cursor that we can use now (and update later) 117 // 2. Return it immediately 118 // 3. Spawn a thread that will update the cursor when results arrive or search fails 119 120 final MatrixCursor matrixCursor = new MatrixCursor(ExchangeProvider.GAL_PROJECTION); 121 final MyMergeCursor mergedResultCursor = 122 new MyMergeCursor(new Cursor[] {contactsCursor, matrixCursor}); 123 mergedResultCursor.setSeparatorPosition(contactsCursor.getCount()); 124 mSeparatorDisplayCount = -1; 125 mSeparatorTotalCount = -1; 126 new Thread(new Runnable() { 127 public void run() { 128 // Uri format is account/constraint 129 Uri galUri = 130 ExchangeProvider.GAL_URI.buildUpon() 131 .appendPath(Long.toString(mAccount.mId)) 132 .appendPath(constraintString).build(); 133 if (DEBUG_GAL_LOG) { 134 Log.d(Email.LOG_TAG, "Query: " + galUri); 135 } 136 // Use ExchangeProvider to get the results of the GAL query 137 final Cursor galCursor = 138 mContentResolver.query(galUri, ExchangeProvider.GAL_PROJECTION, 139 null, null, null); 140 // There are three result cases to handle here. 141 // 1. matrixCursor is closed - this means the UI no longer cares about us 142 // 2. gal cursor is null or empty - remove separator and exit 143 // 3. gal cursor has results - update separator and add results to matrix cursor 144 145 // Case 1: The merged cursor has already been dropped, (e.g. results superceded) 146 if (mergedResultCursor.isClosed()) { 147 if (DEBUG_GAL_LOG) { 148 Log.d(Email.LOG_TAG, "Drop result (cursor closed, bg thread)"); 149 } 150 return; 151 } 152 153 // Cases 2 & 3 have UI aspects, so do them in the UI thread 154 mActivity.runOnUiThread(new Runnable() { 155 public void run() { 156 // Case 1: (final re-check): Merged cursor already dropped 157 if (mergedResultCursor.isClosed()) { 158 if (DEBUG_GAL_LOG) { 159 Log.d(Email.LOG_TAG, "Drop result (cursor closed, ui thread)"); 160 } 161 return; 162 } 163 164 // Case 2: Gal cursor is null or empty 165 if (galCursor == null || galCursor.getCount() == 0) { 166 if (DEBUG_GAL_LOG) { 167 Log.d(Email.LOG_TAG, "Drop empty result"); 168 } 169 mergedResultCursor.setSeparatorPosition(ListView.INVALID_POSITION); 170 GalEmailAddressAdapter.this.notifyDataSetChanged(); 171 return; 172 } 173 174 // Case 3: Real results 175 galCursor.moveToPosition(-1); 176 while (galCursor.moveToNext()) { 177 MatrixCursor.RowBuilder rb = matrixCursor.newRow(); 178 rb.add(galCursor.getLong(ExchangeProvider.GAL_COLUMN_ID)); 179 rb.add(galCursor.getString(ExchangeProvider.GAL_COLUMN_DISPLAYNAME)); 180 rb.add(galCursor.getString(ExchangeProvider.GAL_COLUMN_DATA)); 181 } 182 // Replace the separator text with "totals" 183 mSeparatorDisplayCount = galCursor.getCount(); 184 mSeparatorTotalCount = 185 galCursor.getExtras().getInt(ExchangeProvider.EXTRAS_TOTAL_RESULTS); 186 // Notify UI that the cursor changed 187 if (DEBUG_GAL_LOG) { 188 Log.d(Email.LOG_TAG, "Notify result, added=" + mSeparatorDisplayCount); 189 } 190 GalEmailAddressAdapter.this.notifyDataSetChanged(); 191 }}); 192 }}).start(); 193 return mergedResultCursor; 194 } 195 196 /* 197 * The following series of overrides insert the separator between contacts & GAL contacts 198 * TODO: extract most of this into a CursorAdapter superclass, and share with AccountFolderList 199 */ 200 201 /** 202 * Get the separator position, which is tucked into the cursor to deal with threading. 203 * Result is invalid for any other cursor types (e.g. the raw contacts cursor) 204 */ 205 private int getSeparatorPosition() { 206 Cursor c = this.getCursor(); 207 if (c instanceof MyMergeCursor) { 208 return ((MyMergeCursor)c).getSeparatorPosition(); 209 } else { 210 return ListView.INVALID_POSITION; 211 } 212 } 213 214 /** 215 * Prevents the separator view from recycling into the other views 216 */ 217 @Override 218 public int getItemViewType(int position) { 219 if (position == getSeparatorPosition()) { 220 return IGNORE_ITEM_VIEW_TYPE; 221 } 222 return super.getItemViewType(position); 223 } 224 225 /** 226 * Injects the separator view when required 227 */ 228 @Override 229 public View getView(int position, View convertView, ViewGroup parent) { 230 // The base class's getView() checks for mDataValid at the beginning, but we don't have 231 // to do that, because if the cursor is invalid getCount() returns 0, in which case this 232 // method wouldn't get called. 233 234 // Handle the separator here - create & bind 235 if (position == getSeparatorPosition()) { 236 View separator; 237 separator = mInflater.inflate(R.layout.recipient_dropdown_separator, parent, false); 238 TextView text1 = (TextView) separator.findViewById(R.id.text1); 239 View progress = separator.findViewById(R.id.progress); 240 String bannerText; 241 if (mSeparatorDisplayCount == -1) { 242 // Display "Searching <account>..." 243 bannerText = mContext.getString(R.string.gal_searching_fmt, mAccountEmailDomain); 244 progress.setVisibility(View.VISIBLE); 245 } else { 246 if (mSeparatorDisplayCount == mSeparatorTotalCount) { 247 // Display "x results from <account>" 248 bannerText = mContext.getResources().getQuantityString( 249 R.plurals.gal_completed_fmt, mSeparatorDisplayCount, 250 mSeparatorDisplayCount, mAccountEmailDomain); 251 } else { 252 // Display "First x results from <account>" 253 bannerText = mContext.getString(R.string.gal_completed_limited_fmt, 254 mSeparatorDisplayCount, mAccountEmailDomain); 255 } 256 progress.setVisibility(View.GONE); 257 } 258 text1.setText(bannerText); 259 return separator; 260 } 261 return super.getView(getRealPosition(position), convertView, parent); 262 } 263 264 /** 265 * Forces navigation to skip over the separator 266 */ 267 @Override 268 public boolean areAllItemsEnabled() { 269 return false; 270 } 271 272 /** 273 * Forces navigation to skip over the separator 274 */ 275 @Override 276 public boolean isEnabled(int position) { 277 return position != getSeparatorPosition(); 278 } 279 280 /** 281 * Adjusts list count to include separator 282 */ 283 @Override 284 public int getCount() { 285 int count = super.getCount(); 286 if (getSeparatorPosition() != ListView.INVALID_POSITION) { 287 // Increment for separator, if we have anything to show. 288 count += 1; 289 } 290 return count; 291 } 292 293 /** 294 * Converts list position to cursor position 295 */ 296 private int getRealPosition(int pos) { 297 int separatorPosition = getSeparatorPosition(); 298 if (separatorPosition == ListView.INVALID_POSITION) { 299 // No separator, identity map 300 return pos; 301 } else if (pos <= separatorPosition) { 302 // Before or at the separator, identity map 303 return pos; 304 } else { 305 // After the separator, remove 1 from the pos to get the real underlying pos 306 return pos - 1; 307 } 308 } 309 310 /** 311 * Returns the item using external position numbering (no separator) 312 */ 313 @Override 314 public Object getItem(int pos) { 315 return super.getItem(getRealPosition(pos)); 316 } 317 318 /** 319 * Returns the item id using external position numbering (no separator) 320 */ 321 @Override 322 public long getItemId(int pos) { 323 if (pos == getSeparatorPosition()) { 324 return View.NO_ID; 325 } 326 return super.getItemId(getRealPosition(pos)); 327 } 328 329 /** 330 * Lightweight override of MergeCursor. Synchronizes "mClosed" / "isClosed()" so we 331 * can safely check if it has been closed, in the threading jumble of our adapter. 332 * Also holds the separator position, so it can be tracked with the cursor itself and avoid 333 * errors when multiple cursors are in flight. 334 */ 335 private static class MyMergeCursor extends MergeCursor { 336 337 private int mSeparatorPosition; 338 339 public MyMergeCursor(Cursor[] cursors) { 340 super(cursors); 341 mClosed = false; 342 mSeparatorPosition = ListView.INVALID_POSITION; 343 } 344 345 @Override 346 public synchronized void close() { 347 super.close(); 348 if (DEBUG_GAL_LOG) { 349 Log.d(Email.LOG_TAG, "Closing MyMergeCursor"); 350 } 351 } 352 353 @Override 354 public synchronized boolean isClosed() { 355 return super.isClosed(); 356 } 357 358 void setSeparatorPosition(int newPos) { 359 mSeparatorPosition = newPos; 360 } 361 362 int getSeparatorPosition() { 363 return mSeparatorPosition; 364 } 365 } 366 } 367