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  */
16 
17 package com.android.dialer.contactsfragment;
18 
19 import android.content.Context;
20 import android.database.Cursor;
21 import android.net.Uri;
22 import android.provider.ContactsContract.Contacts;
23 import android.support.annotation.IntDef;
24 import android.support.v4.util.ArrayMap;
25 import android.support.v7.widget.RecyclerView;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import com.android.dialer.common.Assert;
30 import com.android.dialer.common.LogUtil;
31 import com.android.dialer.contactphoto.ContactPhotoManager;
32 import com.android.dialer.contactsfragment.ContactsFragment.Header;
33 import com.android.dialer.contactsfragment.ContactsFragment.OnContactSelectedListener;
34 import com.android.dialer.lettertile.LetterTileDrawable;
35 import java.lang.annotation.Retention;
36 import java.lang.annotation.RetentionPolicy;
37 
38 /** List adapter for the union of all contacts associated with every account on the device. */
39 final class ContactsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
40 
41   private static final int UNKNOWN_VIEW_TYPE = 0;
42   private static final int ADD_CONTACT_VIEW_TYPE = 1;
43   private static final int CONTACT_VIEW_TYPE = 2;
44 
45   /** An Enum for the different row view types shown by this adapter. */
46   @Retention(RetentionPolicy.SOURCE)
47   @IntDef({UNKNOWN_VIEW_TYPE, ADD_CONTACT_VIEW_TYPE, CONTACT_VIEW_TYPE})
48   @interface ContactsViewType {}
49 
50   private final ArrayMap<ContactViewHolder, Integer> holderMap = new ArrayMap<>();
51   private final Context context;
52   private final @Header int header;
53   private final OnContactSelectedListener onContactSelectedListener;
54 
55   // List of contact sublist headers
56   private String[] headers = new String[0];
57   // Number of contacts that correspond to each header in {@code headers}.
58   private int[] counts = new int[0];
59   // Cursor with list of contacts
60   private Cursor cursor;
61 
ContactsAdapter( Context context, @Header int header, OnContactSelectedListener onContactSelectedListener)62   ContactsAdapter(
63       Context context, @Header int header, OnContactSelectedListener onContactSelectedListener) {
64     this.context = context;
65     this.header = header;
66     this.onContactSelectedListener = Assert.isNotNull(onContactSelectedListener);
67   }
68 
updateCursor(Cursor cursor)69   void updateCursor(Cursor cursor) {
70     this.cursor = cursor;
71     headers = cursor.getExtras().getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
72     counts = cursor.getExtras().getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
73     if (counts != null) {
74       int sum = 0;
75       for (int count : counts) {
76         sum += count;
77       }
78 
79       if (sum != cursor.getCount()) {
80         LogUtil.e(
81             "ContactsAdapter", "Count sum (%d) != cursor count (%d).", sum, cursor.getCount());
82       }
83     }
84     notifyDataSetChanged();
85   }
86 
87   @Override
onCreateViewHolder( ViewGroup parent, @ContactsViewType int viewType)88   public RecyclerView.ViewHolder onCreateViewHolder(
89       ViewGroup parent, @ContactsViewType int viewType) {
90     switch (viewType) {
91       case ADD_CONTACT_VIEW_TYPE:
92         return new AddContactViewHolder(
93             LayoutInflater.from(context).inflate(R.layout.add_contact_row, parent, false));
94       case CONTACT_VIEW_TYPE:
95         return new ContactViewHolder(
96             LayoutInflater.from(context).inflate(R.layout.contact_row, parent, false),
97             onContactSelectedListener);
98       case UNKNOWN_VIEW_TYPE:
99       default:
100         throw Assert.createIllegalStateFailException("Invalid view type: " + viewType);
101     }
102   }
103 
104   @Override
onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position)105   public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
106     if (viewHolder instanceof AddContactViewHolder) {
107       return;
108     }
109 
110     ContactViewHolder contactViewHolder = (ContactViewHolder) viewHolder;
111     holderMap.put(contactViewHolder, position);
112     cursor.moveToPosition(position);
113     if (header != Header.NONE) {
114       cursor.moveToPrevious();
115     }
116 
117     String name = getDisplayName(cursor);
118     String header = getHeaderString(position);
119     Uri contactUri = getContactUri(cursor);
120 
121     ContactPhotoManager.getInstance(context)
122         .loadDialerThumbnailOrPhoto(
123             contactViewHolder.getPhoto(),
124             contactUri,
125             getPhotoId(cursor),
126             getPhotoUri(cursor),
127             name,
128             LetterTileDrawable.TYPE_DEFAULT);
129 
130     String photoDescription =
131         context.getString(
132             com.android.dialer.contactphoto.R.string.description_quick_contact_for, name);
133     contactViewHolder.getPhoto().setContentDescription(photoDescription);
134 
135     // Always show the view holder's header if it's the first item in the list. Otherwise, compare
136     // it to the previous element and only show the anchored header if the row elements fall into
137     // the same sublists.
138     boolean showHeader = position == 0 || !header.equals(getHeaderString(position - 1));
139     contactViewHolder.bind(header, name, contactUri, getContactId(cursor), showHeader);
140   }
141 
142   /**
143    * Returns {@link #ADD_CONTACT_VIEW_TYPE} if the adapter was initialized with {@link
144    * Header#ADD_CONTACT} and the position is 0. Otherwise, {@link #CONTACT_VIEW_TYPE}.
145    */
146   @Override
getItemViewType(int position)147   public @ContactsViewType int getItemViewType(int position) {
148     if (header != Header.NONE && position == 0) {
149       return ADD_CONTACT_VIEW_TYPE;
150     }
151     return CONTACT_VIEW_TYPE;
152   }
153 
154   @Override
onViewRecycled(RecyclerView.ViewHolder contactViewHolder)155   public void onViewRecycled(RecyclerView.ViewHolder contactViewHolder) {
156     super.onViewRecycled(contactViewHolder);
157     if (contactViewHolder instanceof ContactViewHolder) {
158       holderMap.remove(contactViewHolder);
159     }
160   }
161 
refreshHeaders()162   void refreshHeaders() {
163     for (ContactViewHolder holder : holderMap.keySet()) {
164       int position = holderMap.get(holder);
165       boolean showHeader =
166           position == 0 || !getHeaderString(position).equals(getHeaderString(position - 1));
167       int visibility = showHeader ? View.VISIBLE : View.INVISIBLE;
168       holder.getHeaderView().setVisibility(visibility);
169     }
170   }
171 
172   @Override
getItemCount()173   public int getItemCount() {
174     int count = cursor == null || cursor.isClosed() ? 0 : cursor.getCount();
175     // Manually insert the header if one exists.
176     if (header != Header.NONE) {
177       count++;
178     }
179     return count;
180   }
181 
getDisplayName(Cursor cursor)182   private static String getDisplayName(Cursor cursor) {
183     return cursor.getString(ContactsCursorLoader.CONTACT_DISPLAY_NAME);
184   }
185 
getPhotoId(Cursor cursor)186   private static long getPhotoId(Cursor cursor) {
187     return cursor.getLong(ContactsCursorLoader.CONTACT_PHOTO_ID);
188   }
189 
getPhotoUri(Cursor cursor)190   private static Uri getPhotoUri(Cursor cursor) {
191     String photoUri = cursor.getString(ContactsCursorLoader.CONTACT_PHOTO_URI);
192     return photoUri == null ? null : Uri.parse(photoUri);
193   }
194 
getContactUri(Cursor cursor)195   private static Uri getContactUri(Cursor cursor) {
196     long contactId = getContactId(cursor);
197     String lookupKey = cursor.getString(ContactsCursorLoader.CONTACT_LOOKUP_KEY);
198     return Contacts.getLookupUri(contactId, lookupKey);
199   }
200 
getContactId(Cursor cursor)201   private static long getContactId(Cursor cursor) {
202     return cursor.getLong(ContactsCursorLoader.CONTACT_ID);
203   }
204 
getHeaderString(int position)205   String getHeaderString(int position) {
206     if (header != Header.NONE) {
207       if (position == 0) {
208         return "+";
209       }
210       position--;
211     }
212 
213     int index = -1;
214     int sum = 0;
215     while (sum <= position) {
216       sum += counts[++index];
217     }
218     return headers[index];
219   }
220 }
221