QuickContactActivity.java revision f43f573340fd9de5d30b43d7c96cac1ec9021e58
1b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org/* 2b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org * Copyright (C) 2009 The Android Open Source Project 3b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org * 4b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org * Licensed under the Apache License, Version 2.0 (the "License"); 5b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org * you may not use this file except in compliance with the License. 6b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org * You may obtain a copy of the License at 7b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org * 8b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org * http://www.apache.org/licenses/LICENSE-2.0 9b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org * 10b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org * Unless required by applicable law or agreed to in writing, software 11b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org * distributed under the License is distributed on an "AS IS" BASIS, 12b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org * See the License for the specific language governing permissions and 14b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org * limitations under the License. 15a557f436b9d694d5a0a045e0295e1794f2df48eapbos@webrtc.org */ 16b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org 17b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgpackage com.android.contacts.quickcontact; 18b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org 19b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.animation.Animator; 20b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.animation.Animator.AnimatorListener; 21b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.animation.AnimatorListenerAdapter; 22b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.animation.ArgbEvaluator; 23b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.animation.ObjectAnimator; 24b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.app.Activity; 25b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.app.Fragment; 26b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.app.LoaderManager.LoaderCallbacks; 27b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.content.ActivityNotFoundException; 28b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.content.ContentUris; 29b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.content.Intent; 30b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.content.Loader; 31b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.content.pm.PackageManager; 32b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.graphics.Bitmap; 33b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.graphics.Color; 34b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.graphics.ColorFilter; 35b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.graphics.drawable.BitmapDrawable; 36b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.graphics.drawable.ColorDrawable; 37b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.graphics.drawable.Drawable; 38b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.graphics.PorterDuff; 39b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.graphics.PorterDuffColorFilter; 40b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.net.Uri; 41b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.os.AsyncTask; 42b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.os.Bundle; 43b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.os.Trace; 44b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.provider.ContactsContract; 45b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.provider.ContactsContract.CommonDataKinds.Email; 46b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.provider.ContactsContract.CommonDataKinds.Phone; 47b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.provider.ContactsContract.CommonDataKinds.SipAddress; 48b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 49b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.provider.ContactsContract.CommonDataKinds.Website; 50b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.provider.ContactsContract.Contacts; 51b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.provider.ContactsContract.QuickContact; 52b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.provider.ContactsContract.RawContacts; 53b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.support.v7.graphics.Palette; 54b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.text.TextUtils; 55b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.util.Log; 56b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.view.Menu; 57b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.view.MenuItem; 58b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.view.MenuInflater; 59b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.view.View; 60b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.view.View.OnClickListener; 61b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.view.WindowManager; 62b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.widget.ImageView; 63b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.widget.Toast; 64b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport android.widget.Toolbar; 65b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org 66b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.ContactSaveService; 67b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.ContactsActivity; 68b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.Collapser; 69b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.R; 70b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.editor.SelectAccountDialogFragment; 71b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.lettertiles.LetterTileDrawable; 72b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.list.ShortcutIntentBuilder; 7348c4b75e8d0d02294460e357ddb3a07ce295b964pbos@webrtc.orgimport com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener; 74b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.model.AccountTypeManager; 75b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.model.Contact; 76b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.model.ContactLoader; 77b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.model.RawContact; 78b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.model.account.AccountType; 79b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.model.account.AccountWithDataSet; 80b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.model.dataitem.DataItem; 81b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.model.dataitem.DataKind; 82b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.model.dataitem.EmailDataItem; 83b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.model.dataitem.ImDataItem; 84b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.model.dataitem.PhoneDataItem; 85b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.common.util.DataStatus; 86b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.detail.ContactDetailDisplayUtils; 87b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.interactions.CalendarInteractionsLoader; 88b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.interactions.CallLogInteractionsLoader; 89b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.interactions.ContactDeletionInteraction; 90b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.interactions.ContactInteraction; 91b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.interactions.SmsInteractionsLoader; 92b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.quickcontact.ExpandingEntryCardView.Entry; 93b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.util.ImageViewDrawableSetter; 94b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.util.SchedulingUtils; 95b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.widget.MultiShrinkScroller; 96b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener; 97b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.google.common.base.Preconditions; 98b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport com.google.common.collect.Lists; 99b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.org 100b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport java.util.ArrayList; 101b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport java.util.Arrays; 102b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport java.util.Collections; 103b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport java.util.Comparator; 104b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport java.util.HashMap; 105b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport java.util.HashSet; 106b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport java.util.List; 107b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport java.util.Map; 108b015cbede88899f67a53fbbe581b02ce8e32794andrew@webrtc.orgimport java.util.Set; 109 110/** 111 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads 112 * data asynchronously, and then shows a popup with details centered around 113 * {@link Intent#getSourceBounds()}. 114 */ 115public class QuickContactActivity extends ContactsActivity { 116 117 /** 118 * QuickContacts immediately takes up the full screen. All possible information is shown. 119 * This value for {@link android.provider.ContactsContract.QuickContact#EXTRA_MODE} 120 * should only be used by the Contacts app. 121 */ 122 public static final int MODE_FULLY_EXPANDED = 4; 123 124 private static final String TAG = "QuickContact"; 125 126 private static final String KEY_THEME_COLOR = "theme_color"; 127 128 private static final int ANIMATION_SLIDE_OPEN_DURATION = 250; 129 private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 150; 130 private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1; 131 private static final float SYSTEM_BAR_BRIGHTNESS_FACTOR = 0.7f; 132 private static final int SCRIM_COLOR = Color.argb(0xB2, 0, 0, 0); 133 134 /** This is the Intent action to install a shortcut in the launcher. */ 135 private static final String ACTION_INSTALL_SHORTCUT = 136 "com.android.launcher.action.INSTALL_SHORTCUT"; 137 138 @SuppressWarnings("deprecation") 139 private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY; 140 141 private Uri mLookupUri; 142 private String[] mExcludeMimes; 143 private int mExtraMode; 144 private int mStatusBarColor; 145 private boolean mHasAlreadyBeenOpened; 146 147 private ImageView mPhotoView; 148 private View mTransparentView; 149 private ExpandingEntryCardView mCommunicationCard; 150 private ExpandingEntryCardView mRecentCard; 151 private MultiShrinkScroller mScroller; 152 private SelectAccountDialogFragmentListener mSelectAccountFragmentListener; 153 private AsyncTask<Void, Void, Void> mEntriesAndActionsTask; 154 private ColorDrawable mWindowScrim; 155 private boolean mIsWaitingForOtherPieceOfExitAnimation; 156 private boolean mIsExitAnimationInProgress; 157 private boolean mHasComputedThemeColor; 158 159 private static final int MIN_NUM_COMMUNICATION_ENTRIES_SHOWN = 3; 160 private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3; 161 162 private Contact mContactData; 163 private ContactLoader mContactLoader; 164 private PorterDuffColorFilter mColorFilter; 165 166 private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter(); 167 168 /** 169 * Keeps the default action per mimetype. Empty if no default actions are set 170 */ 171 private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>(); 172 173 /** 174 * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types. 175 * 176 * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog, 177 * in the order specified here.</p> 178 * 179 * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order 180 * specified here.</p> 181 * 182 * <p>The rest go between them, in the order in the array.</p> 183 */ 184 private static final List<String> LEADING_MIMETYPES = Lists.newArrayList( 185 Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE); 186 187 /** See {@link #LEADING_MIMETYPES}. */ 188 private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList( 189 StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE); 190 191 /** Id for the background contact loader */ 192 private static final int LOADER_CONTACT_ID = 0; 193 194 private static final String KEY_LOADER_EXTRA_PHONES = 195 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_PHONES"; 196 197 /** Id for the background Sms Loader */ 198 private static final int LOADER_SMS_ID = 1; 199 private static final int MAX_SMS_RETRIEVE = 3; 200 201 /** Id for the back Calendar Loader */ 202 private static final int LOADER_CALENDAR_ID = 2; 203 private static final String KEY_LOADER_EXTRA_EMAILS = 204 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_EMAILS"; 205 private static final int MAX_PAST_CALENDAR_RETRIEVE = 3; 206 private static final int MAX_FUTURE_CALENDAR_RETRIEVE = 3; 207 private static final long PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 208 180L * 24L * 60L * 60L * 1000L /* 180 days */; 209 private static final long FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 210 36L * 60L * 60L * 1000L /* 36 hours */; 211 212 /** Id for the background Call Log Loader */ 213 private static final int LOADER_CALL_LOG_ID = 3; 214 private static final int MAX_CALL_LOG_RETRIEVE = 3; 215 216 217 private static final int[] mRecentLoaderIds = new int[]{ 218 LOADER_SMS_ID, 219 LOADER_CALENDAR_ID, 220 LOADER_CALL_LOG_ID}; 221 private Map<Integer, List<ContactInteraction>> mRecentLoaderResults; 222 223 private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment"; 224 225 final OnClickListener mEntryClickHandler = new OnClickListener() { 226 @Override 227 public void onClick(View v) { 228 Log.i(TAG, "mEntryClickHandler onClick"); 229 Object intent = v.getTag(); 230 if (intent == null || !(intent instanceof Intent)) { 231 return; 232 } 233 startActivity((Intent) intent); 234 } 235 }; 236 237 /** 238 * Headless fragment used to handle account selection callbacks invoked from 239 * {@link DirectoryContactUtil}. 240 */ 241 public static class SelectAccountDialogFragmentListener extends Fragment 242 implements SelectAccountDialogFragment.Listener { 243 244 private QuickContactActivity mQuickContactActivity; 245 246 public SelectAccountDialogFragmentListener() {} 247 248 @Override 249 public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) { 250 DirectoryContactUtil.createCopy(mQuickContactActivity.mContactData.getContentValues(), 251 account, mQuickContactActivity); 252 } 253 254 @Override 255 public void onAccountSelectorCancelled() {} 256 257 /** 258 * Set the parent activity. Since rotation can cause this fragment to be used across 259 * more than one activity instance, we need to explicitly set this value instead 260 * of making this class non-static. 261 */ 262 public void setQuickContactActivity(QuickContactActivity quickContactActivity) { 263 mQuickContactActivity = quickContactActivity; 264 } 265 } 266 267 final MultiShrinkScrollerListener mMultiShrinkScrollerListener 268 = new MultiShrinkScrollerListener() { 269 @Override 270 public void onScrolledOffBottom() { 271 if (!mIsWaitingForOtherPieceOfExitAnimation) { 272 finish(); 273 return; 274 } 275 mIsWaitingForOtherPieceOfExitAnimation = false; 276 } 277 278 @Override 279 public void onEnterFullscreen() { 280 updateStatusBarColor(); 281 } 282 283 @Override 284 public void onExitFullscreen() { 285 updateStatusBarColor(); 286 } 287 288 @Override 289 public void onStartScrollOffBottom() { 290 // Remove the window shim now that we are starting an Activity exit animation. 291 final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime); 292 final ObjectAnimator animator = ObjectAnimator.ofInt(mWindowScrim, "alpha", 0xFF, 0); 293 animator.addListener(mExitWindowShimAnimationListener); 294 animator.setDuration(duration).start(); 295 mIsWaitingForOtherPieceOfExitAnimation = true; 296 mIsExitAnimationInProgress = true; 297 } 298 }; 299 300 final AnimatorListener mExitWindowShimAnimationListener = new AnimatorListenerAdapter() { 301 @Override 302 public void onAnimationEnd(Animator animation) { 303 if (!mIsWaitingForOtherPieceOfExitAnimation) { 304 finish(); 305 return; 306 } 307 mIsWaitingForOtherPieceOfExitAnimation = false; 308 } 309 }; 310 311 @Override 312 protected void onCreate(Bundle savedInstanceState) { 313 Trace.beginSection("onCreate()"); 314 super.onCreate(savedInstanceState); 315 316 getWindow().setStatusBarColor(Color.TRANSPARENT); 317 // Since we can't disable Window animations from the Launcher, we can minimize the 318 // silliness of the animation by setting the navigation bar transparent. 319 getWindow().setNavigationBarColor(Color.TRANSPARENT); 320 321 processIntent(getIntent()); 322 323 // Show QuickContact in front of soft input 324 getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 325 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 326 327 setContentView(R.layout.quickcontact_activity); 328 329 mCommunicationCard = (ExpandingEntryCardView) findViewById(R.id.communication_card); 330 mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card); 331 mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller); 332 333 mCommunicationCard.setOnClickListener(mEntryClickHandler); 334 mCommunicationCard.setTitle(getResources().getString(R.string.communication_card_title)); 335 mCommunicationCard.setExpandButtonText( 336 getResources().getString(R.string.expanding_entry_card_view_see_all)); 337 338 mRecentCard.setOnClickListener(mEntryClickHandler); 339 mRecentCard.setTitle(getResources().getString(R.string.recent_card_title)); 340 341 mPhotoView = (ImageView) findViewById(R.id.photo); 342 mTransparentView = findViewById(R.id.transparent_view); 343 if (mScroller != null) { 344 mTransparentView.setOnClickListener(new OnClickListener() { 345 @Override 346 public void onClick(View v) { 347 mScroller.scrollOffBottom(); 348 } 349 }); 350 } 351 352 final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 353 setActionBar(toolbar); 354 getActionBar().setTitle(null); 355 // Put a TextView with a known resource id into the ActionBar. This allows us to easily 356 // find the correct TextView location & size later. 357 toolbar.addView(getLayoutInflater().inflate(R.layout.quickcontact_title_placeholder, null)); 358 359 mHasAlreadyBeenOpened = savedInstanceState != null; 360 361 mWindowScrim = new ColorDrawable(SCRIM_COLOR); 362 getWindow().setBackgroundDrawable(mWindowScrim); 363 if (!mHasAlreadyBeenOpened) { 364 final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime); 365 ObjectAnimator.ofInt(mWindowScrim, "alpha", 0, 0xFF).setDuration(duration).start(); 366 } 367 368 if (mScroller != null) { 369 mScroller.initialize(mMultiShrinkScrollerListener); 370 if (mHasAlreadyBeenOpened) { 371 mScroller.setVisibility(View.VISIBLE); 372 mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen()); 373 } else { 374 // mScroller needs to perform asynchronous measurements after initalize(), therefore 375 // we can't mark this as GONE. 376 mScroller.setVisibility(View.INVISIBLE); 377 } 378 } 379 380 setHeaderNameText(R.string.missing_name); 381 382 mSelectAccountFragmentListener= (SelectAccountDialogFragmentListener) getFragmentManager() 383 .findFragmentByTag(FRAGMENT_TAG_SELECT_ACCOUNT); 384 if (mSelectAccountFragmentListener == null) { 385 mSelectAccountFragmentListener = new SelectAccountDialogFragmentListener(); 386 getFragmentManager().beginTransaction().add(0, mSelectAccountFragmentListener, 387 FRAGMENT_TAG_SELECT_ACCOUNT).commit(); 388 mSelectAccountFragmentListener.setRetainInstance(true); 389 } 390 mSelectAccountFragmentListener.setQuickContactActivity(this); 391 392 if (savedInstanceState != null) { 393 final int color = savedInstanceState.getInt(KEY_THEME_COLOR, 0); 394 if (color != 0) { 395 // Wait for pre draw. Setting the header tint before the MultiShrinkScroller has 396 // been measured will cause incorrect tinting calculations. 397 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ true, 398 new Runnable() { 399 @Override 400 public void run() { 401 setThemeColor(color); 402 } 403 }); 404 } 405 } 406 407 Trace.endSection(); 408 } 409 410 protected void onActivityResult(int requestCode, int resultCode, 411 Intent data) { 412 if (requestCode == REQUEST_CODE_CONTACT_EDITOR_ACTIVITY && 413 resultCode == ContactDeletionInteraction.RESULT_CODE_DELETED) { 414 // The contact that we were showing has been deleted. 415 finish(); 416 } 417 } 418 419 @Override 420 protected void onNewIntent(Intent intent) { 421 super.onNewIntent(intent); 422 mHasAlreadyBeenOpened = true; 423 mHasComputedThemeColor = false; 424 processIntent(intent); 425 } 426 427 @Override 428 public void onSaveInstanceState(Bundle savedInstanceState) { 429 super.onSaveInstanceState(savedInstanceState); 430 if (mColorFilter != null) { 431 savedInstanceState.putInt(KEY_THEME_COLOR, mColorFilter.getColor()); 432 } 433 } 434 435 private void processIntent(Intent intent) { 436 Uri lookupUri = intent.getData(); 437 438 // Check to see whether it comes from the old version. 439 if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) { 440 final long rawContactId = ContentUris.parseId(lookupUri); 441 lookupUri = RawContacts.getContactLookupUri(getContentResolver(), 442 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 443 } 444 mExtraMode = getIntent().getIntExtra(QuickContact.EXTRA_MODE, 445 QuickContact.MODE_LARGE); 446 final Uri oldLookupUri = mLookupUri; 447 448 mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri"); 449 mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); 450 if (oldLookupUri == null) { 451 mContactLoader = (ContactLoader) getLoaderManager().initLoader( 452 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 453 } else if (oldLookupUri != mLookupUri) { 454 // After copying a directory contact, the contact URI changes. Therefore, 455 // we need to restart the loader and reload the new contact. 456 mContactLoader = (ContactLoader) getLoaderManager().restartLoader( 457 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 458 for (int interactionLoaderId : mRecentLoaderIds) { 459 getLoaderManager().destroyLoader(interactionLoaderId); 460 } 461 } 462 } 463 464 private void runEntranceAnimation() { 465 if (mHasAlreadyBeenOpened) { 466 return; 467 } 468 mHasAlreadyBeenOpened = true; 469 final int bottomScroll = mScroller.getScrollUntilOffBottom() - 1; 470 final ObjectAnimator scrollAnimation 471 = ObjectAnimator.ofInt(mScroller, "scroll", -bottomScroll, 472 mExtraMode != MODE_FULLY_EXPANDED ? 0 : mScroller.getScrollNeededToBeFullScreen()); 473 scrollAnimation.setDuration(ANIMATION_SLIDE_OPEN_DURATION); 474 scrollAnimation.start(); 475 } 476 477 /** Assign this string to the view if it is not empty. */ 478 private void setHeaderNameText(int resId) { 479 if (mScroller != null) { 480 mScroller.setTitle(getText(resId) == null ? null : getText(resId).toString()); 481 } 482 } 483 484 /** Assign this string to the view if it is not empty. */ 485 private void setHeaderNameText(String value) { 486 if (!TextUtils.isEmpty(value)) { 487 if (mScroller != null) { 488 mScroller.setTitle(value); 489 } 490 } 491 } 492 493 /** 494 * Check if the given MIME-type appears in the list of excluded MIME-types 495 * that the most-recent caller requested. 496 */ 497 private boolean isMimeExcluded(String mimeType) { 498 if (mExcludeMimes == null) return false; 499 for (String excludedMime : mExcludeMimes) { 500 if (TextUtils.equals(excludedMime, mimeType)) { 501 return true; 502 } 503 } 504 return false; 505 } 506 507 /** 508 * Handle the result from the ContactLoader 509 */ 510 private void bindContactData(final Contact data) { 511 Trace.beginSection("bindContactData"); 512 mContactData = data; 513 invalidateOptionsMenu(); 514 515 mDefaultsMap.clear(); 516 517 Trace.endSection(); 518 Trace.beginSection("Set display photo & name"); 519 520 mPhotoSetter.setupContactPhoto(data, mPhotoView); 521 extractAndApplyTintFromPhotoViewAsynchronously(); 522 setHeaderNameText(data.getDisplayName()); 523 524 Trace.endSection(); 525 526 // Maintain a list of phone numbers to pass into SmsInteractionsLoader 527 final Set<String> phoneNumbers = new HashSet<>(); 528 // Maintain a list of email addresses to pass into CalendarInteractionsLoader 529 final Set<String> emailAddresses = new HashSet<>(); 530 // List of Entry that makes up the ExpandingEntryCardView 531 final List<Entry> entries = Lists.newArrayList(); 532 533 mEntriesAndActionsTask = new AsyncTask<Void, Void, Void>() { 534 @Override 535 protected Void doInBackground(Void... params) { 536 computeEntriesAndActions(data, phoneNumbers, emailAddresses, entries); 537 return null; 538 } 539 540 @Override 541 protected void onPostExecute(Void aVoid) { 542 super.onPostExecute(aVoid); 543 // Check that original AsyncTask parameters are still valid and the activity 544 // is still running before binding to UI. A new intent could invalidate 545 // the results, for example. 546 if (data == mContactData && !isCancelled()) { 547 bindEntriesAndActions(entries, phoneNumbers, emailAddresses); 548 showActivity(); 549 } 550 } 551 }; 552 mEntriesAndActionsTask.execute(); 553 } 554 555 private void bindEntriesAndActions(List<Entry> entries, 556 Set<String> phoneNumbers, 557 Set<String> emailAddresses) { 558 Trace.beginSection("start sms loader"); 559 final Bundle phonesExtraBundle = new Bundle(); 560 phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES, 561 phoneNumbers.toArray(new String[phoneNumbers.size()])); 562 getLoaderManager().initLoader( 563 LOADER_SMS_ID, 564 phonesExtraBundle, 565 mLoaderInteractionsCallbacks); 566 Trace.endSection(); 567 568 Trace.beginSection("start call log loader"); 569 getLoaderManager().initLoader( 570 LOADER_CALL_LOG_ID, 571 phonesExtraBundle, 572 mLoaderInteractionsCallbacks); 573 Trace.endSection(); 574 575 Trace.beginSection("start calendar loader"); 576 final Bundle emailsExtraBundle = new Bundle(); 577 emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS, 578 emailAddresses.toArray(new String[emailAddresses.size()])); 579 getLoaderManager().initLoader( 580 LOADER_CALENDAR_ID, 581 emailsExtraBundle, 582 mLoaderInteractionsCallbacks); 583 Trace.endSection(); 584 585 Trace.beginSection("bind communicate card"); 586 if (entries.size() > 0) { 587 mCommunicationCard.initialize(entries, 588 /* numInitialVisibleEntries = */ MIN_NUM_COMMUNICATION_ENTRIES_SHOWN, 589 /* isExpanded = */ false); 590 } 591 592 final boolean hasData = !entries.isEmpty(); 593 mCommunicationCard.setVisibility(hasData ? View.VISIBLE : View.GONE); 594 595 Trace.endSection(); 596 } 597 598 private void showActivity() { 599 if (mScroller != null) { 600 mScroller.setVisibility(View.VISIBLE); 601 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 602 new Runnable() { 603 @Override 604 public void run() { 605 runEntranceAnimation(); 606 } 607 }); 608 } 609 } 610 611 private void computeEntriesAndActions(Contact data, Set<String> phoneNumbers, 612 Set<String> emailAddresses, List<Entry> entries) { 613 Trace.beginSection("inflate entries and actions"); 614 615 // Map from {@link String} MIME-type to a list of {@link Action}. 616 final ActionMultiMap actions = new ActionMultiMap(); 617 618 final ResolveCache cache = ResolveCache.getInstance(this); 619 for (RawContact rawContact : data.getRawContacts()) { 620 for (DataItem dataItem : rawContact.getDataItems()) { 621 final String mimeType = dataItem.getMimeType(); 622 final AccountType accountType = rawContact.getAccountType(this); 623 final DataKind dataKind = AccountTypeManager.getInstance(this) 624 .getKindOrFallback(accountType, mimeType); 625 626 if (dataItem instanceof PhoneDataItem) { 627 phoneNumbers.add(((PhoneDataItem) dataItem).getNormalizedNumber()); 628 } 629 630 if (dataItem instanceof EmailDataItem) { 631 emailAddresses.add(((EmailDataItem) dataItem).getAddress()); 632 } 633 634 // Skip this data item if MIME-type excluded 635 if (isMimeExcluded(mimeType)) continue; 636 637 final long dataId = dataItem.getId(); 638 final boolean isPrimary = dataItem.isPrimary(); 639 final boolean isSuperPrimary = dataItem.isSuperPrimary(); 640 641 if (dataKind != null) { 642 // Build an action for this data entry, find a mapping to a UI 643 // element, build its summary from the cursor, and collect it 644 // along with all others of this MIME-type. 645 final Action action = new DataAction(getApplicationContext(), 646 dataItem, dataKind); 647 final boolean wasAdded = considerAdd(action, cache, isSuperPrimary, actions); 648 if (wasAdded) { 649 // Remember the default 650 if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) { 651 mDefaultsMap.put(mimeType, action); 652 } 653 } 654 } 655 656 // Handle Email rows with presence data as Im entry 657 final DataStatus status = data.getStatuses().get(dataId); 658 if (status != null && dataItem instanceof EmailDataItem) { 659 final EmailDataItem email = (EmailDataItem) dataItem; 660 final ImDataItem im = ImDataItem.createFromEmail(email); 661 if (dataKind != null) { 662 final DataAction action = new DataAction(getApplicationContext(), 663 im, dataKind); 664 action.setPresence(status.getPresence()); 665 considerAdd(action, cache, isSuperPrimary, actions); 666 } 667 } 668 } 669 } 670 671 Trace.endSection(); 672 Trace.beginSection("collapsing action list"); 673 674 // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources) 675 for (List<Action> actionChildren : actions.values()) { 676 Collapser.collapseList(actionChildren); 677 } 678 679 Trace.endSection(); 680 Trace.beginSection("sort mimetypes"); 681 682 /* 683 * Sorting is a multi part step. The end result is to a have a sorted list of the most 684 * used actions, one per mimetype. Then, within each mimetype, the list of actions for that 685 * type is also sorted, based off of {super primary, primary, times used} in that order. 686 */ 687 final List<Action> topActions = new ArrayList<>(); 688 final List<Action> allActions = new ArrayList<>(); 689 for (List<Action> mimeTypeActions : actions.values()) { 690 Collections.sort(mimeTypeActions, new Comparator<Action>() { 691 @Override 692 public int compare(Action lhs, Action rhs) { 693 /* 694 * Actions are compared to the same mimetype based off of three qualities: 695 * 1. Super primary 696 * 2. Primary 697 * 3. Times used 698 */ 699 if (lhs.isSuperPrimary()) { 700 return -1; 701 } else if (rhs.isSuperPrimary()) { 702 return 1; 703 } else if (lhs.isPrimary() && !rhs.isPrimary()) { 704 return -1; 705 } else if (!lhs.isPrimary() && rhs.isPrimary()) { 706 return 1; 707 } else { 708 int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed(); 709 int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed(); 710 711 return rhsTimesUsed - lhsTimesUsed; 712 } 713 } 714 }); 715 topActions.add(mimeTypeActions.get(0)); 716 // Add all the other actions and remove the top one 717 allActions.addAll(mimeTypeActions); 718 allActions.remove(mimeTypeActions.get(0)); 719 } 720 721 // topActions now contains the top action for each mimetype. This list now needs to be 722 // sorted, based off of {times used, last used, statically defined} in that order. 723 Collections.sort(topActions, new Comparator<Action>() { 724 @Override 725 public int compare(Action lhs, Action rhs) { 726 int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed(); 727 int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed(); 728 int timesUsedDifference = rhsTimesUsed - lhsTimesUsed; 729 if (timesUsedDifference != 0) { 730 return timesUsedDifference; 731 } 732 733 long lhsLastTimeUsed = lhs.getLastTimeUsed() == null ? 0 : lhs.getLastTimeUsed(); 734 long rhsLastTimeUsed = rhs.getLastTimeUsed() == null ? 0 : rhs.getLastTimeUsed(); 735 long lastTimeUsedDifference = rhsLastTimeUsed - lhsLastTimeUsed; 736 if (lastTimeUsedDifference > 0) { 737 return 1; 738 } else if (lastTimeUsedDifference < 0) { 739 return -1; 740 } 741 742 // Times used and last time used are the same. Resort to statically defined. 743 String lhsMimeType = lhs.getMimeType(); 744 String rhsMimeType = rhs.getMimeType(); 745 for (String mimeType : LEADING_MIMETYPES) { 746 if (lhsMimeType.equals(mimeType)) { 747 return -1; 748 } else if (rhsMimeType.equals(mimeType)) { 749 return 1; 750 } 751 } 752 // Trailing types come last, so flip the returns 753 for (String mimeType : TRAILING_MIMETYPES) { 754 if (lhsMimeType.equals(mimeType)) { 755 return 1; 756 } else if (rhsMimeType.equals(mimeType)) { 757 return -1; 758 } 759 } 760 return 0; 761 } 762 }); 763 764 entries.addAll(actionsToEntries(topActions)); 765 entries.addAll(actionsToEntries(allActions)); 766 Trace.endSection(); 767 } 768 769 /** 770 * Asynchronously extract the most vibrant color from the PhotoView. Once extracted, 771 * apply this tint to {@link MultiShrinkScroller}. This operation takes about 20-30ms 772 * on a Nexus 5. 773 */ 774 private void extractAndApplyTintFromPhotoViewAsynchronously() { 775 if (mScroller == null) { 776 return; 777 } 778 final Drawable imageViewDrawable = mPhotoView.getDrawable(); 779 new AsyncTask<Void, Void, Integer>() { 780 @Override 781 protected Integer doInBackground(Void... params) { 782 if (imageViewDrawable instanceof BitmapDrawable) { 783 final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap(); 784 return colorFromBitmap(bitmap); 785 } 786 if (imageViewDrawable instanceof LetterTileDrawable) { 787 return ((LetterTileDrawable) imageViewDrawable).getColor(); 788 } 789 return 0; 790 } 791 792 @Override 793 protected void onPostExecute(Integer color) { 794 super.onPostExecute(color); 795 if (mHasComputedThemeColor) { 796 // If we had previously computed a theme color from the contact photo, 797 // then do not update the theme color. Changing the theme color several 798 // seconds after QC has started, as a result of an updated/upgraded photo, 799 // is a jarring experience. On the other hand, changing the theme color after 800 // a rotation or onNewIntent() is perfectly fine. 801 return; 802 } 803 // Check that the Photo has not changed. If it has changed, the new tint 804 // color needs to be extracted 805 if (imageViewDrawable == mPhotoView.getDrawable()) { 806 mHasComputedThemeColor = true; 807 setThemeColor(color); 808 } 809 } 810 }.execute(); 811 } 812 813 private void setThemeColor(int color) { 814 // If the color is invalid, use the predefined default 815 if (color == 0) { 816 color = getResources().getColor(R.color.actionbar_background_color); 817 } 818 mScroller.setHeaderTintColor(color); 819 820 // Create a darker version of the actionbar color. HSV is device dependent 821 // and not perceptually-linear. Therefore, we can't say mStatusBarColor is 822 // 70% as bright as the action bar color. We can only say: it is a bit darker. 823 final float hsvComponents[] = new float[3]; 824 Color.colorToHSV(color, hsvComponents); 825 hsvComponents[2] *= SYSTEM_BAR_BRIGHTNESS_FACTOR; 826 mStatusBarColor = Color.HSVToColor(hsvComponents); 827 updateStatusBarColor(); 828 829 mColorFilter = 830 new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP); 831 mCommunicationCard.setColorAndFilter(color, mColorFilter); 832 mRecentCard.setColorAndFilter(color, mColorFilter); 833 } 834 835 private void updateStatusBarColor() { 836 if (mScroller == null) { 837 return; 838 } 839 final int desiredStatusBarColor; 840 // Only use a custom status bar color if QuickContacts touches the top of the viewport. 841 if (mScroller.getScrollNeededToBeFullScreen() <= 0) { 842 desiredStatusBarColor = mStatusBarColor; 843 } else { 844 desiredStatusBarColor = Color.TRANSPARENT; 845 } 846 // Animate to the new color. 847 if (desiredStatusBarColor != getWindow().getStatusBarColor()) { 848 final ObjectAnimator animation = ObjectAnimator.ofInt(getWindow(), "statusBarColor", 849 getWindow().getStatusBarColor(), desiredStatusBarColor); 850 animation.setDuration(ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION); 851 animation.setEvaluator(new ArgbEvaluator()); 852 animation.start(); 853 } 854 } 855 856 private int colorFromBitmap(Bitmap bitmap) { 857 // Author of Palette recommends using 24 colors when analyzing profile photos. 858 final int NUMBER_OF_PALETTE_COLORS = 24; 859 final Palette palette = Palette.generate(bitmap, NUMBER_OF_PALETTE_COLORS); 860 if (palette != null && palette.getVibrantColor() != null) { 861 return palette.getVibrantColor().getRgb(); 862 } 863 return 0; 864 } 865 866 /** 867 * Consider adding the given {@link Action}, which will only happen if 868 * {@link PackageManager} finds an application to handle 869 * {@link Action#getIntent()}. 870 * @param action the action to handle 871 * @param resolveCache cache of applications that can handle actions 872 * @param front indicates whether to add the action to the front of the list 873 * @param actions where to put the action. 874 * @return true if action has been added 875 */ 876 private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front, 877 ActionMultiMap actions) { 878 if (resolveCache.hasResolve(action)) { 879 actions.put(action.getMimeType(), action, front); 880 return true; 881 } 882 return false; 883 } 884 885 /** 886 * Converts a list of Action into a list of Entry 887 * @param actions The list of Action to convert 888 * @return The converted list of Entry 889 */ 890 private List<Entry> actionsToEntries(List<Action> actions) { 891 List<Entry> entries = new ArrayList<>(); 892 for (Action action : actions) { 893 final String header = action.getBody() == null ? null : action.getBody().toString(); 894 final String footer = action.getBody() == null ? null : action.getBody().toString(); 895 String body = null; 896 Drawable icon = null; 897 switch (action.getMimeType()) { 898 case Phone.CONTENT_ITEM_TYPE: 899 icon = getResources().getDrawable(R.drawable.ic_phone_24dp); 900 break; 901 case Email.CONTENT_ITEM_TYPE: 902 icon = getResources().getDrawable(R.drawable.ic_email_24dp); 903 break; 904 case StructuredPostal.CONTENT_ITEM_TYPE: 905 icon = getResources().getDrawable(R.drawable.ic_place_24dp); 906 break; 907 default: 908 icon = ResolveCache.getInstance(this).getIcon(action); 909 } 910 entries.add(new Entry(icon, header, body, footer, action.getIntent(), 911 /* isEditable= */ false)); 912 913 // Add SMS in addition to phone calls 914 if (action.getMimeType().equals(Phone.CONTENT_ITEM_TYPE)) { 915 entries.add(new Entry(getResources().getDrawable(R.drawable.ic_message_24dp), 916 getResources().getString(R.string.send_message), null, header, 917 action.getAlternateIntent(), /* isEditable = */ false)); 918 } 919 } 920 return entries; 921 } 922 923 private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) { 924 List<Entry> entries = new ArrayList<>(); 925 for (ContactInteraction interaction : interactions) { 926 entries.add(new Entry(interaction.getIcon(this), 927 interaction.getViewHeader(this), 928 interaction.getViewBody(this), 929 interaction.getBodyIcon(this), 930 interaction.getViewFooter(this), 931 interaction.getFooterIcon(this), 932 interaction.getIntent(), 933 /* isEditable = */ false)); 934 } 935 return entries; 936 } 937 938 private LoaderCallbacks<Contact> mLoaderContactCallbacks = 939 new LoaderCallbacks<Contact>() { 940 @Override 941 public void onLoaderReset(Loader<Contact> loader) { 942 } 943 944 @Override 945 public void onLoadFinished(Loader<Contact> loader, Contact data) { 946 Trace.beginSection("onLoadFinished()"); 947 948 if (isFinishing()) { 949 return; 950 } 951 if (data.isError()) { 952 // This shouldn't ever happen, so throw an exception. The {@link ContactLoader} 953 // should log the actual exception. 954 throw new IllegalStateException("Failed to load contact", data.getException()); 955 } 956 if (data.isNotFound()) { 957 if (mHasAlreadyBeenOpened) { 958 finish(); 959 } else { 960 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri()); 961 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 962 Toast.LENGTH_LONG).show(); 963 } 964 return; 965 } 966 967 bindContactData(data); 968 969 Trace.endSection(); 970 } 971 972 @Override 973 public Loader<Contact> onCreateLoader(int id, Bundle args) { 974 if (mLookupUri == null) { 975 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early"); 976 } 977 // Load all contact data. We need loadGroupMetaData=true to determine whether the 978 // contact is invisible. If it is, we need to display an "Add to Contacts" MenuItem. 979 return new ContactLoader(getApplicationContext(), mLookupUri, 980 true /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/, 981 true /*postViewNotification*/, true /*computeFormattedPhoneNumber*/); 982 } 983 }; 984 985 @Override 986 public void onBackPressed() { 987 if (mScroller != null) { 988 if (!mIsExitAnimationInProgress) { 989 mScroller.scrollOffBottom(); 990 } 991 } else { 992 super.onBackPressed(); 993 } 994 } 995 996 @Override 997 public void finish() { 998 super.finish(); 999 1000 // override transitions to skip the standard window animations 1001 overridePendingTransition(0, 0); 1002 } 1003 1004 private LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks = 1005 new LoaderCallbacks<List<ContactInteraction>>() { 1006 1007 @Override 1008 public Loader<List<ContactInteraction>> onCreateLoader(int id, Bundle args) { 1009 Log.v(TAG, "onCreateLoader"); 1010 Loader<List<ContactInteraction>> loader = null; 1011 switch (id) { 1012 case LOADER_SMS_ID: 1013 Log.v(TAG, "LOADER_SMS_ID"); 1014 loader = new SmsInteractionsLoader( 1015 QuickContactActivity.this, 1016 args.getStringArray(KEY_LOADER_EXTRA_PHONES), 1017 MAX_SMS_RETRIEVE); 1018 break; 1019 case LOADER_CALENDAR_ID: 1020 Log.v(TAG, "LOADER_CALENDAR_ID"); 1021 loader = new CalendarInteractionsLoader( 1022 QuickContactActivity.this, 1023 Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_EMAILS)), 1024 MAX_FUTURE_CALENDAR_RETRIEVE, 1025 MAX_PAST_CALENDAR_RETRIEVE, 1026 FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR, 1027 PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR); 1028 break; 1029 case LOADER_CALL_LOG_ID: 1030 Log.v(TAG, "LOADER_CALL_LOG_ID"); 1031 loader = new CallLogInteractionsLoader( 1032 QuickContactActivity.this, 1033 args.getStringArray(KEY_LOADER_EXTRA_PHONES), 1034 MAX_CALL_LOG_RETRIEVE); 1035 } 1036 return loader; 1037 } 1038 1039 @Override 1040 public void onLoadFinished(Loader<List<ContactInteraction>> loader, 1041 List<ContactInteraction> data) { 1042 if (mRecentLoaderResults == null) { 1043 mRecentLoaderResults = new HashMap<Integer, List<ContactInteraction>>(); 1044 } 1045 Log.v(TAG, "onLoadFinished ~ loader.getId() " + loader.getId() + " data.size() " + 1046 data.size()); 1047 mRecentLoaderResults.put(loader.getId(), data); 1048 1049 if (isAllRecentDataLoaded()) { 1050 bindRecentData(); 1051 } 1052 } 1053 1054 @Override 1055 public void onLoaderReset(Loader<List<ContactInteraction>> loader) { 1056 mRecentLoaderResults.remove(loader.getId()); 1057 } 1058 1059 }; 1060 1061 private boolean isAllRecentDataLoaded() { 1062 return mRecentLoaderResults.size() == mRecentLoaderIds.length; 1063 } 1064 1065 private void bindRecentData() { 1066 List<ContactInteraction> allInteractions = new ArrayList<>(); 1067 for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) { 1068 allInteractions.addAll(loaderInteractions); 1069 } 1070 1071 // Sort the interactions by most recent 1072 Collections.sort(allInteractions, new Comparator<ContactInteraction>() { 1073 @Override 1074 public int compare(ContactInteraction a, ContactInteraction b) { 1075 return a.getInteractionDate() >= b.getInteractionDate() ? -1 : 1; 1076 } 1077 }); 1078 1079 if (allInteractions.size() > 0) { 1080 mRecentCard.initialize(contactInteractionsToEntries(allInteractions), 1081 /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN, 1082 /* isExpanded = */ false); 1083 mRecentCard.setVisibility(View.VISIBLE); 1084 } 1085 } 1086 1087 @Override 1088 protected void onStop() { 1089 super.onStop(); 1090 1091 if (mEntriesAndActionsTask != null) { 1092 // Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's 1093 // results on the UI thread. In some circumstances Activities are killed without 1094 // onStop() being called. This is not a problem, because in these circumstances 1095 // the entire process will be killed. 1096 mEntriesAndActionsTask.cancel(/* mayInterruptIfRunning = */ false); 1097 } 1098 } 1099 1100 /** 1101 * Returns true if it is possible to edit the current contact. 1102 */ 1103 private boolean isContactEditable() { 1104 return mContactData != null && !mContactData.isDirectoryEntry(); 1105 } 1106 1107 private void editContact() { 1108 final Intent intent = new Intent(Intent.ACTION_EDIT, mLookupUri); 1109 mContactLoader.cacheResult(); 1110 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 1111 startActivityForResult(intent, REQUEST_CODE_CONTACT_EDITOR_ACTIVITY); 1112 } 1113 1114 private void toggleStar(MenuItem starredMenuItem) { 1115 // Make sure there is a contact 1116 if (mLookupUri != null) { 1117 // Read the current starred value from the UI instead of using the last 1118 // loaded state. This allows rapid tapping without writing the same 1119 // value several times 1120 final boolean isStarred = starredMenuItem.isChecked(); 1121 1122 // To improve responsiveness, swap out the picture (and tag) in the UI already 1123 ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem, 1124 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1125 !isStarred); 1126 1127 // Now perform the real save 1128 Intent intent = ContactSaveService.createSetStarredIntent( 1129 QuickContactActivity.this, mLookupUri, !isStarred); 1130 startService(intent); 1131 } 1132 } 1133 1134 /** 1135 * Calls into the contacts provider to get a pre-authorized version of the given URI. 1136 */ 1137 private Uri getPreAuthorizedUri(Uri uri) { 1138 final Bundle uriBundle = new Bundle(); 1139 uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, uri); 1140 final Bundle authResponse = getContentResolver().call( 1141 ContactsContract.AUTHORITY_URI, 1142 ContactsContract.Authorization.AUTHORIZATION_METHOD, 1143 null, 1144 uriBundle); 1145 if (authResponse != null) { 1146 return (Uri) authResponse.getParcelable( 1147 ContactsContract.Authorization.KEY_AUTHORIZED_URI); 1148 } else { 1149 return uri; 1150 } 1151 } 1152 private void shareContact() { 1153 final String lookupKey = mContactData.getLookupKey(); 1154 Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey); 1155 if (mContactData.isUserProfile()) { 1156 // User is sharing the profile. We don't want to force the receiver to have 1157 // the highly-privileged READ_PROFILE permission, so we need to request a 1158 // pre-authorized URI from the provider. 1159 shareUri = getPreAuthorizedUri(shareUri); 1160 } 1161 1162 final Intent intent = new Intent(Intent.ACTION_SEND); 1163 intent.setType(Contacts.CONTENT_VCARD_TYPE); 1164 intent.putExtra(Intent.EXTRA_STREAM, shareUri); 1165 1166 // Launch chooser to share contact via 1167 final CharSequence chooseTitle = getText(R.string.share_via); 1168 final Intent chooseIntent = Intent.createChooser(intent, chooseTitle); 1169 1170 try { 1171 this.startActivity(chooseIntent); 1172 } catch (ActivityNotFoundException ex) { 1173 Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show(); 1174 } 1175 } 1176 1177 /** 1178 * Creates a launcher shortcut with the current contact. 1179 */ 1180 private void createLauncherShortcutWithContact() { 1181 final ShortcutIntentBuilder builder = new ShortcutIntentBuilder(this, 1182 new OnShortcutIntentCreatedListener() { 1183 1184 @Override 1185 public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) { 1186 // Broadcast the shortcutIntent to the launcher to create a 1187 // shortcut to this contact 1188 shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT); 1189 QuickContactActivity.this.sendBroadcast(shortcutIntent); 1190 1191 // Send a toast to give feedback to the user that a shortcut to this 1192 // contact was added to the launcher. 1193 Toast.makeText(QuickContactActivity.this, 1194 R.string.createContactShortcutSuccessful, 1195 Toast.LENGTH_SHORT).show(); 1196 } 1197 1198 }); 1199 builder.createContactShortcutIntent(mLookupUri); 1200 } 1201 1202 @Override 1203 public boolean onCreateOptionsMenu(Menu menu) { 1204 MenuInflater inflater = getMenuInflater(); 1205 inflater.inflate(R.menu.quickcontact, menu); 1206 return true; 1207 } 1208 1209 @Override 1210 public boolean onPrepareOptionsMenu(Menu menu) { 1211 if (mContactData != null) { 1212 final MenuItem starredMenuItem = menu.findItem(R.id.menu_star); 1213 ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem, 1214 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1215 mContactData.getStarred()); 1216 // Configure edit MenuItem 1217 final MenuItem editMenuItem = menu.findItem(R.id.menu_edit); 1218 editMenuItem.setVisible(true); 1219 if (DirectoryContactUtil.isDirectoryContact(mContactData) || InvisibleContactUtil 1220 .isInvisibleAndAddable(mContactData, this)) { 1221 editMenuItem.setIcon(R.drawable.ic_person_add_tinted_24dp); 1222 } else if (isContactEditable()) { 1223 editMenuItem.setIcon(R.drawable.ic_create_24dp); 1224 } else { 1225 editMenuItem.setVisible(false); 1226 } 1227 return true; 1228 } 1229 return false; 1230 } 1231 1232 @Override 1233 public boolean onOptionsItemSelected(MenuItem item) { 1234 switch (item.getItemId()) { 1235 case R.id.menu_star: 1236 toggleStar(item); 1237 return true; 1238 case R.id.menu_edit: 1239 if (DirectoryContactUtil.isDirectoryContact(mContactData)) { 1240 DirectoryContactUtil.addToMyContacts(mContactData, this, getFragmentManager(), 1241 mSelectAccountFragmentListener); 1242 } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) { 1243 InvisibleContactUtil.addToDefaultGroup(mContactData, this); 1244 } else if (isContactEditable()) { 1245 editContact(); 1246 } 1247 return true; 1248 case R.id.menu_share: 1249 shareContact(); 1250 return true; 1251 case R.id.menu_create_contact_shortcut: 1252 createLauncherShortcutWithContact(); 1253 return true; 1254 default: 1255 return super.onOptionsItemSelected(item); 1256 } 1257 } 1258} 1259