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