diff options
Diffstat (limited to 'src')
6 files changed, 124 insertions, 108 deletions
diff --git a/src/com/android/car/calendar/CarCalendarView.java b/src/com/android/car/calendar/CarCalendarView.java index 07b9516..1a63588 100644 --- a/src/com/android/car/calendar/CarCalendarView.java +++ b/src/com/android/car/calendar/CarCalendarView.java @@ -17,7 +17,6 @@ package com.android.car.calendar; import static com.google.common.base.Verify.verify; -import static com.google.common.base.Verify.verifyNotNull; import android.Manifest; import android.util.Log; @@ -25,10 +24,10 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import androidx.annotation.NonNull; 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; @@ -64,15 +63,8 @@ class CarCalendarView { /** Holds an instance of either {@link LocalDate} or {@link Event} for each item in the list. */ private final List<CalendarItem> mRecyclerViewItems = new ArrayList<>(); - private final RecyclerView.Adapter mAdapter = new EventRecyclerViewAdapter(); - private final Observer<ImmutableList<Event>> mEventsObserver = - events -> { - if (DEBUG) Log.d(TAG, "Events changed"); - updateRecyclerViewItems(events); - - // TODO(jdp) Only change the affected items (DiffUtil) to allow animated changes. - mAdapter.notifyDataSetChanged(); - }; + private final RecyclerView.Adapter<ViewHolder> mAdapter = new EventRecyclerViewAdapter(); + private final Observer<ImmutableList<Event>> mEventsObserver = this::onEventsChanged; CarCalendarView( CarCalendarActivity carCalendarActivity, @@ -102,23 +94,30 @@ class CarCalendarView { private void showWithPermission() { EventsLiveData eventsLiveData = mCarCalendarViewModel.getEventsLiveData(); eventsLiveData.observe(mCarCalendarActivity, mEventsObserver); - updateRecyclerViewItems(verifyNotNull(eventsLiveData.getValue())); + } + + private void onEventsChanged(ImmutableList<Event> 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<Event> carCalendarEvents) { + private void updateRecyclerViewItems(@Nullable ImmutableList<Event> events) { + if (DEBUG) Log.d(TAG, "Update events"); LocalDate currentDate = null; mRecyclerViewItems.clear(); - if (carCalendarEvents == null) { + if (events == null) { mNoEventsTextView.setVisibility(View.VISIBLE); mNoEventsTextView.setText(R.string.no_calendars); return; } - if (carCalendarEvents.isEmpty()) { + if (events.isEmpty()) { mNoEventsTextView.setVisibility(View.VISIBLE); mNoEventsTextView.setText(R.string.no_events); return; @@ -130,7 +129,7 @@ class CarCalendarView { // add the event rows after looking at all events for the day. List<CalendarItem> eventItems = null; List<EventCalendarItem> allDayEventItems = null; - for (Event event : carCalendarEvents) { + for (Event event : events) { LocalDate date = event.getDayStartInstant().atZone(ZoneId.systemDefault()).toLocalDate(); @@ -177,17 +176,15 @@ class CarCalendarView { mRecyclerViewItems.addAll(eventItems); } - private class EventRecyclerViewAdapter extends RecyclerView.Adapter { - - @NonNull + private class EventRecyclerViewAdapter extends RecyclerView.Adapter<ViewHolder> { @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { CalendarItem.Type type = CalendarItem.Type.values()[viewType]; return type.createViewHolder(parent); } @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + public void onBindViewHolder(ViewHolder holder, int position) { mRecyclerViewItems.get(position).bind(holder); } diff --git a/src/com/android/car/calendar/DrawableStateImageButton.java b/src/com/android/car/calendar/DrawableStateImageButton.java deleted file mode 100644 index 4ebd06e..0000000 --- a/src/com/android/car/calendar/DrawableStateImageButton.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 android.content.Context; -import android.util.AttributeSet; -import android.widget.ImageButton; - -import androidx.annotation.Nullable; - -import com.android.car.ui.uxr.DrawableStateView; - -/** - * An {@link ImageButton} that implements {@link DrawableStateView}, for allowing additional states - * such as ux restriction. - * - * @see com.android.car.ui.uxr.DrawableStateButton - * - * TODO(jdp) Move this to car-ui-lib. - */ -public class DrawableStateImageButton extends ImageButton implements DrawableStateView { - - private int[] mState; - - public DrawableStateImageButton(Context context) { - super(context); - } - - public DrawableStateImageButton(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public DrawableStateImageButton( - Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public DrawableStateImageButton( - Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - @Override - public void setDrawableState(int[] state) { - mState = state; - refreshDrawableState(); - } - - @Override - public int[] onCreateDrawableState(int extraSpace) { - if (mState == null) { - return super.onCreateDrawableState(extraSpace); - } else { - return mergeDrawableStates( - super.onCreateDrawableState(extraSpace + mState.length), mState); - } - } -} diff --git a/src/com/android/car/calendar/EventCalendarItem.java b/src/com/android/car/calendar/EventCalendarItem.java index 0642baa..a7c023d 100644 --- a/src/com/android/car/calendar/EventCalendarItem.java +++ b/src/com/android/car/calendar/EventCalendarItem.java @@ -30,6 +30,7 @@ import android.text.style.StyleSpan; import android.view.LayoutInflater; import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; @@ -171,8 +172,8 @@ class EventCalendarItem implements CalendarItem { private final TextView mTitleView; private final TextView mDescriptionView; - private final DrawableStateImageButton mPrimaryActionButton; - private final DrawableStateImageButton mSecondaryActionButton; + private final ImageButton mPrimaryActionButton; + private final ImageButton mSecondaryActionButton; private final int mCalendarIndicatorSize; private final int mCalendarIndicatorPadding; @ColorInt private final int mTimeTextColor; diff --git a/src/com/android/car/calendar/common/Dialer.java b/src/com/android/car/calendar/common/Dialer.java index 889a3c8..df843aa 100644 --- a/src/com/android/car/calendar/common/Dialer.java +++ b/src/com/android/car/calendar/common/Dialer.java @@ -26,6 +26,8 @@ import android.util.Log; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; +import java.util.Objects; + import javax.annotation.Nullable; /** Calls the default dialer with an optional access code. */ @@ -94,5 +96,18 @@ public class Dialer { .add("mAccess", mAccess) .toString(); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NumberAndAccess that = (NumberAndAccess) o; + return mNumber.equals(that.mNumber) && Objects.equals(mAccess, that.mAccess); + } + + @Override + public int hashCode() { + return Objects.hash(mNumber, mAccess); + } } } diff --git a/src/com/android/car/calendar/common/Event.java b/src/com/android/car/calendar/common/Event.java index 4395d33..6f88717 100644 --- a/src/com/android/car/calendar/common/Event.java +++ b/src/com/android/car/calendar/common/Event.java @@ -20,6 +20,9 @@ import com.android.car.calendar.common.Dialer.NumberAndAccess; import java.time.Duration; import java.time.Instant; +import java.util.Objects; + +import javax.annotation.Nullable; /** * An immutable value representing a calendar event. Should contain only details that are relevant @@ -34,9 +37,7 @@ public final class Event { NONE, } - /** - * The details required for display of the calendar indicator. - */ + /** The details required for display of the calendar indicator. */ public static class CalendarDetails { private final String mName; private final int mColor; @@ -53,6 +54,19 @@ public final class Event { public String getName() { return mName; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CalendarDetails that = (CalendarDetails) o; + return mColor == that.mColor && mName.equals(that.mName); + } + + @Override + public int hashCode() { + return Objects.hash(mName, mColor); + } } private final boolean mAllDay; @@ -62,8 +76,8 @@ public final class Event { private final Instant mDayEndInstant; private final String mTitle; private final Status mStatus; - private final String mLocation; - private final NumberAndAccess mNumberAndAccess; + @Nullable private final String mLocation; + @Nullable private final NumberAndAccess mNumberAndAccess; private final CalendarDetails mCalendarDetails; Event( @@ -74,8 +88,8 @@ public final class Event { Instant dayEndInstant, String title, Status status, - String location, - NumberAndAccess numberAndAccess, + @Nullable String location, + @Nullable NumberAndAccess numberAndAccess, CalendarDetails calendarDetails) { mAllDay = allDay; mStartInstant = startInstant; @@ -109,6 +123,7 @@ public final class Event { return mTitle; } + @Nullable public NumberAndAccess getNumberAndAccess() { return mNumberAndAccess; } @@ -117,6 +132,7 @@ public final class Event { return mCalendarDetails; } + @Nullable public String getLocation() { return mLocation; } @@ -132,4 +148,36 @@ public final class Event { public Duration getDuration() { return Duration.between(getStartInstant(), getEndInstant()); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Event event = (Event) o; + return mAllDay == event.mAllDay + && mStartInstant.equals(event.mStartInstant) + && mDayStartInstant.equals(event.mDayStartInstant) + && mEndInstant.equals(event.mEndInstant) + && mDayEndInstant.equals(event.mDayEndInstant) + && mTitle.equals(event.mTitle) + && mStatus == event.mStatus + && Objects.equals(mLocation, event.mLocation) + && Objects.equals(mNumberAndAccess, event.mNumberAndAccess) + && mCalendarDetails.equals(event.mCalendarDetails); + } + + @Override + public int hashCode() { + return Objects.hash( + mAllDay, + mStartInstant, + mDayStartInstant, + mEndInstant, + mDayEndInstant, + mTitle, + mStatus, + mLocation, + mNumberAndAccess, + mCalendarDetails); + } } diff --git a/src/com/android/car/calendar/common/EventsLiveData.java b/src/com/android/car/calendar/common/EventsLiveData.java index 12c91e7..92ae0bb 100644 --- a/src/com/android/car/calendar/common/EventsLiveData.java +++ b/src/com/android/car/calendar/common/EventsLiveData.java @@ -31,7 +31,9 @@ import android.provider.CalendarContract; import android.provider.CalendarContract.Instances; import android.util.Log; +import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; @@ -44,6 +46,7 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Objects; import javax.annotation.Nullable; @@ -53,12 +56,17 @@ import javax.annotation.Nullable; * Provider</a>. * * <p>While in the active state the content provider is observed for changes. + * + * <p>When the value given to the observer is null it signals that there are no calendars. */ public class EventsLiveData extends LiveData<ImmutableList<Event>> { private static final String TAG = "CarCalendarEventsLiveData"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + // The duration to delay before updating the value to reduce the frequency of changes. + private static final int UPDATE_DELAY_MILLIS = 1000; + // Sort events by start date and title. private static final Comparator<Event> EVENT_COMPARATOR = Comparator.comparing(Event::getDayStartInstant).thenComparing(Event::getTitle); @@ -68,19 +76,22 @@ public class EventsLiveData extends LiveData<ImmutableList<Event>> { private final ContentResolver mContentResolver; private final EventDescriptions mEventDescriptions; private final EventLocations mLocations; + private final Runnable mUpdateIfChangedRunnable = this::updateIfChanged; /** The event instances cursor is a field to allow observers to be managed. */ @Nullable private Cursor mEventsCursor; @Nullable private ContentObserver mEventInstancesObserver; + // This can be updated on the background thread but read from any thread. + private volatile boolean mValueUpdated; + public EventsLiveData( Clock clock, Handler backgroundHandler, ContentResolver contentResolver, EventDescriptions eventDescriptions, EventLocations locations) { - super(ImmutableList.of()); mClock = clock; mBackgroundHandler = backgroundHandler; mContentResolver = contentResolver; @@ -89,8 +100,16 @@ public class EventsLiveData extends LiveData<ImmutableList<Event>> { } /** Refreshes the event instances and sets the new value which notifies observers. */ - private void update() { - postValue(getEventsUntilTomorrow()); + private void updateIfChanged() { + Log.d(TAG, "Update if changed"); + ImmutableList<Event> latest = getEventsUntilTomorrow(); + ImmutableList<Event> current = getValue(); + + // Always post the first value even if it is null. + if (!mValueUpdated || !Objects.equals(latest, current)) { + postValue(latest); + mValueUpdated = true; + } } /** Queries the content provider for event instances. */ @@ -167,7 +186,7 @@ public class EventsLiveData extends LiveData<ImmutableList<Event>> { @Override public void onChange(boolean selfChange) { if (DEBUG) Log.d(TAG, "Events changed"); - update(); + updateWithDelay(); } }; cursor.setNotificationUri(mContentResolver, eventInstanceUri); @@ -176,6 +195,13 @@ public class EventsLiveData extends LiveData<ImmutableList<Event>> { return cursor; } + private void updateWithDelay() { + // Do not update the events until there have been no changes for a given duration. + Log.d(TAG, "Events changed"); + mBackgroundHandler.removeCallbacks(mUpdateIfChangedRunnable); + mBackgroundHandler.postDelayed(mUpdateIfChangedRunnable, UPDATE_DELAY_MILLIS); + } + /** Can return multiple events for a single cursor row when an event spans multiple days. */ private List<Event> createEventsForRow( Cursor eventInstancesCursor, EventDescriptions eventDescriptions) { @@ -262,13 +288,14 @@ public class EventsLiveData extends LiveData<ImmutableList<Event>> { if (DEBUG) Log.d(TAG, "Live data inactive"); mBackgroundHandler.post(this::cancelScheduledUpdate); mBackgroundHandler.post(this::tearDownCursor); + mValueUpdated = false; } - /** Calls {@link #update()} every minute to keep the displayed time range correct. */ + /** Calls {@link #updateIfChanged()} every minute to keep the displayed time range correct. */ private void updateAndScheduleNext() { if (DEBUG) Log.d(TAG, "Update and schedule"); if (hasActiveObservers()) { - update(); + updateIfChanged(); ZonedDateTime now = ZonedDateTime.now(mClock); ZonedDateTime truncatedNowTime = now.truncatedTo(MINUTES); ZonedDateTime updateTime = truncatedNowTime.plus(1, MINUTES); |