1 /* 2 * Copyright (C) 2015 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 package com.android.car.dialer; 17 18 import android.content.ContentResolver; 19 import android.content.Context; 20 import android.content.CursorLoader; 21 import android.content.res.Resources; 22 import android.database.ContentObserver; 23 import android.database.Cursor; 24 import android.graphics.Canvas; 25 import android.graphics.Paint; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.support.annotation.Nullable; 30 import android.support.v4.app.Fragment; 31 import android.support.v7.widget.RecyclerView; 32 import android.util.Log; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.widget.LinearLayout; 37 38 import com.android.car.dialer.telecom.PhoneLoader; 39 import com.android.car.dialer.telecom.UiCallManager; 40 import com.android.car.view.PagedListView; 41 42 /** 43 * Contains a list of contacts. The call types can be any of the CALL_TYPE_* fields from 44 * {@link PhoneLoader}. 45 */ 46 public class StrequentsFragment extends Fragment { 47 private static final String TAG = "Em.StrequentsFrag"; 48 49 private static final String KEY_MAX_CLICKS = "max_clicks"; 50 private static final int DEFAULT_MAX_CLICKS = 6; 51 52 private UiCallManager mUiCallManager; 53 private StrequentsAdapter mAdapter; 54 private CursorLoader mSpeedialCursorLoader; 55 private CursorLoader mCallLogCursorLoader; 56 private Context mContext; 57 private PagedListView mListView; 58 private Cursor mStrequentCursor; 59 private Cursor mCallLogCursor; 60 private boolean mHasLoadedData; 61 62 public static StrequentsFragment newInstance(UiCallManager callManager) { 63 StrequentsFragment fragment = new StrequentsFragment(); 64 fragment.mUiCallManager = callManager; 65 return fragment; 66 } 67 68 @Override 69 public void onCreate(@Nullable Bundle savedInstanceState) { 70 super.onCreate(savedInstanceState); 71 if (Log.isLoggable(TAG, Log.DEBUG)) { 72 Log.d(TAG, "onCreate"); 73 } 74 } 75 76 @Override 77 public View onCreateView(LayoutInflater inflater, ViewGroup container, 78 Bundle savedInstanceState) { 79 if (Log.isLoggable(TAG, Log.DEBUG)) { 80 Log.d(TAG, "onCreateView"); 81 } 82 83 mContext = getContext(); 84 85 View view = inflater.inflate(R.layout.strequents_fragment, container, false); 86 mListView = (PagedListView) view.findViewById(R.id.list_view); 87 mListView.getLayoutManager().setOffsetRows(true); 88 89 mSpeedialCursorLoader = PhoneLoader.registerCallObserver(PhoneLoader.CALL_TYPE_SPEED_DIAL, 90 mContext, (loader, cursor) -> { 91 if (Log.isLoggable(TAG, Log.DEBUG)) { 92 Log.d(TAG, "PhoneLoader: onLoadComplete (CALL_TYPE_SPEED_DIAL)"); 93 } 94 95 onLoadStrequentCursor(cursor); 96 97 if (mContext != null) { 98 mListView.addItemDecoration(new Decoration(mContext)); 99 } 100 }); 101 102 // Get the latest call log from the call logs history. 103 mCallLogCursorLoader = PhoneLoader.registerCallObserver(PhoneLoader.CALL_TYPE_ALL, mContext, 104 (loader, cursor) -> { 105 if (Log.isLoggable(TAG, Log.DEBUG)) { 106 Log.d(TAG, "PhoneLoader: onLoadComplete (CALL_TYPE_ALL)"); 107 } 108 onLoadCallLogCursor(cursor); 109 }); 110 111 ContentResolver contentResolver = mContext.getContentResolver(); 112 contentResolver.registerContentObserver(mSpeedialCursorLoader.getUri(), 113 false, new SpeedDialContentObserver(new Handler())); 114 contentResolver.registerContentObserver(mCallLogCursorLoader.getUri(), 115 false, new CallLogContentObserver(new Handler())); 116 117 // Maximum number of forward acting clicks the user can perform 118 Bundle args = getArguments(); 119 int maxClicks = args == null 120 ? DEFAULT_MAX_CLICKS 121 : args.getInt(KEY_MAX_CLICKS, DEFAULT_MAX_CLICKS /* G.maxForwardClicks.get() */); 122 // We want to show one fewer page than max clicks to allow clicking on an item, 123 // but, the first page is "free" since it doesn't take any clicks to show 124 final int maxPages = maxClicks < 0 ? -1 : maxClicks; 125 if (Log.isLoggable(TAG, Log.VERBOSE)) { 126 Log.v(TAG, "Max clicks: " + maxClicks + ", Max pages: " + maxPages); 127 } 128 129 mListView.setLightMode(); 130 mAdapter = new StrequentsAdapter(mContext, mUiCallManager); 131 mAdapter.setStrequentsListener(viewHolder -> { 132 if (Log.isLoggable(TAG, Log.DEBUG)) { 133 Log.d(TAG, "onContactedClicked"); 134 } 135 136 mUiCallManager.safePlaceCall((String) viewHolder.itemView.getTag(), false); 137 }); 138 mListView.setMaxPages(maxPages); 139 mListView.setAdapter(mAdapter); 140 141 if (Log.isLoggable(TAG, Log.DEBUG)) { 142 Log.d(TAG, "setItemAnimator"); 143 } 144 145 mListView.getRecyclerView().setItemAnimator(new StrequentsItemAnimator()); 146 return view; 147 } 148 149 @Override 150 public void onDestroyView() { 151 super.onDestroyView(); 152 153 if (Log.isLoggable(TAG, Log.DEBUG)) { 154 Log.d(TAG, "onDestroyView"); 155 } 156 157 mAdapter.setStrequentCursor(null); 158 mAdapter.setLastCallCursor(null); 159 mCallLogCursorLoader.reset(); 160 mSpeedialCursorLoader.reset(); 161 mCallLogCursor = null; 162 mStrequentCursor = null; 163 mHasLoadedData = false; 164 mContext = null; 165 } 166 167 private void loadDataIntoAdapter() { 168 if (Log.isLoggable(TAG, Log.DEBUG)) { 169 Log.d(TAG, "loadDataIntoAdapter"); 170 } 171 172 mHasLoadedData = true; 173 mAdapter.setLastCallCursor(mCallLogCursor); 174 mAdapter.setStrequentCursor(mStrequentCursor); 175 } 176 177 private void onLoadStrequentCursor(Cursor cursor) { 178 if (Log.isLoggable(TAG, Log.DEBUG)) { 179 Log.d(TAG, "onLoadStrequentCursor"); 180 } 181 182 if (cursor == null) { 183 throw new IllegalArgumentException( 184 "cursor was null in on speed dial fetched"); 185 } 186 187 mStrequentCursor = cursor; 188 if (mCallLogCursor != null) { 189 if (mHasLoadedData) { 190 mAdapter.setStrequentCursor(cursor); 191 } else { 192 loadDataIntoAdapter(); 193 } 194 } 195 } 196 197 private void onLoadCallLogCursor(Cursor cursor) { 198 if (cursor == null) { 199 throw new IllegalArgumentException( 200 "cursor was null in on calls fetched"); 201 } 202 203 mCallLogCursor = cursor; 204 if (mStrequentCursor != null) { 205 if (mHasLoadedData) { 206 mAdapter.setLastCallCursor(cursor); 207 } else { 208 loadDataIntoAdapter(); 209 } 210 } 211 } 212 213 /** 214 * A {@link ContentResolver} that is responsible for reloading the user's starred and frequent 215 * contacts. 216 */ 217 private class SpeedDialContentObserver extends ContentObserver { 218 public SpeedDialContentObserver(Handler handler) { 219 super(handler); 220 } 221 222 @Override 223 public void onChange(boolean selfChange) { 224 onChange(selfChange, null); 225 } 226 227 @Override 228 public void onChange(boolean selfChange, Uri uri) { 229 if (Log.isLoggable(TAG, Log.DEBUG)) { 230 Log.d(TAG, "SpeedDialContentObserver onChange() called. Reloading strequents."); 231 } 232 mSpeedialCursorLoader.startLoading(); 233 } 234 } 235 236 /** 237 * A {@link ContentResolver} that is responsible for reloading the user's recent calls. 238 */ 239 private class CallLogContentObserver extends ContentObserver { 240 public CallLogContentObserver(Handler handler) { 241 super(handler); 242 } 243 244 @Override 245 public void onChange(boolean selfChange) { 246 onChange(selfChange, null); 247 } 248 249 @Override 250 public void onChange(boolean selfChange, Uri uri) { 251 if (Log.isLoggable(TAG, Log.DEBUG)) { 252 Log.d(TAG, "CallLogContentObserver onChange() called. Reloading call log."); 253 } 254 mCallLogCursorLoader.startLoading(); 255 } 256 } 257 258 /** 259 * Decoration for the speed dial cards. This ItemDecoration will not show a divider between 260 * the dialpad item and the first speed dial item and the divider is offset but a couple of 261 * pixels to offset the fact that the cards overlap. 262 */ 263 private static class Decoration extends RecyclerView.ItemDecoration { 264 private final Paint mPaint; 265 private final int mPaintAlpha; 266 private final int mDividerHeight; 267 268 public Decoration(Context context) { 269 Resources res = context.getResources(); 270 mPaint = new Paint(); 271 mPaint.setColor(res.getColor(R.color.car_list_divider)); 272 mDividerHeight = res.getDimensionPixelSize(R.dimen.car_divider_height); 273 mPaintAlpha = mPaint.getAlpha(); 274 } 275 276 @Override 277 public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { 278 StrequentsAdapter adapter = (StrequentsAdapter) parent.getAdapter(); 279 280 if (adapter.getItemCount() <= 0) { 281 return; 282 } 283 284 final int childCount = parent.getChildCount(); 285 286 // Don't draw decoration line on last item of the list. 287 for (int i = 0; i < childCount - 1; i++) { 288 final View child = parent.getChildAt(i); 289 290 // If the child is focused then the decoration will look bad with the focus 291 // highlight so don't draw it. 292 if (child.isFocused()) { 293 continue; 294 } 295 296 // The left edge of the divider should align with the left edge of text_container. 297 LinearLayout container = child.findViewById(R.id.container); 298 View textContainer = child.findViewById(R.id.text_container); 299 View card = child.findViewById(R.id.call_log_card); 300 301 int left = textContainer.getLeft() + container.getLeft() + card.getLeft(); 302 int right = left + textContainer.getWidth(); 303 304 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); 305 int bottom = child.getBottom() + lp.bottomMargin 306 + Math.round(child.getTranslationY()); 307 int top = bottom - mDividerHeight; 308 309 if (top >= c.getHeight() || top < 0) { 310 break; 311 } 312 313 mPaint.setAlpha(Math.round(container.getAlpha() * mPaintAlpha)); 314 c.drawRect(left, top, right, bottom, mPaint); 315 } 316 } 317 } 318 } 319