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
17package com.android.incallui;
18
19import com.google.common.annotations.VisibleForTesting;
20
21import android.content.Context;
22import android.location.Address;
23import android.text.TextUtils;
24import android.text.format.DateFormat;
25import android.util.Pair;
26import android.view.LayoutInflater;
27import android.view.View;
28import android.view.ViewGroup;
29import android.widget.ArrayAdapter;
30import android.widget.ImageView;
31import android.widget.ListAdapter;
32import android.widget.RelativeLayout;
33import android.widget.RelativeLayout.LayoutParams;
34import android.widget.TextView;
35
36import com.android.dialer.R;
37
38import java.text.ParseException;
39import java.text.SimpleDateFormat;
40import java.util.ArrayList;
41import java.util.Calendar;
42import java.util.Date;
43import java.util.List;
44import 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 */
53public 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