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