blob: 88070fe379283f81821951ed64f31616ad8f5166 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.incallui;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.location.Address;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.RelativeLayout;
import android.widget.RelativeLayout.LayoutParams;
import android.widget.TextView;
import com.android.dialer.R;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/**
* Wrapper class for objects that are used in generating the context about the contact in the InCall
* screen.
*
* This handles generating the appropriate resource for the ListAdapter based on whether the contact
* is a business contact or not and logic for the manipulation of data for the call context.
*/
public class InCallContactInteractions {
private static final String TAG = InCallContactInteractions.class.getSimpleName();
private Context mContext;
private InCallContactInteractionsListAdapter mListAdapter;
private boolean mIsBusiness;
private View mBusinessHeaderView;
private LayoutInflater mInflater;
public InCallContactInteractions(Context context, boolean isBusiness) {
mContext = context;
mInflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
switchContactType(isBusiness);
}
public InCallContactInteractionsListAdapter getListAdapter() {
return mListAdapter;
}
/**
* Switches the "isBusiness" value, if applicable. Recreates the list adapter with the resource
* corresponding to the new isBusiness value if the "isBusiness" value is switched.
*
* @param isBusiness Whether or not the contact is a business.
*
* @return {@code true} if a new list adapter was created, {@code} otherwise.
*/
public boolean switchContactType(boolean isBusiness) {
if (mIsBusiness != isBusiness || mListAdapter == null) {
mIsBusiness = isBusiness;
mListAdapter = new InCallContactInteractionsListAdapter(mContext,
mIsBusiness ? R.layout.business_context_info_list_item
: R.layout.person_context_info_list_item);
return true;
}
return false;
}
public View getBusinessListHeaderView() {
if (mBusinessHeaderView == null) {
mBusinessHeaderView = mInflater.inflate(
R.layout.business_contact_context_list_header, null);
}
return mBusinessHeaderView;
}
public void setBusinessInfo(Address address, float distance,
List<Pair<Calendar, Calendar>> openingHours) {
mListAdapter.clear();
List<ContactContextInfo> info = new ArrayList<ContactContextInfo>();
// Hours of operation
if (openingHours != null) {
BusinessContextInfo hoursInfo = constructHoursInfo(openingHours);
if (hoursInfo != null) {
info.add(hoursInfo);
}
}
// Location information
if (address != null) {
BusinessContextInfo locationInfo = constructLocationInfo(address, distance);
info.add(locationInfo);
}
mListAdapter.addAll(info);
}
/**
* Construct a BusinessContextInfo object containing hours of operation information.
* The format is:
* [Open now/Closed now]
* [Hours]
*
* @param openingHours
* @return BusinessContextInfo object with the schedule icon, the heading set to whether the
* business is open or not and the details set to the hours of operation.
*/
private BusinessContextInfo constructHoursInfo(List<Pair<Calendar, Calendar>> openingHours) {
try {
return constructHoursInfo(Calendar.getInstance(), openingHours);
} catch (Exception e) {
// Catch all exceptions here because we don't want any crashes if something goes wrong.
Log.e(TAG, "Error constructing hours info: ", e);
}
return null;
}
/**
* Pass in arbitrary current calendar time.
*/
@VisibleForTesting
BusinessContextInfo constructHoursInfo(Calendar currentTime,
List<Pair<Calendar, Calendar>> openingHours) {
if (currentTime == null || openingHours == null || openingHours.size() == 0) {
return null;
}
BusinessContextInfo hoursInfo = new BusinessContextInfo();
hoursInfo.iconId = R.drawable.ic_schedule_white_24dp;
boolean isOpenNow = false;
// This variable records which interval the current time is after. 0 denotes none of the
// intervals, 1 after the first interval, etc. It is also the index of the interval the
// current time is in (if open) or the next interval (if closed).
int afterInterval = 0;
// This variable counts the number of time intervals in today's opening hours.
int todaysIntervalCount = 0;
for (Pair<Calendar, Calendar> hours : openingHours) {
if (hours.first.compareTo(currentTime) <= 0
&& currentTime.compareTo(hours.second) < 0) {
// If the current time is on or after the opening time and strictly before the
// closing time, then this business is open.
isOpenNow = true;
}
if (currentTime.get(Calendar.DAY_OF_YEAR) == hours.first.get(Calendar.DAY_OF_YEAR)) {
todaysIntervalCount += 1;
}
if (currentTime.compareTo(hours.second) > 0) {
// This assumes that the list of intervals is sorted by time.
afterInterval += 1;
}
}
hoursInfo.heading = isOpenNow ? mContext.getString(R.string.open_now)
: mContext.getString(R.string.closed_now);
/*
* The following logic determines what to display in various cases for hours of operation.
*
* - Display all intervals if open now and number of intervals is <=2.
* - Display next closing time if open now and number of intervals is >2.
* - Display next opening time if currently closed but opens later today.
* - Display last time it closed today if closed now and tomorrow's hours are unknown.
* - Display tomorrow's first open time if closed today and tomorrow's hours are known.
*
* NOTE: The logic below assumes that the intervals are sorted by ascending time. Possible
* TODO to modify the logic above and ensure this is true.
*/
if (isOpenNow) {
if (todaysIntervalCount == 1) {
hoursInfo.detail = getTimeSpanStringForHours(openingHours.get(0));
} else if (todaysIntervalCount == 2) {
hoursInfo.detail = mContext.getString(
R.string.opening_hours,
getTimeSpanStringForHours(openingHours.get(0)),
getTimeSpanStringForHours(openingHours.get(1)));
} else if (afterInterval < openingHours.size()) {
// This check should not be necessary since if it is currently open, we should not
// be after the last interval, but just in case, we don't want to crash.
hoursInfo.detail = mContext.getString(
R.string.closes_today_at,
getFormattedTimeForCalendar(openingHours.get(afterInterval).second));
}
} else { // Currently closed
final int lastIntervalToday = todaysIntervalCount - 1;
if (todaysIntervalCount == 0) { // closed today
hoursInfo.detail = mContext.getString(
R.string.opens_tomorrow_at,
getFormattedTimeForCalendar(openingHours.get(0).first));
} else if (currentTime.after(openingHours.get(lastIntervalToday).second)) {
// Passed hours for today
if (todaysIntervalCount < openingHours.size()) {
// If all of today's intervals are exhausted, assume the next are tomorrow's.
hoursInfo.detail = mContext.getString(
R.string.opens_tomorrow_at,
getFormattedTimeForCalendar(
openingHours.get(todaysIntervalCount).first));
} else {
// Grab the last time it was open today.
hoursInfo.detail = mContext.getString(
R.string.closed_today_at,
getFormattedTimeForCalendar(
openingHours.get(lastIntervalToday).second));
}
} else if (afterInterval < openingHours.size()) {
// This check should not be necessary since if it is currently before the last
// interval, afterInterval should be less than the count of intervals, but just in
// case, we don't want to crash.
hoursInfo.detail = mContext.getString(
R.string.opens_today_at,
getFormattedTimeForCalendar(openingHours.get(afterInterval).first));
}
}
return hoursInfo;
}
String getFormattedTimeForCalendar(Calendar calendar) {
return DateFormat.getTimeFormat(mContext).format(calendar.getTime());
}
String getTimeSpanStringForHours(Pair<Calendar, Calendar> hours) {
return mContext.getString(R.string.open_time_span,
getFormattedTimeForCalendar(hours.first),
getFormattedTimeForCalendar(hours.second));
}
/**
* Construct a BusinessContextInfo object with the location information of the business.
* The format is:
* [Straight line distance in miles or kilometers]
* [Address without state/country/etc.]
*
* @param address An Address object containing address details of the business
* @param distance The distance to the location in meters
* @return A BusinessContextInfo object with the location icon, the heading as the distance to
* the business and the details containing the address.
*/
private BusinessContextInfo constructLocationInfo(Address address, float distance) {
return constructLocationInfo(Locale.getDefault(), address, distance);
}
@VisibleForTesting
BusinessContextInfo constructLocationInfo(Locale locale, Address address,
float distance) {
if (address == null) {
return null;
}
BusinessContextInfo locationInfo = new BusinessContextInfo();
locationInfo.iconId = R.drawable.ic_location_on_white_24dp;
if (distance != DistanceHelper.DISTANCE_NOT_FOUND) {
//TODO: add a setting to allow the user to select "KM" or "MI" as their distance units.
if (Locale.US.equals(locale)) {
locationInfo.heading = mContext.getString(R.string.distance_imperial_away,
distance * DistanceHelper.MILES_PER_METER);
} else {
locationInfo.heading = mContext.getString(R.string.distance_metric_away,
distance * DistanceHelper.KILOMETERS_PER_METER);
}
}
if (address.getLocality() != null) {
locationInfo.detail = mContext.getString(
R.string.display_address,
address.getAddressLine(0),
address.getLocality());
} else {
locationInfo.detail = address.getAddressLine(0);
}
return locationInfo;
}
/**
* Get the appropriate title for the context.
* @return The "Business info" title for a business contact and the "Recent messages" title for
* personal contacts.
*/
public String getContactContextTitle() {
return mIsBusiness
? mContext.getResources().getString(R.string.business_contact_context_title)
: mContext.getResources().getString(R.string.person_contact_context_title);
}
public static abstract class ContactContextInfo {
public abstract void bindView(View listItem);
}
public static class BusinessContextInfo extends ContactContextInfo {
int iconId;
String heading;
String detail;
@Override
public void bindView(View listItem) {
ImageView imageView = (ImageView) listItem.findViewById(R.id.icon);
TextView headingTextView = (TextView) listItem.findViewById(R.id.heading);
TextView detailTextView = (TextView) listItem.findViewById(R.id.detail);
if (this.iconId == 0 || (this.heading == null && this.detail == null)) {
return;
}
imageView.setImageDrawable(listItem.getContext().getDrawable(this.iconId));
headingTextView.setText(this.heading);
headingTextView.setVisibility(TextUtils.isEmpty(this.heading)
? View.GONE : View.VISIBLE);
detailTextView.setText(this.detail);
detailTextView.setVisibility(TextUtils.isEmpty(this.detail)
? View.GONE : View.VISIBLE);
}
}
public static class PersonContextInfo extends ContactContextInfo {
boolean isIncoming;
String message;
String detail;
@Override
public void bindView(View listItem) {
TextView messageTextView = (TextView) listItem.findViewById(R.id.message);
TextView detailTextView = (TextView) listItem.findViewById(R.id.detail);
if (this.message == null || this.detail == null) {
return;
}
messageTextView.setBackgroundResource(this.isIncoming ?
R.drawable.incoming_sms_background : R.drawable.outgoing_sms_background);
messageTextView.setText(this.message);
LayoutParams messageLayoutParams = (LayoutParams) messageTextView.getLayoutParams();
messageLayoutParams.addRule(this.isIncoming?
RelativeLayout.ALIGN_PARENT_START : RelativeLayout.ALIGN_PARENT_END);
messageTextView.setLayoutParams(messageLayoutParams);
LayoutParams detailLayoutParams = (LayoutParams) detailTextView.getLayoutParams();
detailLayoutParams.addRule(this.isIncoming ?
RelativeLayout.ALIGN_PARENT_START : RelativeLayout.ALIGN_PARENT_END);
detailTextView.setLayoutParams(detailLayoutParams);
detailTextView.setText(this.detail);
}
}
/**
* A list adapter for call context information. We use the same adapter for both business and
* contact context.
*/
private class InCallContactInteractionsListAdapter extends ArrayAdapter<ContactContextInfo> {
// The resource id of the list item layout.
int mResId;
public InCallContactInteractionsListAdapter(Context context, int resource) {
super(context, resource);
mResId = resource;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View listItem = mInflater.inflate(mResId, null);
ContactContextInfo item = getItem(position);
if (item == null) {
return listItem;
}
item.bindView(listItem);
return listItem;
}
}
}