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