1 /*
2  * Copyright 2020 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.car.calendar;
18 
19 import android.Manifest;
20 import android.graphics.Paint;
21 import android.graphics.Typeface;
22 import android.graphics.drawable.InsetDrawable;
23 import android.graphics.drawable.ShapeDrawable;
24 import android.graphics.drawable.shapes.OvalShape;
25 import android.text.SpannableString;
26 import android.text.style.ForegroundColorSpan;
27 import android.text.style.ImageSpan;
28 import android.text.style.StyleSpan;
29 import android.view.LayoutInflater;
30 import android.view.View.OnClickListener;
31 import android.view.ViewGroup;
32 import android.widget.ImageButton;
33 import android.widget.TextView;
34 import android.widget.Toast;
35 
36 import androidx.annotation.ColorInt;
37 import androidx.annotation.DrawableRes;
38 import androidx.core.content.ContextCompat;
39 import androidx.recyclerview.widget.RecyclerView;
40 
41 import com.android.car.calendar.common.CalendarFormatter;
42 import com.android.car.calendar.common.Dialer;
43 import com.android.car.calendar.common.Event;
44 import com.android.car.calendar.common.Navigator;
45 
46 import com.google.common.base.Joiner;
47 import com.google.common.base.Strings;
48 import com.google.common.collect.Lists;
49 
50 import javax.annotation.Nullable;
51 
52 /** An item in the calendar list view that shows a single event. */
53 class EventCalendarItem implements CalendarItem {
54     private final Event mEvent;
55     private final CalendarFormatter mFormatter;
56     private final Navigator mNavigator;
57     private final Dialer mDialer;
58     private final CarCalendarActivity mCarCalendarActivity;
59 
EventCalendarItem( Event event, CalendarFormatter formatter, Navigator navigator, Dialer dialer, CarCalendarActivity carCalendarActivity)60     EventCalendarItem(
61             Event event,
62             CalendarFormatter formatter,
63             Navigator navigator,
64             Dialer dialer,
65             CarCalendarActivity carCalendarActivity) {
66         mEvent = event;
67         mFormatter = formatter;
68         mNavigator = navigator;
69         mDialer = dialer;
70         mCarCalendarActivity = carCalendarActivity;
71     }
72 
73     @Override
getType()74     public Type getType() {
75         return Type.EVENT;
76     }
77 
78     @Override
bind(RecyclerView.ViewHolder holder)79     public void bind(RecyclerView.ViewHolder holder) {
80         EventViewHolder eventViewHolder = (EventViewHolder) holder;
81 
82         EventAction primaryAction;
83         if (!Strings.isNullOrEmpty(mEvent.getLocation())) {
84             primaryAction =
85                     new EventAction(
86                             R.drawable.ic_navigation_gm2_24px,
87                             mEvent.getLocation(),
88                             (view) -> mNavigator.navigate(mEvent.getLocation()));
89         } else {
90             primaryAction =
91                     new EventAction(
92                             R.drawable.ic_navigation_gm2_24px, /* descriptionText */
93                             null, /* onClickHandler */
94                             null);
95         }
96 
97         EventAction secondaryAction;
98         Dialer.NumberAndAccess numberAndAccess = mEvent.getNumberAndAccess();
99         if (numberAndAccess != null) {
100             String dialDescriptionText;
101             if (numberAndAccess.getAccess() != null) {
102                 dialDescriptionText =
103                         mCarCalendarActivity.getString(
104                                 R.string.phone_number_with_pin,
105                                 numberAndAccess.getNumber(),
106                                 numberAndAccess.getAccess());
107 
108             } else {
109                 dialDescriptionText =
110                         mCarCalendarActivity.getString(
111                                 R.string.phone_number, numberAndAccess.getNumber());
112             }
113             secondaryAction =
114                     new EventAction(
115                             R.drawable.ic_phone_gm2_24px,
116                             dialDescriptionText,
117                             (view) -> dial(numberAndAccess));
118         } else {
119             secondaryAction =
120                     new EventAction(
121                             R.drawable.ic_phone_gm2_24px,
122                             /* descriptionText */ null,
123                             /* onClickListener= */ null);
124         }
125 
126         String timeRangeText;
127         if (!mEvent.isAllDay()) {
128             timeRangeText =
129                     mFormatter.getTimeRangeText(mEvent.getStartInstant(), mEvent.getEndInstant());
130         } else {
131             timeRangeText = mCarCalendarActivity.getString(R.string.all_day_event);
132         }
133         eventViewHolder.update(
134                 timeRangeText,
135                 mEvent.getTitle(),
136                 mEvent.getCalendarDetails().getColor(),
137                 mEvent.getCalendarDetails().getName(),
138                 primaryAction,
139                 secondaryAction,
140                 mEvent.getStatus());
141     }
142 
dial(Dialer.NumberAndAccess numberAndAccess)143     private void dial(Dialer.NumberAndAccess numberAndAccess) {
144         mCarCalendarActivity.runWithPermission(
145                 Manifest.permission.CALL_PHONE,
146                 () -> {
147                     if (!mDialer.dial(numberAndAccess)) {
148                         Toast.makeText(mCarCalendarActivity, R.string.no_dialler, Toast.LENGTH_LONG)
149                                 .show();
150                     }
151                 });
152     }
153 
154     private static class EventAction {
155         @DrawableRes private final int mIconResourceId;
156         @Nullable private final String mDescriptionText;
157         @Nullable private final OnClickListener mOnClickListener;
158 
EventAction( int iconResourceId, @Nullable String descriptionText, @Nullable OnClickListener onClickListener)159         private EventAction(
160                 int iconResourceId,
161                 @Nullable String descriptionText,
162                 @Nullable OnClickListener onClickListener) {
163             this.mIconResourceId = iconResourceId;
164             this.mDescriptionText = descriptionText;
165             this.mOnClickListener = onClickListener;
166         }
167     }
168 
169     static class EventViewHolder extends RecyclerView.ViewHolder {
170         private static final String FIELD_SEPARATOR = ", ";
171         private static final Joiner JOINER = Joiner.on(FIELD_SEPARATOR).skipNulls();
172 
173         private final TextView mTitleView;
174         private final TextView mDescriptionView;
175         private final ImageButton mPrimaryActionButton;
176         private final ImageButton mSecondaryActionButton;
177         private final int mCalendarIndicatorSize;
178         private final int mCalendarIndicatorPadding;
179         @ColorInt private final int mTimeTextColor;
180 
EventViewHolder(ViewGroup parent)181         EventViewHolder(ViewGroup parent) {
182             super(
183                     LayoutInflater.from(parent.getContext())
184                             .inflate(R.layout.event_item, parent, /* attachToRoot= */ false));
185             mTitleView = itemView.findViewById(R.id.event_title);
186             mDescriptionView = itemView.findViewById(R.id.description_text);
187             mPrimaryActionButton = itemView.findViewById(R.id.primary_action_button);
188             mSecondaryActionButton = itemView.findViewById(R.id.secondary_action_button);
189             mCalendarIndicatorSize =
190                     (int) parent.getResources().getDimension(R.dimen.car_calendar_indicator_width);
191             mCalendarIndicatorPadding =
192                     (int) parent.getResources().getDimension(R.dimen.car_ui_padding_1);
193             mTimeTextColor =
194                     ContextCompat.getColor(parent.getContext(), R.color.car_ui_text_color_primary);
195         }
196 
update( String timeRangeText, String title, @ColorInt int calendarColor, String calendarName, EventAction primaryAction, EventAction secondaryAction, Event.Status status)197         void update(
198                 String timeRangeText,
199                 String title,
200                 @ColorInt int calendarColor,
201                 String calendarName,
202                 EventAction primaryAction,
203                 EventAction secondaryAction,
204                 Event.Status status) {
205 
206             mTitleView.setText(title);
207 
208             String detailText = null;
209             if (primaryAction.mDescriptionText != null) {
210                 detailText = primaryAction.mDescriptionText;
211             } else if (secondaryAction.mDescriptionText != null) {
212                 detailText = secondaryAction.mDescriptionText;
213             }
214             SpannableString descriptionSpannable =
215                     createDescriptionSpannable(
216                             calendarColor, calendarName, timeRangeText, detailText);
217 
218             mDescriptionView.setText(descriptionSpannable);
219 
220             // Strike-through all text fields when the event was declined.
221             setTextFlags(
222                     Paint.STRIKE_THRU_TEXT_FLAG,
223                     /* add= */ status.equals(Event.Status.DECLINED),
224                     mTitleView,
225                     mDescriptionView);
226 
227             mPrimaryActionButton.setImageResource(primaryAction.mIconResourceId);
228             if (primaryAction.mOnClickListener != null) {
229                 mPrimaryActionButton.setEnabled(true);
230                 mPrimaryActionButton.setContentDescription(primaryAction.mDescriptionText);
231                 mPrimaryActionButton.setOnClickListener(primaryAction.mOnClickListener);
232             } else {
233                 mPrimaryActionButton.setEnabled(false);
234                 mPrimaryActionButton.setContentDescription(null);
235                 mPrimaryActionButton.setOnClickListener(null);
236             }
237 
238             mSecondaryActionButton.setImageResource(secondaryAction.mIconResourceId);
239             if (secondaryAction.mOnClickListener != null) {
240                 mSecondaryActionButton.setEnabled(true);
241                 mSecondaryActionButton.setContentDescription(secondaryAction.mDescriptionText);
242                 mSecondaryActionButton.setOnClickListener(secondaryAction.mOnClickListener);
243             } else {
244                 mSecondaryActionButton.setEnabled(false);
245                 mSecondaryActionButton.setContentDescription(null);
246                 mSecondaryActionButton.setOnClickListener(null);
247             }
248         }
249 
createDescriptionSpannable( @olorInt int calendarColor, String calendarName, String timeRangeText, @Nullable String detailText)250         private SpannableString createDescriptionSpannable(
251                 @ColorInt int calendarColor,
252                 String calendarName,
253                 String timeRangeText,
254                 @Nullable String detailText) {
255             ShapeDrawable calendarIndicatorDrawable = new ShapeDrawable(new OvalShape());
256             calendarIndicatorDrawable.getPaint().setColor(calendarColor);
257 
258             calendarIndicatorDrawable.setBounds(
259                     /* left= */ 0,
260                     /* top= */ 0,
261                     /* right= */ mCalendarIndicatorSize,
262                     /* bottom= */ mCalendarIndicatorSize);
263 
264             // Add padding to the right of the image to separate it from the text.
265             InsetDrawable insetDrawable =
266                     new InsetDrawable(
267                             calendarIndicatorDrawable,
268                             /* insetLeft= */ 0,
269                             /* insetTop= */ 0,
270                             /* insetRight= */ mCalendarIndicatorPadding,
271                             /* insetBottom= */ 0);
272 
273             insetDrawable.setBounds(
274                     /* left= */ 0,
275                     /* top= */ 0,
276                     /* right= */ mCalendarIndicatorSize + mCalendarIndicatorPadding,
277                     /* bottom= */ mCalendarIndicatorSize);
278 
279             String descriptionText =
280                     JOINER.join(Lists.newArrayList(calendarName, timeRangeText, detailText));
281             SpannableString descriptionSpannable = new SpannableString(descriptionText);
282             ImageSpan calendarIndicatorSpan =
283                     new ImageSpan(insetDrawable, ImageSpan.ALIGN_BASELINE);
284             int calendarNameEnd = calendarName.length() + FIELD_SEPARATOR.length();
285             descriptionSpannable.setSpan(
286                     calendarIndicatorSpan, /* start= */ 0, calendarNameEnd, /* flags= */ 0);
287             int timeEnd = calendarNameEnd + timeRangeText.length();
288             descriptionSpannable.setSpan(
289                     new StyleSpan(Typeface.BOLD), calendarNameEnd, timeEnd, /* flags= */ 0);
290             descriptionSpannable.setSpan(
291                     new ForegroundColorSpan(mTimeTextColor),
292                     calendarNameEnd,
293                     timeEnd,
294                     /* flags= */ 0);
295             return descriptionSpannable;
296         }
297 
298         /**
299          * Set paint flags on the given text views.
300          *
301          * @param flags The combined {@link Paint} flags to set or unset.
302          * @param set Set the flags if true, otherwise unset.
303          * @param views The views to apply the flags to.
304          */
setTextFlags(int flags, boolean set, TextView... views)305         private void setTextFlags(int flags, boolean set, TextView... views) {
306             for (TextView view : views) {
307                 if (set) {
308                     view.setPaintFlags(view.getPaintFlags() | flags);
309                 } else {
310                     view.setPaintFlags(view.getPaintFlags() & ~flags);
311                 }
312             }
313         }
314     }
315 }
316