1 /* 2 * Copyright (C) 2011 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package com.android.dialer.list; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.database.Cursor; 22 import android.database.DataSetObserver; 23 import android.view.MotionEvent; 24 import android.view.View; 25 import android.view.ViewConfiguration; 26 import android.view.ViewGroup; 27 import android.widget.BaseAdapter; 28 import android.widget.FrameLayout; 29 30 import com.android.dialer.R; 31 import com.android.dialer.calllog.CallLogAdapter; 32 import com.android.dialer.calllog.CallLogNotificationsHelper; 33 import com.android.dialer.calllog.CallLogQueryHandler; 34 import com.android.dialer.list.SwipeHelper.OnItemGestureListener; 35 import com.android.dialer.list.SwipeHelper.SwipeHelperCallback; 36 37 /** 38 * An adapter that combines items from {@link com.android.contacts.common.list.ContactTileAdapter} 39 * and {@link com.android.dialer.calllog.CallLogAdapter} into a single list. 40 */ 41 public class PhoneFavoriteMergedAdapter extends BaseAdapter { 42 43 private class CustomDataSetObserver extends DataSetObserver { 44 @Override 45 public void onChanged() { 46 notifyDataSetChanged(); 47 } 48 } 49 50 private static final String TAG = PhoneFavoriteMergedAdapter.class.getSimpleName(); 51 52 private static final int TILE_INTERACTION_TEASER_VIEW_POSITION = 2; 53 private static final int TILE_INTERACTION_TEASER_VIEW_ID = -2; 54 private static final int ALL_CONTACTS_BUTTON_ITEM_ID = -1; 55 private final PhoneFavoritesTileAdapter mContactTileAdapter; 56 private final CallLogAdapter mCallLogAdapter; 57 private final View mShowAllContactsButton; 58 private final PhoneFavoriteFragment mFragment; 59 private final TileInteractionTeaserView mTileInteractionTeaserView; 60 61 private final int mCallLogPadding; 62 63 private final Context mContext; 64 65 private final DataSetObserver mObserver; 66 67 private final CallLogQueryHandler mCallLogQueryHandler; 68 69 private final OnItemGestureListener mCallLogOnItemSwipeListener = 70 new OnItemGestureListener() { 71 @Override 72 public void onSwipe(View view) { 73 mCallLogQueryHandler.markNewCallsAsOld(); 74 mCallLogQueryHandler.markNewVoicemailsAsOld(); 75 CallLogNotificationsHelper.removeMissedCallNotifications(); 76 CallLogNotificationsHelper.updateVoicemailNotifications(mContext); 77 mFragment.dismissShortcut(((View) view.getParent()).getHeight()); 78 } 79 80 @Override 81 public void onTouch() {} 82 83 @Override 84 public boolean isSwipeEnabled() { 85 return true; 86 } 87 }; 88 89 private final CallLogQueryHandler.Listener mCallLogQueryHandlerListener = 90 new CallLogQueryHandler.Listener() { 91 @Override 92 public void onVoicemailStatusFetched(Cursor statusCursor) {} 93 94 @Override 95 public void onCallsFetched(Cursor combinedCursor) { 96 mCallLogAdapter.invalidateCache(); 97 mCallLogAdapter.changeCursor(combinedCursor); 98 mCallLogAdapter.notifyDataSetChanged(); 99 } 100 }; 101 102 public PhoneFavoriteMergedAdapter(Context context, 103 PhoneFavoriteFragment fragment, 104 PhoneFavoritesTileAdapter contactTileAdapter, 105 CallLogAdapter callLogAdapter, 106 View showAllContactsButton, 107 TileInteractionTeaserView tileInteractionTeaserView) { 108 final Resources resources = context.getResources(); 109 mContext = context; 110 mFragment = fragment; 111 mCallLogPadding = resources.getDimensionPixelSize(R.dimen.recent_call_log_item_padding); 112 mContactTileAdapter = contactTileAdapter; 113 mCallLogAdapter = callLogAdapter; 114 mObserver = new CustomDataSetObserver(); 115 mCallLogAdapter.registerDataSetObserver(mObserver); 116 mContactTileAdapter.registerDataSetObserver(mObserver); 117 mShowAllContactsButton = showAllContactsButton; 118 mTileInteractionTeaserView = tileInteractionTeaserView; 119 mCallLogQueryHandler = new CallLogQueryHandler(mContext.getContentResolver(), 120 mCallLogQueryHandlerListener); 121 } 122 123 @Override 124 public int getCount() { 125 if (mContactTileAdapter.getCount() > 0) { 126 return mContactTileAdapter.getCount() + mCallLogAdapter.getCount() + 1 + 127 getTeaserViewCount(); 128 } else { 129 return mCallLogAdapter.getCount(); 130 } 131 } 132 133 @Override 134 public Object getItem(int position) { 135 final int callLogAdapterCount = mCallLogAdapter.getCount(); 136 137 if (callLogAdapterCount > 0) { 138 if (position < callLogAdapterCount) { 139 return mCallLogAdapter.getItem(position); 140 } 141 } 142 // Set position to the position of the actual favorite contact in the favorites adapter 143 position = getAdjustedFavoritePosition(position, callLogAdapterCount); 144 145 return mContactTileAdapter.getItem(position); 146 } 147 148 /** 149 * In order to ensure that items have stable ids (for animation purposes), we need to 150 * guarantee that every single item has a unique ID, even across data set changes. 151 * 152 * These are the ranges of IDs reserved for each item type. 153 * 154 * -(N + 1) to -3: CallLogAdapterItems, where N is equal to the number of call log items 155 * -2: Teaser 156 * -1: All contacts button 157 * 0 to (N -1): Rows of tiled contacts, where N is equal to the max rows of tiled contacts 158 * N to infinity: Rows of regular contacts. Their item id is calculated by N + contact_id, 159 * where contact_id is guaranteed to never be negative. 160 */ 161 @Override 162 public long getItemId(int position) { 163 final int callLogAdapterCount = mCallLogAdapter.getCount(); 164 if (position < callLogAdapterCount) { 165 // Call log items are not animated, so reusing their position for IDs is fine. 166 return ALL_CONTACTS_BUTTON_ITEM_ID - 2 - position; 167 } else if (position == TILE_INTERACTION_TEASER_VIEW_POSITION + callLogAdapterCount && 168 mTileInteractionTeaserView.getShouldDisplayInList()){ 169 return TILE_INTERACTION_TEASER_VIEW_ID; 170 } else if (position < (callLogAdapterCount + mContactTileAdapter.getCount() + 171 getTeaserViewCount())) { 172 return mContactTileAdapter.getItemId( 173 getAdjustedFavoritePosition(position, callLogAdapterCount)); 174 } else { 175 // All contacts button 176 return ALL_CONTACTS_BUTTON_ITEM_ID; 177 } 178 } 179 180 @Override 181 public boolean hasStableIds() { 182 return true; 183 } 184 185 @Override 186 public int getViewTypeCount() { 187 return (mContactTileAdapter.getViewTypeCount() + /* Favorite and frequent */ 188 mCallLogAdapter.getViewTypeCount() + /* Recent call log */ 189 getTeaserViewCount() + /* Teaser */ 190 1); /* Show all contacts button. */ 191 } 192 193 @Override 194 public int getItemViewType(int position) { 195 final int callLogAdapterCount = mCallLogAdapter.getCount(); 196 197 if (position < callLogAdapterCount) { 198 // View type of the call log adapter is the last view type of the contact tile adapter 199 // + 1 200 return mContactTileAdapter.getViewTypeCount(); 201 } else if (position == TILE_INTERACTION_TEASER_VIEW_POSITION + callLogAdapterCount && 202 mTileInteractionTeaserView.getShouldDisplayInList()) { 203 // View type of the teaser row is the last view type of the contact tile adapter + 3 204 return mContactTileAdapter.getViewTypeCount() + 2; 205 } else if (position < getCount() - 1) { 206 return mContactTileAdapter.getItemViewType( 207 getAdjustedFavoritePosition(position, callLogAdapterCount)); 208 } else { 209 // View type of the show all contact button is the last view type of the contact tile 210 // adapter + 2 211 return mContactTileAdapter.getViewTypeCount() + 1; 212 } 213 } 214 215 @Override 216 public View getView(int position, View convertView, ViewGroup parent) { 217 final int callLogAdapterCount = mCallLogAdapter.getCount(); 218 219 if ((position == getCount() - 1) && (mContactTileAdapter.getCount() > 0)) { 220 return mShowAllContactsButton; 221 } 222 223 if (mTileInteractionTeaserView.getShouldDisplayInList()) { 224 if (position == TILE_INTERACTION_TEASER_VIEW_POSITION + callLogAdapterCount) { 225 return mTileInteractionTeaserView; 226 } 227 } 228 229 if (callLogAdapterCount > 0) { 230 if (position == 0) { 231 final SwipeableCallLogRow wrapper; 232 if (convertView == null) { 233 wrapper = new SwipeableCallLogRow(mContext); 234 wrapper.setOnItemSwipeListener(mCallLogOnItemSwipeListener); 235 } else { 236 wrapper = (SwipeableCallLogRow) convertView; 237 } 238 239 // Special case wrapper view for the most recent call log item. This allows 240 // us to create a card-like effect for the more recent call log item in 241 // the PhoneFavoriteMergedAdapter, but keep the original look of the item in 242 // the CallLogAdapter. 243 final View view = mCallLogAdapter.getView(position, convertView == null ? 244 null : wrapper.getChildAt(0), parent); 245 wrapper.removeAllViews(); 246 final View callLogItem = view.findViewById(R.id.call_log_list_item); 247 // Reset the internal call log item view if it is being recycled 248 callLogItem.setTranslationX(0); 249 callLogItem.setAlpha(1); 250 wrapper.addView(view); 251 return wrapper; 252 } 253 } 254 255 // Set position to the position of the actual favorite contact in the 256 // favorites adapter 257 position = getAdjustedFavoritePosition(position, callLogAdapterCount); 258 259 // Favorites section 260 final View view = mContactTileAdapter.getView(position, convertView, parent); 261 if (position >= mContactTileAdapter.getMaxTiledRows()) { 262 final FrameLayout frameLayout = (FrameLayout) view; 263 final View child = frameLayout.getChildAt(0); 264 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( 265 FrameLayout.LayoutParams.WRAP_CONTENT, 266 FrameLayout.LayoutParams.WRAP_CONTENT); 267 child.setLayoutParams(params); 268 } 269 return view; 270 } 271 272 @Override 273 public boolean areAllItemsEnabled() { 274 return mCallLogAdapter.areAllItemsEnabled() && mContactTileAdapter.areAllItemsEnabled(); 275 } 276 277 @Override 278 public boolean isEnabled(int position) { 279 final int callLogAdapterCount = mCallLogAdapter.getCount(); 280 if (position < callLogAdapterCount) { 281 return mCallLogAdapter.isEnabled(position); 282 } else { // For favorites section 283 return mContactTileAdapter.isEnabled( 284 getAdjustedFavoritePosition(position, callLogAdapterCount)); 285 } 286 } 287 288 private int getAdjustedFavoritePosition(int position, int callLogAdapterCount) { 289 if (position - callLogAdapterCount > TILE_INTERACTION_TEASER_VIEW_POSITION && 290 mTileInteractionTeaserView.getShouldDisplayInList()) { 291 return position - callLogAdapterCount - 1; 292 } else { 293 return position - callLogAdapterCount; 294 } 295 } 296 297 private int getTeaserViewCount() { 298 return (mContactTileAdapter.getCount() > TILE_INTERACTION_TEASER_VIEW_POSITION && 299 mTileInteractionTeaserView.getShouldDisplayInList() ? 1 : 0); 300 } 301 302 /** 303 * The swipeable call log row. 304 * See also {@link PhoneFavoritesTileAdapter.ContactTileRow}. 305 */ 306 private class SwipeableCallLogRow extends FrameLayout implements SwipeHelperCallback { 307 private SwipeHelper mSwipeHelper; 308 private OnItemGestureListener mOnItemSwipeListener; 309 310 public SwipeableCallLogRow(Context context) { 311 super(context); 312 final float densityScale = getResources().getDisplayMetrics().density; 313 final float pagingTouchSlop = ViewConfiguration.get(context) 314 .getScaledPagingTouchSlop(); 315 mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, 316 densityScale, pagingTouchSlop); 317 } 318 319 @Override 320 public void addView(View view) { 321 view.setBackgroundResource(R.drawable.dialer_recent_card_bg); 322 323 final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( 324 FrameLayout.LayoutParams.MATCH_PARENT, 325 FrameLayout.LayoutParams.WRAP_CONTENT); 326 params.setMarginsRelative(mCallLogPadding, mCallLogPadding, mCallLogPadding, 327 mCallLogPadding); 328 view.setLayoutParams(params); 329 330 super.addView(view); 331 } 332 333 @Override 334 public View getChildAtPosition(MotionEvent ev) { 335 return getChildCount() > 0 ? getChildAt(0) : null; 336 } 337 338 @Override 339 public View getChildContentView(View v) { 340 return v.findViewById(R.id.call_log_list_item); 341 } 342 343 @Override 344 public void onScroll() {} 345 346 @Override 347 public boolean canChildBeDismissed(View v) { 348 return true; 349 } 350 351 @Override 352 public void onBeginDrag(View v) { 353 // We do this so the underlying ScrollView knows that it won't get 354 // the chance to intercept events anymore 355 requestDisallowInterceptTouchEvent(true); 356 } 357 358 @Override 359 public void onChildDismissed(View v) { 360 if (v != null && mOnItemSwipeListener != null) { 361 mOnItemSwipeListener.onSwipe(v); 362 } 363 } 364 365 @Override 366 public void onDragCancelled(View v) {} 367 368 @Override 369 public boolean onInterceptTouchEvent(MotionEvent ev) { 370 if (mSwipeHelper != null) { 371 return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev); 372 } else { 373 return super.onInterceptTouchEvent(ev); 374 } 375 } 376 377 @Override 378 public boolean onTouchEvent(MotionEvent ev) { 379 if (mSwipeHelper != null) { 380 return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev); 381 } else { 382 return super.onTouchEvent(ev); 383 } 384 } 385 386 public void setOnItemSwipeListener(OnItemGestureListener listener) { 387 mOnItemSwipeListener = listener; 388 } 389 } 390 } 391