aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-05-11 05:10:03 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-05-11 05:10:03 +0000
commitd6b39497035f43a7803a415ad55e217806bf44d2 (patch)
treebfd660ff13f094c3282632f35eb1a2bc9feec2cf
parent5ec54b8a17fe5ec5e648dbf08833e0d448c50cb9 (diff)
parent8c06fda999803bbb705bc66efa5d38a8061f4c8b (diff)
downloadCalendar-d6b39497035f43a7803a415ad55e217806bf44d2.tar.gz
Snap for 8570526 from 8c06fda999803bbb705bc66efa5d38a8061f4c8b to mainline-media-swcodec-release
Change-Id: Iebd63d1a094f63357780307f08d6e650535a3ab9
-rw-r--r--Android.bp11
-rw-r--r--AndroidManifest.xml15
-rw-r--r--res/layout/calendar.xml40
-rw-r--r--res/layout/event_item.xml4
-rw-r--r--res/values-it/strings.xml2
-rw-r--r--res/values-pt-rPT/strings.xml2
-rw-r--r--res/values-te/strings.xml2
-rw-r--r--src/com/android/car/calendar/CarCalendarActivity.java30
-rw-r--r--src/com/android/car/calendar/CarCalendarView.java39
-rw-r--r--src/com/android/car/calendar/CarCalendarViewModel.java13
-rw-r--r--src/com/android/car/calendar/DrawableStateImageButton.java72
-rw-r--r--src/com/android/car/calendar/EventCalendarItem.java7
-rw-r--r--src/com/android/car/calendar/common/Dialer.java15
-rw-r--r--src/com/android/car/calendar/common/Event.java62
-rw-r--r--src/com/android/car/calendar/common/EventDescriptions.java68
-rw-r--r--src/com/android/car/calendar/common/EventsLiveData.java39
-rw-r--r--tests/ui/Android.bp4
-rw-r--r--tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java23
-rw-r--r--tests/unit/Android.bp4
-rw-r--r--tests/unit/src/com/android/car/calendar/common/EventDescriptionsTest.java23
-rw-r--r--tests/unit/src/com/android/car/calendar/common/EventsLiveDataTest.java223
21 files changed, 400 insertions, 298 deletions
diff --git a/Android.bp b/Android.bp
index e79e37b..ccddad5 100644
--- a/Android.bp
+++ b/Android.bp
@@ -13,18 +13,24 @@
// limitations under the License.
//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
android_app {
name: "CarCalendarApp",
srcs: ["src/**/*.java"],
resource_dirs: ["res"],
- platform_apis: true,
+ sdk_version: "current",
+ target_sdk_version: "29",
+ min_sdk_version: "28",
optimize: {
enabled: false,
},
dex_preopt: {
enabled: false,
},
- aaptflags: ["--auto-add-overlay"],
+ libs: ["android.car-stubs"],
static_libs: [
"car-ui-lib",
"androidx.lifecycle_lifecycle-extensions",
@@ -33,5 +39,4 @@ android_app {
"androidx.lifecycle_lifecycle-viewmodel",
"guava",
],
- libs: ["android.car"],
}
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 4921dae..56ce764 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -18,10 +18,6 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.android.car.calendar">
- <uses-sdk
- android:minSdkVersion="28"
- android:targetSdkVersion="29"/>
-
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.CALL_PHONE" />
@@ -29,7 +25,7 @@
android:allowBackup="true"
android:icon="@drawable/ic_calendar_sync"
android:label="@string/app_name"
- android:theme="@style/Theme.CarUi"
+ android:theme="@style/Theme.CarUi.WithToolbar"
android:supportsRtl="true">
<activity android:name=".CarCalendarActivity"
@@ -41,15 +37,6 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
-
- <!-- Work around b/113294940. -->
- <provider
- android:name="androidx.lifecycle.ProcessLifecycleOwnerInitializer"
- tools:replace="android:authorities"
- android:authorities="${applicationId}.lifecycle-tests"
- android:exported="false"
- android:multiprocess="true" />
-
</application>
</manifest>
diff --git a/res/layout/calendar.xml b/res/layout/calendar.xml
index 4c31ddc..7fb0bb0 100644
--- a/res/layout/calendar.xml
+++ b/res/layout/calendar.xml
@@ -13,35 +13,23 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout
+<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent"
- android:layout_width="match_parent"
- android:orientation="vertical">
-
- <com.android.car.ui.toolbar.Toolbar
- android:id="@+id/toolbar"
+ android:layout_width="match_parent">
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/events"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:title="@string/app_name"
+ android:layout_height="match_parent"
/>
- <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent">
- <com.android.car.ui.recyclerview.CarUiRecyclerView
- android:id="@+id/events"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- />
-
- <TextView
- android:id="@+id/no_events_text"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_margin="@dimen/car_ui_list_item_start_inset"
- android:gravity="center"
- android:textAppearance="@style/NoEventsText"/>
-
- </FrameLayout>
+ <TextView
+ android:id="@+id/no_events_text"
+ android:maxWidth="200dp"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="@dimen/car_ui_list_item_start_inset"
+ android:gravity="center"
+ android:textAppearance="@style/NoEventsText"/>
-</LinearLayout>
+</FrameLayout>
diff --git a/res/layout/event_item.xml b/res/layout/event_item.xml
index 68dda47..5d70907 100644
--- a/res/layout/event_item.xml
+++ b/res/layout/event_item.xml
@@ -46,7 +46,7 @@
</LinearLayout>
<!-- Secondary action icon. -->
- <com.android.car.calendar.DrawableStateImageButton
+ <android.widget.ImageButton
android:id="@+id/primary_action_button"
android:layout_width="@dimen/car_ui_list_item_height"
android:layout_height="@dimen/car_ui_list_item_height"
@@ -57,7 +57,7 @@
android:layout_gravity="center"/>
<!-- Secondary action icon. -->
- <com.android.car.calendar.DrawableStateImageButton
+ <android.widget.ImageButton
android:id="@+id/secondary_action_button"
android:layout_width="@dimen/car_ui_list_item_height"
android:layout_height="@dimen/car_ui_list_item_height"
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 8bca97b..e21316d 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -25,7 +25,7 @@
<skip />
<string name="phone_number_with_pin" msgid="6760582665093825412">"PIN del numero <xliff:g id="NUMBER">%1$s</xliff:g>: <xliff:g id="PIN_0">%2$s</xliff:g>"</string>
<plurals name="all_day_title" formatted="false" msgid="7938279592034934265">
+ <item quantity="one">%d eventi che durano tutto il giorno</item>
<item quantity="other">%d eventi che durano tutto il giorno</item>
- <item quantity="one">%d evento che dura tutto il giorno</item>
</plurals>
</resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 5ce55d2..32888c0 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -25,7 +25,7 @@
<skip />
<string name="phone_number_with_pin" msgid="6760582665093825412">"<xliff:g id="NUMBER">%1$s</xliff:g> PIN: <xliff:g id="PIN_0">%2$s</xliff:g>"</string>
<plurals name="all_day_title" formatted="false" msgid="7938279592034934265">
- <item quantity="other">%d eventos de todo o dia</item>
<item quantity="one">%d evento de todo o dia</item>
+ <item quantity="other">%d eventos de todo o dia</item>
</plurals>
</resources>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 1e3902c..24f6f3e 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -19,7 +19,7 @@
<string name="app_name" msgid="1756632159204796305">"Calendar"</string>
<string name="no_dialler" msgid="1448265958659890310">"డయలర్ అందుబాటులో లేదు"</string>
<string name="no_events" msgid="2454460886004475314">"షెడ్యూల్ చేసిన ఈవెంట్‌లు లేవు. మీరు ఖాళీగా ఉన్నారు!"</string>
- <string name="no_calendars" msgid="5059614627806215716">"Calendar ప్రారంభం కావచ్చు, లేదా మీరు సెట్టింగ్‌లను మీ సహచర యాప్‌లో చెక్ చేయాలి"</string>
+ <string name="no_calendars" msgid="5059614627806215716">"క్యాలెండర్ ప్రారంభం కావడానికి సమయం పడుతున్నట్టు ఉంది, లేదా సహచర యాప్‌లోని మీ సెట్టింగ్‌లను చెక్ చేసి చూడండి"</string>
<string name="all_day_event" msgid="5817490740700803034">"పూర్తి రోజు"</string>
<!-- no translation found for phone_number (3879925006862072135) -->
<skip />
diff --git a/src/com/android/car/calendar/CarCalendarActivity.java b/src/com/android/car/calendar/CarCalendarActivity.java
index 97e2031..94e0db6 100644
--- a/src/com/android/car/calendar/CarCalendarActivity.java
+++ b/src/com/android/car/calendar/CarCalendarActivity.java
@@ -20,6 +20,7 @@ import android.content.ContentResolver;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.StrictMode;
+import android.telephony.TelephonyManager;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -32,6 +33,8 @@ import androidx.lifecycle.ViewModelProvider;
import com.android.car.calendar.common.CalendarFormatter;
import com.android.car.calendar.common.Dialer;
import com.android.car.calendar.common.Navigator;
+import com.android.car.ui.core.CarUi;
+import com.android.car.ui.toolbar.ToolbarController;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
@@ -55,10 +58,16 @@ public class CarCalendarActivity extends FragmentActivity {
super.onCreate(savedInstanceState);
maybeEnableStrictMode();
+ ToolbarController toolbar = CarUi.requireToolbar(this);
+ toolbar.setTitle(R.string.app_name);
+
// Tests can set fake dependencies before onCreate.
if (mDependencies == null) {
mDependencies = new Dependencies(
- Locale.getDefault(), Clock.systemDefaultZone(), getContentResolver());
+ Locale.getDefault(),
+ Clock.systemDefaultZone(),
+ getContentResolver(),
+ getSystemService(TelephonyManager.class));
}
CarCalendarViewModel carCalendarViewModel =
@@ -67,6 +76,7 @@ public class CarCalendarActivity extends FragmentActivity {
new CarCalendarViewModelFactory(
mDependencies.mResolver,
mDependencies.mLocale,
+ mDependencies.mTelephonyManager,
mDependencies.mClock))
.get(CarCalendarViewModel.class);
@@ -142,12 +152,18 @@ public class CarCalendarActivity extends FragmentActivity {
private static class CarCalendarViewModelFactory implements ViewModelProvider.Factory {
private final ContentResolver mResolver;
+ private final TelephonyManager mTelephonyManager;
private final Locale mLocale;
private final Clock mClock;
- CarCalendarViewModelFactory(ContentResolver resolver, Locale locale, Clock clock) {
+ CarCalendarViewModelFactory(
+ ContentResolver resolver,
+ Locale locale,
+ TelephonyManager telephonyManager,
+ Clock clock) {
mResolver = resolver;
mLocale = locale;
+ mTelephonyManager = telephonyManager;
mClock = clock;
}
@@ -155,7 +171,7 @@ public class CarCalendarActivity extends FragmentActivity {
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> aClass) {
- return (T) new CarCalendarViewModel(mResolver, mLocale, mClock);
+ return (T) new CarCalendarViewModel(mResolver, mLocale, mTelephonyManager, mClock);
}
}
@@ -163,11 +179,17 @@ public class CarCalendarActivity extends FragmentActivity {
private final Locale mLocale;
private final Clock mClock;
private final ContentResolver mResolver;
+ private final TelephonyManager mTelephonyManager;
- Dependencies(Locale locale, Clock clock, ContentResolver resolver) {
+ Dependencies(
+ Locale locale,
+ Clock clock,
+ ContentResolver resolver,
+ TelephonyManager telephonyManager) {
mLocale = locale;
mClock = clock;
mResolver = resolver;
+ mTelephonyManager = telephonyManager;
}
}
}
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/CarCalendarViewModel.java b/src/com/android/car/calendar/CarCalendarViewModel.java
index c8e80ee..337f794 100644
--- a/src/com/android/car/calendar/CarCalendarViewModel.java
+++ b/src/com/android/car/calendar/CarCalendarViewModel.java
@@ -17,7 +17,9 @@
package com.android.car.calendar;
import android.content.ContentResolver;
+import android.os.Handler;
import android.os.HandlerThread;
+import android.telephony.TelephonyManager;
import android.util.Log;
import androidx.annotation.Nullable;
@@ -38,14 +40,17 @@ class CarCalendarViewModel extends ViewModel {
private final Clock mClock;
private final ContentResolver mResolver;
private final Locale mLocale;
+ private final TelephonyManager mTelephonyManager;
@Nullable private EventsLiveData mEventsLiveData;
- CarCalendarViewModel(ContentResolver resolver, Locale locale, Clock clock) {
+ CarCalendarViewModel(ContentResolver resolver, Locale locale,
+ TelephonyManager telephonyManager, Clock clock) {
+ mLocale = locale;
if (DEBUG) Log.d(TAG, "Creating view model");
mResolver = resolver;
+ mTelephonyManager = telephonyManager;
mHandlerThread.start();
- mLocale = locale;
mClock = clock;
}
@@ -55,9 +60,9 @@ class CarCalendarViewModel extends ViewModel {
mEventsLiveData =
new EventsLiveData(
mClock,
- mHandlerThread.getThreadHandler(),
+ new Handler(mHandlerThread.getLooper()),
mResolver,
- new EventDescriptions(mLocale),
+ new EventDescriptions(mLocale, mTelephonyManager),
new EventLocations());
}
return mEventsLiveData;
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..e83c21c 100644
--- a/src/com/android/car/calendar/EventCalendarItem.java
+++ b/src/com/android/car/calendar/EventCalendarItem.java
@@ -17,7 +17,6 @@
package com.android.car.calendar;
import android.Manifest;
-import android.annotation.ColorInt;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.drawable.InsetDrawable;
@@ -30,9 +29,11 @@ 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;
+import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
@@ -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/EventDescriptions.java b/src/com/android/car/calendar/common/EventDescriptions.java
index e6aad5b..8d2030f 100644
--- a/src/com/android/car/calendar/common/EventDescriptions.java
+++ b/src/com/android/car/calendar/common/EventDescriptions.java
@@ -16,20 +16,15 @@
package com.android.car.calendar.common;
-import static com.android.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL;
-import static com.android.i18n.phonenumbers.PhoneNumberUtil.ValidationResult.IS_POSSIBLE;
-import static com.android.i18n.phonenumbers.PhoneNumberUtil.ValidationResult.IS_POSSIBLE_LOCAL_ONLY;
-import static com.android.i18n.phonenumbers.PhoneNumberUtil.ValidationResult.TOO_LONG;
-
import static com.google.common.base.Verify.verifyNotNull;
import android.net.Uri;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
import com.android.car.calendar.common.Dialer.NumberAndAccess;
-import com.android.i18n.phonenumbers.NumberParseException;
-import com.android.i18n.phonenumbers.PhoneNumberUtil;
-import com.android.i18n.phonenumbers.Phonenumber;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import java.util.LinkedHashMap;
@@ -45,37 +40,45 @@ import javax.annotation.Nullable;
public class EventDescriptions {
// Requires a phone number to include only numbers, spaces and dash, optionally a leading "+".
- // The number must be at least 6 characters.
+ // The number must be at least 6 characters and can contain " " or "-" but end with a digit.
// The access code must be at least 3 characters.
// The number and the access to include "pin" or "code" between the numbers.
private static final Pattern PHONE_PIN_PATTERN =
Pattern.compile(
- "(\\+?[\\d -]{6,})(?:.*\\b(?:PIN|code)\\b.*?([\\d,;#*]{3,}))?",
+ "(\\+?[\\d -]{6,}\\d)(?:.*\\b(?:PIN|code)\\b.*?([\\d,;#*]{3,}))?",
Pattern.CASE_INSENSITIVE);
// Matches numbers in the encoded format "<tel: ... >".
private static final Pattern TEL_PIN_PATTERN =
Pattern.compile("<tel:(\\+?[\\d -]{6,})([\\d,;#*]{3,})?>");
- private static final PhoneNumberUtil PHONE_NUMBER_UTIL = PhoneNumberUtil.getInstance();
-
// Ensure numbers are over 5 digits to reduce false positives.
- private static final int MIN_NATIONAL_NUMBER = 10_000;
+ private static final int MIN_DIGITS = 5;
- private final Locale mLocale;
+ private final String mCountryIso;
- public EventDescriptions(Locale locale) {
- mLocale = locale;
+ public EventDescriptions(Locale locale, TelephonyManager telephonyManager) {
+ String networkCountryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+ if (!Strings.isNullOrEmpty(networkCountryIso)) {
+ mCountryIso = networkCountryIso;
+ } else {
+ mCountryIso = locale.getCountry();
+ }
}
/** Find conference call data embedded in the description. */
public List<NumberAndAccess> extractNumberAndPins(String descriptionText) {
String decoded = Uri.decode(descriptionText);
+ // Use a map keyed by number to act like a set and only add a single number.
Map<String, NumberAndAccess> results = new LinkedHashMap<>();
addMatchedNumbers(decoded, results, PHONE_PIN_PATTERN);
+
+ // Add the most restrictive precise format last to replace others with the same number.
addMatchedNumbers(decoded, results, TEL_PIN_PATTERN);
- return ImmutableList.copyOf(results.values());
+
+ // Reverse order so the most precise format is first.
+ return ImmutableList.copyOf(results.values()).reverse();
}
private void addMatchedNumbers(
@@ -93,27 +96,18 @@ public class EventDescriptions {
private NumberAndAccess validNumberAndAccess(Matcher phoneFormatMatcher) {
String number = verifyNotNull(phoneFormatMatcher.group(1));
String access = phoneFormatMatcher.group(2);
- try {
- Phonenumber.PhoneNumber phoneNumber =
- PHONE_NUMBER_UTIL.parse(number, mLocale.getCountry());
- PhoneNumberUtil.ValidationResult result =
- PHONE_NUMBER_UTIL.isPossibleNumberWithReason(phoneNumber);
- if (isAcceptableResult(result)) {
- if (phoneNumber.getNationalNumber() < MIN_NATIONAL_NUMBER) {
- return null;
- }
- String formatted = PHONE_NUMBER_UTIL.format(phoneNumber, INTERNATIONAL);
- return new NumberAndAccess(formatted, access);
- }
- } catch (NumberParseException e) {
- // Ignore invalid numbers.
+
+ // Ensure that there are a minimum number of digits to reduce false positives.
+ String onlyDigits = number.replaceAll("\\D", "");
+ if (onlyDigits.length() < MIN_DIGITS) {
+ return null;
}
- return null;
- }
- private boolean isAcceptableResult(PhoneNumberUtil.ValidationResult result) {
- // The result can be too long and still valid because the US locale is used by default
- // which does not accept valid long numbers from other regions.
- return result == IS_POSSIBLE || result == IS_POSSIBLE_LOCAL_ONLY || result == TOO_LONG;
+ // Keep local numbers in local format which the dialer can make more sense of.
+ String formatted = PhoneNumberUtils.formatNumber(number, mCountryIso);
+ if (formatted == null) {
+ return null;
+ }
+ return new NumberAndAccess(formatted, access);
}
}
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);
diff --git a/tests/ui/Android.bp b/tests/ui/Android.bp
index 829e312..0480d95 100644
--- a/tests/ui/Android.bp
+++ b/tests/ui/Android.bp
@@ -13,6 +13,10 @@
// limitations under the License.
//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
android_test {
name: "CarCalendarUiTests",
srcs: ["src/**/*.java"],
diff --git a/tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java b/tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java
index 5c7883c..e591a80 100644
--- a/tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java
+++ b/tests/ui/src/com/android/car/calendar/CarCalendarUiTest.java
@@ -35,6 +35,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.provider.CalendarContract;
+import android.telephony.TelephonyManager;
import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver;
@@ -97,6 +98,9 @@ public class CarCalendarUiTest {
private List<Object[]> mTestEventRows;
+ // If set to true fake dependencies will not be set and the real provider will be used.
+ private boolean mDoNotSetFakeDependencies;
+
// These can be set in the test thread and read on the main thread.
private volatile CountDownLatch mEventChangesLatch;
@@ -104,9 +108,12 @@ public class CarCalendarUiTest {
public void setUp() {
ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(mLifecycleCallback);
mTestEventRows = new ArrayList<>();
+ mDoNotSetFakeDependencies = false;
}
private void onActivityLifecycleChanged(Activity activity, Stage stage) {
+ if (mDoNotSetFakeDependencies) return;
+
if (stage.equals(Stage.PRE_ON_CREATE)) {
setActivityDependencies((CarCalendarActivity) activity);
} else if (stage.equals(Stage.CREATED)) {
@@ -122,7 +129,8 @@ public class CarCalendarUiTest {
new TestCalendarContentProvider(context);
mockContentResolver.addProvider(CalendarContract.AUTHORITY, testCalendarContentProvider);
activity.mDependencies =
- new CarCalendarActivity.Dependencies(LOCALE, fixedTimeClock, mockContentResolver);
+ new CarCalendarActivity.Dependencies(LOCALE, fixedTimeClock, mockContentResolver,
+ activity.getSystemService(TelephonyManager.class));
}
private void observeEventsLiveData(CarCalendarActivity activity) {
@@ -154,9 +162,18 @@ public class CarCalendarUiTest {
}
@Test
- public void calendar_titleShows() {
+ public void withFakeDependencies_titleShows() {
try (ActivityScenario<CarCalendarActivity> ignored =
- ActivityScenario.launch(CarCalendarActivity.class)) {
+ ActivityScenario.launch(CarCalendarActivity.class)) {
+ onView(withText(R.string.app_name)).check(matches(isDisplayed()));
+ }
+ }
+
+ @Test
+ public void withoutFakeDependencies_titleShows() {
+ mDoNotSetFakeDependencies = true;
+ try (ActivityScenario<CarCalendarActivity> ignored =
+ ActivityScenario.launch(CarCalendarActivity.class)) {
onView(withText(R.string.app_name)).check(matches(isDisplayed()));
}
}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 42c7ee0..0108383 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -13,6 +13,10 @@
// limitations under the License.
//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
android_test {
name: "CarCalendarUnitTests",
srcs: ["src/**/*.java"],
diff --git a/tests/unit/src/com/android/car/calendar/common/EventDescriptionsTest.java b/tests/unit/src/com/android/car/calendar/common/EventDescriptionsTest.java
index 358e9cf..bea029f 100644
--- a/tests/unit/src/com/android/car/calendar/common/EventDescriptionsTest.java
+++ b/tests/unit/src/com/android/car/calendar/common/EventDescriptionsTest.java
@@ -18,28 +18,36 @@ package com.android.car.calendar.common;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
import android.net.Uri;
+import android.telephony.TelephonyManager;
import com.google.common.collect.Iterables;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.Mockito;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Locale;
public class EventDescriptionsTest {
-
private static final String BASE_NUMBER = "30 303986300";
private static final String LOCAL_NUMBER = "0" + BASE_NUMBER;
private static final String INTERNATIONAL_NUMBER = "+49 " + BASE_NUMBER;
private static final String ACCESS = ",,12;3*45#";
+ private static final String COUNTRY_ISO_CODE = "DE";
+
private EventDescriptions mEventDescriptions;
+ private TelephonyManager mMockTelephonyManager;
@Before
public void setUp() {
- mEventDescriptions = new EventDescriptions(Locale.GERMANY);
+ mMockTelephonyManager = Mockito.mock(TelephonyManager.class);
+ when(mMockTelephonyManager.getNetworkCountryIso()).thenReturn(COUNTRY_ISO_CODE);
+ mEventDescriptions = new EventDescriptions(Locale.GERMANY, mMockTelephonyManager);
}
@Test
@@ -48,7 +56,7 @@ public class EventDescriptionsTest {
mEventDescriptions.extractNumberAndPins(LOCAL_NUMBER);
assertThat(numberAndAccesses).isNotEmpty();
Dialer.NumberAndAccess numberAndAccess = Iterables.getFirst(numberAndAccesses, null);
- assertThat(numberAndAccess.getNumber()).isEqualTo(INTERNATIONAL_NUMBER);
+ assertThat(numberAndAccess.getNumber()).isEqualTo(LOCAL_NUMBER);
}
@Test
@@ -62,9 +70,10 @@ public class EventDescriptionsTest {
@Test
public void extractNumberAndPin_internationalNumberWithDifferentLocale_resultIsInternational() {
- mEventDescriptions = new EventDescriptions(Locale.FRANCE);
+ EventDescriptions eventDescriptions =
+ new EventDescriptions(Locale.FRANCE, mMockTelephonyManager);
List<Dialer.NumberAndAccess> numberAndAccesses =
- mEventDescriptions.extractNumberAndPins(INTERNATIONAL_NUMBER);
+ eventDescriptions.extractNumberAndPins(INTERNATIONAL_NUMBER);
assertThat(numberAndAccesses).isNotEmpty();
Dialer.NumberAndAccess numberAndAccess = Iterables.getFirst(numberAndAccesses, null);
assertThat(numberAndAccess.getNumber()).isEqualTo(INTERNATIONAL_NUMBER);
@@ -105,8 +114,8 @@ public class EventDescriptionsTest {
List<Dialer.NumberAndAccess> numberAndAccesses =
mEventDescriptions.extractNumberAndPins(input);
- // The local number is valid but repeated so only included once.
- assertThat(numberAndAccesses).hasSize(1);
+ // Keep all variations of a base number.
+ assertThat(numberAndAccesses).hasSize(3);
}
@Test
diff --git a/tests/unit/src/com/android/car/calendar/common/EventsLiveDataTest.java b/tests/unit/src/com/android/car/calendar/common/EventsLiveDataTest.java
index ff00e8d..79b5e29 100644
--- a/tests/unit/src/com/android/car/calendar/common/EventsLiveDataTest.java
+++ b/tests/unit/src/com/android/car/calendar/common/EventsLiveDataTest.java
@@ -21,6 +21,8 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static java.time.temporal.ChronoUnit.HOURS;
@@ -138,12 +140,11 @@ public class EventsLiveDataTest {
@Test
@UiThreadTest
public void addObserver_queryMade() throws InterruptedException {
- // Expect onChanged to be called for when we start to observe and when the data is read.
- CountDownLatch latch = new CountDownLatch(2);
- mEventsLiveData.observeForever((value) -> latch.countDown());
+ // Observing triggers content to be read.
+ mEventsLiveData.observeForever((unused) -> { /* Do nothing */ });
// Wait for the data to be read on the background thread.
- latch.await(5, TimeUnit.SECONDS);
+ mTestContentProvider.awaitCalendarQuery();
assertThat(mTestContentProvider.mTestEventCursor).isNotNull();
}
@@ -151,49 +152,63 @@ public class EventsLiveDataTest {
@Test
@UiThreadTest
public void addObserver_contentObserved() throws InterruptedException {
- // Expect onChanged to be called for when we start to observe and when the data is read.
- CountDownLatch latch = new CountDownLatch(2);
- mEventsLiveData.observeForever((value) -> latch.countDown());
+ // Observing triggers content to be read.
+ mEventsLiveData.observeForever((unused) -> { /* Do nothing */ });
// Wait for the data to be read on the background thread.
- latch.await(5, TimeUnit.SECONDS);
+ mTestContentProvider.awaitCalendarQuery();
- assertThat(mTestContentProvider.mTestEventCursor.mLastContentObserver).isNotNull();
+ awaitAndAssertDone(mTestContentProvider.mTestEventCursor.mRegisterContentObserverLatch);
}
@Test
- @UiThreadTest
- public void removeObserver_contentNotObserved() throws InterruptedException {
- // Expect onChanged when we observe, when the data is read, and when we stop observing.
- final CountDownLatch latch = new CountDownLatch(2);
- Observer<ImmutableList<Event>> observer = (value) -> latch.countDown();
- mEventsLiveData.observeForever(observer);
+ public void addObserver_observerCalled() throws InterruptedException {
+ // Observing triggers content to be read.
+ Observer<ImmutableList<Event>> mockObserver = mock(Observer.class);
+ runOnMain(() -> mEventsLiveData.observeForever(mockObserver));
- // Wait for the data to be read on the background thread.
- latch.await(5, TimeUnit.SECONDS);
+ // TODO(jdp) This method of verifying an async behaviour is easier to read.
+ verify(mockObserver, timeout(1000).times(1)).onChanged(any());
+ }
- final CountDownLatch latch2 = new CountDownLatch(1);
- mEventsLiveData.removeObserver(observer);
+ @Test
+ public void addTwoObservers_bothObserversCalled() throws InterruptedException {
+ // Observing triggers content to be read.
+ Observer<ImmutableList<Event>> mockObserver1 = mock(Observer.class);
+ runOnMain(() -> mEventsLiveData.observeForever(mockObserver1));
+ Observer<ImmutableList<Event>> mockObserver2 = mock(Observer.class);
+ runOnMain(() -> mEventsLiveData.observeForever(mockObserver2));
+
+ verify(mockObserver1, timeout(1000).times(1)).onChanged(any());
+ verify(mockObserver2, timeout(1000).times(1)).onChanged(any());
+ }
- // Wait for the observer to be unregistered on the background thread.
- latch2.await(5, TimeUnit.SECONDS);
+ @Test
+ public void removeObserver_contentNotObserved() throws InterruptedException {
+ // Observing triggers content to be read.
+ Observer<ImmutableList<Event>> observer = (unused) -> { /* Do nothing */ };
+ runOnMain(() -> mEventsLiveData.observeForever(observer));
+
+ // Wait for the data to be read on the background thread.
+ mTestContentProvider.awaitCalendarQuery();
- assertThat(mTestContentProvider.mTestEventCursor.mLastContentObserver).isNull();
+ awaitAndAssertDone(mTestContentProvider.mTestEventCursor.mRegisterContentObserverLatch);
+ runOnMain(() -> mEventsLiveData.removeObserver(observer));
+ awaitAndAssertDone(mTestContentProvider.mTestEventCursor.mUnregisterContentObserverLatch);
}
@Test
public void addObserver_oneEventResult() throws InterruptedException {
-
mTestContentProvider.addRow(buildTestRowWithDuration(CURRENT_DATE_TIME, 1));
- // Expect onChanged to be called for when we start to observe and when the data is read.
- CountDownLatch latch = new CountDownLatch(2);
+ // Expect onChanged to be called for when the data is read.
+ CountDownLatch latch = new CountDownLatch(1);
// Must add observer on main thread.
runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
// Wait for the data to be read on the background thread.
- latch.await(5, TimeUnit.SECONDS);
+ awaitAndAssertDone(latch);
ImmutableList<Event> events = mEventsLiveData.getValue();
assertThat(events).isNotNull();
@@ -213,12 +228,14 @@ public class EventsLiveDataTest {
}
@Test
- public void changeCursorData_onChangedCalled() throws InterruptedException {
- // Expect onChanged to be called for when we start to observe and when the data is read.
- CountDownLatch initializeCountdownLatch = new CountDownLatch(2);
+ public void notifyDataChange_dataNotChanged_onChangedNotCalled() throws InterruptedException {
+ mTestContentProvider.addRow(buildTestRow());
+
+ // Expect onChanged to be called for when the data is read.
+ CountDownLatch initializeCountdownLatch = new CountDownLatch(1);
- // Expect the same init callbacks as above but with an extra when the data is updated.
- CountDownLatch changeCountdownLatch = new CountDownLatch(3);
+ // Expect the same callback as above but with an extra when the data is updated.
+ CountDownLatch changeCountdownLatch = new CountDownLatch(2);
// Must add observer on main thread.
runOnMain(
@@ -231,32 +248,54 @@ public class EventsLiveDataTest {
}));
// Wait for the data to be read on the background thread.
- initializeCountdownLatch.await(5, TimeUnit.SECONDS);
+ awaitAndAssertDone(initializeCountdownLatch);
- // Signal that the content has changed.
+ // Signal that the content has changed but do not update the data.
mTestContentProvider.mTestEventCursor.signalDataChanged();
// Wait for the changed data to be read on the background thread.
- changeCountdownLatch.await(5, TimeUnit.SECONDS);
- }
-
- private void runOnMain(Runnable runnable) {
- InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+ awaitAndAssertNotDone(changeCountdownLatch);
}
@Test
- public void addObserver_updateScheduled() throws InterruptedException {
- mTestHandler.setExpectedMessageCount(2);
+ public void notifyDataChange_dataChanged_onChangedCalled() throws InterruptedException {
+ mTestContentProvider.addRow(buildTestRow());
+
+ // Expect onChanged to be called for when the data is read.
+ CountDownLatch initializeCountdownLatch = new CountDownLatch(1);
+
+ // Expect the same callback as above but with an extra when the data is updated.
+ CountDownLatch changeCountdownLatch = new CountDownLatch(2);
// Must add observer on main thread.
runOnMain(
() ->
mEventsLiveData.observeForever(
+ // Count down both latches when data is changed.
(value) -> {
- /* Do nothing */
+ initializeCountdownLatch.countDown();
+ changeCountdownLatch.countDown();
}));
- mTestHandler.awaitExpectedMessages(5);
+ // Wait for the data to be read on the background thread.
+ awaitAndAssertDone(initializeCountdownLatch);
+
+ // Change the data and signal that the content has changed.
+ mTestContentProvider.addRow(buildTestRowWithTitle("Another event"));
+ mTestContentProvider.mTestEventCursor.signalDataChanged();
+
+ // Wait for the changed data to be read on the background thread.
+ awaitAndAssertDone(changeCountdownLatch);
+ }
+
+ @Test
+ public void addObserver_updateScheduled() throws InterruptedException {
+ mTestHandler.setExpectedMessageCount(2);
+
+ // Must add observer on main thread.
+ runOnMain(() -> mEventsLiveData.observeForever((unused) -> { /* Do nothing */ }));
+
+ mTestHandler.awaitExpectedMessages();
// Show that a message was scheduled for the future.
assertThat(mTestHandler.mLastUptimeMillis).isAtLeast(SystemClock.uptimeMillis());
@@ -265,13 +304,14 @@ public class EventsLiveDataTest {
@Test
public void noCalendars_valueNull() throws InterruptedException {
mTestContentProvider.mAddFakeCalendar = false;
+ mTestContentProvider.addRow(buildTestRow());
- // Expect onChanged to be called for when we start to observe and when the data is read.
- CountDownLatch latch = new CountDownLatch(2);
+ // Expect onChanged to be called for when the data is read.
+ CountDownLatch latch = new CountDownLatch(1);
runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
// Wait for the data to be read on the background thread.
- latch.await(5, TimeUnit.SECONDS);
+ awaitAndAssertDone(latch);
assertThat(mEventsLiveData.getValue()).isNull();
}
@@ -280,15 +320,9 @@ public class EventsLiveDataTest {
@UiThreadTest
public void noCalendars_contentObserved() throws InterruptedException {
mTestContentProvider.mAddFakeCalendar = false;
-
- // Expect onChanged to be called for when we start to observe and when the data is read.
- CountDownLatch latch = new CountDownLatch(2);
- mEventsLiveData.observeForever((value) -> latch.countDown());
-
- // Wait for the data to be read on the background thread.
- latch.await(5, TimeUnit.SECONDS);
-
- assertThat(mTestContentProvider.mTestEventCursor.mLastContentObserver).isNotNull();
+ mEventsLiveData.observeForever((unused) -> { /* Do nothing */ });
+ mTestContentProvider.awaitCalendarQuery();
+ awaitAndAssertDone(mTestContentProvider.mTestEventCursor.mRegisterContentObserverLatch);
}
@Test
@@ -296,13 +330,12 @@ public class EventsLiveDataTest {
// Replace the default event with one that lasts 24 hours.
mTestContentProvider.addRow(buildTestRowWithDuration(CURRENT_DATE_TIME, 24));
- // Expect onChanged to be called for when we start to observe and when the data is read.
- CountDownLatch latch = new CountDownLatch(2);
+ CountDownLatch latch = new CountDownLatch(1);
runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
// Wait for the data to be read on the background thread.
- latch.await(5, TimeUnit.SECONDS);
+ awaitAndAssertDone(latch);
// Expect an event for the 2 parts of the split event instance.
assertThat(mEventsLiveData.getValue()).hasSize(2);
@@ -314,13 +347,12 @@ public class EventsLiveDataTest {
int hours = 48;
mTestContentProvider.addRow(buildTestRowWithDuration(CURRENT_DATE_TIME, hours));
- // Expect onChanged to be called for when we start to observe and when the data is read.
- CountDownLatch latch = new CountDownLatch(2);
+ CountDownLatch latch = new CountDownLatch(1);
runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
// Wait for the data to be read on the background thread.
- latch.await(5, TimeUnit.SECONDS);
+ awaitAndAssertDone(latch);
Event middlePartEvent = mEventsLiveData.getValue().get(1);
@@ -338,13 +370,12 @@ public class EventsLiveDataTest {
mTestContentProvider.addRow(buildTestRowWithDuration(twoHoursAfterCurrentTime, 1));
mTestContentProvider.addRow(buildTestRowWithDuration(CURRENT_DATE_TIME, 1));
- // Expect onChanged to be called for when we start to observe and when the data is read.
- CountDownLatch latch = new CountDownLatch(2);
+ CountDownLatch latch = new CountDownLatch(1);
runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
// Wait for the data to be read on the background thread.
- latch.await(5, TimeUnit.SECONDS);
+ awaitAndAssertDone(latch);
ImmutableList<Event> events = mEventsLiveData.getValue();
@@ -357,17 +388,17 @@ public class EventsLiveDataTest {
@Test
public void multipleEvents_resultsSortedTitle() throws InterruptedException {
// Replace the default event with two that are out of time order.
- mTestContentProvider.addRow(buildTestRowWithTitle(CURRENT_DATE_TIME, "Title B"));
- mTestContentProvider.addRow(buildTestRowWithTitle(CURRENT_DATE_TIME, "Title A"));
- mTestContentProvider.addRow(buildTestRowWithTitle(CURRENT_DATE_TIME, "Title C"));
+ mTestContentProvider.addRow(buildTestRowWithTitle("Title B"));
+ mTestContentProvider.addRow(buildTestRowWithTitle("Title A"));
+ mTestContentProvider.addRow(buildTestRowWithTitle("Title C"));
- // Expect onChanged to be called for when we start to observe and when the data is read.
- CountDownLatch latch = new CountDownLatch(2);
+ // Expect onChanged to be called for when the data is read.
+ CountDownLatch latch = new CountDownLatch(1);
runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
// Wait for the data to be read on the background thread.
- latch.await(5, TimeUnit.SECONDS);
+ awaitAndAssertDone(latch);
ImmutableList<Event> events = mEventsLiveData.getValue();
@@ -383,13 +414,13 @@ public class EventsLiveDataTest {
CURRENT_DATE_TIME.withZoneSameLocal(ZoneId.of("UTC")).truncatedTo(ChronoUnit.DAYS);
mTestContentProvider.addRow(buildTestRowAllDay(utcMidnightStart));
- // Expect onChanged to be called for when we start to observe and when the data is read.
- CountDownLatch latch = new CountDownLatch(2);
+ // Expect onChanged to be called when the data is read.
+ CountDownLatch latch = new CountDownLatch(1);
runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
// Wait for the data to be read on the background thread.
- latch.await(5, TimeUnit.SECONDS);
+ awaitAndAssertDone(latch);
ImmutableList<Event> events = mEventsLiveData.getValue();
@@ -407,22 +438,35 @@ public class EventsLiveDataTest {
// Set the time to 23:XX in the BERLIN_ZONE_ID which will be after the event end time.
mTestClock.setTime(CURRENT_DATE_TIME.with(ChronoField.HOUR_OF_DAY, 23));
- // Expect onChanged to be called for when we start to observe and when the data is read.
- CountDownLatch latch = new CountDownLatch(2);
+ // Expect onChanged to be called for when the data is read.
+ CountDownLatch latch = new CountDownLatch(1);
runOnMain(() -> mEventsLiveData.observeForever((value) -> latch.countDown()));
// Wait for the data to be read on the background thread.
- latch.await(5, TimeUnit.SECONDS);
+ awaitAndAssertDone(latch);
// Show that the event is included even though its end time is before the current time.
assertThat(mEventsLiveData.getValue()).isNotEmpty();
}
+ private void runOnMain(Runnable runnable) {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+ }
+
+ private static void awaitAndAssertDone(CountDownLatch latch) throws InterruptedException {
+ assertThat(latch.await(2, TimeUnit.SECONDS)).isTrue();
+ }
+
+ private static void awaitAndAssertNotDone(CountDownLatch latch) throws InterruptedException {
+ assertThat(latch.await(2, TimeUnit.SECONDS)).isFalse();
+ }
+
private static class TestContentProvider extends MockContentProvider {
TestEventCursor mTestEventCursor;
boolean mAddFakeCalendar = true;
List<Object[]> mEventRows = new ArrayList<>();
+ CountDownLatch mCalendarQueryLatch = new CountDownLatch(1);
TestContentProvider(Context context) {
super(context);
@@ -449,14 +493,20 @@ public class EventsLiveDataTest {
if (mAddFakeCalendar) {
calendarsCursor.addRow(new String[] {"Test value"});
}
+ mCalendarQueryLatch.countDown();
return calendarsCursor;
}
throw new IllegalStateException("Unexpected query uri " + uri);
}
+ void awaitCalendarQuery() throws InterruptedException {
+ awaitAndAssertDone(mCalendarQueryLatch);
+ }
+
static class TestEventCursor extends MatrixCursor {
final Uri mUri;
- ContentObserver mLastContentObserver;
+ CountDownLatch mRegisterContentObserverLatch = new CountDownLatch(1);
+ CountDownLatch mUnregisterContentObserverLatch = new CountDownLatch(1);
TestEventCursor(Uri uri) {
super(
@@ -477,13 +527,13 @@ public class EventsLiveDataTest {
@Override
public void registerContentObserver(ContentObserver observer) {
super.registerContentObserver(observer);
- mLastContentObserver = observer;
+ mRegisterContentObserverLatch.countDown();
}
@Override
public void unregisterContentObserver(ContentObserver observer) {
super.unregisterContentObserver(observer);
- mLastContentObserver = null;
+ mUnregisterContentObserverLatch.countDown();
}
void signalDataChanged() {
@@ -519,8 +569,8 @@ public class EventsLiveDataTest {
mCountDownLatch = new CountDownLatch(expectedMessageCount);
}
- void awaitExpectedMessages(int seconds) throws InterruptedException {
- mCountDownLatch.await(seconds, TimeUnit.SECONDS);
+ void awaitExpectedMessages() throws InterruptedException {
+ awaitAndAssertDone(mCountDownLatch);
}
@Override
@@ -573,10 +623,7 @@ public class EventsLiveDataTest {
}
static long addHoursAndTruncate(ZonedDateTime dateTime, int hours) {
- return dateTime.truncatedTo(HOURS)
- .plus(Duration.ofHours(hours))
- .toInstant()
- .toEpochMilli();
+ return dateTime.truncatedTo(HOURS).plus(Duration.ofHours(hours)).toInstant().toEpochMilli();
}
static Object[] buildTestRowWithDuration(ZonedDateTime startDateTime, int eventDurationHours) {
@@ -588,8 +635,12 @@ public class EventsLiveDataTest {
return buildTestRowWithDuration(startDateTime, 24, EVENT_TITLE, true);
}
- static Object[] buildTestRowWithTitle(ZonedDateTime startDateTime, String title) {
- return buildTestRowWithDuration(startDateTime, 1, title, EVENT_ALL_DAY);
+ static Object[] buildTestRowWithTitle(String title) {
+ return buildTestRowWithDuration(CURRENT_DATE_TIME, 1, title, EVENT_ALL_DAY);
+ }
+
+ static Object[] buildTestRow() {
+ return buildTestRowWithDuration(CURRENT_DATE_TIME, 1, EVENT_TITLE, EVENT_ALL_DAY);
}
static Object[] buildTestRowWithDuration(