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.app.Fragment; 19 import android.app.LoaderManager; 20 import android.content.CursorLoader; 21 import android.content.Intent; 22 import android.content.Loader; 23 import android.database.Cursor; 24 import android.net.Uri; 25 import android.os.Bundle; 26 import android.provider.ContactsContract; 27 import android.support.annotation.ColorInt; 28 import android.support.annotation.Nullable; 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 import com.android.car.view.PagedListView; 40 41 import java.util.ArrayList; 42 import 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 */ 49 public 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 newInstance(Uri uri, @Nullable RecyclerView.OnScrollListener listener)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 onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 onViewCreated(View view, Bundle savedInstanceState)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 onActivityCreated(Bundle savedInstanceState)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 */ addOnScrollListener(RecyclerView.OnScrollListener onScrollListener)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 onDestroy()123 public void onDestroy() { 124 // Clear all scroll listeners. 125 mListView.getRecyclerView().removeOnScrollListener(null); 126 super.onDestroy(); 127 } 128 129 @Override onCreateLoader(int id, Bundle args)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 onLoadFinished(Loader<Cursor> loader, Cursor cursor)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 onLoaderReset(Loader loader)156 public void onLoaderReset(Loader loader) { } 157 vdebug()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 ContactDetailViewHolder(View v)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 ContactDetailsAdapter(Cursor cursor)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 */ setBackground(ContactDetailViewHolder viewHolder)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 getItemViewType(int position)278 public int getItemViewType(int position) { 279 return position == 0 ? ID_HEADER : ID_CONTENT; 280 } 281 282 @Override setMaxItems(int maxItems)283 public void setMaxItems(int maxItems) { 284 // Ignore. 285 } 286 287 @Override getItemCount()288 public int getItemCount() { 289 return mPhoneNumbers.size() + 1; // +1 for the header row. 290 } 291 292 @Override onCreateViewHolder(ViewGroup parent, int viewType)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 onBindViewHolder(ContactDetailViewHolder viewHolder, int position)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