1/*
2 * Copyright (C) 2017 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 */
16package com.android.car.dialer;
17
18import android.app.Fragment;
19import android.app.LoaderManager;
20import android.content.CursorLoader;
21import android.content.Intent;
22import android.content.Loader;
23import android.database.Cursor;
24import android.net.Uri;
25import android.os.Bundle;
26import android.provider.ContactsContract;
27import android.support.annotation.ColorInt;
28import android.support.annotation.Nullable;
29import android.support.v7.widget.RecyclerView;
30import android.util.Log;
31import android.util.Pair;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.ViewGroup;
35import android.widget.ImageView;
36import android.widget.TextView;
37
38import com.android.car.dialer.telecom.TelecomUtils;
39import com.android.car.view.PagedListView;
40
41import java.util.ArrayList;
42import java.util.List;
43
44/**
45 * A fragment that shows the name of the contact, the photo and all listed phone numbers. It is
46 * primarily used to respond to the results of search queries but supplyig it with the content://
47 * uri of a contact should work too.
48 */
49public class ContactDetailsFragment extends Fragment
50        implements LoaderManager.LoaderCallbacks<Cursor> {
51    private static final String TAG = "ContactDetailsFragment";
52    private static final String TELEPHONE_URI_PREFIX = "tel:";
53
54    private static final int DETAILS_LOADER_QUERY_ID = 1;
55    private static final int PHONE_LOADER_QUERY_ID = 2;
56
57    private static final String KEY_URI = "uri";
58
59    private static final String[] CONTACT_DETAILS_PROJECTION = {
60        ContactsContract.Contacts._ID,
61        ContactsContract.Contacts.DISPLAY_NAME,
62        ContactsContract.Contacts.PHOTO_URI,
63        ContactsContract.Contacts.HAS_PHONE_NUMBER
64    };
65
66    private PagedListView mListView;
67    private List<RecyclerView.OnScrollListener> mOnScrollListeners = new ArrayList<>();
68
69    public static ContactDetailsFragment newInstance(Uri uri,
70            @Nullable RecyclerView.OnScrollListener listener) {
71        ContactDetailsFragment fragment = new ContactDetailsFragment();
72        fragment.addOnScrollListener(listener);
73
74        Bundle args = new Bundle();
75        args.putParcelable(KEY_URI, uri);
76        fragment.setArguments(args);
77
78        return fragment;
79    }
80
81    @Override
82    public View onCreateView(LayoutInflater inflater, ViewGroup container,
83            Bundle savedInstanceState) {
84        return inflater.inflate(R.layout.contact_details, container, false);
85    }
86
87    @Override
88    public void onViewCreated(View view, Bundle savedInstanceState) {
89        mListView = view.findViewById(R.id.list_view);
90        mListView.setLightMode();
91
92        RecyclerView recyclerView = mListView.getRecyclerView();
93        for (RecyclerView.OnScrollListener listener : mOnScrollListeners) {
94            recyclerView.addOnScrollListener(listener);
95        }
96
97        mOnScrollListeners.clear();
98    }
99
100    @Override
101    public void onActivityCreated(Bundle savedInstanceState) {
102        super.onActivityCreated(savedInstanceState);
103        getLoaderManager().initLoader(DETAILS_LOADER_QUERY_ID, null, this);
104    }
105
106    /**
107     * Adds a {@link android.support.v7.widget.RecyclerView.OnScrollListener} to be notified when
108     * the contact details are scrolled.
109     *
110     * @see RecyclerView#addOnScrollListener(RecyclerView.OnScrollListener)
111     */
112    public void addOnScrollListener(RecyclerView.OnScrollListener onScrollListener) {
113        // If the view has not been created yet, then queue the setting of the scroll listener.
114        if (mListView == null) {
115            mOnScrollListeners.add(onScrollListener);
116            return;
117        }
118
119        mListView.getRecyclerView().addOnScrollListener(onScrollListener);
120    }
121
122    @Override
123    public void onDestroy() {
124        // Clear all scroll listeners.
125        mListView.getRecyclerView().removeOnScrollListener(null);
126        super.onDestroy();
127    }
128
129    @Override
130    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
131        if (vdebug()) {
132            Log.d(TAG, "onCreateLoader id=" + id);
133        }
134
135        if (id != DETAILS_LOADER_QUERY_ID) {
136            return null;
137        }
138
139        Uri contactUri = getArguments().getParcelable(KEY_URI);
140        return new CursorLoader(getContext(), contactUri, CONTACT_DETAILS_PROJECTION,
141                null /* selection */, null /* selectionArgs */, null /* sortOrder */);
142    }
143
144    @Override
145    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
146        if (vdebug()) {
147            Log.d(TAG, "onLoadFinished");
148        }
149
150        if (cursor.moveToFirst()) {
151            mListView.setAdapter(new ContactDetailsAdapter(cursor));
152        }
153    }
154
155    @Override
156    public void onLoaderReset(Loader loader) {  }
157
158    private boolean vdebug() {
159        return Log.isLoggable(TAG, Log.DEBUG);
160    }
161
162    private class ContactDetailViewHolder extends RecyclerView.ViewHolder {
163        public View card;
164        public ImageView leftIcon;
165        public TextView title;
166        public TextView text;
167        public ImageView rightIcon;
168
169        public ContactDetailViewHolder(View v) {
170            super(v);
171            card = v.findViewById(R.id.card);
172            leftIcon = v.findViewById(R.id.icon);
173            title = v.findViewById(R.id.title);
174            text = v.findViewById(R.id.text);
175            rightIcon = v.findViewById(R.id.right_icon);
176        }
177    }
178
179    private class ContactDetailsAdapter extends RecyclerView.Adapter<ContactDetailViewHolder>
180            implements PagedListView.ItemCap {
181
182        private static final int ID_HEADER = 1;
183        private static final int ID_CONTENT = 2;
184
185        private final String mContactName;
186        @ColorInt private int mIconTint;
187
188        private List<Pair<String, String>> mPhoneNumbers = new ArrayList<>();
189
190        public ContactDetailsAdapter(Cursor cursor) {
191            super();
192
193            mIconTint = getContext().getColor(R.color.contact_details_icon_tint);
194
195            int idColIdx = cursor.getColumnIndex(ContactsContract.Contacts._ID);
196            String contactId = cursor.getString(idColIdx);
197            int nameColIdx = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
198            mContactName = cursor.getString(nameColIdx);
199            int hasPhoneColIdx = cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER);
200            boolean hasPhoneNumber = Integer.parseInt(cursor.getString(hasPhoneColIdx)) > 0;
201
202            if (!hasPhoneNumber) {
203                return;
204            }
205
206            // Fetch the phone number from the contacts db using another loader.
207            getLoaderManager().initLoader(PHONE_LOADER_QUERY_ID, null,
208                    new LoaderManager.LoaderCallbacks<Cursor>() {
209                        @Override
210                        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
211                            return new CursorLoader(getContext(),
212                                    ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
213                                    null, /* All columns **/
214                                    ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
215                                    new String[] { contactId },
216                                    null /* sortOrder */);
217                        }
218
219                        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
220                            while (cursor.moveToNext()) {
221                                int typeColIdx = cursor.getColumnIndex(
222                                        ContactsContract.CommonDataKinds.Phone.TYPE);
223                                int type = cursor.getInt(typeColIdx);
224                                int numberColIdx = cursor.getColumnIndex(
225                                        ContactsContract.CommonDataKinds.Phone.NUMBER);
226                                String number = cursor.getString(numberColIdx);
227                                String numberType;
228                                switch (type) {
229                                    case ContactsContract.CommonDataKinds.Phone.TYPE_HOME:
230                                        numberType = getString(R.string.type_home);
231                                        break;
232                                    case ContactsContract.CommonDataKinds.Phone.TYPE_WORK:
233                                        numberType = getString(R.string.type_work);
234                                        break;
235                                    case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE:
236                                        numberType = getString(R.string.type_mobile);
237                                        break;
238                                    default:
239                                        numberType = getString(R.string.type_other);
240                                }
241                                mPhoneNumbers.add(new Pair<>(numberType,
242                                        TelecomUtils.getFormattedNumber(getContext(), number)));
243                                notifyItemInserted(mPhoneNumbers.size());
244                            }
245                            notifyDataSetChanged();
246                        }
247
248                        public void onLoaderReset(Loader loader) {  }
249                    });
250        }
251
252        /**
253         * Appropriately sets the background for the View that is being bound. This method will
254         * allow for rounded corners on either the top or bottom of a card.
255         */
256        private void setBackground(ContactDetailViewHolder viewHolder) {
257            int itemCount = getItemCount();
258            int adapterPosition = viewHolder.getAdapterPosition();
259
260            if (itemCount == 1) {
261                // Only element - all corners are rounded
262                viewHolder.card.setBackgroundResource(
263                        R.drawable.car_card_rounded_top_bottom_background);
264            } else if (adapterPosition == 0) {
265                // First element gets rounded top
266                viewHolder.card.setBackgroundResource(R.drawable.car_card_rounded_top_background);
267            } else if (adapterPosition == itemCount - 1) {
268                // Last one has a rounded bottom
269                viewHolder.card.setBackgroundResource(
270                        R.drawable.car_card_rounded_bottom_background);
271            } else {
272                // Middle have no rounded corners
273                viewHolder.card.setBackgroundResource(R.color.car_card);
274            }
275        }
276
277        @Override
278        public int getItemViewType(int position) {
279            return position == 0 ? ID_HEADER : ID_CONTENT;
280        }
281
282        @Override
283        public void setMaxItems(int maxItems) {
284            // Ignore.
285        }
286
287        @Override
288        public int getItemCount() {
289            return mPhoneNumbers.size() + 1;  // +1 for the header row.
290        }
291
292        @Override
293        public ContactDetailViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
294            int layoutResId;
295            switch (viewType) {
296                case ID_HEADER:
297                    layoutResId = R.layout.contact_detail_name_image;
298                    break;
299                case ID_CONTENT:
300                    layoutResId = R.layout.contact_details_number;
301                    break;
302                default:
303                    Log.e(TAG, "Unknown view type " + viewType);
304                    return null;
305            }
306
307            View view = LayoutInflater.from(parent.getContext()).inflate(layoutResId, null);
308            return new ContactDetailViewHolder(view);
309        }
310
311        @Override
312        public void onBindViewHolder(ContactDetailViewHolder viewHolder, int position) {
313            switch (viewHolder.getItemViewType()) {
314                case ID_HEADER:
315                    viewHolder.title.setText(mContactName);
316                    if (!mPhoneNumbers.isEmpty()) {
317                        String firstNumber = mPhoneNumbers.get(0).second;
318                        TelecomUtils.setContactBitmapAsync(getContext(), viewHolder.rightIcon,
319                                mContactName, firstNumber);
320                    }
321                    // Just in case a viewholder object gets recycled.
322                    viewHolder.card.setOnClickListener(null);
323                    break;
324                case ID_CONTENT:
325                    Pair<String, String> data = mPhoneNumbers.get(position - 1);
326                    viewHolder.title.setText(data.first);  // Type.
327                    viewHolder.text.setText(data.second);  // Number.
328                    viewHolder.leftIcon.setImageResource(R.drawable.ic_phone);
329                    viewHolder.leftIcon.setColorFilter(mIconTint);
330                    viewHolder.card.setOnClickListener(v -> {
331                        Intent callIntent = new Intent(Intent.ACTION_CALL);
332                        callIntent.setData(Uri.parse(TELEPHONE_URI_PREFIX + data.second));
333                        getContext().startActivity(callIntent);
334                    });
335                    break;
336                default:
337                    Log.e(TAG, "Unknown view type " + viewHolder.getItemViewType());
338                    return;
339            }
340            setBackground(viewHolder);
341        }
342    }
343}
344