Home | History | Annotate | Download | only in incallui
      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 
     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 
     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      */
     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 
     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 
    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      */
    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
    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 
    245     String getFormattedTimeForCalendar(Calendar calendar) {
    246         return DateFormat.getTimeFormat(mContext).format(calendar.getTime());
    247     }
    248 
    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      */
    266     private BusinessContextInfo constructLocationInfo(Address address, float distance) {
    267         return constructLocationInfo(Locale.getDefault(), address, distance);
    268     }
    269 
    270     @VisibleForTesting
    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      */
    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 {
    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
    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
    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 
    381         public InCallContactInteractionsListAdapter(Context context, int resource) {
    382             super(context, resource);
    383             mResId = resource;
    384         }
    385 
    386         @Override
    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