/* * Copyright 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.calendar; import static com.google.common.base.Verify.verify; import android.Manifest; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.Nullable; import androidx.lifecycle.Observer; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.ViewHolder; import com.android.car.calendar.common.CalendarFormatter; import com.android.car.calendar.common.Dialer; import com.android.car.calendar.common.Event; import com.android.car.calendar.common.EventsLiveData; import com.android.car.calendar.common.Navigator; import com.android.car.ui.recyclerview.CarUiRecyclerView; import com.google.common.collect.ImmutableList; import java.time.Duration; import java.time.LocalDate; import java.time.ZoneId; import java.util.ArrayList; import java.util.List; /** The main calendar app view. */ class CarCalendarView { private static final String TAG = "CarCalendarView"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); /** Activity referenced as concrete type to access permissions methods. */ private final CarCalendarActivity mCarCalendarActivity; /** The main calendar view. */ private final CarCalendarViewModel mCarCalendarViewModel; private final Navigator mNavigator; private final Dialer mDialer; private final CalendarFormatter mFormatter; private final TextView mNoEventsTextView; /** Holds an instance of either {@link LocalDate} or {@link Event} for each item in the list. */ private final List mRecyclerViewItems = new ArrayList<>(); private final RecyclerView.Adapter mAdapter = new EventRecyclerViewAdapter(); private final Observer> mEventsObserver = this::onEventsChanged; CarCalendarView( CarCalendarActivity carCalendarActivity, CarCalendarViewModel carCalendarViewModel, Navigator navigator, Dialer dialer, CalendarFormatter formatter) { mCarCalendarActivity = carCalendarActivity; mCarCalendarViewModel = carCalendarViewModel; mNavigator = navigator; mDialer = dialer; mFormatter = formatter; carCalendarActivity.setContentView(R.layout.calendar); CarUiRecyclerView calendarRecyclerView = carCalendarActivity.findViewById(R.id.events); mNoEventsTextView = carCalendarActivity.findViewById(R.id.no_events_text); calendarRecyclerView.setHasFixedSize(true); calendarRecyclerView.setAdapter(mAdapter); } void show() { // TODO(jdp) If permission is denied then show some UI to allow them to retry. mCarCalendarActivity.runWithPermission( Manifest.permission.READ_CALENDAR, this::showWithPermission); } private void showWithPermission() { EventsLiveData eventsLiveData = mCarCalendarViewModel.getEventsLiveData(); eventsLiveData.observe(mCarCalendarActivity, mEventsObserver); } private void onEventsChanged(ImmutableList events) { updateRecyclerViewItems(events); // TODO(jdp) Only change the affected items (DiffUtil) to allow animated changes. mAdapter.notifyDataSetChanged(); } /** * If the events list is null there is no calendar data available. If the events list is empty * there is calendar data but no events. */ private void updateRecyclerViewItems(@Nullable ImmutableList events) { if (DEBUG) Log.d(TAG, "Update events"); LocalDate currentDate = null; mRecyclerViewItems.clear(); if (events == null) { mNoEventsTextView.setVisibility(View.VISIBLE); mNoEventsTextView.setText(R.string.no_calendars); return; } if (events.isEmpty()) { mNoEventsTextView.setVisibility(View.VISIBLE); mNoEventsTextView.setText(R.string.no_events); return; } mNoEventsTextView.setVisibility(View.GONE); // Add all rows in the calendar list. // A day might have all-day events that need to be added before regular events so we need to // add the event rows after looking at all events for the day. List eventItems = null; List allDayEventItems = null; for (Event event : events) { LocalDate date = event.getDayStartInstant().atZone(ZoneId.systemDefault()).toLocalDate(); // Start a new section when the date changes. if (!date.equals(currentDate)) { verify( currentDate == null || !date.isBefore(currentDate), "Expected events to be sorted by start time"); currentDate = date; // Add the events from the previous day. if (eventItems != null) { verify(allDayEventItems != null); addAllEvents(allDayEventItems, eventItems); } mRecyclerViewItems.add(new TitleCalendarItem(date, mFormatter)); allDayEventItems = new ArrayList<>(); eventItems = new ArrayList<>(); } // Events that last 24 hours or longer are also shown with all day events. if (event.isAllDay() || event.getDuration().compareTo(Duration.ofDays(1)) >= 0) { // Only add a row when necessary because hiding it can leave padding or decorations. allDayEventItems.add( new EventCalendarItem( event, mFormatter, mNavigator, mDialer, mCarCalendarActivity)); } else { eventItems.add( new EventCalendarItem( event, mFormatter, mNavigator, mDialer, mCarCalendarActivity)); } } addAllEvents(allDayEventItems, eventItems); } private void addAllEvents( List allDayEventItems, List eventItems) { if (allDayEventItems.size() > 1) { mRecyclerViewItems.add(new AllDayEventsItem(allDayEventItems)); } else if (allDayEventItems.size() == 1) { mRecyclerViewItems.add(allDayEventItems.get(0)); } mRecyclerViewItems.addAll(eventItems); } private class EventRecyclerViewAdapter extends RecyclerView.Adapter { @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { CalendarItem.Type type = CalendarItem.Type.values()[viewType]; return type.createViewHolder(parent); } @Override public void onBindViewHolder(ViewHolder holder, int position) { mRecyclerViewItems.get(position).bind(holder); } @Override public int getItemCount() { return mRecyclerViewItems.size(); } @Override public int getItemViewType(int position) { return mRecyclerViewItems.get(position).getType().ordinal(); } } }