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 17 package com.android.incallui; 18 19 import com.google.common.annotations.VisibleForTesting; 20 21 import android.content.Context; 22 import android.location.Address; 23 import android.text.TextUtils; 24 import android.text.format.DateFormat; 25 import android.util.Pair; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.widget.ArrayAdapter; 30 import android.widget.ImageView; 31 import android.widget.ListAdapter; 32 import android.widget.RelativeLayout; 33 import android.widget.RelativeLayout.LayoutParams; 34 import android.widget.TextView; 35 36 import com.android.dialer.R; 37 38 import java.text.ParseException; 39 import java.text.SimpleDateFormat; 40 import java.util.ArrayList; 41 import java.util.Calendar; 42 import java.util.Date; 43 import java.util.List; 44 import java.util.Locale; 45 46 /** 47 * Wrapper class for objects that are used in generating the context about the contact in the InCall 48 * screen. 49 * 50 * This handles generating the appropriate resource for the ListAdapter based on whether the contact 51 * is a business contact or not and logic for the manipulation of data for the call context. 52 */ 53 public class InCallContactInteractions { 54 private static final String TAG = InCallContactInteractions.class.getSimpleName(); 55 56 private Context mContext; 57 private InCallContactInteractionsListAdapter mListAdapter; 58 private boolean mIsBusiness; 59 private View mBusinessHeaderView; 60 private LayoutInflater mInflater; 61 InCallContactInteractions(Context context, boolean isBusiness)62 public InCallContactInteractions(Context context, boolean isBusiness) { 63 mContext = context; 64 mInflater = (LayoutInflater) 65 context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 66 switchContactType(isBusiness); 67 } 68 getListAdapter()69 public InCallContactInteractionsListAdapter getListAdapter() { 70 return mListAdapter; 71 } 72 73 /** 74 * Switches the "isBusiness" value, if applicable. Recreates the list adapter with the resource 75 * corresponding to the new isBusiness value if the "isBusiness" value is switched. 76 * 77 * @param isBusiness Whether or not the contact is a business. 78 * 79 * @return {@code true} if a new list adapter was created, {@code} otherwise. 80 */ switchContactType(boolean isBusiness)81 public boolean switchContactType(boolean isBusiness) { 82 if (mIsBusiness != isBusiness || mListAdapter == null) { 83 mIsBusiness = isBusiness; 84 mListAdapter = new InCallContactInteractionsListAdapter(mContext, 85 mIsBusiness ? R.layout.business_context_info_list_item 86 : R.layout.person_context_info_list_item); 87 return true; 88 } 89 return false; 90 } 91 getBusinessListHeaderView()92 public View getBusinessListHeaderView() { 93 if (mBusinessHeaderView == null) { 94 mBusinessHeaderView = mInflater.inflate( 95 R.layout.business_contact_context_list_header, null); 96 } 97 return mBusinessHeaderView; 98 } 99 setBusinessInfo(Address address, float distance, List<Pair<Calendar, Calendar>> openingHours)100 public void setBusinessInfo(Address address, float distance, 101 List<Pair<Calendar, Calendar>> openingHours) { 102 mListAdapter.clear(); 103 List<ContactContextInfo> info = new ArrayList<ContactContextInfo>(); 104 105 // Hours of operation 106 if (openingHours != null) { 107 BusinessContextInfo hoursInfo = constructHoursInfo(openingHours); 108 if (hoursInfo != null) { 109 info.add(hoursInfo); 110 } 111 } 112 113 // Location information 114 if (address != null) { 115 BusinessContextInfo locationInfo = constructLocationInfo(address, distance); 116 info.add(locationInfo); 117 } 118 119 mListAdapter.addAll(info); 120 } 121 122 /** 123 * Construct a BusinessContextInfo object containing hours of operation information. 124 * The format is: 125 * [Open now/Closed now] 126 * [Hours] 127 * 128 * @param openingHours 129 * @return BusinessContextInfo object with the schedule icon, the heading set to whether the 130 * business is open or not and the details set to the hours of operation. 131 */ constructHoursInfo(List<Pair<Calendar, Calendar>> openingHours)132 private BusinessContextInfo constructHoursInfo(List<Pair<Calendar, Calendar>> openingHours) { 133 try { 134 return constructHoursInfo(Calendar.getInstance(), openingHours); 135 } catch (Exception e) { 136 // Catch all exceptions here because we don't want any crashes if something goes wrong. 137 Log.e(TAG, "Error constructing hours info: ", e); 138 } 139 return null; 140 } 141 142 /** 143 * Pass in arbitrary current calendar time. 144 */ 145 @VisibleForTesting constructHoursInfo(Calendar currentTime, List<Pair<Calendar, Calendar>> openingHours)146 BusinessContextInfo constructHoursInfo(Calendar currentTime, 147 List<Pair<Calendar, Calendar>> openingHours) { 148 if (currentTime == null || openingHours == null || openingHours.size() == 0) { 149 return null; 150 } 151 152 BusinessContextInfo hoursInfo = new BusinessContextInfo(); 153 hoursInfo.iconId = R.drawable.ic_schedule_white_24dp; 154 155 boolean isOpenNow = false; 156 // This variable records which interval the current time is after. 0 denotes none of the 157 // intervals, 1 after the first interval, etc. It is also the index of the interval the 158 // current time is in (if open) or the next interval (if closed). 159 int afterInterval = 0; 160 // This variable counts the number of time intervals in today's opening hours. 161 int todaysIntervalCount = 0; 162 163 for (Pair<Calendar, Calendar> hours : openingHours) { 164 if (hours.first.compareTo(currentTime) <= 0 165 && currentTime.compareTo(hours.second) < 0) { 166 // If the current time is on or after the opening time and strictly before the 167 // closing time, then this business is open. 168 isOpenNow = true; 169 } 170 171 if (currentTime.get(Calendar.DAY_OF_YEAR) == hours.first.get(Calendar.DAY_OF_YEAR)) { 172 todaysIntervalCount += 1; 173 } 174 175 if (currentTime.compareTo(hours.second) > 0) { 176 // This assumes that the list of intervals is sorted by time. 177 afterInterval += 1; 178 } 179 } 180 181 hoursInfo.heading = isOpenNow ? mContext.getString(R.string.open_now) 182 : mContext.getString(R.string.closed_now); 183 184 /* 185 * The following logic determines what to display in various cases for hours of operation. 186 * 187 * - Display all intervals if open now and number of intervals is <=2. 188 * - Display next closing time if open now and number of intervals is >2. 189 * - Display next opening time if currently closed but opens later today. 190 * - Display last time it closed today if closed now and tomorrow's hours are unknown. 191 * - Display tomorrow's first open time if closed today and tomorrow's hours are known. 192 * 193 * NOTE: The logic below assumes that the intervals are sorted by ascending time. Possible 194 * TODO to modify the logic above and ensure this is true. 195 */ 196 if (isOpenNow) { 197 if (todaysIntervalCount == 1) { 198 hoursInfo.detail = getTimeSpanStringForHours(openingHours.get(0)); 199 } else if (todaysIntervalCount == 2) { 200 hoursInfo.detail = mContext.getString( 201 R.string.opening_hours, 202 getTimeSpanStringForHours(openingHours.get(0)), 203 getTimeSpanStringForHours(openingHours.get(1))); 204 } else if (afterInterval < openingHours.size()) { 205 // This check should not be necessary since if it is currently open, we should not 206 // be after the last interval, but just in case, we don't want to crash. 207 hoursInfo.detail = mContext.getString( 208 R.string.closes_today_at, 209 getFormattedTimeForCalendar(openingHours.get(afterInterval).second)); 210 } 211 } else { // Currently closed 212 final int lastIntervalToday = todaysIntervalCount - 1; 213 if (todaysIntervalCount == 0) { // closed today 214 hoursInfo.detail = mContext.getString( 215 R.string.opens_tomorrow_at, 216 getFormattedTimeForCalendar(openingHours.get(0).first)); 217 } else if (currentTime.after(openingHours.get(lastIntervalToday).second)) { 218 // Passed hours for today 219 if (todaysIntervalCount < openingHours.size()) { 220 // If all of today's intervals are exhausted, assume the next are tomorrow's. 221 hoursInfo.detail = mContext.getString( 222 R.string.opens_tomorrow_at, 223 getFormattedTimeForCalendar( 224 openingHours.get(todaysIntervalCount).first)); 225 } else { 226 // Grab the last time it was open today. 227 hoursInfo.detail = mContext.getString( 228 R.string.closed_today_at, 229 getFormattedTimeForCalendar( 230 openingHours.get(lastIntervalToday).second)); 231 } 232 } else if (afterInterval < openingHours.size()) { 233 // This check should not be necessary since if it is currently before the last 234 // interval, afterInterval should be less than the count of intervals, but just in 235 // case, we don't want to crash. 236 hoursInfo.detail = mContext.getString( 237 R.string.opens_today_at, 238 getFormattedTimeForCalendar(openingHours.get(afterInterval).first)); 239 } 240 } 241 242 return hoursInfo; 243 } 244 getFormattedTimeForCalendar(Calendar calendar)245 String getFormattedTimeForCalendar(Calendar calendar) { 246 return DateFormat.getTimeFormat(mContext).format(calendar.getTime()); 247 } 248 getTimeSpanStringForHours(Pair<Calendar, Calendar> hours)249 String getTimeSpanStringForHours(Pair<Calendar, Calendar> hours) { 250 return mContext.getString(R.string.open_time_span, 251 getFormattedTimeForCalendar(hours.first), 252 getFormattedTimeForCalendar(hours.second)); 253 } 254 255 /** 256 * Construct a BusinessContextInfo object with the location information of the business. 257 * The format is: 258 * [Straight line distance in miles or kilometers] 259 * [Address without state/country/etc.] 260 * 261 * @param address An Address object containing address details of the business 262 * @param distance The distance to the location in meters 263 * @return A BusinessContextInfo object with the location icon, the heading as the distance to 264 * the business and the details containing the address. 265 */ constructLocationInfo(Address address, float distance)266 private BusinessContextInfo constructLocationInfo(Address address, float distance) { 267 return constructLocationInfo(Locale.getDefault(), address, distance); 268 } 269 270 @VisibleForTesting constructLocationInfo(Locale locale, Address address, float distance)271 BusinessContextInfo constructLocationInfo(Locale locale, Address address, 272 float distance) { 273 if (address == null) { 274 return null; 275 } 276 277 BusinessContextInfo locationInfo = new BusinessContextInfo(); 278 locationInfo.iconId = R.drawable.ic_location_on_white_24dp; 279 if (distance != DistanceHelper.DISTANCE_NOT_FOUND) { 280 //TODO: add a setting to allow the user to select "KM" or "MI" as their distance units. 281 if (Locale.US.equals(locale)) { 282 locationInfo.heading = mContext.getString(R.string.distance_imperial_away, 283 distance * DistanceHelper.MILES_PER_METER); 284 } else { 285 locationInfo.heading = mContext.getString(R.string.distance_metric_away, 286 distance * DistanceHelper.KILOMETERS_PER_METER); 287 } 288 } 289 if (address.getLocality() != null) { 290 locationInfo.detail = mContext.getString( 291 R.string.display_address, 292 address.getAddressLine(0), 293 address.getLocality()); 294 } else { 295 locationInfo.detail = address.getAddressLine(0); 296 } 297 return locationInfo; 298 } 299 300 /** 301 * Get the appropriate title for the context. 302 * @return The "Business info" title for a business contact and the "Recent messages" title for 303 * personal contacts. 304 */ getContactContextTitle()305 public String getContactContextTitle() { 306 return mIsBusiness 307 ? mContext.getResources().getString(R.string.business_contact_context_title) 308 : mContext.getResources().getString(R.string.person_contact_context_title); 309 } 310 311 public static abstract class ContactContextInfo { bindView(View listItem)312 public abstract void bindView(View listItem); 313 } 314 315 public static class BusinessContextInfo extends ContactContextInfo { 316 int iconId; 317 String heading; 318 String detail; 319 320 @Override bindView(View listItem)321 public void bindView(View listItem) { 322 ImageView imageView = (ImageView) listItem.findViewById(R.id.icon); 323 TextView headingTextView = (TextView) listItem.findViewById(R.id.heading); 324 TextView detailTextView = (TextView) listItem.findViewById(R.id.detail); 325 326 if (this.iconId == 0 || (this.heading == null && this.detail == null)) { 327 return; 328 } 329 330 imageView.setImageDrawable(listItem.getContext().getDrawable(this.iconId)); 331 332 headingTextView.setText(this.heading); 333 headingTextView.setVisibility(TextUtils.isEmpty(this.heading) 334 ? View.GONE : View.VISIBLE); 335 336 detailTextView.setText(this.detail); 337 detailTextView.setVisibility(TextUtils.isEmpty(this.detail) 338 ? View.GONE : View.VISIBLE); 339 340 } 341 } 342 343 public static class PersonContextInfo extends ContactContextInfo { 344 boolean isIncoming; 345 String message; 346 String detail; 347 348 @Override bindView(View listItem)349 public void bindView(View listItem) { 350 TextView messageTextView = (TextView) listItem.findViewById(R.id.message); 351 TextView detailTextView = (TextView) listItem.findViewById(R.id.detail); 352 353 if (this.message == null || this.detail == null) { 354 return; 355 } 356 357 messageTextView.setBackgroundResource(this.isIncoming ? 358 R.drawable.incoming_sms_background : R.drawable.outgoing_sms_background); 359 messageTextView.setText(this.message); 360 LayoutParams messageLayoutParams = (LayoutParams) messageTextView.getLayoutParams(); 361 messageLayoutParams.addRule(this.isIncoming? 362 RelativeLayout.ALIGN_PARENT_START : RelativeLayout.ALIGN_PARENT_END); 363 messageTextView.setLayoutParams(messageLayoutParams); 364 365 LayoutParams detailLayoutParams = (LayoutParams) detailTextView.getLayoutParams(); 366 detailLayoutParams.addRule(this.isIncoming ? 367 RelativeLayout.ALIGN_PARENT_START : RelativeLayout.ALIGN_PARENT_END); 368 detailTextView.setLayoutParams(detailLayoutParams); 369 detailTextView.setText(this.detail); 370 } 371 } 372 373 /** 374 * A list adapter for call context information. We use the same adapter for both business and 375 * contact context. 376 */ 377 private class InCallContactInteractionsListAdapter extends ArrayAdapter<ContactContextInfo> { 378 // The resource id of the list item layout. 379 int mResId; 380 InCallContactInteractionsListAdapter(Context context, int resource)381 public InCallContactInteractionsListAdapter(Context context, int resource) { 382 super(context, resource); 383 mResId = resource; 384 } 385 386 @Override getView(int position, View convertView, ViewGroup parent)387 public View getView(int position, View convertView, ViewGroup parent) { 388 View listItem = mInflater.inflate(mResId, null); 389 ContactContextInfo item = getItem(position); 390 391 if (item == null) { 392 return listItem; 393 } 394 395 item.bindView(listItem); 396 return listItem; 397 } 398 } 399 } 400