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