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