Merge "Remove RestrictTo from internal items in navigation and fragment" into androidx-main
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
index 5ea093a..6e9c5ba 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
@@ -52,6 +52,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import androidx.annotation.StyleableRes;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.R;
import androidx.appcompat.app.AlertDialog;
@@ -80,6 +81,8 @@
@AppCompatShadowedAttributes
public class AppCompatSpinner extends Spinner implements TintableBackgroundView {
+ @SuppressLint("ResourceType")
+ @StyleableRes
private static final int[] ATTRS_ANDROID_SPINNERMODE = {android.R.attr.spinnerMode};
private static final int MAX_ITEMS_MEASURED = 15;
diff --git a/appsearch/appsearch-builtin-types/api/current.txt b/appsearch/appsearch-builtin-types/api/current.txt
index e867102c..e387d30 100644
--- a/appsearch/appsearch-builtin-types/api/current.txt
+++ b/appsearch/appsearch-builtin-types/api/current.txt
@@ -12,16 +12,94 @@
package androidx.appsearch.builtintypes {
- @androidx.appsearch.annotation.Document public class Timer {
+ @androidx.appsearch.annotation.Document(name="builtin:Alarm") public class Alarm {
+ method public long getBlackoutEndTimeMillis();
+ method public long getBlackoutStartTimeMillis();
+ method public long getCreationTimestampMillis();
+ method public int[]? getDaysOfWeek();
+ method @IntRange(from=0, to=23) public int getHour();
+ method public String getId();
+ method @IntRange(from=0, to=59) public int getMinute();
+ method public String? getName();
+ method public String getNamespace();
+ method public androidx.appsearch.builtintypes.AlarmInstance? getNextInstance();
+ method public androidx.appsearch.builtintypes.AlarmInstance? getPreviousInstance();
+ method public String? getRingtone();
+ method public int getScore();
+ method public long getTtlMillis();
+ method public boolean isEnabled();
+ method public boolean isVibrate();
+ }
+
+ public static final class Alarm.Builder {
+ ctor public Alarm.Builder(String, String);
+ ctor public Alarm.Builder(androidx.appsearch.builtintypes.Alarm);
+ method public androidx.appsearch.builtintypes.Alarm build();
+ method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutEndTimeMillis(long);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutStartTimeMillis(long);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setCreationTimestampMillis(long);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setDaysOfWeek(@IntRange(from=java.util.Calendar.SUNDAY, to=java.util.Calendar.SATURDAY) int...);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setEnabled(boolean);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setHour(@IntRange(from=0, to=23) int);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setMinute(@IntRange(from=0, to=59) int);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setName(String?);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setNextInstance(androidx.appsearch.builtintypes.AlarmInstance?);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setPreviousInstance(androidx.appsearch.builtintypes.AlarmInstance?);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setRingtone(String?);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setScore(int);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setTtlMillis(long);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setVibrate(boolean);
+ }
+
+ @androidx.appsearch.annotation.Document(name="builtin:AlarmInstance") public class AlarmInstance {
+ method public long getCreationTimestampMillis();
+ method @IntRange(from=1, to=31) public int getDay();
+ method @IntRange(from=0, to=23) public int getHour();
+ method public String getId();
+ method @IntRange(from=0, to=59) public int getMinute();
+ method @IntRange(from=java.util.Calendar.JANUARY, to=java.util.Calendar.DECEMBER) public int getMonth();
+ method public String getNamespace();
+ method public int getScore();
+ method public long getSnoozeDurationMillis();
+ method public int getStatus();
+ method public long getTtlMillis();
+ method public int getYear();
+ field public static final int STATUS_DISMISSED = 3; // 0x3
+ field public static final int STATUS_FIRING = 2; // 0x2
+ field public static final int STATUS_MISSED = 5; // 0x5
+ field public static final int STATUS_SCHEDULED = 1; // 0x1
+ field public static final int STATUS_SNOOZED = 4; // 0x4
+ field public static final int STATUS_UNKNOWN = 0; // 0x0
+ }
+
+ public static final class AlarmInstance.Builder {
+ ctor public AlarmInstance.Builder(String, String);
+ ctor public AlarmInstance.Builder(androidx.appsearch.builtintypes.AlarmInstance);
+ method public androidx.appsearch.builtintypes.AlarmInstance build();
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setCreationTimestampMillis(long);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setDay(@IntRange(from=1, to=31) int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setHour(@IntRange(from=0, to=23) int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setMinute(@IntRange(from=0, to=59) int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setMonth(@IntRange(from=java.util.Calendar.JANUARY, to=java.util.Calendar.DECEMBER) int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setScore(int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setSnoozeDurationMillis(long);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setStatus(int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setTtlMillis(long);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setYear(int);
+ }
+
+ @androidx.appsearch.annotation.Document(name="builtin:Timer") public class Timer {
+ method public long getCreationTimestampMillis();
method public long getDurationMillis();
- method public long getExpireTimeMillis();
method public String getId();
method public String? getName();
method public String getNamespace();
method public long getRemainingTimeMillis();
method public String? getRingtone();
method public int getScore();
- method public int getTimerStatus();
+ method public long getStartTimeMillis();
+ method public long getStartTimeMillisInElapsedRealtime();
+ method public int getStatus();
method public long getTtlMillis();
method public boolean isVibrate();
field public static final int STATUS_EXPIRED = 3; // 0x3
@@ -34,14 +112,17 @@
public static final class Timer.Builder {
ctor public Timer.Builder(String, String);
+ ctor public Timer.Builder(androidx.appsearch.builtintypes.Timer);
method public androidx.appsearch.builtintypes.Timer build();
+ method public androidx.appsearch.builtintypes.Timer.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.Timer.Builder setDurationMillis(long);
- method public androidx.appsearch.builtintypes.Timer.Builder setExpireTimeMillis(long);
method public androidx.appsearch.builtintypes.Timer.Builder setName(String?);
method public androidx.appsearch.builtintypes.Timer.Builder setRemainingTimeMillis(long);
method public androidx.appsearch.builtintypes.Timer.Builder setRingtone(String?);
method public androidx.appsearch.builtintypes.Timer.Builder setScore(int);
- method public androidx.appsearch.builtintypes.Timer.Builder setTimerStatus(int);
+ method public androidx.appsearch.builtintypes.Timer.Builder setStartTimeMillis(long);
+ method public androidx.appsearch.builtintypes.Timer.Builder setStartTimeMillisInElapsedRealtime(long);
+ method public androidx.appsearch.builtintypes.Timer.Builder setStatus(int);
method public androidx.appsearch.builtintypes.Timer.Builder setTtlMillis(long);
method public androidx.appsearch.builtintypes.Timer.Builder setVibrate(boolean);
}
diff --git a/appsearch/appsearch-builtin-types/api/public_plus_experimental_current.txt b/appsearch/appsearch-builtin-types/api/public_plus_experimental_current.txt
index e867102c..e387d30 100644
--- a/appsearch/appsearch-builtin-types/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch-builtin-types/api/public_plus_experimental_current.txt
@@ -12,16 +12,94 @@
package androidx.appsearch.builtintypes {
- @androidx.appsearch.annotation.Document public class Timer {
+ @androidx.appsearch.annotation.Document(name="builtin:Alarm") public class Alarm {
+ method public long getBlackoutEndTimeMillis();
+ method public long getBlackoutStartTimeMillis();
+ method public long getCreationTimestampMillis();
+ method public int[]? getDaysOfWeek();
+ method @IntRange(from=0, to=23) public int getHour();
+ method public String getId();
+ method @IntRange(from=0, to=59) public int getMinute();
+ method public String? getName();
+ method public String getNamespace();
+ method public androidx.appsearch.builtintypes.AlarmInstance? getNextInstance();
+ method public androidx.appsearch.builtintypes.AlarmInstance? getPreviousInstance();
+ method public String? getRingtone();
+ method public int getScore();
+ method public long getTtlMillis();
+ method public boolean isEnabled();
+ method public boolean isVibrate();
+ }
+
+ public static final class Alarm.Builder {
+ ctor public Alarm.Builder(String, String);
+ ctor public Alarm.Builder(androidx.appsearch.builtintypes.Alarm);
+ method public androidx.appsearch.builtintypes.Alarm build();
+ method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutEndTimeMillis(long);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutStartTimeMillis(long);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setCreationTimestampMillis(long);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setDaysOfWeek(@IntRange(from=java.util.Calendar.SUNDAY, to=java.util.Calendar.SATURDAY) int...);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setEnabled(boolean);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setHour(@IntRange(from=0, to=23) int);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setMinute(@IntRange(from=0, to=59) int);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setName(String?);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setNextInstance(androidx.appsearch.builtintypes.AlarmInstance?);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setPreviousInstance(androidx.appsearch.builtintypes.AlarmInstance?);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setRingtone(String?);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setScore(int);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setTtlMillis(long);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setVibrate(boolean);
+ }
+
+ @androidx.appsearch.annotation.Document(name="builtin:AlarmInstance") public class AlarmInstance {
+ method public long getCreationTimestampMillis();
+ method @IntRange(from=1, to=31) public int getDay();
+ method @IntRange(from=0, to=23) public int getHour();
+ method public String getId();
+ method @IntRange(from=0, to=59) public int getMinute();
+ method @IntRange(from=java.util.Calendar.JANUARY, to=java.util.Calendar.DECEMBER) public int getMonth();
+ method public String getNamespace();
+ method public int getScore();
+ method public long getSnoozeDurationMillis();
+ method public int getStatus();
+ method public long getTtlMillis();
+ method public int getYear();
+ field public static final int STATUS_DISMISSED = 3; // 0x3
+ field public static final int STATUS_FIRING = 2; // 0x2
+ field public static final int STATUS_MISSED = 5; // 0x5
+ field public static final int STATUS_SCHEDULED = 1; // 0x1
+ field public static final int STATUS_SNOOZED = 4; // 0x4
+ field public static final int STATUS_UNKNOWN = 0; // 0x0
+ }
+
+ public static final class AlarmInstance.Builder {
+ ctor public AlarmInstance.Builder(String, String);
+ ctor public AlarmInstance.Builder(androidx.appsearch.builtintypes.AlarmInstance);
+ method public androidx.appsearch.builtintypes.AlarmInstance build();
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setCreationTimestampMillis(long);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setDay(@IntRange(from=1, to=31) int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setHour(@IntRange(from=0, to=23) int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setMinute(@IntRange(from=0, to=59) int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setMonth(@IntRange(from=java.util.Calendar.JANUARY, to=java.util.Calendar.DECEMBER) int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setScore(int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setSnoozeDurationMillis(long);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setStatus(int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setTtlMillis(long);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setYear(int);
+ }
+
+ @androidx.appsearch.annotation.Document(name="builtin:Timer") public class Timer {
+ method public long getCreationTimestampMillis();
method public long getDurationMillis();
- method public long getExpireTimeMillis();
method public String getId();
method public String? getName();
method public String getNamespace();
method public long getRemainingTimeMillis();
method public String? getRingtone();
method public int getScore();
- method public int getTimerStatus();
+ method public long getStartTimeMillis();
+ method public long getStartTimeMillisInElapsedRealtime();
+ method public int getStatus();
method public long getTtlMillis();
method public boolean isVibrate();
field public static final int STATUS_EXPIRED = 3; // 0x3
@@ -34,14 +112,17 @@
public static final class Timer.Builder {
ctor public Timer.Builder(String, String);
+ ctor public Timer.Builder(androidx.appsearch.builtintypes.Timer);
method public androidx.appsearch.builtintypes.Timer build();
+ method public androidx.appsearch.builtintypes.Timer.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.Timer.Builder setDurationMillis(long);
- method public androidx.appsearch.builtintypes.Timer.Builder setExpireTimeMillis(long);
method public androidx.appsearch.builtintypes.Timer.Builder setName(String?);
method public androidx.appsearch.builtintypes.Timer.Builder setRemainingTimeMillis(long);
method public androidx.appsearch.builtintypes.Timer.Builder setRingtone(String?);
method public androidx.appsearch.builtintypes.Timer.Builder setScore(int);
- method public androidx.appsearch.builtintypes.Timer.Builder setTimerStatus(int);
+ method public androidx.appsearch.builtintypes.Timer.Builder setStartTimeMillis(long);
+ method public androidx.appsearch.builtintypes.Timer.Builder setStartTimeMillisInElapsedRealtime(long);
+ method public androidx.appsearch.builtintypes.Timer.Builder setStatus(int);
method public androidx.appsearch.builtintypes.Timer.Builder setTtlMillis(long);
method public androidx.appsearch.builtintypes.Timer.Builder setVibrate(boolean);
}
diff --git a/appsearch/appsearch-builtin-types/api/restricted_current.txt b/appsearch/appsearch-builtin-types/api/restricted_current.txt
index 616ae1e..b69955c 100644
--- a/appsearch/appsearch-builtin-types/api/restricted_current.txt
+++ b/appsearch/appsearch-builtin-types/api/restricted_current.txt
@@ -14,16 +14,94 @@
package androidx.appsearch.builtintypes {
- @androidx.appsearch.annotation.Document public class Timer {
+ @androidx.appsearch.annotation.Document(name="builtin:Alarm") public class Alarm {
+ method public long getBlackoutEndTimeMillis();
+ method public long getBlackoutStartTimeMillis();
+ method public long getCreationTimestampMillis();
+ method public int[]? getDaysOfWeek();
+ method @IntRange(from=0, to=23) public int getHour();
+ method public String getId();
+ method @IntRange(from=0, to=59) public int getMinute();
+ method public String? getName();
+ method public String getNamespace();
+ method public androidx.appsearch.builtintypes.AlarmInstance? getNextInstance();
+ method public androidx.appsearch.builtintypes.AlarmInstance? getPreviousInstance();
+ method public String? getRingtone();
+ method public int getScore();
+ method public long getTtlMillis();
+ method public boolean isEnabled();
+ method public boolean isVibrate();
+ }
+
+ public static final class Alarm.Builder {
+ ctor public Alarm.Builder(String, String);
+ ctor public Alarm.Builder(androidx.appsearch.builtintypes.Alarm);
+ method public androidx.appsearch.builtintypes.Alarm build();
+ method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutEndTimeMillis(long);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutStartTimeMillis(long);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setCreationTimestampMillis(long);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setDaysOfWeek(@IntRange(from=java.util.Calendar.SUNDAY, to=java.util.Calendar.SATURDAY) int...);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setEnabled(boolean);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setHour(@IntRange(from=0, to=23) int);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setMinute(@IntRange(from=0, to=59) int);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setName(String?);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setNextInstance(androidx.appsearch.builtintypes.AlarmInstance?);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setPreviousInstance(androidx.appsearch.builtintypes.AlarmInstance?);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setRingtone(String?);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setScore(int);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setTtlMillis(long);
+ method public androidx.appsearch.builtintypes.Alarm.Builder setVibrate(boolean);
+ }
+
+ @androidx.appsearch.annotation.Document(name="builtin:AlarmInstance") public class AlarmInstance {
+ method public long getCreationTimestampMillis();
+ method @IntRange(from=1, to=31) public int getDay();
+ method @IntRange(from=0, to=23) public int getHour();
+ method public String getId();
+ method @IntRange(from=0, to=59) public int getMinute();
+ method @IntRange(from=java.util.Calendar.JANUARY, to=java.util.Calendar.DECEMBER) public int getMonth();
+ method public String getNamespace();
+ method public int getScore();
+ method public long getSnoozeDurationMillis();
+ method public int getStatus();
+ method public long getTtlMillis();
+ method public int getYear();
+ field public static final int STATUS_DISMISSED = 3; // 0x3
+ field public static final int STATUS_FIRING = 2; // 0x2
+ field public static final int STATUS_MISSED = 5; // 0x5
+ field public static final int STATUS_SCHEDULED = 1; // 0x1
+ field public static final int STATUS_SNOOZED = 4; // 0x4
+ field public static final int STATUS_UNKNOWN = 0; // 0x0
+ }
+
+ public static final class AlarmInstance.Builder {
+ ctor public AlarmInstance.Builder(String, String);
+ ctor public AlarmInstance.Builder(androidx.appsearch.builtintypes.AlarmInstance);
+ method public androidx.appsearch.builtintypes.AlarmInstance build();
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setCreationTimestampMillis(long);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setDay(@IntRange(from=1, to=31) int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setHour(@IntRange(from=0, to=23) int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setMinute(@IntRange(from=0, to=59) int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setMonth(@IntRange(from=java.util.Calendar.JANUARY, to=java.util.Calendar.DECEMBER) int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setScore(int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setSnoozeDurationMillis(long);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setStatus(int);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setTtlMillis(long);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder setYear(int);
+ }
+
+ @androidx.appsearch.annotation.Document(name="builtin:Timer") public class Timer {
+ method public long getCreationTimestampMillis();
method public long getDurationMillis();
- method public long getExpireTimeMillis();
method public String getId();
method public String? getName();
method public String getNamespace();
method public long getRemainingTimeMillis();
method public String? getRingtone();
method public int getScore();
- method public int getTimerStatus();
+ method public long getStartTimeMillis();
+ method public long getStartTimeMillisInElapsedRealtime();
+ method public int getStatus();
method public long getTtlMillis();
method public boolean isVibrate();
field public static final int STATUS_EXPIRED = 3; // 0x3
@@ -36,14 +114,17 @@
public static final class Timer.Builder {
ctor public Timer.Builder(String, String);
+ ctor public Timer.Builder(androidx.appsearch.builtintypes.Timer);
method public androidx.appsearch.builtintypes.Timer build();
+ method public androidx.appsearch.builtintypes.Timer.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.Timer.Builder setDurationMillis(long);
- method public androidx.appsearch.builtintypes.Timer.Builder setExpireTimeMillis(long);
method public androidx.appsearch.builtintypes.Timer.Builder setName(String?);
method public androidx.appsearch.builtintypes.Timer.Builder setRemainingTimeMillis(long);
method public androidx.appsearch.builtintypes.Timer.Builder setRingtone(String?);
method public androidx.appsearch.builtintypes.Timer.Builder setScore(int);
- method public androidx.appsearch.builtintypes.Timer.Builder setTimerStatus(int);
+ method public androidx.appsearch.builtintypes.Timer.Builder setStartTimeMillis(long);
+ method public androidx.appsearch.builtintypes.Timer.Builder setStartTimeMillisInElapsedRealtime(long);
+ method public androidx.appsearch.builtintypes.Timer.Builder setStatus(int);
method public androidx.appsearch.builtintypes.Timer.Builder setTtlMillis(long);
method public androidx.appsearch.builtintypes.Timer.Builder setVibrate(boolean);
}
diff --git a/appsearch/appsearch-builtin-types/build.gradle b/appsearch/appsearch-builtin-types/build.gradle
index 5dea7cc..5e7d9bd 100644
--- a/appsearch/appsearch-builtin-types/build.gradle
+++ b/appsearch/appsearch-builtin-types/build.gradle
@@ -35,6 +35,7 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.multidex)
+ androidTestImplementation("junit:junit:4.13")
}
androidx {
diff --git a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/app/ShortcutAdapterTest.java b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/app/ShortcutAdapterTest.java
index 6c60cb6..5c8188f 100644
--- a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/app/ShortcutAdapterTest.java
+++ b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/app/ShortcutAdapterTest.java
@@ -106,15 +106,16 @@
private static Timer timer(@NonNull final String id, @NonNull final String name) {
return new Timer.Builder(ShortcutAdapter.DEFAULT_NAMESPACE, id)
- .setTtlMillis(1000)
+ .setScore(1)
+ .setTtlMillis(6000)
+ .setCreationTimestampMillis(100)
.setName(name)
.setDurationMillis(1000)
- .setTimerStatus(Timer.STATUS_STARTED)
.setRemainingTimeMillis(500)
- .setExpireTimeMillis(2000)
.setRingtone("clock://ringtone/1")
- .setScore(1)
+ .setStatus(Timer.STATUS_STARTED)
.setVibrate(true)
+ .setStartTimeMillis(750)
.build();
}
}
diff --git a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/AlarmInstanceTest.java b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/AlarmInstanceTest.java
new file mode 100644
index 0000000..b37aae7e
--- /dev/null
+++ b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/AlarmInstanceTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.builtintypes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.GenericDocument;
+
+import org.junit.Test;
+
+import java.util.Calendar;
+
+public class AlarmInstanceTest {
+ @Test
+ public void testBuilder() {
+ AlarmInstance alarmInstance = new AlarmInstance.Builder("namespace", "id")
+ .setScore(1)
+ .setTtlMillis(20000)
+ .setCreationTimestampMillis(100)
+ .setYear(2022)
+ .setMonth(Calendar.DECEMBER)
+ .setDay(1)
+ .setHour(7)
+ .setMinute(30)
+ .setStatus(AlarmInstance.STATUS_SCHEDULED)
+ .setSnoozeDurationMillis(10000)
+ .build();
+
+ assertThat(alarmInstance.getNamespace()).isEqualTo("namespace");
+ assertThat(alarmInstance.getId()).isEqualTo("id");
+ assertThat(alarmInstance.getScore()).isEqualTo(1);
+ assertThat(alarmInstance.getTtlMillis()).isEqualTo(20000);
+ assertThat(alarmInstance.getCreationTimestampMillis()).isEqualTo(100);
+ assertThat(alarmInstance.getYear()).isEqualTo(2022);
+ assertThat(alarmInstance.getMonth()).isEqualTo(Calendar.DECEMBER);
+ assertThat(alarmInstance.getDay()).isEqualTo(1);
+ assertThat(alarmInstance.getHour()).isEqualTo(7);
+ assertThat(alarmInstance.getMinute()).isEqualTo(30);
+ assertThat(alarmInstance.getStatus()).isEqualTo(1);
+ assertThat(alarmInstance.getSnoozeDurationMillis()).isEqualTo(10000);
+ }
+
+ @Test
+ public void testBuilderCopy_returnsAlarmInstanceWithAllFieldsCopied() {
+ AlarmInstance alarmInstance1 = new AlarmInstance.Builder("namespace", "id")
+ .setScore(1)
+ .setTtlMillis(20000)
+ .setCreationTimestampMillis(100)
+ .setYear(2022)
+ .setMonth(Calendar.DECEMBER)
+ .setDay(1)
+ .setHour(7)
+ .setMinute(30)
+ .setStatus(AlarmInstance.STATUS_SCHEDULED)
+ .setSnoozeDurationMillis(10000)
+ .build();
+
+ AlarmInstance alarmInstance2 = new AlarmInstance.Builder(alarmInstance1).build();
+ assertThat(alarmInstance1.getNamespace()).isEqualTo(alarmInstance2.getNamespace());
+ assertThat(alarmInstance1.getId()).isEqualTo(alarmInstance2.getId());
+ assertThat(alarmInstance1.getScore()).isEqualTo(alarmInstance2.getScore());
+ assertThat(alarmInstance1.getTtlMillis()).isEqualTo(alarmInstance2.getTtlMillis());
+ assertThat(alarmInstance1.getCreationTimestampMillis())
+ .isEqualTo(alarmInstance2.getCreationTimestampMillis());
+ assertThat(alarmInstance1.getYear()).isEqualTo(alarmInstance2.getYear());
+ assertThat(alarmInstance1.getMonth()).isEqualTo(alarmInstance2.getMonth());
+ assertThat(alarmInstance1.getDay()).isEqualTo(alarmInstance2.getDay());
+ assertThat(alarmInstance1.getHour()).isEqualTo(alarmInstance2.getHour());
+ assertThat(alarmInstance1.getMinute()).isEqualTo(alarmInstance2.getMinute());
+ assertThat(alarmInstance1.getStatus()).isEqualTo(alarmInstance2.getStatus());
+ assertThat(alarmInstance1.getSnoozeDurationMillis())
+ .isEqualTo(alarmInstance2.getSnoozeDurationMillis());
+ }
+
+ @Test
+ public void testToGenericDocument() throws Exception {
+ AlarmInstance alarmInstance = new AlarmInstance.Builder("namespace", "id")
+ .setScore(1)
+ .setTtlMillis(20000)
+ .setCreationTimestampMillis(100)
+ .setYear(2022)
+ .setMonth(Calendar.DECEMBER)
+ .setDay(1)
+ .setHour(7)
+ .setMinute(30)
+ .setStatus(AlarmInstance.STATUS_SCHEDULED)
+ .setSnoozeDurationMillis(10000)
+ .build();
+
+ GenericDocument genericDocument = GenericDocument.fromDocumentClass(alarmInstance);
+ assertThat(genericDocument.getSchemaType()).isEqualTo("builtin:AlarmInstance");
+ assertThat(genericDocument.getNamespace()).isEqualTo("namespace");
+ assertThat(genericDocument.getId()).isEqualTo("id");
+ assertThat(genericDocument.getScore()).isEqualTo(1);
+ assertThat(genericDocument.getTtlMillis()).isEqualTo(20000);
+ assertThat(genericDocument.getCreationTimestampMillis()).isEqualTo(100);
+ assertThat(genericDocument.getPropertyLong("year")).isEqualTo(2022);
+ assertThat(genericDocument.getPropertyLong("month")).isEqualTo(Calendar.DECEMBER);
+ assertThat(genericDocument.getPropertyLong("day")).isEqualTo(1);
+ assertThat(genericDocument.getPropertyLong("hour")).isEqualTo(7);
+ assertThat(genericDocument.getPropertyLong("minute")).isEqualTo(30);
+ assertThat(genericDocument.getPropertyLong("status")).isEqualTo(1);
+ assertThat(genericDocument.getPropertyLong("snoozeDurationMillis")).isEqualTo(10000);
+ }
+}
diff --git a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/AlarmTest.java b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/AlarmTest.java
new file mode 100644
index 0000000..b953f60
--- /dev/null
+++ b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/AlarmTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.builtintypes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.GenericDocument;
+
+import org.junit.Test;
+
+import java.util.Calendar;
+
+public class AlarmTest {
+ @Test
+ public void testBuilder() {
+ AlarmInstance alarmInstance1 = new AlarmInstance.Builder("namespace", "instanceId1")
+ .build();
+ AlarmInstance alarmInstance2 = new AlarmInstance.Builder("namespace", "instanceId2")
+ .build();
+ Alarm alarm = new Alarm.Builder("namespace", "id")
+ .setScore(1)
+ .setTtlMillis(20000)
+ .setCreationTimestampMillis(100)
+ .setName("my alarm")
+ .setEnabled(true)
+ .setDaysOfWeek(Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY,
+ Calendar.THURSDAY, Calendar.FRIDAY)
+ .setHour(12)
+ .setMinute(0)
+ .setBlackoutStartTimeMillis(1000)
+ .setBlackoutEndTimeMillis(2000)
+ .setRingtone("clock://ringtone/1")
+ .setVibrate(true)
+ .setPreviousInstance(alarmInstance1)
+ .setNextInstance(alarmInstance2)
+ .build();
+
+ assertThat(alarm.getNamespace()).isEqualTo("namespace");
+ assertThat(alarm.getId()).isEqualTo("id");
+ assertThat(alarm.getScore()).isEqualTo(1);
+ assertThat(alarm.getTtlMillis()).isEqualTo(20000);
+ assertThat(alarm.getCreationTimestampMillis()).isEqualTo(100);
+ assertThat(alarm.getName()).isEqualTo("my alarm");
+ assertThat(alarm.isEnabled()).isTrue();
+ assertThat(alarm.getDaysOfWeek()).asList().containsExactly(Calendar.MONDAY,
+ Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY);
+ assertThat(alarm.getHour()).isEqualTo(12);
+ assertThat(alarm.getMinute()).isEqualTo(0);
+ assertThat(alarm.getBlackoutStartTimeMillis()).isEqualTo(1000);
+ assertThat(alarm.getBlackoutEndTimeMillis()).isEqualTo(2000);
+ assertThat(alarm.getRingtone()).isEqualTo("clock://ringtone/1");
+ assertThat(alarm.isVibrate()).isTrue();
+ assertThat(alarm.getPreviousInstance()).isEqualTo(alarmInstance1);
+ assertThat(alarm.getNextInstance()).isEqualTo(alarmInstance2);
+ }
+
+ @Test
+ public void testBuilderCopy_returnsAlarmWithAllFieldsCopied() {
+ AlarmInstance alarmInstance1 = new AlarmInstance.Builder("namespace", "instanceId1")
+ .build();
+ AlarmInstance alarmInstance2 = new AlarmInstance.Builder("namespace", "instanceId2")
+ .build();
+ Alarm alarm1 = new Alarm.Builder("namespace", "id")
+ .setScore(1)
+ .setTtlMillis(20000)
+ .setCreationTimestampMillis(100)
+ .setName("my alarm")
+ .setEnabled(true)
+ .setDaysOfWeek(Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY,
+ Calendar.THURSDAY, Calendar.FRIDAY)
+ .setHour(12)
+ .setMinute(0)
+ .setBlackoutStartTimeMillis(1000)
+ .setBlackoutEndTimeMillis(2000)
+ .setRingtone("clock://ringtone/1")
+ .setVibrate(true)
+ .setPreviousInstance(alarmInstance1)
+ .setNextInstance(alarmInstance2)
+ .build();
+
+ Alarm alarm2 = new Alarm.Builder(alarm1).build();
+ assertThat(alarm1.getNamespace()).isEqualTo(alarm2.getNamespace());
+ assertThat(alarm1.getId()).isEqualTo(alarm2.getId());
+ assertThat(alarm1.getScore()).isEqualTo(alarm2.getScore());
+ assertThat(alarm1.getTtlMillis()).isEqualTo(alarm2.getTtlMillis());
+ assertThat(alarm1.getCreationTimestampMillis())
+ .isEqualTo(alarm2.getCreationTimestampMillis());
+ assertThat(alarm1.getName()).isEqualTo(alarm2.getName());
+ assertThat(alarm1.isEnabled()).isEqualTo(alarm2.isEnabled());
+ assertThat(alarm1.getDaysOfWeek()).isEqualTo(alarm2.getDaysOfWeek());
+ assertThat(alarm1.getHour()).isEqualTo(alarm2.getHour());
+ assertThat(alarm1.getMinute()).isEqualTo(alarm2.getMinute());
+ assertThat(alarm1.getBlackoutStartTimeMillis())
+ .isEqualTo(alarm2.getBlackoutStartTimeMillis());
+ assertThat(alarm1.getBlackoutEndTimeMillis()).isEqualTo(alarm2.getBlackoutEndTimeMillis());
+ assertThat(alarm1.getRingtone()).isEqualTo(alarm2.getRingtone());
+ assertThat(alarm1.isVibrate()).isEqualTo(alarm2.isVibrate());
+ assertThat(alarm1.getPreviousInstance()).isEqualTo(alarm2.getPreviousInstance());
+ assertThat(alarm1.getNextInstance()).isEqualTo(alarm2.getNextInstance());
+ }
+
+ @Test
+ public void testToGenericDocument() throws Exception {
+ AlarmInstance alarmInstance1 = new AlarmInstance.Builder("namespace", "instanceId1")
+ .setCreationTimestampMillis(100)
+ .build();
+ AlarmInstance alarmInstance2 = new AlarmInstance.Builder("namespace", "instanceId2")
+ .setCreationTimestampMillis(100)
+ .build();
+ Alarm alarm = new Alarm.Builder("namespace", "id")
+ .setScore(1)
+ .setTtlMillis(20000)
+ .setCreationTimestampMillis(100)
+ .setName("my alarm")
+ .setEnabled(true)
+ .setDaysOfWeek(Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY,
+ Calendar.THURSDAY, Calendar.FRIDAY)
+ .setHour(12)
+ .setMinute(0)
+ .setBlackoutStartTimeMillis(1000)
+ .setBlackoutEndTimeMillis(2000)
+ .setRingtone("clock://ringtone/1")
+ .setVibrate(true)
+ .setPreviousInstance(alarmInstance1)
+ .setNextInstance(alarmInstance2)
+ .build();
+
+ GenericDocument genericDocument = GenericDocument.fromDocumentClass(alarm);
+ assertThat(genericDocument.getSchemaType()).isEqualTo("builtin:Alarm");
+ assertThat(genericDocument.getNamespace()).isEqualTo("namespace");
+ assertThat(genericDocument.getId()).isEqualTo("id");
+ assertThat(genericDocument.getScore()).isEqualTo(1);
+ assertThat(genericDocument.getTtlMillis()).isEqualTo(20000);
+ assertThat(genericDocument.getCreationTimestampMillis()).isEqualTo(100);
+ assertThat(genericDocument.getPropertyString("name")).isEqualTo("my alarm");
+ assertThat(genericDocument.getPropertyBoolean("enabled")).isTrue();
+ assertThat(genericDocument.getPropertyLongArray("daysOfWeek")).asList()
+ .containsExactly(2L, 3L, 4L, 5L, 6L);
+ assertThat(genericDocument.getPropertyLong("hour")).isEqualTo(12);
+ assertThat(genericDocument.getPropertyLong("minute")).isEqualTo(0);
+ assertThat(genericDocument.getPropertyLong("blackoutStartTimeMillis")).isEqualTo(1000);
+ assertThat(genericDocument.getPropertyLong("blackoutEndTimeMillis")).isEqualTo(2000);
+ assertThat(genericDocument.getPropertyString("ringtone")).isEqualTo("clock://ringtone/1");
+ assertThat(genericDocument.getPropertyBoolean("vibrate")).isTrue();
+ assertThat(genericDocument.getPropertyDocument("previousInstance"))
+ .isEqualTo(GenericDocument.fromDocumentClass(alarmInstance1));
+ assertThat(genericDocument.getPropertyDocument("nextInstance"))
+ .isEqualTo(GenericDocument.fromDocumentClass(alarmInstance2));
+ }
+
+ @Test
+ public void testBuilder_invalidByDay_throwsError() {
+ assertThrows(IllegalArgumentException.class, () -> new Alarm.Builder("namespace", "id")
+ .setDaysOfWeek(Calendar.MONDAY, Calendar.SUNDAY, 8)
+ .build());
+ }
+
+ @Test
+ public void testBuilder_invalidHour_throwsError() {
+ assertThrows(IllegalArgumentException.class, () -> new Alarm.Builder("namespace", "id")
+ .setHour(24)
+ .build());
+ }
+
+ @Test
+ public void testBuilder_invalidMinute_throwsError() {
+ assertThrows(IllegalArgumentException.class, () -> new Alarm.Builder("namespace", "id")
+ .setMinute(60)
+ .build());
+ }
+
+ @Test
+ public void testAlarmWithNullDaysOfWeek_shouldReturnNullDaysOfWeek() throws Exception {
+ Alarm alarm = new Alarm.Builder("namespace", "id")
+ .build();
+ GenericDocument alarmGenericDocument = GenericDocument.fromDocumentClass(alarm);
+
+ assertThat(alarm.getDaysOfWeek()).isNull();
+ assertThat(alarmGenericDocument.getPropertyLongArray("daysOfWeek")).isNull();
+ }
+}
diff --git a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/TimerTest.java b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/TimerTest.java
index e4c8b92a..ca78c58 100644
--- a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/TimerTest.java
+++ b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/TimerTest.java
@@ -26,60 +26,99 @@
@Test
public void testBuilder() {
Timer timer = new Timer.Builder("namespace", "id1")
+ .setScore(1)
.setTtlMillis(6000)
+ .setCreationTimestampMillis(100)
.setName("my timer")
.setDurationMillis(1000)
- .setTimerStatus(Timer.STATUS_STARTED)
.setRemainingTimeMillis(500)
- .setExpireTimeMillis(2000)
.setRingtone("clock://ringtone/1")
- .setScore(1)
+ .setStatus(Timer.STATUS_STARTED)
.setVibrate(true)
+ .setStartTimeMillis(750)
+ .setStartTimeMillisInElapsedRealtime(700L)
.build();
assertThat(timer.getNamespace()).isEqualTo("namespace");
assertThat(timer.getId()).isEqualTo("id1");
assertThat(timer.getTtlMillis()).isEqualTo(6000);
+ assertThat(timer.getScore()).isEqualTo(1);
+ assertThat(timer.getCreationTimestampMillis()).isEqualTo(100);
assertThat(timer.getName()).isEqualTo("my timer");
assertThat(timer.getDurationMillis()).isEqualTo(1000);
- assertThat(timer.getTimerStatus()).isEqualTo(1);
assertThat(timer.getRemainingTimeMillis()).isEqualTo(500);
- assertThat(timer.getExpireTimeMillis()).isEqualTo(2000);
assertThat(timer.getRingtone()).isEqualTo("clock://ringtone/1");
- assertThat(timer.getScore()).isEqualTo(1);
+ assertThat(timer.getStatus()).isEqualTo(1);
assertThat(timer.isVibrate()).isEqualTo(true);
+ assertThat(timer.getStartTimeMillis()).isEqualTo(750);
+ assertThat(timer.getStartTimeMillisInElapsedRealtime()).isEqualTo(700);
+ }
+
+ @Test
+ public void testBuilderCopy_allFieldsCopied() {
+ Timer timer1 = new Timer.Builder("namespace", "id1")
+ .setScore(1)
+ .setTtlMillis(6000)
+ .setCreationTimestampMillis(100)
+ .setName("my timer")
+ .setDurationMillis(1000)
+ .setRemainingTimeMillis(500)
+ .setRingtone("clock://ringtone/1")
+ .setStatus(Timer.STATUS_STARTED)
+ .setVibrate(true)
+ .setStartTimeMillis(750)
+ .setStartTimeMillisInElapsedRealtime(700L)
+ .build();
+ Timer timer2 = new Timer.Builder(timer1).build();
+
+ assertThat(timer1.getNamespace()).isEqualTo(timer2.getNamespace());
+ assertThat(timer1.getId()).isEqualTo(timer2.getId());
+ assertThat(timer1.getTtlMillis()).isEqualTo(timer2.getTtlMillis());
+ assertThat(timer1.getScore()).isEqualTo(timer2.getScore());
+ assertThat(timer1.getCreationTimestampMillis())
+ .isEqualTo(timer2.getCreationTimestampMillis());
+ assertThat(timer1.getName()).isEqualTo(timer2.getName());
+ assertThat(timer1.getDurationMillis()).isEqualTo(timer2.getDurationMillis());
+ assertThat(timer1.getRemainingTimeMillis()).isEqualTo(timer2.getRemainingTimeMillis());
+ assertThat(timer1.getRingtone()).isEqualTo(timer2.getRingtone());
+ assertThat(timer1.getStatus()).isEqualTo(timer2.getStatus());
+ assertThat(timer1.isVibrate()).isEqualTo(timer2.isVibrate());
+ assertThat(timer1.getStartTimeMillis()).isEqualTo(timer2.getStartTimeMillis());
+ assertThat(timer1.getStartTimeMillisInElapsedRealtime())
+ .isEqualTo(timer2.getStartTimeMillisInElapsedRealtime());
}
@Test
public void testToGenericDocument() throws Exception {
Timer timer = new Timer.Builder("namespace", "id1")
+ .setScore(1)
.setTtlMillis(6000)
+ .setCreationTimestampMillis(100)
.setName("my timer")
.setDurationMillis(1000)
- .setTimerStatus(Timer.STATUS_STARTED)
.setRemainingTimeMillis(500)
- .setExpireTimeMillis(2000)
.setRingtone("clock://ringtone/1")
- .setScore(1)
+ .setStatus(Timer.STATUS_STARTED)
.setVibrate(true)
+ .setStartTimeMillis(750)
+ .setStartTimeMillisInElapsedRealtime(700L)
.build();
GenericDocument genericDocument = GenericDocument.fromDocumentClass(timer);
+ assertThat(genericDocument.getSchemaType()).isEqualTo("builtin:Timer");
assertThat(genericDocument.getNamespace()).isEqualTo("namespace");
assertThat(genericDocument.getId()).isEqualTo("id1");
- assertThat(genericDocument.getScore()).isEqualTo(1);
assertThat(genericDocument.getTtlMillis()).isEqualTo(6000);
- assertThat(genericDocument.getSchemaType()).isEqualTo("Timer");
- assertThat(genericDocument.getPropertyString("name"))
- .isEqualTo("my timer");
+ assertThat(genericDocument.getScore()).isEqualTo(1);
+ assertThat(genericDocument.getCreationTimestampMillis()).isEqualTo(100);
+ assertThat(genericDocument.getPropertyString("name")).isEqualTo("my timer");
assertThat(genericDocument.getPropertyLong("durationMillis")).isEqualTo(1000);
- assertThat(genericDocument.getPropertyLong("timerStatus"))
- .isEqualTo(1);
assertThat(genericDocument.getPropertyLong("remainingTimeMillis")).isEqualTo(500);
- assertThat(genericDocument.getPropertyLong("expireTimeMillis"))
- .isEqualTo(2000);
- assertThat(genericDocument.getPropertyString("ringtone"))
- .isEqualTo("clock://ringtone/1");
- assertThat(genericDocument.getPropertyBoolean("vibrate")).isEqualTo(true);
+ assertThat(genericDocument.getPropertyString("ringtone")).isEqualTo("clock://ringtone/1");
+ assertThat(genericDocument.getPropertyLong("status")).isEqualTo(1);
+ assertThat(genericDocument.getPropertyBoolean("vibrate")).isTrue();
+ assertThat(genericDocument.getPropertyLong("startTimeMillis")).isEqualTo(750);
+ assertThat(genericDocument.getPropertyLong("startTimeMillisInElapsedRealtime"))
+ .isEqualTo(700);
}
}
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Alarm.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Alarm.java
new file mode 100644
index 0000000..69d384b
--- /dev/null
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Alarm.java
@@ -0,0 +1,507 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.builtintypes;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.core.util.Preconditions;
+
+import java.util.Calendar;
+
+/**
+ * AppSearch document representing an Alarm entity.
+ */
+@Document(name = "builtin:Alarm")
+public class Alarm {
+ @Document.Namespace
+ private final String mNamespace;
+
+ @Document.Id
+ private final String mId;
+
+ @Document.Score
+ private final int mScore;
+
+ @Document.CreationTimestampMillis
+ private final long mCreationTimestampMillis;
+
+ @Document.TtlMillis
+ private final long mTtlMillis;
+
+ @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ private final String mName;
+
+ @Document.BooleanProperty
+ private final boolean mEnabled;
+
+ @Document.LongProperty
+ private final int[] mDaysOfWeek;
+
+ @Document.LongProperty
+ private final int mHour;
+
+ @Document.LongProperty
+ private final int mMinute;
+
+ @Document.LongProperty
+ private final long mBlackoutStartTimeMillis;
+
+ @Document.LongProperty
+ private final long mBlackoutEndTimeMillis;
+
+ @Document.StringProperty
+ private final String mRingtone;
+
+ @Document.BooleanProperty
+ private final boolean mVibrate;
+
+ @Document.DocumentProperty
+ private final AlarmInstance mPreviousInstance;
+
+ @Document.DocumentProperty
+ private final AlarmInstance mNextInstance;
+
+ Alarm(String namespace, String id, int score, long creationTimestampMillis, long ttlMillis,
+ String name, boolean enabled, int[] daysOfWeek, int hour, int minute,
+ long blackoutStartTimeMillis, long blackoutEndTimeMillis, String ringtone,
+ boolean vibrate, AlarmInstance previousInstance, AlarmInstance nextInstance) {
+ mNamespace = namespace;
+ mId = id;
+ mScore = score;
+ mCreationTimestampMillis = creationTimestampMillis;
+ mTtlMillis = ttlMillis;
+ mName = name;
+ mEnabled = enabled;
+ mDaysOfWeek = daysOfWeek;
+ mHour = hour;
+ mMinute = minute;
+ mBlackoutStartTimeMillis = blackoutStartTimeMillis;
+ mBlackoutEndTimeMillis = blackoutEndTimeMillis;
+ mRingtone = ringtone;
+ mVibrate = vibrate;
+ mPreviousInstance = previousInstance;
+ mNextInstance = nextInstance;
+ }
+
+ /** Returns the namespace of the {@link Alarm}. */
+ @NonNull
+ public String getNamespace() {
+ return mNamespace;
+ }
+
+ /** Returns the unique identifier of the {@link Alarm}. */
+ @NonNull
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the user-provided opaque document score of the {@link Alarm}, which can be
+ * used for ranking using
+ * {@link androidx.appsearch.app.SearchSpec.RankingStrategy#RANKING_STRATEGY_DOCUMENT_SCORE}.
+ */
+ public int getScore() {
+ return mScore;
+ }
+
+ /**
+ * Returns the creation timestamp for the {@link Alarm} document, in milliseconds using the
+ * {@link System#currentTimeMillis()} time base.
+ */
+ public long getCreationTimestampMillis() {
+ return mCreationTimestampMillis;
+ }
+
+ /**
+ * Returns the time-to-live (TTL) for the {@link Alarm} document in milliseconds using the
+ * {@link System#currentTimeMillis()} time base.
+ *
+ * <p>The {@link Alarm} document will be automatically deleted when the TTL expires.
+ */
+ public long getTtlMillis() {
+ return mTtlMillis;
+ }
+
+ /** Returns the name associated with the {@link Alarm}. */
+ @Nullable
+ public String getName() {
+ return mName;
+ }
+
+ /** Returns whether or not the {@link Alarm} is active. */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * Returns the scheduled days for repeating the {@link Alarm}.
+ *
+ * <p>Days of the week can be {@link java.util.Calendar#MONDAY},
+ * {@link java.util.Calendar#TUESDAY}, {@link java.util.Calendar#WEDNESDAY},
+ * {@link java.util.Calendar#THURSDAY}, {@link java.util.Calendar#FRIDAY},
+ * {@link java.util.Calendar#SATURDAY}, or {@link java.util.Calendar#SUNDAY}.
+ *
+ * <p>If null, or if the list is empty, then the {@link Alarm} does not repeat.
+ */
+ @Nullable
+ public int[] getDaysOfWeek() {
+ return mDaysOfWeek;
+ }
+
+ /**
+ * Returns the hour the {@link Alarm} will fire.
+ *
+ * <p>Hours are specified by integers from 0 to 23.
+ */
+ @IntRange(from = 0, to = 23)
+ public int getHour() {
+ return mHour;
+ }
+
+ /**
+ * Returns the minute the {@link Alarm} will fire.
+ *
+ * <p>Minutes are specified by integers from 0 to 59.
+ */
+ @IntRange(from = 0, to = 59)
+ public int getMinute() {
+ return mMinute;
+ }
+
+ /**
+ * Returns the start time for the {@link Alarm} blackout period in milliseconds using the
+ * {@link System#currentTimeMillis()} time base.
+ *
+ * <p>A blackout period means the {@link Alarm} will not fire during this period.
+ *
+ * <p>The value {@code 0} indicates that the blackout period has no start time.
+ *
+ * <p>If both blackoutStartTime and blackoutEndTime are {@code 0}, then the blackout period
+ * is not defined for this {@link Alarm}.
+ */
+ public long getBlackoutStartTimeMillis() {
+ return mBlackoutStartTimeMillis;
+ }
+
+ /**
+ * Returns the end time for the {@link Alarm} blackout period in milliseconds using the
+ * {@link System#currentTimeMillis()} time base.
+ *
+ * <p>A blackout period means the {@link Alarm} will not fire during this period.
+ *
+ * <p>The value {@code 0} indicates that the blackout period has no end time.
+ *
+ * <p>If both blackoutStartTime and blackoutEndTime are {@code 0}, then the blackout period
+ * is not defined for this {@link Alarm}.
+ */
+ public long getBlackoutEndTimeMillis() {
+ return mBlackoutEndTimeMillis;
+ }
+
+ /**
+ * Returns the ringtone of the {@link Alarm} as a content URI to be played, or
+ * {@link android.provider.AlarmClock#VALUE_RINGTONE_SILENT} if no ringtone will be played.
+ */
+ @Nullable
+ public String getRingtone() {
+ return mRingtone;
+ }
+
+ /** Returns whether or not to activate the device vibrator when the {@link Alarm} fires. */
+ public boolean isVibrate() {
+ return mVibrate;
+ }
+
+ /**
+ * Returns the previous {@link AlarmInstance} associated with the {@link Alarm}.
+ *
+ * <p>The previous {@link AlarmInstance} is most recent past instance that was fired. If the
+ * {@link Alarm} has no past instances, then null will be returned.
+ *
+ * <p>See {@link AlarmInstance}.
+ */
+ @Nullable
+ public AlarmInstance getPreviousInstance() {
+ return mPreviousInstance;
+ }
+
+ /**
+ * Returns the next {@link AlarmInstance} associated with the {@link Alarm}.
+ *
+ * <p>The next {@link AlarmInstance} is the immediate future instance that is scheduled to fire.
+ * If the {@link Alarm} has no future instances, then null will be returned.
+ *
+ * <p>See {@link AlarmInstance}.
+ */
+ @Nullable
+ public AlarmInstance getNextInstance() {
+ return mNextInstance;
+ }
+
+ /** Builder for {@link Alarm}. */
+ public static final class Builder {
+ private final String mNamespace;
+ private final String mId;
+ private int mScore;
+ private long mCreationTimestampMillis;
+ private long mTtlMillis;
+ private String mName;
+ private boolean mEnabled;
+ private int[] mDaysOfWeek;
+ private int mHour;
+ private int mMinute;
+ private long mBlackoutStartTimeMillis;
+ private long mBlackoutEndTimeMillis;
+ private String mRingtone;
+ private boolean mVibrate;
+ private AlarmInstance mPreviousInstance;
+ private AlarmInstance mNextInstance;
+
+ /**
+ * Constructor for {@link Alarm.Builder}.
+ *
+ * @param namespace Namespace for the {@link Alarm} Document. See
+ * {@link Document.Namespace}.
+ * @param id Unique identifier for the {@link Alarm} Document. See {@link Document.Id}.
+ */
+ public Builder(@NonNull String namespace, @NonNull String id) {
+ mNamespace = Preconditions.checkNotNull(namespace);
+ mId = Preconditions.checkNotNull(id);
+
+ // Default for unset creationTimestampMillis. AppSearch will internally convert this
+ // to current time when creating the GenericDocument.
+ mCreationTimestampMillis = -1;
+ }
+
+ /**
+ * Constructor for {@link Alarm.Builder} with all the existing values of an {@link Alarm}.
+ */
+ public Builder(@NonNull Alarm alarm) {
+ this(alarm.getNamespace(), alarm.getId());
+ mScore = alarm.getScore();
+ mCreationTimestampMillis = alarm.getCreationTimestampMillis();
+ mTtlMillis = alarm.getTtlMillis();
+ mName = alarm.getName();
+ mEnabled = alarm.isEnabled();
+ mDaysOfWeek = alarm.getDaysOfWeek();
+ mHour = alarm.getHour();
+ mMinute = alarm.getMinute();
+ mBlackoutStartTimeMillis = alarm.getBlackoutStartTimeMillis();
+ mBlackoutEndTimeMillis = alarm.getBlackoutEndTimeMillis();
+ mRingtone = alarm.getRingtone();
+ mVibrate = alarm.isVibrate();
+ mPreviousInstance = alarm.getPreviousInstance();
+ mNextInstance = alarm.getNextInstance();
+ }
+
+ /**
+ * Sets the opaque document score of the {@link Alarm}, which can be used for
+ * ranking using
+ * {@link androidx.appsearch.app.SearchSpec.RankingStrategy#RANKING_STRATEGY_DOCUMENT_SCORE}
+ *
+ * <p>See {@link Document.Score}
+ */
+ @NonNull
+ public Alarm.Builder setScore(int score) {
+ mScore = score;
+ return this;
+ }
+
+ /**
+ * Sets the Creation Timestamp of the {@link Alarm} document, in milliseconds using the
+ * {@link System#currentTimeMillis()} time base.
+ *
+ * <p>If not set, then the current timestamp will be used.
+ *
+ * <p>See {@link Document.CreationTimestampMillis}
+ */
+ @NonNull
+ public Alarm.Builder setCreationTimestampMillis(long creationTimestampMillis) {
+ mCreationTimestampMillis = creationTimestampMillis;
+ return this;
+ }
+
+ /**
+ * Sets the time-to-live (TTL) for the {@link Alarm} document in milliseconds using the
+ * {@link System#currentTimeMillis()} time base.
+ *
+ * <p>The {@link Alarm} document will be automatically deleted when the TTL expires.
+ *
+ * <p>If set to 0, then the document will never expire.
+ *
+ * <p>See {@link Document.TtlMillis}
+ */
+ @NonNull
+ public Alarm.Builder setTtlMillis(long ttlMillis) {
+ mTtlMillis = ttlMillis;
+ return this;
+ }
+
+ /** Sets the name of the {@link Alarm}. */
+ @NonNull
+ public Alarm.Builder setName(@Nullable String name) {
+ mName = name;
+ return this;
+ }
+
+ /** Sets whether or not the {@link Alarm} is active. */
+ @NonNull
+ public Builder setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets the scheduled days for repeating the {@link Alarm}.
+ *
+ * <p>Days of the week can be {@link java.util.Calendar#MONDAY},
+ * {@link java.util.Calendar#TUESDAY}, {@link java.util.Calendar#WEDNESDAY},
+ * {@link java.util.Calendar#THURSDAY}, {@link java.util.Calendar#FRIDAY},
+ * {@link java.util.Calendar#SATURDAY}, or {@link java.util.Calendar#SUNDAY}.
+ *
+ * <p>If not set, or if the list is empty, then the {@link Alarm} does not repeat.
+ */
+ @NonNull
+ public Builder setDaysOfWeek(
+ @Nullable
+ @IntRange(from = Calendar.SUNDAY, to = Calendar.SATURDAY) int... daysOfWeek) {
+ if (daysOfWeek != null) {
+ for (int day : daysOfWeek) {
+ Preconditions.checkArgumentInRange(day, Calendar.SUNDAY, Calendar.SATURDAY,
+ "daysOfWeek");
+ }
+ }
+ mDaysOfWeek = daysOfWeek;
+ return this;
+ }
+
+ /**
+ * Sets the hour the {@link Alarm} will fire.
+ *
+ * <p>Hours are specified by integers from 0 to 23.
+ */
+ @NonNull
+ public Builder setHour(@IntRange(from = 0, to = 23) int hour) {
+ mHour = Preconditions.checkArgumentInRange(hour, 0, 23, "hour");
+ return this;
+ }
+
+ /**
+ * Sets the minute the {@link Alarm} will fire.
+ *
+ * <p>Minutes are specified by integers from 0 to 59.
+ */
+ @NonNull
+ public Builder setMinute(@IntRange(from = 0, to = 59) int minute) {
+ mMinute = Preconditions.checkArgumentInRange(minute, 0, 59, "minute");
+ return this;
+ }
+
+ /**
+ * Sets the start time for the {@link Alarm} blackout period in milliseconds using the
+ * {@link System#currentTimeMillis()} time base.
+ *
+ * <p>A blackout period means the {@link Alarm} will not fire during this period.
+ *
+ * <p>If not set, or set to 0, then the blackout period has no start time.
+ *
+ * <p>If neither blackoutStartTime nor blackoutEndTime are set, or if they are both set
+ * to 0, then the {@link Alarm} has no blackout period.
+ */
+ @NonNull
+ public Builder setBlackoutStartTimeMillis(long blackoutStartTimeMillis) {
+ mBlackoutStartTimeMillis = blackoutStartTimeMillis;
+ return this;
+ }
+
+ /**
+ * Sets the end time for the {@link Alarm} blackout period in milliseconds using the
+ * {@link System#currentTimeMillis()} time base.
+ *
+ * <p>A blackout period means the {@link Alarm} will not fire during this period.
+ *
+ * <p>If not set, or set to 0, then the blackout period has no end time.
+ *
+ * <p>If neither blackoutStartTime nor blackoutEndTime are set, or if they are both set
+ * to 0, then the {@link Alarm} has no blackout period.
+ */
+ @NonNull
+ public Builder setBlackoutEndTimeMillis(long blackoutEndTimeMillis) {
+ mBlackoutEndTimeMillis = blackoutEndTimeMillis;
+ return this;
+ }
+
+ /**
+ * Sets the content URI for the ringtone to be played, or
+ * {@link android.provider.AlarmClock#VALUE_RINGTONE_SILENT} if no ringtone will be played.
+ */
+ @NonNull
+ public Builder setRingtone(@Nullable String ringtone) {
+ mRingtone = ringtone;
+ return this;
+ }
+
+ /** Sets whether or not to activate the device vibrator when the {@link Alarm} fires. */
+ @NonNull
+ public Builder setVibrate(boolean vibrate) {
+ mVibrate = vibrate;
+ return this;
+ }
+
+ /**
+ * Sets the previous {@link AlarmInstance} associated with the {@link Alarm}.
+ *
+ * <p>The previous {@link AlarmInstance} is most recent past instance that was fired. If
+ * not set, then the {@link Alarm} has no past instances.
+ *
+ * <p>See {@link AlarmInstance}.
+ */
+ @NonNull
+ public Builder setPreviousInstance(@Nullable AlarmInstance previousInstance) {
+ mPreviousInstance = previousInstance;
+ return this;
+ }
+
+ /**
+ * Sets the next {@link AlarmInstance} associated with the {@link Alarm}.
+ *
+ * <p>The next {@link AlarmInstance} is the immediate future instance that is scheduled
+ * to fire. If not set, then the {@link Alarm} has no future instances.
+ *
+ * <p>See {@link AlarmInstance}.
+ */
+ @NonNull
+ public Builder setNextInstance(@Nullable AlarmInstance nextInstance) {
+ mNextInstance = nextInstance;
+ return this;
+ }
+
+ /** Builds the {@link Alarm}. */
+ @NonNull
+ public Alarm build() {
+ Preconditions.checkNotNull(mId);
+ Preconditions.checkNotNull(mNamespace);
+
+ return new Alarm(mNamespace, mId, mScore, mCreationTimestampMillis, mTtlMillis, mName,
+ mEnabled, mDaysOfWeek, mHour, mMinute, mBlackoutStartTimeMillis,
+ mBlackoutEndTimeMillis, mRingtone, mVibrate, mPreviousInstance, mNextInstance);
+ }
+ }
+}
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/AlarmInstance.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/AlarmInstance.java
new file mode 100644
index 0000000..bf20151
--- /dev/null
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/AlarmInstance.java
@@ -0,0 +1,408 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.builtintypes;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.Document;
+import androidx.core.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Calendar;
+
+/**
+ * AppSearch document representing an AlarmInstance entity.
+ *
+ * <p>An {@link AlarmInstance} must be associated with an {@link Alarm}. It represents a
+ * particular point in time for that Alarm. For example, if an Alarm is set to
+ * repeat every Monday, then each AlarmInstance for it will be the exact Mondays that the Alarm
+ * did trigger.
+ *
+ * <p>Year, month, day, hour, and minute are used over timestamp to ensure the
+ * {@link AlarmInstance} remains unchanged across timezones. E.g. An AlarmInstance set to fire at
+ * 7am GMT should also fire at 7am when the timezone is changed to PST.
+ */
+@Document(name = "builtin:AlarmInstance")
+public class AlarmInstance {
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @IntDef({STATUS_UNKNOWN, STATUS_SCHEDULED, STATUS_FIRING, STATUS_DISMISSED, STATUS_SNOOZED,
+ STATUS_MISSED})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Status {}
+
+ /** The {@link AlarmInstance} is in an unknown error state. */
+ public static final int STATUS_UNKNOWN = 0;
+ /** The {@link AlarmInstance} is scheduled to fire at some point in the future. */
+ public static final int STATUS_SCHEDULED = 1;
+ /** The {@link AlarmInstance} is firing. */
+ public static final int STATUS_FIRING = 2;
+ /** The {@link AlarmInstance} has been dismissed. */
+ public static final int STATUS_DISMISSED = 3;
+ /** The {@link AlarmInstance} has been snoozed. */
+ public static final int STATUS_SNOOZED = 4;
+ /** The {@link AlarmInstance} has been missed. */
+ public static final int STATUS_MISSED = 5;
+
+ @Document.Namespace
+ private final String mNamespace;
+
+ @Document.Id
+ private final String mId;
+
+ @Document.Score
+ private final int mScore;
+
+ @Document.CreationTimestampMillis
+ private final long mCreationTimestampMillis;
+
+ @Document.TtlMillis
+ private final long mTtlMillis;
+
+ @Document.LongProperty
+ private final int mYear;
+
+ @Document.LongProperty
+ private final int mMonth;
+
+ @Document.LongProperty
+ private final int mDay;
+
+ @Document.LongProperty
+ private final int mHour;
+
+ @Document.LongProperty
+ private final int mMinute;
+
+ @Document.LongProperty
+ private final int mStatus;
+
+ @Document.LongProperty
+ private final long mSnoozeDurationMillis;
+
+ AlarmInstance(String namespace, String id, int score, long creationTimestampMillis,
+ long ttlMillis, int year, int month, int day, int hour, int minute, int status,
+ long snoozeDurationMillis) {
+ mNamespace = namespace;
+ mId = id;
+ mScore = score;
+ mCreationTimestampMillis = creationTimestampMillis;
+ mTtlMillis = ttlMillis;
+ mYear = year;
+ mMonth = month;
+ mDay = day;
+ mHour = hour;
+ mMinute = minute;
+ mStatus = status;
+ mSnoozeDurationMillis = snoozeDurationMillis;
+ }
+
+ /** Returns the namespace of the {@link AlarmInstance}. */
+ @NonNull
+ public String getNamespace() {
+ return mNamespace;
+ }
+
+ /** Returns the unique identifier of the {@link AlarmInstance}. */
+ @NonNull
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the user-provided opaque document score of the {@link AlarmInstance}, which can be
+ * used for ranking using
+ * {@link androidx.appsearch.app.SearchSpec.RankingStrategy#RANKING_STRATEGY_DOCUMENT_SCORE}.
+ */
+ public int getScore() {
+ return mScore;
+ }
+
+ /**
+ * Returns the creation timestamp for the {@link AlarmInstance} document, in milliseconds
+ * using the {@link System#currentTimeMillis()} time base.
+ */
+ public long getCreationTimestampMillis() {
+ return mCreationTimestampMillis;
+ }
+
+ /**
+ * Returns the time-to-live (TTL) for the {@link AlarmInstance} document in milliseconds using
+ * the {@link System#currentTimeMillis()} time base.
+ *
+ * <p>The {@link AlarmInstance} document will be automatically deleted when the TTL expires.
+ */
+ public long getTtlMillis() {
+ return mTtlMillis;
+ }
+
+ /** Returns the year {@link AlarmInstance} is scheduled to fire. */
+ public int getYear() {
+ return mYear;
+ }
+
+ /**
+ * Returns the month {@link AlarmInstance} is scheduled to fire.
+ *
+ * <p>Month should range from {@link java.util.Calendar#JANUARY} to
+ * {@link java.util.Calendar#DECEMBER}.
+ */
+ @IntRange(from = Calendar.JANUARY, to = Calendar.DECEMBER)
+ public int getMonth() {
+ return mMonth;
+ }
+
+ /**
+ * Returns the day of the month {@link AlarmInstance} is scheduled to fire.
+ *
+ * <p>Days are specified by integers from 1 to 31.
+ */
+ @IntRange(from = 1, to = 31)
+ public int getDay() {
+ return mDay;
+ }
+
+ /**
+ * Returns the hour {@link AlarmInstance} is scheduled to fire.
+ *
+ * <p>Hours are specified by integers from 0 to 23.
+ */
+ @IntRange(from = 0, to = 23)
+ public int getHour() {
+ return mHour;
+ }
+
+ /**
+ * Returns the minute {@link AlarmInstance} is scheduled to fire.
+ *
+ * <p>Minutes are specified by integers from 0 to 59.
+ */
+ @IntRange(from = 0, to = 59)
+ public int getMinute() {
+ return mMinute;
+ }
+
+ /**
+ * Returns the current status of the {@link AlarmInstance}.
+ *
+ * <p>Status can be either {@link AlarmInstance#STATUS_UNKNOWN},
+ * {@link AlarmInstance#STATUS_SCHEDULED}, {@link AlarmInstance#STATUS_FIRING},
+ * {@link AlarmInstance#STATUS_DISMISSED}, {@link AlarmInstance#STATUS_SNOOZED}, or
+ * {@link AlarmInstance#STATUS_MISSED}.
+ */
+ @Status
+ public int getStatus() {
+ return mStatus;
+ }
+
+ /**
+ * Returns the length of time in milliseconds the {@link AlarmInstance} will remain snoozed
+ * before it fires again, or -1 if the {@link AlarmInstance} does not support snoozing.
+ */
+ public long getSnoozeDurationMillis() {
+ return mSnoozeDurationMillis;
+ }
+
+ /** Builder for {@link AlarmInstance}. */
+ public static final class Builder {
+ private final String mNamespace;
+ private final String mId;
+ private int mScore;
+ private long mCreationTimestampMillis;
+ private long mTtlMillis;
+ private int mYear;
+ private int mMonth;
+ private int mDay;
+ private int mHour;
+ private int mMinute;
+ private int mStatus;
+ private long mSnoozeDurationMillis;
+
+ /**
+ * Constructor for {@link AlarmInstance.Builder}.
+ *
+ * @param namespace Namespace for the {@link AlarmInstance} Document. See
+ * {@link Document.Namespace}.
+ * @param id Unique identifier for the {@link AlarmInstance} Document. See
+ * {@link Document.Id}.
+ */
+ public Builder(@NonNull String namespace, @NonNull String id) {
+ mNamespace = Preconditions.checkNotNull(namespace);
+ mId = Preconditions.checkNotNull(id);
+
+ // Default for unset creationTimestampMillis. AppSearch will internally convert this
+ // to current time when creating the GenericDocument.
+ mCreationTimestampMillis = -1;
+ // default for snooze length. Indicates no snoozing.
+ mSnoozeDurationMillis = -1;
+ }
+
+ /**
+ * Constructor for {@link AlarmInstance.Builder} with all the existing values of an
+ * {@link AlarmInstance}.
+ */
+ public Builder(@NonNull AlarmInstance alarmInstance) {
+ this(alarmInstance.getNamespace(), alarmInstance.getId());
+ mScore = alarmInstance.getScore();
+ mCreationTimestampMillis = alarmInstance.getCreationTimestampMillis();
+ mTtlMillis = alarmInstance.getTtlMillis();
+ mYear = alarmInstance.getYear();
+ mMonth = alarmInstance.getMonth();
+ mDay = alarmInstance.getDay();
+ mHour = alarmInstance.getHour();
+ mMinute = alarmInstance.getMinute();
+ mStatus = alarmInstance.getStatus();
+ mSnoozeDurationMillis = alarmInstance.getSnoozeDurationMillis();
+ }
+
+ /**
+ * Sets the opaque document score of the {@link AlarmInstance}, which can be used for
+ * ranking using
+ * {@link androidx.appsearch.app.SearchSpec.RankingStrategy#RANKING_STRATEGY_DOCUMENT_SCORE}
+ *
+ * <p>See {@link Document.Score}
+ */
+ @NonNull
+ public Builder setScore(int score) {
+ mScore = score;
+ return this;
+ }
+
+ /**
+ * Sets the Creation Timestamp of the {@link AlarmInstance} document, in milliseconds
+ * using the {@link System#currentTimeMillis()} time base.
+ *
+ * <p>If not set, then the current timestamp will be used.
+ *
+ * <p>See {@link Document.CreationTimestampMillis}
+ */
+ @NonNull
+ public Builder setCreationTimestampMillis(long creationTimestampMillis) {
+ mCreationTimestampMillis = creationTimestampMillis;
+ return this;
+ }
+
+ /**
+ * Sets the time-to-live (TTL) for the {@link AlarmInstance} document in milliseconds using
+ * the {@link System#currentTimeMillis()} time base.
+ *
+ * <p>The {@link AlarmInstance} document will be automatically deleted when the TTL expires.
+ *
+ * <p>If set to 0, then the document will never expire.
+ *
+ * <p>See {@link Document.TtlMillis}
+ */
+ @NonNull
+ public Builder setTtlMillis(long ttlMillis) {
+ mTtlMillis = ttlMillis;
+ return this;
+ }
+
+ /** Sets the year {@link AlarmInstance} is scheduled to fire. */
+ @NonNull
+ public Builder setYear(int year) {
+ mYear = year;
+ return this;
+ }
+
+ /**
+ * Sets the month {@link AlarmInstance} is scheduled to fire.
+ *
+ * <p>Month should range from {@link java.util.Calendar#JANUARY} to
+ * {@link java.util.Calendar#DECEMBER}.
+ */
+ @NonNull
+ public Builder setMonth(
+ @IntRange(from = Calendar.JANUARY, to = Calendar.DECEMBER) int month) {
+ mMonth = Preconditions.checkArgumentInRange(month, Calendar.JANUARY,
+ Calendar.DECEMBER, "month");
+ return this;
+ }
+
+ /**
+ * Sets the day of the month {@link AlarmInstance} is scheduled to fire.
+ *
+ * <p>Days are specified by integers from 1 to 31.
+ */
+ @NonNull
+ public Builder setDay(@IntRange(from = 1, to = 31) int day) {
+ mDay = Preconditions.checkArgumentInRange(day, 1, 31, "day");
+ return this;
+ }
+
+ /**
+ * Sets the hour {@link AlarmInstance} is scheduled to fire.
+ *
+ * <p>Hours are specified by integers from 0 to 23.
+ */
+ @NonNull
+ public Builder setHour(@IntRange(from = 0, to = 23) int hour) {
+ mHour = Preconditions.checkArgumentInRange(hour, 0, 23, "hour");
+ return this;
+ }
+
+ /**
+ * Sets the minute {@link AlarmInstance} is scheduled to fire.
+ *
+ * <p>Minutes are specified by integers from 0 to 59.
+ */
+ @NonNull
+ public Builder setMinute(@IntRange(from = 0, to = 59) int minute) {
+ mMinute = Preconditions.checkArgumentInRange(minute, 0, 59, "minute");
+ return this;
+ }
+
+ /**
+ * Sets the current status of the {@link AlarmInstance}.
+ *
+ * <p>Status can be either {@link AlarmInstance#STATUS_UNKNOWN},
+ * {@link AlarmInstance#STATUS_SCHEDULED}, {@link AlarmInstance#STATUS_FIRING},
+ * {@link AlarmInstance#STATUS_DISMISSED}, {@link AlarmInstance#STATUS_SNOOZED}, or
+ * {@link AlarmInstance#STATUS_MISSED}.
+ */
+ @NonNull
+ public Builder setStatus(@Status int status) {
+ mStatus = status;
+ return this;
+ }
+
+ /**
+ * Sets the length of time in milliseconds the {@link AlarmInstance} will remain snoozed
+ * before it fires again.
+ *
+ * <p>If not set, or set to -1, then the {@link AlarmInstance} does not support snoozing.
+ */
+ @NonNull
+ public Builder setSnoozeDurationMillis(long snoozeDurationMillis) {
+ mSnoozeDurationMillis = snoozeDurationMillis;
+ return this;
+ }
+
+ /** Builds the {@link AlarmInstance}. */
+ @NonNull
+ public AlarmInstance build() {
+ Preconditions.checkNotNull(mId);
+ Preconditions.checkNotNull(mNamespace);
+
+ return new AlarmInstance(mNamespace, mId, mScore, mCreationTimestampMillis, mTtlMillis,
+ mYear, mMonth, mDay, mHour, mMinute, mStatus, mSnoozeDurationMillis);
+ }
+ }
+}
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
index 608c858..0bac43d 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
@@ -16,6 +16,8 @@
package androidx.appsearch.builtintypes;
+import android.os.SystemClock;
+
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -30,7 +32,7 @@
/**
* AppSearch document representing a Timer entity.
*/
-@Document
+@Document(name = "builtin:Timer")
public class Timer {
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -61,44 +63,53 @@
@Document.Score
private final int mScore;
+ @Document.CreationTimestampMillis
+ private final long mCreationTimestampMillis;
+
@Document.TtlMillis
private final long mTtlMillis;
- @Document.StringProperty
- private final String mRingtone;
-
@Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
private final String mName;
@Document.LongProperty
private final long mDurationMillis;
- @Document.BooleanProperty
- private final boolean mVibrate;
+ @Document.LongProperty
+ private final long mStartTimeMillis;
+
+ @Document.LongProperty
+ private final long mStartTimeMillisInElapsedRealtime;
@Document.LongProperty
private final long mRemainingTimeMillis;
- @Document.LongProperty
- private final int mTimerStatus;
+ @Document.StringProperty
+ private final String mRingtone;
@Document.LongProperty
- private final long mExpireTimeMillis;
+ private final int mStatus;
- Timer(String namespace, String id, int score, long ttlMillis, String ringtone,
- String name, long durationMillis, boolean vibrate, long remainingTimeMillis,
- int timerStatus, long expireTimeMillis) {
+ @Document.BooleanProperty
+ private final boolean mVibrate;
+
+ Timer(String namespace, String id, int score, long creationTimestampMillis, long ttlMillis,
+ String name, long durationMillis, long startTimeMillis,
+ long startTimeMillisInElapsedRealtime, long remainingTimeMillis, String ringtone,
+ int status, boolean vibrate) {
mNamespace = namespace;
mId = id;
mScore = score;
+ mCreationTimestampMillis = creationTimestampMillis;
mTtlMillis = ttlMillis;
- mRingtone = ringtone;
mName = name;
mDurationMillis = durationMillis;
- mVibrate = vibrate;
+ mStartTimeMillis = startTimeMillis;
+ mStartTimeMillisInElapsedRealtime = startTimeMillisInElapsedRealtime;
mRemainingTimeMillis = remainingTimeMillis;
- mTimerStatus = timerStatus;
- mExpireTimeMillis = expireTimeMillis;
+ mRingtone = ringtone;
+ mStatus = status;
+ mVibrate = vibrate;
}
/** Returns the namespace of the {@link Timer}. */
@@ -123,7 +134,15 @@
}
/**
- * Returns the TTL for the {@link Timer} document in milliseconds using the
+ * Returns the creation timestamp for the {@link Timer} document, in milliseconds using the
+ * {@link System#currentTimeMillis()} time base.
+ */
+ public long getCreationTimestampMillis() {
+ return mCreationTimestampMillis;
+ }
+
+ /**
+ * Returns the time-to-live (TTL) for the {@link Timer} document in milliseconds using the
* {@link System#currentTimeMillis()} time base.
*
* <p>The {@link Timer} document will be automatically deleted when the TTL expires.
@@ -132,6 +151,80 @@
return mTtlMillis;
}
+ /** Returns the name associated with the {@link Timer}. */
+ @Nullable
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the total duration of the {@link Timer}, in milliseconds.
+ */
+ public long getDurationMillis() {
+ return mDurationMillis;
+ }
+
+ /**
+ * Returns the time at which the {@link Timer} was started in milliseconds using the
+ * {@link System#currentTimeMillis()} time base.
+ *
+ *
+ * <p>If the {@link Timer} is in a {@link Timer#STATUS_STARTED} state, then its expire time
+ * can be calculated using:
+ * <pre>{@code
+ * long expireTime = timer.getStartTimeMillis + timer.getRemainingTimeMillis();
+ * }</pre>
+ *
+ * <p>See {@link #getStartTimeMillisInElapsedRealtime()} to see how startTimeMillis and
+ * startTimeMillisInElapsedRealtime should be used.
+ */
+ public long getStartTimeMillis() {
+ return mStartTimeMillis;
+ }
+
+ /**
+ * Returns the time at which the {@link Timer} was started in milliseconds using the
+ * {@link android.os.SystemClock#elapsedRealtime()} time base, or -1 if not present.
+ *
+ * <p>If present, startTimeMillisInElapsedRealtime should be the preferred value used to do
+ * accurate time keeping in {@link Timer}.
+ *
+ * <p>If not present, or if {@link SystemClock#elapsedRealtime()} is unreliable, for example
+ * after a device reboot, or the {@link Timer} document is moved to a different device, then
+ * startTimeMillis should be used instead for time keeping.
+ *
+ * <p>If the {@link Timer} is in a {@link Timer#STATUS_STARTED} state, then its expire time
+ * can be calculated using:
+ * <pre>{@code
+ * long elapsedTime = SystemClock.elapsedRealtime() -
+ * timer.getStartTimeMillisInElapsedRealtime();
+ * long expireTime = System.currentTimeMillis() + timer.getRemainingTimeMillis() - elapsedTime;
+ * }</pre>
+ */
+ public long getStartTimeMillisInElapsedRealtime() {
+ return mStartTimeMillisInElapsedRealtime;
+ }
+
+ /**
+ * Returns the amount of time remaining when the {@link Timer} was started, paused or reset,
+ * in milliseconds.
+ *
+ * <p>The current remaining time can also be calculate using either
+ * {@link #getStartTimeMillis()} or {@link #getStartTimeMillisInElapsedRealtime()}:
+ * <pre>{@code
+ * long elapsedTime = System.currentTimeMillis() - timer.getStartTimeMillis();
+ * long currentRemainingTime = timer.getRemainingTimeMillis() - elapsedTime;
+ * }</pre>
+ * <pre>{@code
+ * long elapsedTime = SystemClock.elapsedRealtime() -
+ * timer.getStartTimeMillisInElapsedRealtime();
+ * long currentRemainingTime = timer.getRemainingTimeMillis() - elapsedTime;
+ * }</pre>
+ */
+ public long getRemainingTimeMillis() {
+ return mRemainingTimeMillis;
+ }
+
/**
* Returns the ringtone of the {@link Timer} as a content URI to be played, or
* {@link android.provider.AlarmClock#VALUE_RINGTONE_SILENT} if no ringtone will be played.
@@ -141,33 +234,6 @@
return mRingtone;
}
- /** Returns the name associated with the {@link Timer}. */
- @Nullable
- public String getName() {
- return mName;
- }
-
- /**
- * Returns the total duration of the {@link Timer} when it was first created, in milliseconds
- * using the {@link System#currentTimeMillis()} time base.
- */
- public long getDurationMillis() {
- return mDurationMillis;
- }
-
- /** Returns whether or not to activate the device vibrator when the {@link Timer} expires. */
- public boolean isVibrate() {
- return mVibrate;
- }
-
- /**
- * Returns the amount of time remaining when the {@link Timer} was started or paused, in
- * milliseconds using the {@link System#currentTimeMillis()} time base.
- */
- public long getRemainingTimeMillis() {
- return mRemainingTimeMillis;
- }
-
/**
* Returns the current status of the {@link Timer}.
*
@@ -176,19 +242,13 @@
* {@link Timer#STATUS_RESET}.
*/
@Status
- public int getTimerStatus() {
- return mTimerStatus;
+ public int getStatus() {
+ return mStatus;
}
- /**
- * Returns the time at which the {@link Timer} will, or did expire in milliseconds since
- * epoch.
- *
- * <p>Unlike {@link Timer#getTtlMillis()}, the {@link Timer} document will not be
- * automatically deleted when the expire time is reached.
- */
- public long getExpireTimeMillis() {
- return mExpireTimeMillis;
+ /** Returns whether or not to activate the device vibrator when the {@link Timer} expires. */
+ public boolean isVibrate() {
+ return mVibrate;
}
/** Builder for {@link Timer}. */
@@ -196,25 +256,51 @@
private final String mNamespace;
private final String mId;
private int mScore;
+ private long mCreationTimestampMillis;
private long mTtlMillis;
- private String mRingtone;
private String mName;
private long mDurationMillis;
- private boolean mVibrate;
+ private long mStartTimeMillis;
+ private long mStartTimeMillisInElapsedRealtime;
private long mRemainingTimeMillis;
- private int mTimerStatus;
- private long mExpireTimeMillis;
+ private String mRingtone;
+ private int mStatus;
+ private boolean mVibrate;
/**
* Constructor for {@link Timer.Builder}.
*
- * @param id Unique identifier for the {@link Timer} Document. See {@link Document.Id}.
* @param namespace Namespace for the {@link Timer} Document. See
* {@link Document.Namespace}.
+ * @param id Unique identifier for the {@link Timer} Document. See {@link Document.Id}.
*/
public Builder(@NonNull String namespace, @NonNull String id) {
mNamespace = Preconditions.checkNotNull(namespace);
mId = Preconditions.checkNotNull(id);
+
+ // Default for unset creationTimestampMillis. AppSearch will internally convert this
+ // to current time when creating the GenericDocument.
+ mCreationTimestampMillis = -1;
+ // Default for unset startTimeMillisInElapsedRealtime
+ mStartTimeMillisInElapsedRealtime = -1;
+ }
+
+ /**
+ * Constructor for {@link Timer.Builder} with all the existing values of a {@link Timer}.
+ */
+ public Builder(@NonNull Timer timer) {
+ this(timer.getNamespace(), timer.getId());
+ mScore = timer.getScore();
+ mCreationTimestampMillis = timer.getCreationTimestampMillis();
+ mTtlMillis = timer.getTtlMillis();
+ mName = timer.getName();
+ mDurationMillis = timer.getDurationMillis();
+ mStartTimeMillis = timer.getStartTimeMillis();
+ mStartTimeMillisInElapsedRealtime = timer.getStartTimeMillisInElapsedRealtime();
+ mRemainingTimeMillis = timer.getRemainingTimeMillis();
+ mRingtone = timer.getRingtone();
+ mStatus = timer.getStatus();
+ mVibrate = timer.isVibrate();
}
/**
@@ -230,12 +316,26 @@
}
/**
- * Sets the TTL for the {@link Timer} document in milliseconds using the
+ * Sets the creation timestamp of the {@link Timer} document, in milliseconds using the
+ * {@link System#currentTimeMillis()} time base.
+ *
+ * <p>If not set, then the current timestamp will be used.
+ *
+ * <p>See {@link Document.CreationTimestampMillis}
+ */
+ @NonNull
+ public Builder setCreationTimestampMillis(long creationTimestampMillis) {
+ mCreationTimestampMillis = creationTimestampMillis;
+ return this;
+ }
+
+ /**
+ * Sets the time-to-live (TTL) for the {@link Timer} document in milliseconds using the
* {@link System#currentTimeMillis()} time base.
*
* <p>The {@link Timer} document will be automatically deleted when the TTL expires.
*
- * <p>If set to 0, then the document will never expire.
+ * <p>If not set, then the document will never expire.
*
* <p>See {@link Document.TtlMillis}
*/
@@ -245,6 +345,64 @@
return this;
}
+ /** Sets the name. */
+ @NonNull
+ public Builder setName(@Nullable String name) {
+ mName = name;
+ return this;
+ }
+
+ /**
+ * Sets the total duration of the {@link Timer}, in milliseconds.
+ */
+ @NonNull
+ public Builder setDurationMillis(long durationMillis) {
+ mDurationMillis = durationMillis;
+ return this;
+ }
+
+ /**
+ * Sets the time at which the {@link Timer} was started in milliseconds using the
+ * {@link System#currentTimeMillis()} time base.
+ *
+ * <p>See {@link #setStartTimeMillisInElapsedRealtime(long)} on how startTimeMillis and
+ * startTimeMillisInElapsedRealtime should be used.
+ */
+ @NonNull
+ public Builder setStartTimeMillis(long startTimeMillis) {
+ mStartTimeMillis = startTimeMillis;
+ return this;
+ }
+
+ /**
+ * Sets the time at which the {@link Timer} was started in milliseconds using the
+ * {@link android.os.SystemClock#elapsedRealtime()} time base.
+ *
+ * <p>startTimeMillis and startTimeMillisInElapsedRealtime should be sampled at
+ * the same time, using {@link System#currentTimeMillis()} and
+ * {@link android.os.SystemClock#elapsedRealtime()} respectively.
+ *
+ * <p>In situations where the reader cannot reliably use
+ * {@link android.os.SystemClock#elapsedRealtime()}, for example if the reader is not on
+ * the same device where the {@link Timer} document is written, then
+ * startTimeMillisInElapsedRealtime should not be set.
+ */
+ @NonNull
+ public Builder setStartTimeMillisInElapsedRealtime(long startTimeMillisInElapsedRealtime) {
+ mStartTimeMillisInElapsedRealtime = startTimeMillisInElapsedRealtime;
+ return this;
+ }
+
+ /**
+ * Sets the amount of time remaining when the {@link Timer} was started, paused or reset,
+ * in milliseconds.
+ */
+ @NonNull
+ public Builder setRemainingTimeMillis(long remainingTimeMillis) {
+ mRemainingTimeMillis = remainingTimeMillis;
+ return this;
+ }
+
/**
* Sets the content URI for the ringtone to be played, or
* {@link android.provider.AlarmClock#VALUE_RINGTONE_SILENT} if no ringtone will be played.
@@ -255,20 +413,16 @@
return this;
}
- /** Sets the name. */
- @NonNull
- public Builder setName(@Nullable String name) {
- mName = name;
- return this;
- }
-
/**
- * Sets the total duration of the {@link Timer} when it was first created in milliseconds
- * using the {@link System#currentTimeMillis()} time base.
+ * Sets the current status of the {@link Timer}.
+ *
+ * <p>Status can be {@link Timer#STATUS_UNKNOWN}, {@link Timer#STATUS_STARTED},
+ * {@link Timer#STATUS_PAUSED}, {@link Timer#STATUS_EXPIRED}, {@link Timer#STATUS_MISSED},
+ * or {@link Timer#STATUS_RESET}.
*/
@NonNull
- public Builder setDurationMillis(long durationMillis) {
- mDurationMillis = durationMillis;
+ public Builder setStatus(@Status int status) {
+ mStatus = status;
return this;
}
@@ -279,52 +433,15 @@
return this;
}
- /**
- * Sets the amount of time remaining when the {@link Timer} was started or paused, in
- * milliseconds using the {@link System#currentTimeMillis()} time base.
- */
- @NonNull
- public Builder setRemainingTimeMillis(long remainingTimeMillis) {
- mRemainingTimeMillis = remainingTimeMillis;
- return this;
- }
-
- /**
- * Sets the current status of the {@link Timer}.
- *
- * @param timerStatus Can be {@link Timer#STATUS_UNKNOWN}, {@link Timer#STATUS_STARTED},
- * {@link Timer#STATUS_PAUSED}, {@link Timer#STATUS_EXPIRED}, {@link Timer#STATUS_MISSED}
- * , or {@link Timer#STATUS_RESET}.
- */
- @NonNull
- public Builder setTimerStatus(@Status int timerStatus) {
- mTimerStatus = timerStatus;
- return this;
- }
-
- /**
- * Sets the time at which the {@link Timer} will, or did expire in milliseconds since epoch.
- *
- * <p>If set to 0, then the {@link Timer} will never expire.
- *
- * <p>Unlike {@link Builder#setTtlMillis(long)}, the {@link Timer} document will not be
- * automatically deleted when the expire time is reached.
- */
- @NonNull
- public Builder setExpireTimeMillis(long expireTimeMillis) {
- mExpireTimeMillis = expireTimeMillis;
- return this;
- }
-
/** Builds the {@link Timer}. */
@NonNull
public Timer build() {
Preconditions.checkNotNull(mId);
Preconditions.checkNotNull(mNamespace);
- return new Timer(mNamespace, mId, mScore, mTtlMillis, mRingtone, mName,
- mDurationMillis, mVibrate, mRemainingTimeMillis, mTimerStatus,
- mExpireTimeMillis);
+ return new Timer(mNamespace, mId, mScore, mCreationTimestampMillis, mTtlMillis, mName,
+ mDurationMillis, mStartTimeMillis, mStartTimeMillisInElapsedRealtime,
+ mRemainingTimeMillis, mRingtone, mStatus, mVibrate);
}
}
}
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
index 2920535..cfe4150 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
@@ -52,9 +52,9 @@
@Suppress("DEPRECATION")
@SuppressLint("NewApi")
dirUsableByAppAndShell = when {
- Build.VERSION.SDK_INT in 30..31 -> {
- // On Android R, S we are using the media directory because that is the directory
- // that the shell has access to. Context: b/181601156
+ Build.VERSION.SDK_INT in 29..31 -> {
+ // On Android Q, R and S we are using the media directory because that is
+ // the directory that the shell has access to. Context: b/181601156
InstrumentationRegistry.getInstrumentation().context.getFirstMountedMediaDir()
}
Build.VERSION.SDK_INT <= 22 -> {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/AtraceTag.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/AtraceTag.kt
index 3cdaa64..d897d5d 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/AtraceTag.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/AtraceTag.kt
@@ -32,6 +32,11 @@
val tag: String
) {
ActivityManager("am"),
+ Audio("audio") {
+ override fun supported(api: Int, rooted: Boolean): Boolean {
+ return api >= 23
+ }
+ },
BinderDriver("binder_driver") {
override fun supported(api: Int, rooted: Boolean): Boolean {
return api >= 24
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
index 2dd0f5f..3b96d05 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
@@ -57,6 +57,7 @@
),
atrace_categories = listOf(
AtraceTag.ActivityManager,
+ AtraceTag.Audio,
AtraceTag.BinderDriver,
AtraceTag.Camera,
AtraceTag.Dalvik,
diff --git a/benchmark/benchmark-macro/api/restricted_current.txt b/benchmark/benchmark-macro/api/restricted_current.txt
index 3292f02..32672e61 100644
--- a/benchmark/benchmark-macro/api/restricted_current.txt
+++ b/benchmark/benchmark-macro/api/restricted_current.txt
@@ -4,6 +4,10 @@
@RequiresApi(29) public final class Api29Kt {
}
+ @RequiresApi(23) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class AudioUnderrunMetric extends androidx.benchmark.macro.Metric {
+ ctor public AudioUnderrunMetric();
+ }
+
public enum BaselineProfileMode {
enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Disable;
enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Require;
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AudioUnderrunQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AudioUnderrunQueryTest.kt
new file mode 100644
index 0000000..f3fd1ca
--- /dev/null
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AudioUnderrunQueryTest.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 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 androidx.benchmark.macro.perfetto
+
+import androidx.benchmark.macro.createTempFileFromAsset
+import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+
+@SdkSuppress(minSdkVersion = 23)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class AudioUnderrunQueryTest {
+ @Test
+ fun validateFixedTrace() {
+ assumeTrue(isAbiSupported())
+
+ // the trace was generated during 2 seconds AudioUnderrunBenchmark scenario run
+ val traceFile = createTempFileFromAsset("api23_audio_underrun", ".perfetto-trace")
+
+ val subMetrics = AudioUnderrunQuery.getSubMetrics(traceFile.absolutePath)
+ val expectedMetrics = AudioUnderrunQuery.SubMetrics(2212, 892)
+
+ assertEquals(expectedMetrics, subMetrics)
+ }
+}
\ No newline at end of file
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 135615c..bca9891 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -353,15 +353,15 @@
* Represents the default compilation mode for the platform, on an end user's device.
*
* This is a post-store-install app configuration for this device's SDK
- * version - [`Partial(BaselineProfileMode.IncludeIfAvailable)`][Partial] on
- * API 24+, and [Full] prior to API 24 (where all apps are fully AOT compiled).
+ * version - [`Partial(BaselineProfileMode.UseIfAvailable)`][Partial] on API 24+, and
+ * [Full] prior to API 24 (where all apps are fully AOT compiled).
*
* On API 24+, Baseline Profile pre-compilation is used if possible, but no error will be
* thrown if installation fails.
*
* Generally, it is preferable to explicitly pass a compilation mode, such as
- * [Partial(BaselineProfileMode.Include)][Partial] to avoid ambiguity, and e.g. validate an
- * app's BaselineProfile can be correctly used.
+ * [`Partial(BaselineProfileMode.Required)`][Partial] to avoid ambiguity, and e.g. validate
+ * an app's BaselineProfile can be correctly used.
*/
@JvmField
val DEFAULT: CompilationMode = if (Build.VERSION.SDK_INT >= 24) {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
index 84ed44d..242b31e 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -21,6 +21,7 @@
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.benchmark.Shell
+import androidx.benchmark.macro.perfetto.AudioUnderrunQuery
import androidx.benchmark.macro.perfetto.FrameTimingQuery
import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric
import androidx.benchmark.macro.perfetto.PerfettoResultsParser.parseStartupResult
@@ -56,6 +57,50 @@
private fun Long.nsToDoubleMs(): Double = this / 1_000_000.0
/**
+ * Metric which captures information about playing audio.
+ *
+ * Each time an instance of [android.media.AudioTrack] is started, the systems repeatedly
+ * logs the number of audio frames available for output. This doesn't work when audio offload is
+ * enabled. No logs are generated while there is no active track.
+ *
+ * Test fails in case of multiple active tracks during a single iteration.
+ *
+ * This outputs the following measurements:
+ *
+ * * `totalMs` - Total duration of played audio captured during the iteration.
+ * The test fails if no counters are detected.
+ *
+ * * `zeroMs` - Duration of played audio when zero audio frames were available for output. Each
+ * counter with zero frames indicates a gap in audio playing.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Suppress("CanSealedSubClassBeObject")
+@RequiresApi(23)
+public class AudioUnderrunMetric : Metric() {
+ internal override fun configure(packageName: String) {
+ }
+
+ internal override fun start() {
+ }
+
+ internal override fun stop() {
+ }
+
+ internal override fun getMetrics(captureInfo: CaptureInfo, tracePath: String): IterationResult {
+ val subMetrics = AudioUnderrunQuery.getSubMetrics(tracePath)
+
+ return IterationResult(
+ singleMetrics = mapOf(
+ "totalMs" to subMetrics.totalMs.toDouble(),
+ "zeroMs" to subMetrics.zeroMs.toDouble()
+ ),
+ sampledMetrics = emptyMap(),
+ timelineRangeNs = null
+ )
+ }
+}
+
+/**
* Legacy version of FrameTimingMetric, based on 'dumpsys gfxinfo' instead of trace data.
*
* Temporary - to be removed after transition to FrameTimingMetric
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt
new file mode 100644
index 0000000..12db8c3
--- /dev/null
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2021 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 androidx.benchmark.macro.perfetto
+
+internal object AudioUnderrunQuery {
+ private fun getFullQuery() = """
+ SELECT track.name, counter.value, counter.ts
+ FROM track
+ JOIN counter ON track.id = counter.track_id
+ WHERE track.type = 'process_counter_track' AND track.name LIKE 'nRdy%'
+ """.trimIndent()
+
+ data class SubMetrics(
+ val totalMs: Int,
+ val zeroMs: Int
+ )
+
+ fun getSubMetrics(
+ absoluteTracePath: String
+ ): SubMetrics {
+ val queryResult = PerfettoTraceProcessor.rawQuery(
+ absoluteTracePath = absoluteTracePath,
+ query = getFullQuery()
+ )
+
+ val resultLines = queryResult.split("\n")
+
+ if (resultLines.first() != "\"name\",\"value\",\"ts\"") {
+ throw IllegalStateException("query failed!")
+ }
+
+ // we can't measure duration when there is only one time stamp
+ if (resultLines.size <= 3) {
+ throw RuntimeException("No playing audio detected")
+ }
+
+ var trackName: String? = null
+ var lastTs: Long? = null
+
+ var totalNs: Long = 0
+ var zeroNs: Long = 0
+
+ resultLines
+ .drop(1) // column names
+ .dropLast(1) // empty line
+ .forEach {
+ val lineVals = it.split(",")
+ if (lineVals.size != VAL_MAX)
+ throw IllegalStateException("query failed")
+
+ if (trackName == null) {
+ trackName = lineVals[VAL_NAME]
+ } else if (!trackName.equals(lineVals[VAL_NAME])) {
+ throw RuntimeException("There could be only one AudioTrack per measure")
+ }
+
+ if (lastTs == null) {
+ lastTs = lineVals[VAL_TS].toLong()
+ } else {
+ val frameNs = lineVals[VAL_TS].toLong() - lastTs!!
+ lastTs = lineVals[VAL_TS].toLong()
+
+ totalNs += frameNs
+
+ val frameCounter = lineVals[VAL_VALUE].toDouble().toInt()
+
+ if (frameCounter == 0)
+ zeroNs += frameNs
+ }
+ }
+
+ return SubMetrics((totalNs / 1_000_000).toInt(), (zeroNs / 1_000_000).toInt())
+ }
+
+ private const val VAL_NAME = 0
+ private const val VAL_VALUE = 1
+ private const val VAL_TS = 2
+ private const val VAL_MAX = 3
+}
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index 80943c0..69c5a3b 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -62,6 +62,15 @@
</intent-filter>
</activity>
+ <activity
+ android:name=".AudioActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="androidx.benchmark.integration.macrobenchmark.target.AUDIO_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
<!--
Activities need to be exported so the macrobenchmark can discover them
-->
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/AudioActivity.kt b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/AudioActivity.kt
new file mode 100644
index 0000000..752e908
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/AudioActivity.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2021 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 androidx.benchmark.integration.macrobenchmark.target
+
+import android.media.AudioAttributes
+import android.media.AudioFormat
+import android.media.AudioTrack
+import android.os.Build
+import android.os.Bundle
+import android.widget.TextView
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import kotlin.concurrent.thread
+import kotlin.math.sin
+
+@RequiresApi(Build.VERSION_CODES.M)
+class AudioActivity() : AppCompatActivity() {
+ private lateinit var thread: Thread
+
+ @Synchronized get
+ @Synchronized set
+ private var finished = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_audio)
+
+ findViewById<TextView>(R.id.audioTextNotice).setText(R.string.audio_notice)
+
+ val sampleRateHz = 22050
+ val bufferDurationMs = 250
+ val buffer = generateBuffer(bufferDurationMs, 500, sampleRateHz)
+
+ // plays beeps continuously until activity is destroyed
+ thread = thread {
+ var format = AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+ .setSampleRate(sampleRateHz)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+ .build()
+
+ var attributes = AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .build()
+
+ val track = AudioTrack.Builder()
+ .setAudioAttributes(attributes)
+ .setAudioFormat(format)
+ .setBufferSizeInBytes(buffer.size * 2)
+ .build()
+
+ track.play()
+
+ while (!finished) {
+ var currentTime = System.currentTimeMillis()
+ track.write(buffer, 0, buffer.size)
+
+ // sleep twice as buffer duration to generate pauses
+ val targetTime = currentTime + bufferDurationMs * 2
+ Thread.sleep(targetTime - System.currentTimeMillis())
+ }
+
+ track.stop()
+ }
+ }
+
+ override fun onDestroy() {
+ finished = true
+ thread.join()
+
+ super.onDestroy()
+ }
+
+ private fun generateBuffer(durationMs: Int, frequency: Int, sampleRateHz: Int): ShortArray {
+ val numSamples = durationMs * sampleRateHz / 1000
+
+ val buffer = ShortArray(numSamples)
+ for (i in 0 until numSamples) {
+ val sample = sin(2 * Math.PI * i / (sampleRateHz / frequency)) * 0.1
+ buffer[i] = (sample * Short.MAX_VALUE).toInt().toShort()
+ }
+
+ return buffer
+ }
+}
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/activity_audio.xml b/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/activity_audio.xml
new file mode 100644
index 0000000..0dc66b9
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/activity_audio.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2021 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.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/audioTextNotice"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="text" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/res/values/donottranslate-strings.xml b/benchmark/integration-tests/macrobenchmark-target/src/main/res/values/donottranslate-strings.xml
index f8753b2..8aaaec8 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/res/values/donottranslate-strings.xml
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/res/values/donottranslate-strings.xml
@@ -17,4 +17,5 @@
<resources>
<string name="app_notice">Macrobenchmark Integration Test App.</string>
<string name="recyclerDescription">A list of items</string>
+ <string name="audio_notice">Repeatedly plays beeps</string>
</resources>
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
new file mode 100644
index 0000000..ab94e5c
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2021 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 androidx.benchmark.integration.macrobenchmark
+
+import android.content.Intent
+import androidx.benchmark.macro.AudioUnderrunMetric
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 23)
+class AudioUnderrunBenchmark() {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ private lateinit var device: UiDevice
+
+ @Before
+ fun setUp() {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ device = UiDevice.getInstance(instrumentation)
+ }
+
+ @Test
+ fun start() {
+ benchmarkRule.measureRepeated(
+ packageName = PACKAGE_NAME,
+ metrics = listOf(AudioUnderrunMetric()),
+ compilationMode = CompilationMode.Full(),
+ startupMode = StartupMode.WARM,
+ iterations = 1,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = ACTION
+ startActivityAndWait(intent)
+ }
+ ) {
+ // audio is played for a half of duration, ~50% of the frames would be zero
+ Thread.sleep(DURATION_MS.toLong())
+ }
+ }
+
+ companion object {
+ private const val PACKAGE_NAME = "androidx.benchmark.integration.macrobenchmark.target"
+ private const val ACTION = "$PACKAGE_NAME.AUDIO_ACTIVITY"
+ private const val DURATION_MS = 2000
+ }
+}
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index 53d7c42..6345639 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -72,6 +72,7 @@
// Create fake variant tasks since that is what is invoked by developers.
val lintTask = tasks.named("lint")
lintTask.configure { task ->
+ task.dependsOn(tasks.named("exportAtomicLibraryGroupsToText"))
AffectedModuleDetector.configureTaskGuard(task)
}
afterEvaluate {
@@ -122,6 +123,7 @@
}}"
).configure { task ->
AffectedModuleDetector.configureTaskGuard(task)
+ task.dependsOn(tasks.named("exportAtomicLibraryGroupsToText"))
}
tasks.named(
"lintAnalyze${variant.name.replaceFirstChar {
@@ -129,6 +131,7 @@
}}"
).configure { task ->
AffectedModuleDetector.configureTaskGuard(task)
+ task.dependsOn(tasks.named("exportAtomicLibraryGroupsToText"))
}
/* TODO: uncomment when we upgrade to AGP 7.1.0-alpha04
tasks.named("lintReport${variant.name.capitalize(Locale.US)}").configure { task ->
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/doclava/DoclavaTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/doclava/DoclavaTask.kt
index 67e3c24..ef9d034 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/doclava/DoclavaTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/doclava/DoclavaTask.kt
@@ -153,10 +153,15 @@
val args = DoclavaArgumentBuilder()
// classpath
- val classpathString = classpath!!.files.map({ f -> f.toString() }).joinToString(":")
- args.addStringOption("cp", classpathString)
+ val classpathFile = File.createTempFile("doclavaClasspath", ".txt")
+ classpathFile.deleteOnExit()
+ classpathFile.bufferedWriter().use { writer ->
+ val classpathString = classpath!!.files.map({ f -> f.toString() }).joinToString(":")
+ writer.write(classpathString)
+ }
+ args.addStringOption("cp", "@$classpathFile")
args.addStringOption("doclet", "com.google.doclava.Doclava")
- args.addStringOption("docletpath", classpathString)
+ args.addStringOption("docletpath", "@$classpathFile")
args.addOption("quiet")
args.addStringOption("encoding", "UTF-8")
@@ -197,15 +202,21 @@
args.addFileOption("d", destinationDir!!)
// source files
- for (source in sources) {
- for (file in source) {
- val arg = file.toString()
- // Doclava does not know how to parse Kotlin files
- if (!arg.endsWith(".kt")) {
- args.add(arg)
+ val tmpArgs = File.createTempFile("doclavaSourceArgs", ".txt")
+ tmpArgs.deleteOnExit()
+ tmpArgs.bufferedWriter().use { writer ->
+ for (source in sources) {
+ for (file in source) {
+ val arg = file.toString()
+ // Doclava does not know how to parse Kotlin files
+ if (!arg.endsWith(".kt")) {
+ writer.write(arg)
+ writer.newLine()
+ }
}
}
}
+ args.add("@$tmpArgs")
return args.build() + extraArgumentsBuilder.build()
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index 3e011f6..1d0b2c7 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -111,7 +111,7 @@
unzippedSamplesSources,
samplesSourcesConfiguration
)
- val unzippedDocsSources = File(project.buildDir, "unzippedDocsSources")
+ val unzippedDocsSources = File(project.buildDir, "srcs")
val unzipDocsTask = configureUnzipTask(
project,
"unzipDocsSources",
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioPlatformUtilities.kt b/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioPlatformUtilities.kt
index 0bd6f66..3787ab1 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioPlatformUtilities.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioPlatformUtilities.kt
@@ -171,11 +171,6 @@
}
override fun StudioTask.updateJvmHeapSize() {
- val vmoptions =
- File(binaryDirectory, "bin/studio.vmoptions")
- val newText = vmoptions.readText().replace(jvmHeapRegex, "-Xmx4g")
- vmoptions.writeText(newText)
-
val vmoptions64 =
File(binaryDirectory, "bin/studio64.vmoptions")
val newText64 = vmoptions64.readText().replace(jvmHeapRegex, "-Xmx8g")
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
index 5579270..b5d5239 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
@@ -85,6 +85,12 @@
checkApi = RunApiTasks.No("Lint Library"),
compilationTarget = CompilationTarget.HOST
),
+ COMPILER_DAEMON(
+ Publish.SNAPSHOT_AND_RELEASE,
+ sourceJars = false,
+ RunApiTasks.No("Compiler Daemon (Host-only)"),
+ CompilationTarget.HOST
+ ),
COMPILER_PLUGIN(
Publish.SNAPSHOT_AND_RELEASE,
sourceJars = false,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
index f3f6540..3b4ae6a 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
@@ -27,7 +27,7 @@
import androidx.camera.camera2.pipe.integration.impl.CameraProperties
import androidx.camera.core.ExposureState
-internal val EMPTY_RANGE = Range(0, 0)
+internal val EMPTY_RANGE: Range<Int> = Range(0, 0)
/** Adapt [ExposureState] to a [CameraMetadata] instance. */
@SuppressLint("UnsafeOptInUsageError")
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt
index 452bc8e..7324125 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt
@@ -61,7 +61,7 @@
}
}
-internal val EMPTY_RANGE = Range(0, 0)
+internal val EMPTY_RANGE: Range<Int> = Range(0, 0)
/**
* The implementation of the [EvCompCompat]. The [applyAsync] update the new exposure index value
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt
index 1c091d8..41e8e81 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt
@@ -34,24 +34,25 @@
import androidx.camera.core.impl.OptionsBundle
internal const val CAPTURE_REQUEST_ID_STEM = "camera2.captureRequest.option."
-internal val TEMPLATE_TYPE_OPTION = Config.Option.create<Int>(
+internal val TEMPLATE_TYPE_OPTION: Config.Option<Int> = Config.Option.create(
"camera2.captureRequest.templateType",
Int::class.javaPrimitiveType!!
)
-internal val DEVICE_STATE_CALLBACK_OPTION = Config.Option.create<CameraDevice.StateCallback>(
- "camera2.cameraDevice.stateCallback",
- CameraDevice.StateCallback::class.java
-)
-internal val SESSION_STATE_CALLBACK_OPTION =
- Config.Option.create<CameraCaptureSession.StateCallback>(
+internal val DEVICE_STATE_CALLBACK_OPTION: Config.Option<CameraDevice.StateCallback> =
+ Config.Option.create(
+ "camera2.cameraDevice.stateCallback",
+ CameraDevice.StateCallback::class.java
+ )
+internal val SESSION_STATE_CALLBACK_OPTION: Config.Option<CameraCaptureSession.StateCallback> =
+ Config.Option.create(
"camera2.cameraCaptureSession.stateCallback",
CameraCaptureSession.StateCallback::class.java
)
-internal val SESSION_CAPTURE_CALLBACK_OPTION = Config.Option.create<CaptureCallback>(
+internal val SESSION_CAPTURE_CALLBACK_OPTION: Config.Option<CaptureCallback> = Config.Option.create(
"camera2.cameraCaptureSession.captureCallback",
CaptureCallback::class.java
)
-internal val CAPTURE_REQUEST_TAG_OPTION = Config.Option.create<Any>(
+internal val CAPTURE_REQUEST_TAG_OPTION: Config.Option<Any> = Config.Option.create(
"camera2.captureRequest.tag", Any::class.java
)
// TODO: Porting the CameraEventCallback option constant.
diff --git a/camera/camera-camera2/api/public_plus_experimental_current.txt b/camera/camera-camera2/api/public_plus_experimental_current.txt
index 290eb9c..583ba10 100644
--- a/camera/camera-camera2/api/public_plus_experimental_current.txt
+++ b/camera/camera-camera2/api/public_plus_experimental_current.txt
@@ -11,6 +11,7 @@
@RequiresApi(21) @androidx.camera.camera2.interop.ExperimentalCamera2Interop public final class Camera2CameraControl {
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> addCaptureRequestOptions(androidx.camera.camera2.interop.CaptureRequestOptions);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> clearCaptureRequestOptions();
method public static androidx.camera.camera2.interop.Camera2CameraControl from(androidx.camera.core.CameraControl);
method public androidx.camera.camera2.interop.CaptureRequestOptions getCaptureRequestOptions();
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setCaptureRequestOptions(androidx.camera.camera2.interop.CaptureRequestOptions);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
index 75a6bb8..b94be04 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
@@ -25,6 +25,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
+import androidx.camera.camera2.internal.compat.workaround.MaxPreviewSize;
/**
* A singleton class to retrieve display related information.
@@ -37,6 +38,8 @@
@NonNull
private final DisplayManager mDisplayManager;
private volatile Size mPreviewSize = null;
+ private final MaxPreviewSize mMaxPreviewSize = new MaxPreviewSize();
+
private DisplayInfoManager(@NonNull Context context) {
mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
}
@@ -125,9 +128,8 @@
if (displayViewSize.getWidth() * displayViewSize.getHeight()
> MAX_PREVIEW_SIZE.getWidth() * MAX_PREVIEW_SIZE.getHeight()) {
- return MAX_PREVIEW_SIZE;
- } else {
- return displayViewSize;
+ displayViewSize = MAX_PREVIEW_SIZE;
}
+ return mMaxPreviewSize.getMaxPreviewResolution(displayViewSize);
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index e387700..bbbe4d1 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -38,6 +38,7 @@
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
import androidx.camera.camera2.internal.compat.workaround.ExcludedSupportedSizesContainer;
import androidx.camera.camera2.internal.compat.workaround.ExtraSupportedSurfaceCombinationsContainer;
+import androidx.camera.camera2.internal.compat.workaround.ResolutionSelector;
import androidx.camera.camera2.internal.compat.workaround.TargetAspectRatio;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.CameraUnavailableException;
@@ -100,6 +101,7 @@
private Map<Integer, Size[]> mOutputSizesCache = new HashMap<>();
@NonNull
private final DisplayInfoManager mDisplayInfoManager;
+ private final ResolutionSelector mResolutionSelector = new ResolutionSelector();
SupportedSurfaceCombination(@NonNull Context context, @NonNull String cameraId,
@NonNull CameraManagerCompat cameraManagerCompat,
@@ -168,25 +170,9 @@
* @return new {@link SurfaceConfig} object
*/
SurfaceConfig transformSurfaceConfig(int imageFormat, Size size) {
- ConfigType configType;
+ ConfigType configType = getConfigType(imageFormat);
ConfigSize configSize = ConfigSize.NOT_SUPPORT;
- /*
- * PRIV refers to any target whose available sizes are found using
- * StreamConfigurationMap.getOutputSizes(Class) with no direct application-visible format,
- * YUV refers to a target Surface using the ImageFormat.YUV_420_888 format, JPEG refers to
- * the ImageFormat.JPEG format, and RAW refers to the ImageFormat.RAW_SENSOR format.
- */
- if (imageFormat == ImageFormat.YUV_420_888) {
- configType = ConfigType.YUV;
- } else if (imageFormat == ImageFormat.JPEG) {
- configType = ConfigType.JPEG;
- } else if (imageFormat == ImageFormat.RAW_SENSOR) {
- configType = ConfigType.RAW;
- } else {
- configType = ConfigType.PRIV;
- }
-
Size maxSize = fetchMaxSize(imageFormat);
// Compare with surface size definition to determine the surface configuration size
@@ -301,7 +287,7 @@
// Gets the corrected aspect ratio due to device constraints or null if no correction is
// needed.
@TargetAspectRatio.Ratio int targetAspectRatio =
- new TargetAspectRatio().get(imageOutputConfig, mCameraId, mCharacteristics);
+ new TargetAspectRatio().get(mCameraId, mCharacteristics);
switch (targetAspectRatio) {
case TargetAspectRatio.RATIO_4_3:
outputRatio = mIsSensorLandscapeResolution ? ASPECT_RATIO_4_3 : ASPECT_RATIO_3_4;
@@ -492,9 +478,36 @@
}
}
+ supportedResolutions = mResolutionSelector.insertOrPrioritize(
+ getConfigType(config.getInputFormat()),
+ supportedResolutions);
+
return supportedResolutions;
}
+
+ /**
+ * Gets {@link ConfigType} from image format.
+ *
+ * <p> PRIV refers to any target whose available sizes are found using
+ * StreamConfigurationMap.getOutputSizes(Class) with no direct application-visible format,
+ * YUV refers to a target Surface using the ImageFormat.YUV_420_888 format, JPEG refers to
+ * the ImageFormat.JPEG format, and RAW refers to the ImageFormat.RAW_SENSOR format.
+ */
+ @NonNull
+ private SurfaceConfig.ConfigType getConfigType(int imageFormat) {
+
+ if (imageFormat == ImageFormat.YUV_420_888) {
+ return SurfaceConfig.ConfigType.YUV;
+ } else if (imageFormat == ImageFormat.JPEG) {
+ return SurfaceConfig.ConfigType.JPEG;
+ } else if (imageFormat == ImageFormat.RAW_SENSOR) {
+ return SurfaceConfig.ConfigType.RAW;
+ } else {
+ return SurfaceConfig.ConfigType.PRIV;
+ }
+ }
+
@Nullable
private Size getTargetSize(@NonNull ImageOutputConfig imageOutputConfig) {
int targetRotation = imageOutputConfig.getTargetRotation(Surface.ROTATION_0);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
index 1094a2b..ef67bb38 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
@@ -44,8 +44,8 @@
if (ImageCapturePixelHDRPlusQuirk.load()) {
quirks.add(new ImageCapturePixelHDRPlusQuirk());
}
- if (SamsungPreviewTargetAspectRatioQuirk.load()) {
- quirks.add(new SamsungPreviewTargetAspectRatioQuirk());
+ if (SelectResolutionQuirk.load()) {
+ quirks.add(new SelectResolutionQuirk());
}
if (Nexus4AndroidLTargetAspectRatioQuirk.load()) {
quirks.add(new Nexus4AndroidLTargetAspectRatioQuirk());
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SamsungPreviewTargetAspectRatioQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SamsungPreviewTargetAspectRatioQuirk.java
deleted file mode 100644
index 2d789fb..0000000
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SamsungPreviewTargetAspectRatioQuirk.java
+++ /dev/null
@@ -1,57 +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 androidx.camera.camera2.internal.compat.quirk;
-
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.Config;
-import androidx.camera.core.impl.PreviewConfig;
-import androidx.camera.core.impl.Quirk;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Locale;
-
-/**
- * Quirk that produces stretched preview on certain Samsung devices.
- *
- * <p> On certain Samsung devices, the HAL provides 16:9 preview even when the Surface size is
- * set to 4:3, which causes the preview to be stretched in PreviewView.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public class SamsungPreviewTargetAspectRatioQuirk implements Quirk {
-
- // List of devices with the issue.
- private static final List<String> DEVICE_MODELS = Arrays.asList(
- "SM-J710MN", // b/170762209
- "SM-T580" // b/169471824
- );
-
- static boolean load() {
- return "SAMSUNG".equalsIgnoreCase(Build.BRAND)
- && DEVICE_MODELS.contains(android.os.Build.MODEL.toUpperCase(Locale.US));
- }
-
- /**
- * Whether to overwrite the aspect ratio in the config to be 16:9.
- */
- public boolean require16_9(@NonNull Config config) {
- return config instanceof PreviewConfig;
- }
-}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SelectResolutionQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SelectResolutionQuirk.java
new file mode 100644
index 0000000..07e7672
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SelectResolutionQuirk.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2021 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 androidx.camera.camera2.internal.compat.quirk;
+
+import android.os.Build;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+import androidx.camera.core.impl.SurfaceConfig;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Quirk that requires specific resolutions as the workaround.
+ *
+ * <p> This is an allowlist that selects specific resolutions to override the app provided
+ * resolutions. The resolution provided in this file have been manually tested by CameraX team.
+ */
+@RequiresApi(21)
+public class SelectResolutionQuirk implements Quirk {
+
+ private static final List<String> SAMSUNG_DISTORTION_MODELS = Arrays.asList(
+ "SM-T580", // Samsung Galaxy Tab A (2016)
+ "SM-J710MN", // Samsung Galaxy J7 (2016)
+ "SM-A320FL", // Samsung Galaxy A3 (2017)
+ "SM-G570M"); // Samsung Galaxy J5 Prime
+
+ static boolean load() {
+ return isSamsungDistortion();
+ }
+
+ /**
+ * Selects a resolution based on {@link SurfaceConfig.ConfigType}.
+ *
+ * <p> The selected resolution have been manually tested by CameraX team. It is known to
+ * work for the given device/stream.
+ *
+ * @return null if no resolution provided, in which case the calling code should fallback to
+ * user provided target resolution.
+ */
+ @Nullable
+ public Size selectResolution(@NonNull SurfaceConfig.ConfigType configType) {
+ if (isSamsungDistortion()) {
+ // The following resolutions are needed for both the front and the back camera.
+ switch (configType) {
+ case PRIV:
+ return new Size(1920, 1080);
+ case YUV:
+ return new Size(1280, 720);
+ case JPEG:
+ return new Size(3264, 1836);
+ default:
+ return null;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Checks for device model with Samsung output distortion bug (b/190203334).
+ *
+ * <p> The symptom of these devices is that the output of one or many streams, including PRIV,
+ * JPEG and/or YUV, can have an extra 25% crop, and the cropped image is stretched to
+ * fill the Surface, which results in a distorted output. The streams can also have an
+ * extra 25% double crop, in which case the stretched image will not be distorted, but the
+ * FOV is smaller than it should be.
+ *
+ * <p> The behavior is inconsistent in a way that the extra cropping depends on the
+ * resolution of the streams. The existence of the issue also depends on API level and/or
+ * build number. See discussion in go/samsung-camera-distortion.
+ */
+ private static boolean isSamsungDistortion() {
+ return "samsung".equalsIgnoreCase(Build.BRAND)
+ && SAMSUNG_DISTORTION_MODELS.contains(Build.MODEL.toUpperCase(Locale.US));
+ }
+
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/MaxPreviewSize.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/MaxPreviewSize.java
new file mode 100644
index 0000000..6c4da09
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/MaxPreviewSize.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2021 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 androidx.camera.camera2.internal.compat.workaround;
+
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.camera2.internal.compat.quirk.SelectResolutionQuirk;
+import androidx.camera.core.impl.SurfaceConfig;
+
+/**
+ * Helper class that overrides the maximum preview size used in surface combination check.
+ *
+ * @see androidx.camera.camera2.internal.SupportedSurfaceCombination
+ */
+@RequiresApi(21)
+public class MaxPreviewSize {
+
+ @Nullable
+ private final SelectResolutionQuirk mSelectResolutionQuirk;
+
+ /**
+ * Constructs new {@link MaxPreviewSize}.
+ */
+ public MaxPreviewSize() {
+ this(DeviceQuirks.get(SelectResolutionQuirk.class));
+ }
+
+ /**
+ * Constructs new {@link MaxPreviewSize}.
+ */
+ @VisibleForTesting
+ MaxPreviewSize(@Nullable SelectResolutionQuirk selectResolutionQuirk) {
+ mSelectResolutionQuirk = selectResolutionQuirk;
+ }
+
+ /**
+ * Gets the max preview resolution based on the default preview max resolution.
+ *
+ * <p> If select resolution is larger than the default resolution, return the select
+ * resolution. The select resolution has been manually tested on the device. Otherwise,
+ * return the default max resolution.
+ */
+ @NonNull
+ public Size getMaxPreviewResolution(@NonNull Size defaultMaxPreviewResolution) {
+ if (mSelectResolutionQuirk == null) {
+ return defaultMaxPreviewResolution;
+ }
+ Size selectResolution = mSelectResolutionQuirk.selectResolution(
+ SurfaceConfig.ConfigType.PRIV);
+ if (selectResolution == null) {
+ return defaultMaxPreviewResolution;
+ }
+ boolean isSelectResolutionLarger =
+ selectResolution.getWidth() * selectResolution.getHeight()
+ > defaultMaxPreviewResolution.getWidth()
+ * defaultMaxPreviewResolution.getHeight();
+ return isSelectResolutionLarger ? selectResolution : defaultMaxPreviewResolution;
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/ResolutionSelector.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/ResolutionSelector.java
new file mode 100644
index 0000000..c463458
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/ResolutionSelector.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2021 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 androidx.camera.camera2.internal.compat.workaround;
+
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.camera2.internal.compat.quirk.SelectResolutionQuirk;
+import androidx.camera.core.impl.SurfaceConfig;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class that overrides user configured resolution with resolution selected based on device
+ * quirks.
+ */
+@RequiresApi(21)
+public class ResolutionSelector {
+
+ @Nullable
+ private final SelectResolutionQuirk mSelectResolutionQuirk;
+
+ /**
+ * Constructs new {@link ResolutionSelector}.
+ */
+ public ResolutionSelector() {
+ this(DeviceQuirks.get(SelectResolutionQuirk.class));
+ }
+
+ /**
+ * Constructs new {@link ResolutionSelector}.
+ */
+ @VisibleForTesting
+ ResolutionSelector(@Nullable SelectResolutionQuirk selectResolutionQuirk) {
+ mSelectResolutionQuirk = selectResolutionQuirk;
+ }
+
+ /**
+ * Returns a new list of resolution with the selected resolution inserted or prioritized.
+ *
+ * <p> If the list contains the selected resolution, move it to be the first element; if it
+ * does not contain the selected resolution, insert it as the first element; if there is no
+ * device quirk, return the original list.
+ *
+ * @param configType the config type based on which the supported resolution is
+ * calculated.
+ * @param supportedResolutions a ordered list of resolutions calculated by CameraX.
+ */
+ @NonNull
+ public List<Size> insertOrPrioritize(
+ @NonNull SurfaceConfig.ConfigType configType,
+ @NonNull List<Size> supportedResolutions) {
+ if (mSelectResolutionQuirk == null) {
+ return supportedResolutions;
+ }
+ Size selectResolution = mSelectResolutionQuirk.selectResolution(configType);
+ if (selectResolution == null) {
+ return supportedResolutions;
+ }
+ List<Size> newResolutions = new ArrayList<>();
+ newResolutions.add(selectResolution);
+ for (Size size : supportedResolutions) {
+ if (!size.equals(selectResolution)) {
+ newResolutions.add(size);
+ }
+ }
+ return newResolutions;
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatio.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatio.java
index ca81eca..667c0f1 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatio.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatio.java
@@ -25,8 +25,6 @@
import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
import androidx.camera.camera2.internal.compat.quirk.Nexus4AndroidLTargetAspectRatioQuirk;
-import androidx.camera.camera2.internal.compat.quirk.SamsungPreviewTargetAspectRatioQuirk;
-import androidx.camera.core.impl.ImageOutputConfig;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -34,7 +32,6 @@
/**
* Workaround to get corrected target aspect ratio.
*
- * @see SamsungPreviewTargetAspectRatioQuirk
* @see Nexus4AndroidLTargetAspectRatioQuirk
* @see AspectRatioLegacyApi21Quirk
*/
@@ -53,13 +50,8 @@
* Gets corrected target aspect ratio based on device and camera quirks.
*/
@TargetAspectRatio.Ratio
- public int get(@NonNull ImageOutputConfig imageOutputConfig, @NonNull String cameraId,
+ public int get(@NonNull String cameraId,
@NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat) {
- final SamsungPreviewTargetAspectRatioQuirk samsungQuirk =
- DeviceQuirks.get(SamsungPreviewTargetAspectRatioQuirk.class);
- if (samsungQuirk != null && samsungQuirk.require16_9(imageOutputConfig)) {
- return TargetAspectRatio.RATIO_16_9;
- }
final Nexus4AndroidLTargetAspectRatioQuirk nexus4AndroidLQuirk =
DeviceQuirks.get(Nexus4AndroidLTargetAspectRatioQuirk.class);
if (nexus4AndroidLQuirk != null) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraControl.java
index 5b6c39c..3d2d0e9 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraControl.java
@@ -215,15 +215,15 @@
}
/**
- * Clears all capture request options.
+ * Clears all capture request options that is currently applied by the
+ * {@link Camera2CameraControl}.
*
* @return a {@link ListenableFuture} which completes when the repeating
* {@link android.hardware.camera2.CaptureResult} shows the options have be submitted
* completely. The future fails with {@link CameraControl.OperationCanceledException} if newer
* options are set or camera is closed before the current request completes.
- * @hide
*/
- @RestrictTo(Scope.LIBRARY)
+ @SuppressWarnings("AsyncSuffixFuture")
@NonNull
public ListenableFuture<Void> clearCaptureRequestOptions() {
clearCaptureRequestOptionsInternal();
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/ResolutionSelectorQuirkTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/ResolutionSelectorQuirkTest.java
new file mode 100644
index 0000000..23a748d
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/ResolutionSelectorQuirkTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 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 androidx.camera.camera2.internal.compat.quirk;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.util.ReflectionHelpers;
+
+/**
+ * Unit tests for {@link SelectResolutionQuirk}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class ResolutionSelectorQuirkTest {
+
+
+ @Test
+ public void samsungDistortionHasQuirks() {
+ ReflectionHelpers.setStaticField(Build.class, "BRAND", "samsung");
+ // Default is false
+ assertThat(SelectResolutionQuirk.load()).isFalse();
+
+ // Test all samsung models
+ ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-T580");
+ assertThat(SelectResolutionQuirk.load()).isTrue();
+ ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-J710MN");
+ assertThat(SelectResolutionQuirk.load()).isTrue();
+ ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-A320FL");
+ assertThat(SelectResolutionQuirk.load()).isTrue();
+ ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-G570M");
+ assertThat(SelectResolutionQuirk.load()).isTrue();
+ }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/MaxPreviewSizeTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/MaxPreviewSizeTest.kt
new file mode 100644
index 0000000..2886415d
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/MaxPreviewSizeTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2021 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 androidx.camera.camera2.internal.compat.workaround
+
+import android.os.Build
+import android.util.Size
+import androidx.camera.camera2.internal.compat.quirk.SelectResolutionQuirk
+import androidx.camera.core.impl.SurfaceConfig
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+private val SELECT_RESOLUTION_PRIV = Size(1001, 1000)
+private val SELECT_RESOLUTION_YUV = Size(1002, 1000)
+private val SELECT_RESOLUTION_JPEG = Size(1003, 1000)
+
+/**
+ * Unit tests for [MaxPreviewSize].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class MaxPreviewSizeTest {
+
+ private val mMaxPreviewSize = MaxPreviewSize(object : SelectResolutionQuirk() {
+ override fun selectResolution(configType: SurfaceConfig.ConfigType): Size? {
+ return when (configType) {
+ SurfaceConfig.ConfigType.YUV -> SELECT_RESOLUTION_YUV
+ SurfaceConfig.ConfigType.PRIV -> SELECT_RESOLUTION_PRIV
+ SurfaceConfig.ConfigType.JPEG -> SELECT_RESOLUTION_JPEG
+ else -> null
+ }
+ }
+ })
+
+ @Test
+ fun largerThanDefaultPreviewResolution_returnsPrivResolution() {
+ val smallSize = Size(101, 100)
+ assertThat(mMaxPreviewSize.getMaxPreviewResolution(smallSize)).isEqualTo(
+ SELECT_RESOLUTION_PRIV
+ )
+ }
+
+ @Test
+ fun smallerThanDefaultResolution_returnsDefaultResolution() {
+ val largeDefaultSize = Size(10001, 10000)
+ assertThat(mMaxPreviewSize.getMaxPreviewResolution(largeDefaultSize)).isEqualTo(
+ largeDefaultSize
+ )
+ }
+
+ @Test
+ fun noQuirk_returnsOriginalMaxPreviewResolution() {
+ noQuirk_returnsOriginalMaxPreviewResolution(null)
+ }
+
+ @Test
+ fun noResolutionQuirk_returnsOriginalMaxPreviewResolution() {
+ noQuirk_returnsOriginalMaxPreviewResolution(
+ Mockito.mock(
+ SelectResolutionQuirk::class.java
+ )
+ )
+ }
+
+ private fun noQuirk_returnsOriginalMaxPreviewResolution(
+ quirk: SelectResolutionQuirk?
+ ) {
+ val maxPreviewSize = MaxPreviewSize(quirk)
+ val result =
+ maxPreviewSize.getMaxPreviewResolution(SELECT_RESOLUTION_JPEG)
+ assertThat(result).isEqualTo(SELECT_RESOLUTION_JPEG)
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/ResolutionSelectorTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/ResolutionSelectorTest.kt
new file mode 100644
index 0000000..f95f2c3
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/ResolutionSelectorTest.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2021 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 androidx.camera.camera2.internal.compat.workaround
+
+import android.os.Build
+import android.util.Size
+import androidx.camera.camera2.internal.compat.quirk.SelectResolutionQuirk
+import androidx.camera.core.impl.SurfaceConfig
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+private val RESOLUTION_1 = Size(101, 100)
+private val RESOLUTION_2 = Size(102, 100)
+
+private val SELECT_RESOLUTION_PRIV = Size(1001, 1000)
+private val SELECT_RESOLUTION_YUV = Size(1002, 1000)
+private val SELECT_RESOLUTION_JPEG = Size(1003, 1000)
+
+private val SUPPORTED_RESOLUTIONS = listOf(RESOLUTION_1, RESOLUTION_2)
+
+/**
+ * Unit test for [ResolutionSelector].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class ResolutionSelectorTest {
+
+ private val mResolutionSelector = ResolutionSelector(object : SelectResolutionQuirk() {
+ override fun selectResolution(configType: SurfaceConfig.ConfigType): Size? {
+ return when (configType) {
+ SurfaceConfig.ConfigType.YUV -> SELECT_RESOLUTION_YUV
+ SurfaceConfig.ConfigType.PRIV -> SELECT_RESOLUTION_PRIV
+ SurfaceConfig.ConfigType.JPEG -> SELECT_RESOLUTION_JPEG
+ else -> null
+ }
+ }
+ })
+
+ @Test
+ fun hasPrivResolution_prioritized() {
+ hasResolution_prioritized(SurfaceConfig.ConfigType.PRIV, SELECT_RESOLUTION_PRIV)
+ }
+
+ @Test
+ fun hasYuvResolution_prioritized() {
+ hasResolution_prioritized(SurfaceConfig.ConfigType.YUV, SELECT_RESOLUTION_YUV)
+ }
+
+ @Test
+ fun hasJpegResolution_prioritized() {
+ hasResolution_prioritized(SurfaceConfig.ConfigType.JPEG, SELECT_RESOLUTION_JPEG)
+ }
+
+ private fun hasResolution_prioritized(
+ configType: SurfaceConfig.ConfigType,
+ resolution: Size
+ ) {
+ val resolutions: MutableList<Size> = ArrayList<Size>(SUPPORTED_RESOLUTIONS)
+ resolutions.add(resolution)
+ Truth.assertThat(mResolutionSelector.insertOrPrioritize(configType, resolutions))
+ .containsExactly(resolution, RESOLUTION_1, RESOLUTION_2).inOrder()
+ }
+
+ @Test
+ fun noPrivResolution_inserted() {
+ noResolution_inserted(SurfaceConfig.ConfigType.PRIV, SELECT_RESOLUTION_PRIV)
+ }
+
+ @Test
+ fun noYuvResolution_inserted() {
+ noResolution_inserted(SurfaceConfig.ConfigType.YUV, SELECT_RESOLUTION_YUV)
+ }
+
+ @Test
+ fun noJpegResolution_inserted() {
+ noResolution_inserted(SurfaceConfig.ConfigType.JPEG, SELECT_RESOLUTION_JPEG)
+ }
+
+ private fun noResolution_inserted(
+ configType: SurfaceConfig.ConfigType,
+ resolution: Size
+ ) {
+ Truth.assertThat(mResolutionSelector.insertOrPrioritize(configType, SUPPORTED_RESOLUTIONS))
+ .containsExactly(resolution, RESOLUTION_1, RESOLUTION_2).inOrder()
+ }
+
+ @Test
+ fun noQuirk_returnsOriginalSupportedResolutions() {
+ noQuirk_returnsOriginalSupportedResolutions(null)
+ }
+
+ @Test
+ fun noResolution_returnsOriginalSupportedResolutions() {
+ noQuirk_returnsOriginalSupportedResolutions(getEmptyQuirk())
+ }
+
+ private fun noQuirk_returnsOriginalSupportedResolutions(
+ quirk: SelectResolutionQuirk?
+ ) {
+ val resolutionSelector = ResolutionSelector(quirk)
+ val result = resolutionSelector.insertOrPrioritize(
+ SurfaceConfig.ConfigType.PRIV,
+ SUPPORTED_RESOLUTIONS
+ )
+ Truth.assertThat(result).containsExactlyElementsIn(SUPPORTED_RESOLUTIONS)
+ }
+
+ private fun getEmptyQuirk(): SelectResolutionQuirk? {
+ return Mockito.mock(SelectResolutionQuirk::class.java)
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatioTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatioTest.java
index 8049c3b..b5a3aac4 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatioTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatioTest.java
@@ -38,7 +38,6 @@
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.Preview;
import androidx.camera.core.UseCase;
-import androidx.camera.core.impl.ImageOutputConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -66,24 +65,6 @@
@ParameterizedRobolectricTestRunner.Parameters
public static Collection<Object[]> data() {
final List<Object[]> data = new ArrayList<>();
- data.add(new Object[]{new Config("Samsung", "SM-J710MN", true,
- INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_4_3, RATIO_16_9, ALL_API_LEVELS)});
- data.add(new Object[]{new Config("Samsung", "SM-J710MN", true,
- INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_16_9, RATIO_16_9, ALL_API_LEVELS)});
- data.add(new Object[]{new Config("Samsung", "SM-J710MN", false,
- INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_4_3, RATIO_ORIGINAL, ALL_API_LEVELS)});
- data.add(new Object[]{new Config("Samsung", "SM-J710MN", false,
- INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_16_9, RATIO_ORIGINAL,
- ALL_API_LEVELS)});
- data.add(new Object[]{new Config("Samsung", "SM-T580", true,
- INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_4_3, RATIO_16_9, ALL_API_LEVELS)});
- data.add(new Object[]{new Config("Samsung", "SM-T580", true,
- INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_16_9, RATIO_16_9, ALL_API_LEVELS)});
- data.add(new Object[]{new Config("Samsung", "SM-T580", false,
- INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_4_3, RATIO_ORIGINAL, ALL_API_LEVELS)});
- data.add(new Object[]{new Config("Samsung", "SM-T580", false,
- INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_16_9, RATIO_ORIGINAL,
- ALL_API_LEVELS)});
data.add(new Object[]{new Config("Google", "Nexus 4", true,
INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY, RATIO_4_3, RATIO_MAX_JPEG,
new Range<>(21, 22))});
@@ -146,9 +127,7 @@
.setTargetAspectRatio(mConfig.mInputAspectRatio)
.build();
}
- final ImageOutputConfig imageOutputConfig = (ImageOutputConfig) usecase.getCurrentConfig();
-
- final int aspectRatio = new TargetAspectRatio().get(imageOutputConfig,
+ final int aspectRatio = new TargetAspectRatio().get(
BACK_CAMERA_ID, getCharacteristicsCompat(mConfig.mHardwareLevel));
assertThat(aspectRatio).isEqualTo(getExpectedAspectRatio());
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 8f2ccf3..b82e649 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -883,7 +883,9 @@
* output stream under 1080p.
*
* <p>If not set, the default selected resolution will be the best size match to the
- * device's screen resolution, or to 1080p (1920x1080), whichever is smaller.
+ * device's screen resolution, or to 1080p (1920x1080), whichever is smaller. Note that
+ * due to compatibility reasons, CameraX may select a resolution that is larger than the
+ * default screen resolution on certain devices.
*
* <p>When using the <code>camera-camera2</code> CameraX implementation, which resolution
* will be finally selected will depend on the camera device's hardware level and the
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/LoggerTest.java b/camera/camera-core/src/test/java/androidx/camera/core/LoggerTest.java
index c799dad..a5ab258 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/LoggerTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/LoggerTest.java
@@ -25,7 +25,6 @@
import androidx.annotation.Nullable;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.ParameterizedRobolectricTestRunner;
@@ -136,7 +135,6 @@
.hasNoMoreMessages();
}
- @Ignore("b/208252595")
@Config(maxSdk = 23)
@Test
public void log_truncateLongTag() {
@@ -232,15 +230,51 @@
LogAssert hasMessage(int priority, String tag, String message,
@Nullable Throwable throwable) {
final LogItem item = mLogItems.get(mIndex++);
- assertThat(item.type).isEqualTo(priority);
- assertThat(item.tag).isEqualTo(tag);
- assertThat(item.msg).isEqualTo(message);
- assertThat(item.throwable).isEqualTo(throwable);
+
+ try {
+ assertThat(item.type).isEqualTo(priority);
+ assertThat(item.tag).isEqualTo(tag);
+ assertThat(item.msg).isEqualTo(message);
+ assertThat(item.throwable).isEqualTo(throwable);
+ } catch (Throwable e) {
+ // TODO(b/208252595): Dump the log info for the issue clarification.
+ throw new RuntimeException(collectLogItemsInfo() + "\n" + e);
+ }
return this;
}
void hasNoMoreMessages() {
- assertThat(mIndex).isEqualTo(mLogItems.size());
+ try {
+ assertThat(mIndex).isEqualTo(mLogItems.size());
+ } catch (Throwable e) {
+ // TODO(b/207674161): Dump the log info for the issue clarification.
+ throw new RuntimeException(collectLogItemsInfo() + "\n" + e);
+ }
+ }
+
+ private String collectLogItemsInfo() {
+ int counter = 0;
+ String logItemsInfo =
+ "Log items count is " + mLogItems.size() + ", mIndex is " + mIndex + "\n";
+
+ for (int i = mIndex; i >= 0; i--) {
+ if (i >= mLogItems.size()) {
+ continue;
+ }
+
+ LogItem item = mLogItems.get(i);
+ logItemsInfo +=
+ "index: " + i + ", item.type: " + item.type + ", item.tag: " + item.tag
+ + ", item.msg: " + item.msg + ", item.throwable: "
+ + item.throwable + "\n";
+
+ // Prints five items at most in case too many items exist to cause problem.
+ if (counter++ > 5) {
+ break;
+ }
+ }
+
+ return logItemsInfo;
}
}
}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
index e587fd9..9200dae7 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
@@ -52,7 +52,6 @@
import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.ViewPort;
import androidx.camera.view.internal.compat.quirk.DeviceQuirks;
-import androidx.camera.view.internal.compat.quirk.PreviewOneThirdWiderQuirk;
import androidx.camera.view.internal.compat.quirk.TextureViewRotationQuirk;
import androidx.core.util.Preconditions;
@@ -103,12 +102,9 @@
// SurfaceRequest.getResolution().
private Size mResolution;
- // This represents the area of the Surface that should be visible to end users. The value
- // is based on TransformationInfo.getCropRect() with possible corrections due to device quirks.
+ // This represents the area of the Surface that should be visible to end users. The area is
+ // defined by the Viewport class.
private Rect mSurfaceCropRect;
- // This rect represents the size of the viewport in preview. It's always the same as
- // TransformationInfo.getCropRect().
- private Rect mViewportRect;
// TransformationInfo.getRotationDegrees().
private int mPreviewRotationDegrees;
// TransformationInfo.getTargetRotation.
@@ -132,8 +128,7 @@
Size resolution, boolean isFrontCamera) {
Logger.d(TAG, "Transformation info set: " + transformationInfo + " " + resolution + " "
+ isFrontCamera);
- mSurfaceCropRect = getCorrectedCropRect(transformationInfo.getCropRect());
- mViewportRect = transformationInfo.getCropRect();
+ mSurfaceCropRect = transformationInfo.getCropRect();
mPreviewRotationDegrees = transformationInfo.getRotationDegrees();
mTargetRotation = transformationInfo.getTargetRotation();
mResolution = resolution;
@@ -279,28 +274,6 @@
}
/**
- * Gets the vertices of the crop rect in Surface.
- */
- private Rect getCorrectedCropRect(Rect surfaceCropRect) {
- PreviewOneThirdWiderQuirk quirk = DeviceQuirks.get(PreviewOneThirdWiderQuirk.class);
- if (quirk != null) {
- // Correct crop rect if the device has a quirk.
- RectF cropRectF = new RectF(surfaceCropRect);
- Matrix correction = new Matrix();
- correction.setScale(
- quirk.getCropRectScaleX(),
- 1f,
- surfaceCropRect.centerX(),
- surfaceCropRect.centerY());
- correction.mapRect(cropRectF);
- Rect correctRect = new Rect();
- cropRectF.round(correctRect);
- return correctRect;
- }
- return surfaceCropRect;
- }
-
- /**
* Gets the viewport rect in {@link PreviewView} coordinates for the case where viewport's
* aspect ratio doesn't match {@link PreviewView}'s aspect ratio.
*
@@ -380,9 +353,9 @@
*/
private Size getRotatedViewportSize() {
if (is90or270(mPreviewRotationDegrees)) {
- return new Size(mViewportRect.height(), mViewportRect.width());
+ return new Size(mSurfaceCropRect.height(), mSurfaceCropRect.width());
}
- return new Size(mViewportRect.width(), mViewportRect.height());
+ return new Size(mSurfaceCropRect.width(), mSurfaceCropRect.height());
}
/**
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
index f4cfba3..61b8959 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
@@ -40,11 +40,6 @@
static List<Quirk> loadQuirks() {
final List<Quirk> quirks = new ArrayList<>();
- // Load all device specific quirks
- if (PreviewOneThirdWiderQuirk.load()) {
- quirks.add(new PreviewOneThirdWiderQuirk());
- }
-
if (SurfaceViewStretchedQuirk.load()) {
quirks.add(new SurfaceViewStretchedQuirk());
}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java
deleted file mode 100644
index fb8efa1d..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2021 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 androidx.camera.view.internal.compat.quirk;
-
-import android.os.Build;
-
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.Quirk;
-
-/**
- * A quirk where the preview buffer is stretched.
- *
- * <p> The symptom is, the preview's FOV is always 1/3 wider than intended. For example, if the
- * preview Surface is 800x600, it's actually has a FOV of 1066x600 with the same center point,
- * but squeezed to fit the 800x600 buffer.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public class PreviewOneThirdWiderQuirk implements Quirk {
-
- private static final String SAMSUNG_A3_2017 = "A3Y17LTE"; // b/180121821
- private static final String SAMSUNG_J5_PRIME = "ON5XELTE"; // b/183329599
-
- static boolean load() {
- boolean isSamsungJ5PrimeAndApi26 =
- SAMSUNG_J5_PRIME.equals(Build.DEVICE.toUpperCase()) && Build.VERSION.SDK_INT >= 26;
- boolean isSamsungA3 = SAMSUNG_A3_2017.equals(Build.DEVICE.toUpperCase());
- return isSamsungJ5PrimeAndApi26 || isSamsungA3;
- }
-
- /**
- * The mount that the crop rect needs to be scaled in x.
- */
- public float getCropRectScaleX() {
- return 0.75f;
- }
-}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
index bc3c951..b225d17 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
@@ -25,8 +25,6 @@
import androidx.camera.core.SurfaceRequest
import androidx.camera.core.impl.ImageOutputConfig.RotationValue
import androidx.camera.view.TransformUtils.sizeToVertices
-import androidx.camera.view.internal.compat.quirk.PreviewOneThirdWiderQuirk
-import androidx.camera.view.internal.compat.quirk.QuirkInjector
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -84,23 +82,6 @@
}
@Test
- fun withPreviewStretchedQuirk_cropRectIsAdjusted() {
- // Arrange.
- QuirkInjector.inject(PreviewOneThirdWiderQuirk())
-
- // Act.
- mPreviewTransform.setTransformationInfo(
- SurfaceRequest.TransformationInfo.of(FULL_CROP_RECT, 0, 0),
- Size(FULL_CROP_RECT.width(), FULL_CROP_RECT.height()),
- /*isFrontCamera=*/false
- )
-
- // Assert: the crop rect is corrected.
- assertThat(mPreviewTransform.surfaceCropRect).isEqualTo(Rect(8, 0, 53, 40))
- QuirkInjector.clear()
- }
-
- @Test
fun cropRectWidthOffByOnePixel_match() {
assertThat(
isCropRectAspectRatioMatchPreviewView(
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java
deleted file mode 100644
index 17c02f5..0000000
--- a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 2021 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 androidx.camera.view.internal.compat.quirk;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.os.Build;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-import org.robolectric.annotation.internal.DoNotInstrument;
-import org.robolectric.util.ReflectionHelpers;
-
-/**
- * Unit tests for {@link PreviewOneThirdWiderQuirk}.
- */
-@RunWith(RobolectricTestRunner.class)
-@DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-public class PreviewOneThirdWiderQuirkTest {
-
- @Test
- public void quirkExistsOnSamsungA3() {
- ReflectionHelpers.setStaticField(Build.class, "DEVICE", "A3Y17LTE");
- assertPreviewShouldBeCroppedBy25Percent();
- }
-
- @Test
- @Config(minSdk = Build.VERSION_CODES.O)
- public void quirkExistsOnSamsungJ5PrimeApi26AndAbove() {
- ReflectionHelpers.setStaticField(Build.class, "DEVICE", "ON5XELTE");
- assertPreviewShouldBeCroppedBy25Percent();
- }
-
- @Test
- @Config(maxSdk = Build.VERSION_CODES.N_MR1)
- public void quirkDoesNotExistOnSamsungJ5PrimeApi25AndBelow() {
- ReflectionHelpers.setStaticField(Build.class, "DEVICE", "ON5XELTE");
- assertThat(DeviceQuirks.get(PreviewOneThirdWiderQuirk.class)).isNull();
- }
-
- private void assertPreviewShouldBeCroppedBy25Percent() {
- final PreviewOneThirdWiderQuirk quirk = DeviceQuirks.get(PreviewOneThirdWiderQuirk.class);
- assertThat(quirk).isNotNull();
- assertThat(quirk.getCropRectScaleX()).isEqualTo(0.75F);
- }
-}
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
index 5db919d..6192e78 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
@@ -26,11 +26,16 @@
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ResolveInfo;
+import android.graphics.Bitmap;
import android.graphics.Insets;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.util.Log;
+import android.view.PixelCopy;
import android.view.View;
import android.view.WindowInsets;
+import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -111,6 +116,16 @@
LoadingView mLoadingView;
View mActivityContainerView;
View mLocalContentContainerView;
+
+ /** Displays the snapshot of the surface view to avoid a visual glitch when app comes
+ * to foreground. This view sits behind the surface view and will be visible only when surface
+ * is hidden (or not created yet).
+ */
+ ImageView mSurfaceSnapshotView;
+
+ // The handler used to take surface view snapshot.
+ private Handler mSnapshotHandler = new Handler(Looper.myLooper());
+
@Nullable SurfaceHolderListener mSurfaceHolderListener;
@Nullable ActivityLifecycleDelegate mActivityLifecycleDelegate;
@Nullable CarAppViewModel mViewModel;
@@ -250,6 +265,7 @@
mSurfaceView = requireViewById(R.id.template_view_surface);
mErrorMessageView = requireViewById(R.id.error_message_view);
mLoadingView = requireViewById(R.id.loading_view);
+ mSurfaceSnapshotView = requireViewById(R.id.template_view_snapshot);
mActivityContainerView.setOnApplyWindowInsetsListener(mWindowInsetsListener);
// IMPORTANT: The SystemUiVisibility applied here must match the insets provided to the
@@ -291,6 +307,30 @@
mViewModel.bind(getIntent(), mCarActivity, getDisplayId());
}
+ /** Takes a snapshot of the surface view and puts it in the surfaceSnapshotView if succeeded. */
+ private void takeSurfaceSnapshot() {
+ // Nothing to do if the surface is not ready yet.
+ if (mSurfaceView.getHolder().getSurface() == null) {
+ return;
+ }
+ Bitmap bitmap = Bitmap.createBitmap(mSurfaceView.getWidth(), mSurfaceView.getHeight(),
+ Bitmap.Config.ARGB_8888);
+ PixelCopy.request(mSurfaceView, bitmap, status -> {
+ if (status == PixelCopy.SUCCESS) {
+ mSurfaceSnapshotView.setImageBitmap(bitmap);
+ } else {
+ Log.w(LogTags.TAG, "Failed to take snapshot of the surface view");
+ mSurfaceSnapshotView.setImageBitmap(null);
+ }
+ }, mSnapshotHandler);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ takeSurfaceSnapshot();
+ }
+
// TODO(b/189862860): Address SOFT_INPUT_ADJUST_RESIZE deprecation
@SuppressWarnings("deprecation")
private void setSoftInputHandling() {
@@ -313,28 +353,33 @@
private void onStateChanged(@NonNull CarAppViewModel.State state) {
ThreadUtils.runOnMain(() -> {
requireNonNull(mSurfaceView);
+ requireNonNull(mSurfaceSnapshotView);
requireNonNull(mSurfaceHolderListener);
switch (state) {
case IDLE:
mSurfaceView.setVisibility(View.GONE);
+ mSurfaceSnapshotView.setVisibility(View.VISIBLE);
mSurfaceHolderListener.setSurfaceListener(null);
mErrorMessageView.setVisibility(View.GONE);
mLoadingView.setVisibility(View.GONE);
break;
case ERROR:
mSurfaceView.setVisibility(View.GONE);
+ mSurfaceSnapshotView.setVisibility(View.GONE);
mSurfaceHolderListener.setSurfaceListener(null);
mErrorMessageView.setVisibility(View.VISIBLE);
mLoadingView.setVisibility(View.GONE);
break;
case CONNECTING:
mSurfaceView.setVisibility(View.GONE);
+ mSurfaceSnapshotView.setVisibility(View.VISIBLE);
mErrorMessageView.setVisibility(View.GONE);
mLoadingView.setVisibility(View.VISIBLE);
break;
case CONNECTED:
mSurfaceView.setVisibility(View.VISIBLE);
+ mSurfaceSnapshotView.setVisibility(View.VISIBLE);
mErrorMessageView.setVisibility(View.GONE);
mLoadingView.setVisibility(View.GONE);
break;
diff --git a/car/app/app-automotive/src/main/res/layout/activity_template.xml b/car/app/app-automotive/src/main/res/layout/activity_template.xml
index 5ee02db..221dcd3 100644
--- a/car/app/app-automotive/src/main/res/layout/activity_template.xml
+++ b/car/app/app-automotive/src/main/res/layout/activity_template.xml
@@ -5,6 +5,13 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
+ <!-- Used to display the snapshot of the surface view to avoid a visual glitch when app comes
+ to foreground. -->
+ <ImageView
+ android:id="@+id/template_view_snapshot"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
<androidx.car.app.activity.renderer.surface.TemplateSurfaceView
android:id="@+id/template_view_surface"
android:layout_width="match_parent"
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/RoutePreviewDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/RoutePreviewDemoScreen.java
index cc12d54..84cf7ad 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/RoutePreviewDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/RoutePreviewDemoScreen.java
@@ -24,6 +24,7 @@
import androidx.car.app.CarContext;
import androidx.car.app.CarToast;
import androidx.car.app.Screen;
+import androidx.car.app.constraints.ConstraintManager;
import androidx.car.app.model.Action;
import androidx.car.app.model.CarText;
import androidx.car.app.model.DurationSpan;
@@ -32,6 +33,7 @@
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.RoutePreviewNavigationTemplate;
import androidx.car.app.sample.showcase.common.navigation.routing.RoutingDemoModels;
+import androidx.car.app.versioning.CarAppApiLevels;
import java.util.concurrent.TimeUnit;
@@ -41,23 +43,65 @@
super(carContext);
}
+ private CarText createRouteText(int index) {
+ switch (index) {
+ case 0:
+ // Set text variants for the first route.
+ SpannableString shortRouteLongText = new SpannableString(
+ " \u00b7 ---------------- Short" + " " + "route "
+ + "-------------------");
+ shortRouteLongText.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(26)), 0, 1,
+ 0);
+ SpannableString firstRouteShortText = new SpannableString(" \u00b7 Short route");
+ firstRouteShortText.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(26)), 0, 1,
+ 0);
+ return new CarText.Builder(shortRouteLongText)
+ .addVariant(firstRouteShortText)
+ .build();
+ case 1:
+ SpannableString lessBusyRouteText = new SpannableString(" \u00b7 Less busy");
+ lessBusyRouteText.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(24)), 0, 1,
+ 0);
+ return new CarText.Builder(lessBusyRouteText).build();
+ case 2:
+ SpannableString hovRouteText = new SpannableString(" \u00b7 HOV friendly");
+ hovRouteText.setSpan(DurationSpan.create(TimeUnit.MINUTES.toSeconds(867)), 0, 1, 0);
+ return new CarText.Builder(hovRouteText).build();
+ default:
+ SpannableString routeText = new SpannableString(" \u00b7 Long route");
+ routeText.setSpan(DurationSpan.create(TimeUnit.MINUTES.toSeconds(867L + index)),
+ 0, 1, 0);
+ return new CarText.Builder(routeText).build();
+ }
+ }
+
+ private Row createRow(int index) {
+ CarText route = createRouteText(index);
+ String titleText = "Via NE " + (index + 4) + "th Street";
+
+ return new Row.Builder()
+ .setTitle(route)
+ .addText(titleText)
+ .build();
+ }
+
@NonNull
@Override
public Template onGetTemplate() {
- // Set text variants for the first route.
- SpannableString firstRouteLongText = new SpannableString(
- " \u00b7 ---------------- Short" + " " + "route " + "-------------------");
- firstRouteLongText.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(26)), 0, 1, 0);
- SpannableString firstRouteShortText = new SpannableString(" \u00b7 Short Route");
- firstRouteShortText.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(26)), 0, 1, 0);
- CarText firstRoute = new CarText.Builder(firstRouteLongText)
- .addVariant(firstRouteShortText)
- .build();
+ int itemLimit = 3;
+ // Adjust the item limit according to the car constrains.
+ if (getCarContext().getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
+ itemLimit = getCarContext().getCarService(ConstraintManager.class).getContentLimit(
+ ConstraintManager.CONTENT_LIMIT_TYPE_ROUTE_LIST);
+ }
- SpannableString secondRoute = new SpannableString(" \u00b7 Less busy");
- secondRoute.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(24)), 0, 1, 0);
- SpannableString thirdRoute = new SpannableString(" \u00b7 HOV friendly");
- thirdRoute.setSpan(DurationSpan.create(TimeUnit.MINUTES.toSeconds(867)), 0, 1, 0);
+ ItemList.Builder itemListBuilder = new ItemList.Builder()
+ .setOnSelectedListener(this::onRouteSelected)
+ .setOnItemsVisibilityChangedListener(this::onRoutesVisible);
+
+ for (int i = 0; i < itemLimit; i++) {
+ itemListBuilder.addItem(createRow(i));
+ }
// Set text variants for the navigate action text.
CarText navigateActionText =
@@ -65,26 +109,7 @@
+ "route").build();
return new RoutePreviewNavigationTemplate.Builder()
- .setItemList(
- new ItemList.Builder()
- .setOnSelectedListener(this::onRouteSelected)
- .addItem(
- new Row.Builder()
- .setTitle(firstRoute)
- .addText("Via NE 8th Street")
- .build())
- .addItem(
- new Row.Builder()
- .setTitle(secondRoute)
- .addText("Via NE 1st Ave")
- .build())
- .addItem(
- new Row.Builder()
- .setTitle(thirdRoute)
- .addText("Via NE 4th Street")
- .build())
- .setOnItemsVisibilityChangedListener(this::onRoutesVisible)
- .build())
+ .setItemList(itemListBuilder.build())
.setNavigateAction(
new Action.Builder()
.setTitle(navigateActionText)
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/GridTemplateDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/GridTemplateDemoScreen.java
index 9767ef1..86fb82a 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/GridTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/GridTemplateDemoScreen.java
@@ -16,7 +16,7 @@
package androidx.car.app.sample.showcase.common.templates;
-import static androidx.car.app.CarToast.LENGTH_LONG;
+import static androidx.car.app.CarToast.LENGTH_SHORT;
import static androidx.car.app.model.Action.BACK;
import android.content.res.Resources;
@@ -85,35 +85,30 @@
triggerFourthItemLoading();
}
- @NonNull
- @Override
- public Template onGetTemplate() {
- ItemList.Builder gridItemListBuilder = new ItemList.Builder();
-
- // Grid item with an icon and a title.
- gridItemListBuilder.addItem(
- new GridItem.Builder()
+ private GridItem createGridItem(int index) {
+ switch (index) {
+ case 0:
+ // Grid item with an icon and a title.
+ return new GridItem.Builder()
.setImage(new CarIcon.Builder(mIcon).build(), GridItem.IMAGE_TYPE_ICON)
.setTitle("Non-actionable")
- .build());
-
- // Grid item with an icon, a title, onClickListener and no text.
- gridItemListBuilder.addItem(
- new GridItem.Builder()
+ .build();
+ case 1:
+ // Grid item with an icon, a title, onClickListener and no text.
+ return new GridItem.Builder()
.setImage(new CarIcon.Builder(mIcon).build(), GridItem.IMAGE_TYPE_ICON)
.setTitle("Second Item")
.setOnClickListener(
- () ->
- CarToast.makeText(
- getCarContext(),
- "Clicked second item",
- LENGTH_LONG)
- .show())
- .build());
-
- // Grid item with an icon marked as icon, a title, a text and a toggle in unchecked state.
- gridItemListBuilder.addItem(
- new GridItem.Builder()
+ () -> CarToast.makeText(
+ getCarContext(),
+ "Clicked second item",
+ LENGTH_SHORT)
+ .show())
+ .build();
+ case 2:
+ // Grid item with an icon marked as icon, a title, a text and a toggle in
+ // unchecked state.
+ return new GridItem.Builder()
.setImage(new CarIcon.Builder(mIcon).build(), GridItem.IMAGE_TYPE_ICON)
.setTitle("Third Item")
.setText(mThirdItemToggleState ? "Checked" : "Unchecked")
@@ -123,35 +118,33 @@
CarToast.makeText(
getCarContext(),
"Third item checked: " + mThirdItemToggleState,
- LENGTH_LONG)
+ LENGTH_SHORT)
.show();
invalidate();
})
- .build());
-
- // Grid item with an image, a title, a long text and a toggle that takes some time to
- // update.
- if (mIsFourthItemLoading) {
- gridItemListBuilder.addItem(
- new GridItem.Builder()
+ .build();
+ case 3:
+ // Grid item with an image, a title, a long text and a toggle that takes some
+ // time to
+ // update.
+ if (mIsFourthItemLoading) {
+ return new GridItem.Builder()
.setTitle("Fourth")
.setText(mFourthItemToggleState ? "On" : "Off")
.setLoading(true)
- .build());
- } else {
- gridItemListBuilder.addItem(
- new GridItem.Builder()
+ .build();
+ } else {
+ return new GridItem.Builder()
.setImage(new CarIcon.Builder(mImage).build())
.setTitle("Fourth")
.setText(mFourthItemToggleState ? "On" : "Off")
- .setOnClickListener(
- this::triggerFourthItemLoading)
- .build());
- }
-
- // Grid item with a large image, a long title, no text and a toggle in unchecked state.
- gridItemListBuilder.addItem(
- new GridItem.Builder()
+ .setOnClickListener(this::triggerFourthItemLoading)
+ .build();
+ }
+ case 4:
+ // Grid item with a large image, a long title, no text and a toggle in unchecked
+ // state.
+ return new GridItem.Builder()
.setImage(new CarIcon.Builder(mImage).build(), GridItem.IMAGE_TYPE_LARGE)
.setTitle("Fifth Item has a long title set")
.setOnClickListener(
@@ -160,52 +153,62 @@
CarToast.makeText(
getCarContext(),
"Fifth item checked: " + mFifthItemToggleState,
- LENGTH_LONG)
+ LENGTH_SHORT)
.show();
invalidate();
})
- .build());
-
- // Grid item with an image marked as an icon, a long title, a long text and onClickListener.
- gridItemListBuilder.addItem(
- new GridItem.Builder()
- .setImage(new CarIcon.Builder(mIcon).build(), GridItem.IMAGE_TYPE_ICON)
- .setTitle("Sixth Item has a long title set")
- .setText("Sixth Item has a long text set")
- .setOnClickListener(
- () ->
- CarToast.makeText(
- getCarContext(),
- "Clicked sixth item",
- LENGTH_LONG)
- .show())
- .build());
-
- // Some hosts may allow more items in the grid than others, so create more.
- if (getCarContext().getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
- int itemLimit =
- Math.min(MAX_GRID_ITEMS,
- getCarContext().getCarService(ConstraintManager.class).getContentLimit(
- ConstraintManager.CONTENT_LIMIT_TYPE_GRID));
-
- for (int i = 7; i <= itemLimit; i++) {
- String titleText = "Item: " + i;
- String toastText = "Clicked item " + i;
-
- gridItemListBuilder.addItem(
+ .build();
+ case 5:
+ // Grid item with an image marked as an icon, a long title, a long text and
+ // onClickListener.
+ return
new GridItem.Builder()
.setImage(new CarIcon.Builder(mIcon).build(),
GridItem.IMAGE_TYPE_ICON)
- .setTitle(titleText)
+ .setTitle("Sixth Item has a long title set")
+ .setText("Sixth Item has a long text set")
.setOnClickListener(
() ->
CarToast.makeText(
getCarContext(),
- toastText,
- LENGTH_LONG)
+ "Clicked sixth item",
+ LENGTH_SHORT)
.show())
- .build());
- }
+ .build();
+ default:
+ String titleText = (index + 1) + "th item";
+ String toastText = "Clicked " + (index + 1) + "th item";
+
+ return new GridItem.Builder()
+ .setImage(new CarIcon.Builder(mIcon).build(),
+ GridItem.IMAGE_TYPE_ICON)
+ .setTitle(titleText)
+ .setOnClickListener(
+ () ->
+ CarToast.makeText(
+ getCarContext(),
+ toastText,
+ LENGTH_SHORT)
+ .show())
+ .build();
+ }
+ }
+
+ @NonNull
+ @Override
+ public Template onGetTemplate() {
+ int itemLimit = 6;
+ // Adjust the item limit according to the car constrains.
+ if (getCarContext().getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
+ itemLimit =
+ Math.min(MAX_GRID_ITEMS,
+ getCarContext().getCarService(ConstraintManager.class).getContentLimit(
+ ConstraintManager.CONTENT_LIMIT_TYPE_GRID));
+ }
+
+ ItemList.Builder gridItemListBuilder = new ItemList.Builder();
+ for (int i = 0; i <= itemLimit; i++) {
+ gridItemListBuilder.addItem(createGridItem(i));
}
return new GridTemplate.Builder()
@@ -222,7 +225,7 @@
CarToast.makeText(
getCarContext(),
"Clicked Settings",
- LENGTH_LONG)
+ LENGTH_SHORT)
.show())
.build())
.build())
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PaneTemplateDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PaneTemplateDemoScreen.java
index e5d831b..8a3358c 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PaneTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PaneTemplateDemoScreen.java
@@ -28,6 +28,7 @@
import androidx.car.app.CarContext;
import androidx.car.app.CarToast;
import androidx.car.app.Screen;
+import androidx.car.app.constraints.ConstraintManager;
import androidx.car.app.model.Action;
import androidx.car.app.model.ActionStrip;
import androidx.car.app.model.CarColor;
@@ -71,27 +72,43 @@
mCommuteIcon = IconCompat.createWithResource(getCarContext(), R.drawable.ic_commute_24px);
}
- @NonNull
- @Override
- public Template onGetTemplate() {
- Pane.Builder paneBuilder = new Pane.Builder();
-
- // Add a row with a large image.
- paneBuilder.addRow(
- new Row.Builder()
- .setTitle("Row with a large image")
+ private Row createRow(int index) {
+ switch (index) {
+ case 0:
+ // Row with a large image.
+ return new Row.Builder()
+ .setTitle("Row with a large image and long text long text long text long "
+ + "text long text")
.addText("Text text text")
.addText("Text text text")
.setImage(new CarIcon.Builder(mRowLargeIcon).build())
- .build());
-
- // Add a non-clickable rows.
- paneBuilder.addRow(
- new Row.Builder()
- .setTitle("Row title")
+ .build();
+ default:
+ return new Row.Builder()
+ .setTitle("Row title " + (index + 1))
.addText("Row text 1")
.addText("Row text 2")
- .build());
+ .build();
+
+ }
+ }
+
+ @NonNull
+ @Override
+ public Template onGetTemplate() {
+ int listLimit = 4;
+
+ // Adjust the item limit according to the car constrains.
+ if (getCarContext().getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
+ listLimit =
+ getCarContext().getCarService(ConstraintManager.class).getContentLimit(
+ ConstraintManager.CONTENT_LIMIT_TYPE_PANE);
+ }
+
+ Pane.Builder paneBuilder = new Pane.Builder();
+ for (int i = 0; i < listLimit; i++) {
+ paneBuilder.addRow(createRow(i));
+ }
// Also set a large image outside of the rows.
paneBuilder.setImage(new CarIcon.Builder(mPaneImage).build());
diff --git a/compose/compiler/compiler-daemon/OWNERS b/compose/compiler/compiler-daemon/OWNERS
new file mode 100644
index 0000000..4d2a2f1
--- /dev/null
+++ b/compose/compiler/compiler-daemon/OWNERS
@@ -0,0 +1,3 @@
+chuckj@google.com
+diegoperez@google.com
+amaurym@google.com
\ No newline at end of file
diff --git a/compose/compiler/compiler-daemon/build.gradle b/compose/compiler/compiler-daemon/build.gradle
new file mode 100644
index 0000000..c0e3ebf
--- /dev/null
+++ b/compose/compiler/compiler-daemon/build.gradle
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2021 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ id("AndroidXPlugin")
+ id("kotlin")
+ id("application")
+ id("com.github.johnrengelman.shadow")
+}
+
+mainClassName = "androidx.compose.compiler.daemon.MainKt"
+
+dependencies {
+ implementation(libs.kotlinCompiler)
+ implementation(libs.kotlinStdlib)
+ implementation(project(":compose:compiler:compiler-hosted"))
+}
+
+shadowJar {
+ // The jar file MUST begin with "kotlin-compiler".
+ // If this name is not present, the Kotlin compiler will try to find kotlinHome and might cause
+ // a version mismatch if the dist version does not match the one shipped here.
+ // When the artifact starts with "kotlin-compiler", the compiler will not do that search.
+ archiveBaseName = 'kotlin-compiler-daemon'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs = [
+ // Disable the warning "Some JAR files in the classpath have the [...]".
+ // We include the runtime intentionally as part of the jar.
+ "-Xskip-runtime-version-check",
+ ]
+ }
+}
+
+androidx {
+ name = "Compose Compiler Daemon"
+ type = LibraryType.COMPILER_DAEMON
+ mavenGroup = LibraryGroups.Compose.COMPILER
+ inceptionYear = "2021"
+ description = "Compiler Daemon that includes the Compose plugin"
+}
diff --git a/compose/compiler/compiler-daemon/integration-tests/build.gradle b/compose/compiler/compiler-daemon/integration-tests/build.gradle
new file mode 100644
index 0000000..09ac009
--- /dev/null
+++ b/compose/compiler/compiler-daemon/integration-tests/build.gradle
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 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.
+ */
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("kotlin")
+}
+
+dependencies {
+ testImplementation(libs.junit)
+
+ testImplementation(libs.kotlinCompiler)
+ testImplementation(libs.kotlinStdlib)
+ testImplementation(project(":compose:compiler:compiler-daemon"))
+}
+
+androidx {
+ name = "AndroidX Compiler Daemon CLI Tests"
+ publish = Publish.NONE
+ inceptionYear = "2021"
+ description = "Contains test for the compose compiler daemon"
+}
diff --git a/compose/compiler/compiler-daemon/integration-tests/src/test/kotlin/androidx/compose/compiler/daemon/CompilerTest.kt b/compose/compiler/compiler-daemon/integration-tests/src/test/kotlin/androidx/compose/compiler/daemon/CompilerTest.kt
new file mode 100644
index 0000000..15fa946
--- /dev/null
+++ b/compose/compiler/compiler-daemon/integration-tests/src/test/kotlin/androidx/compose/compiler/daemon/CompilerTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2021 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 androidx.compose.compiler.daemon
+
+import org.jetbrains.kotlin.cli.common.ExitCode
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.io.File
+import java.nio.file.Files
+
+@RunWith(Parameterized::class)
+class CompilerTest(
+ @Suppress("UNUSED_PARAMETER") name: String,
+ private val daemonCompiler: DaemonCompiler
+) {
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data(): Array<Array<Any>> = arrayOf(
+ arrayOf(BasicDaemonCompiler::class.java.name, BasicDaemonCompiler),
+ arrayOf(IncrementalDaemonCompiler::class.java.name, IncrementalDaemonCompiler)
+ )
+ }
+
+ @Test
+ fun `files are compiled successfully`() {
+ val sourceDir = Files.createTempDirectory("source")
+ val inputFile = File(sourceDir.toFile(), "Input.kt").apply {
+ writeText(
+ """
+ fun main() {
+ }
+ """.trimIndent()
+ )
+ }
+ val inputFile2 = File(sourceDir.toFile(), "Input2.kt").apply {
+ writeText(
+ """
+ fun main2() {
+ }
+ """.trimIndent()
+ )
+ }
+
+ run {
+ val outputDir = Files.createTempDirectory("output")
+ assertEquals(
+ ExitCode.OK, daemonCompiler.compile(
+ arrayOf(
+ "-d", outputDir.toAbsolutePath().toString(),
+ inputFile.absolutePath
+ ), DaemonCompilerSettings(null)
+ )
+ )
+ assertTrue(File(outputDir.toFile(), "InputKt.class").exists())
+ }
+
+ // Verify multiple input files
+ run {
+ val outputDir = Files.createTempDirectory("output")
+ assertEquals(
+ ExitCode.OK, daemonCompiler.compile(
+ arrayOf(
+ "-d", outputDir.toAbsolutePath().toString(),
+ inputFile.absolutePath, inputFile2.absolutePath
+ ), DaemonCompilerSettings(null)
+ )
+ )
+ assertTrue(File(outputDir.toFile(), "InputKt.class").exists())
+ assertTrue(File(outputDir.toFile(), "Input2Kt.class").exists())
+ }
+ }
+
+ @Test
+ fun `compilation fails with syntax errors`() {
+ val sourceDir = Files.createTempDirectory("source").toFile()
+ val outputDir = Files.createTempDirectory("output")
+ val inputFile = File(sourceDir, "Input.kt")
+ inputFile.writeText(
+ """
+ Invalid code
+ """.trimIndent()
+ )
+
+ assertEquals(
+ ExitCode.COMPILATION_ERROR, daemonCompiler.compile(
+ arrayOf(
+ "-d", outputDir.toAbsolutePath().toString(),
+ inputFile.absolutePath
+ ), DaemonCompilerSettings(null)
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-daemon/integration-tests/src/test/kotlin/androidx/compose/compiler/daemon/DaemonProtocolTest.kt b/compose/compiler/compiler-daemon/integration-tests/src/test/kotlin/androidx/compose/compiler/daemon/DaemonProtocolTest.kt
new file mode 100644
index 0000000..a97752d
--- /dev/null
+++ b/compose/compiler/compiler-daemon/integration-tests/src/test/kotlin/androidx/compose/compiler/daemon/DaemonProtocolTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2021 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 androidx.compose.compiler.daemon
+
+import org.jetbrains.kotlin.cli.common.ExitCode
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.io.ByteArrayOutputStream
+import java.io.OutputStreamWriter
+import java.io.PipedReader
+import java.io.PipedWriter
+import java.io.PrintWriter
+import java.util.concurrent.CountDownLatch
+import kotlin.concurrent.thread
+
+class DaemonProtocolTest {
+
+ @Test
+ fun `verify daemon compiler commands`() {
+ val compileCalls = mutableListOf<String>()
+ val outputWriter = PipedWriter()
+ val daemonOutput = ByteArrayOutputStream()
+ val countDownLatch = CountDownLatch(2)
+ val pipedReader = PipedReader(outputWriter)
+
+ val output = PrintWriter(outputWriter)
+
+ val testDaemon = object : DaemonCompiler {
+ override fun compile(
+ args: Array<String>,
+ daemonCompilerSettings: DaemonCompilerSettings
+ ): ExitCode {
+ val pluginParameter = daemonCompilerSettings.composePluginPath
+ ?.let { "-plugin $it " } ?: ""
+ val otherArgs = args.joinToString(" ")
+ try {
+ compileCalls.add("kotlinc $pluginParameter$otherArgs")
+ return ExitCode.OK
+ } finally {
+ countDownLatch.countDown()
+ }
+ }
+ }
+
+ thread {
+ startInputLoop(
+ testDaemon,
+ DaemonCompilerSettings("pluginPath1"),
+ pipedReader, OutputStreamWriter(daemonOutput)
+ )
+ }
+ output.println(
+ """
+ -a 1
+ -b
+ Test.kt
+ done
+ """.trimIndent()
+ )
+ output.flush()
+
+ countDownLatch.await()
+ assertEquals(
+ """
+ kotlinc -version
+ kotlinc -plugin pluginPath1 -a 1 -b Test.kt
+ """.trimIndent(), compileCalls.joinToString("\n")
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt
new file mode 100644
index 0000000..ebb301a
--- /dev/null
+++ b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2021 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 androidx.compose.compiler.daemon
+
+import org.jetbrains.kotlin.build.DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS
+import org.jetbrains.kotlin.build.report.BuildReporter
+import org.jetbrains.kotlin.build.report.metrics.DoNothingBuildMetricsReporter
+import org.jetbrains.kotlin.cli.common.ExitCode
+import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
+import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
+import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
+import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
+import org.jetbrains.kotlin.config.Services
+import org.jetbrains.kotlin.incremental.ClasspathChanges
+import org.jetbrains.kotlin.incremental.EmptyICReporter
+import org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner
+import org.jetbrains.kotlin.incremental.multiproject.EmptyModulesApiHistory
+import org.jetbrains.kotlin.incremental.withIC
+import java.io.File
+import java.nio.file.Files
+
+internal fun parseArgs(
+ args: Array<String>,
+ composePluginPath: String? = null,
+ compiler: K2JVMCompiler = K2JVMCompiler()
+): K2JVMCompilerArguments {
+ val compilerArgs = compiler.createArguments()
+ compiler.parseArguments(args, compilerArgs)
+
+ composePluginPath?.let {
+ compilerArgs.pluginClasspaths = (compilerArgs.pluginClasspaths ?: emptyArray()) + it
+ compilerArgs.pluginOptions =
+ (compilerArgs.pluginOptions ?: emptyArray()) +
+ "plugin:androidx.compose.plugins.idea:enabled=true"
+ compilerArgs.useIR = true // Force IR since it's required for Compose
+ }
+ return compilerArgs
+}
+
+data class DaemonCompilerSettings(val composePluginPath: String? = null) {
+ companion object {
+ val DefaultSettings = DaemonCompilerSettings()
+ }
+}
+
+interface DaemonCompiler {
+ fun compile(
+ args: Array<String>,
+ daemonCompilerSettings: DaemonCompilerSettings = DaemonCompilerSettings.DefaultSettings
+ ): ExitCode
+}
+
+/**
+ * A [DaemonCompiler] that uses regular `kotlinc` invocations.
+ */
+object BasicDaemonCompiler : DaemonCompiler {
+ private val compiler = K2JVMCompiler()
+
+ override fun compile(
+ args: Array<String>,
+ daemonCompilerSettings: DaemonCompilerSettings
+ ): ExitCode {
+ return try {
+ val compilerArgs = parseArgs(args, daemonCompilerSettings.composePluginPath)
+ compiler.exec(
+ PrintingMessageCollector(System.err, MessageRenderer.PLAIN_RELATIVE_PATHS, true),
+ Services.EMPTY,
+ compilerArgs
+ )
+ } catch (t: Throwable) {
+ t.printStackTrace(System.err)
+ ExitCode.INTERNAL_ERROR
+ }
+ }
+}
+
+/**
+ * A [DaemonCompiler] that calls `kotlinc` in incremental mode.
+ */
+object IncrementalDaemonCompiler : DaemonCompiler {
+ private val compiler = IncrementalJvmCompilerRunner(
+ workingDir = Files.createTempDirectory("workingDir").toFile(),
+ reporter = BuildReporter(EmptyICReporter, DoNothingBuildMetricsReporter),
+ usePreciseJavaTracking = true,
+ outputFiles = emptyList(),
+ buildHistoryFile = Files.createTempFile("build-history", ".bin").toFile(),
+ modulesApiHistory = EmptyModulesApiHistory,
+ kotlinSourceFilesExtensions = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS,
+ classpathChanges = ClasspathChanges.NotAvailable.UnableToCompute
+ )
+
+ override fun compile(
+ args: Array<String>,
+ daemonCompilerSettings: DaemonCompilerSettings
+ ): ExitCode {
+ println("Incremental compiler invoke")
+ return try {
+ val compilerArgs = parseArgs(args, daemonCompilerSettings.composePluginPath)
+ compilerArgs.moduleName = "test"
+ withIC {
+ compiler.compile(
+ compilerArgs.freeArgs.map { File(it) },
+ compilerArgs,
+ PrintingMessageCollector(
+ System.err,
+ MessageRenderer.PLAIN_RELATIVE_PATHS,
+ true
+ ),
+ null,
+ null
+ )
+ }
+ } catch (t: Throwable) {
+ t.printStackTrace(System.err)
+ ExitCode.INTERNAL_ERROR
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/DaemonProtocol.kt b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/DaemonProtocol.kt
new file mode 100644
index 0000000..f4d6970
--- /dev/null
+++ b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/DaemonProtocol.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 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 androidx.compose.compiler.daemon
+
+import java.io.BufferedReader
+import java.io.PrintWriter
+import java.io.Reader
+import java.io.Writer
+import kotlin.system.exitProcess
+
+/**
+ * Starts the input loop listening for commands to be sent to the [daemonCompiler] using the given
+ * [daemonSettings]. This method will listen to input commands via [inputStream] and sending the
+ * output to [outputStream].
+ */
+fun startInputLoop(
+ daemonCompiler: DaemonCompiler,
+ daemonSettings: DaemonCompilerSettings,
+ inputReader: Reader,
+ outputWriter: Writer
+) {
+ val input = BufferedReader(inputReader)
+ val output = PrintWriter(outputWriter)
+ // Show the version and trigger the loading of all the compiler classes
+ daemonCompiler.compile(arrayOf("-version"))
+ while (true) {
+ val commandLineBuilder = mutableListOf<String>()
+ while (commandLineBuilder.lastOrNull() != "done") {
+ output.print(">")
+ val line = input.readLine()!!
+ output.println(line)
+ if (line == "quit") exitProcess(1)
+ commandLineBuilder.add(line)
+ }
+ val exitCode = daemonCompiler.compile(
+ commandLineBuilder.dropLast(1).toTypedArray(), daemonSettings
+ )
+ output.println("RESULT ${exitCode.code}")
+ output.flush()
+ }
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/main.kt b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/main.kt
new file mode 100644
index 0000000..6a504b2
--- /dev/null
+++ b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/main.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2021 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 androidx.compose.compiler.daemon
+
+import org.jetbrains.kotlin.cli.common.environment.setIdeaIoUseFallback
+import java.io.PrintWriter
+import java.nio.charset.Charset
+
+private data class CliOptions(
+ val disableEmbeddedPlugin: Boolean = false,
+ val useIncrementalCompiler: Boolean = false,
+ val studioVersion: String? = null
+)
+
+private fun parseArgumentWithOption(args: Array<String>, argumentName: String): String? {
+ val argumentIndex = args.indexOf(argumentName)
+ return if (argumentIndex != -1) args[argumentIndex + 1] else null
+}
+
+private fun parseCliOptions(args: Array<String>): CliOptions {
+ return CliOptions(
+ // Disables the use of the plugin embedded with the jar
+ disableEmbeddedPlugin = args.contains("-disableEmbedded"),
+ useIncrementalCompiler = args.contains("-incremental"),
+ studioVersion = parseArgumentWithOption(args, "-studio")
+ )
+}
+
+fun main(args: Array<String>) {
+ setIdeaIoUseFallback()
+
+ val cliOptions = parseCliOptions(args)
+ println(cliOptions)
+
+ val jarPath = if (cliOptions.disableEmbeddedPlugin)
+ null
+ else
+ object {}.javaClass.protectionDomain.codeSource.location.path
+ println(jarPath?.let { "Using embedded plugin with path $it" } ?: "No embedded plugin")
+
+ val compiler: DaemonCompiler = if (cliOptions.useIncrementalCompiler) {
+ println("Using IncrementalDaemonCompiler")
+ IncrementalDaemonCompiler
+ } else {
+ println("Using BasicDaemonCompiler")
+ BasicDaemonCompiler
+ }
+ // Show the version and trigger the loading of all the compiler classes
+ compiler.compile(arrayOf("-version"))
+ val settings = DaemonCompilerSettings(jarPath)
+ startInputLoop(compiler,
+ settings,
+ System.`in`.bufferedReader(Charset.defaultCharset()),
+ PrintWriter(System.out)
+ )
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
index 2373eb8..cdaa18f 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
@@ -549,7 +549,7 @@
if (%default and 0b0001 !== 0) {
%dirty = %dirty or 0b0010
}
- if (%default.inv() and 0b0001 !== 0 || %dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%default and 0b0001 !== 0b0001 || %dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
y = null
}
@@ -755,7 +755,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(value)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
A(Wrapper(value), %composer, Wrapper.%stable or 0b1000 and %dirty)
} else {
%composer.skipToGroupEnd()
@@ -794,7 +794,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(item)) 0b0100 else 0b0010
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
A(item, %composer, 0b1110 and %dirty)
A(Wrapper(item), %composer, Wrapper.%stable or 0)
} else {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
index 0417a35..e4b759f 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
@@ -330,7 +330,7 @@
Example({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C:Test.kt#2487m")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -429,7 +429,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(block)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
block(%composer, 0b1110 and %dirty)
} else {
%composer.skipToGroupEnd()
@@ -446,7 +446,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(text)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
used(text)
} else {
%composer.skipToGroupEnd()
@@ -463,12 +463,12 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(value)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
%composer.startMovableGroup(<>, value)
sourceInformation(%composer, "<Wrappe...>")
Wrapper(composableLambda(%composer, <>, true) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C<Leaf("...>:Test.kt#2487m")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Leaf("Value %value", %composer, 0)
} else {
%composer.skipToGroupEnd()
@@ -551,15 +551,15 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(composable)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
emit({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<emit>:Test.kt#2487m")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
emit({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<compos...>:Test.kt#2487m")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
composable(%composer, 0b1110 and %dirty)
} else {
%composer.skipToGroupEnd()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
index 84c8513..aefd2e6 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
@@ -2260,7 +2260,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
val tmp0_safe_receiver = x
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "*<A(b)>")
@@ -2315,7 +2315,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
x?.let { it: Int ->
if (it > 0) {
NA()
@@ -2424,7 +2424,7 @@
internal object ComposableSingletons%TestKt {
val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C<A()>:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
} else {
%composer.skipToGroupEnd()
@@ -2453,7 +2453,7 @@
IW({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<A()>:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
} else {
%composer.skipToGroupEnd()
@@ -2502,7 +2502,7 @@
internal object ComposableSingletons%TestKt {
val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C<effect>:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "*<effect>")
repeat(number) { it: Int ->
@@ -2551,7 +2551,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(<unsafe-coerce>(value))) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
used(value)
A(%composer, 0)
} else {
@@ -2730,7 +2730,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -2759,7 +2759,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -2788,7 +2788,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -2817,7 +2817,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -2846,7 +2846,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -2875,7 +2875,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -2904,7 +2904,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -2933,7 +2933,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -2962,7 +2962,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -2991,7 +2991,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3020,7 +3020,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3049,7 +3049,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3078,7 +3078,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3107,7 +3107,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3136,7 +3136,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3165,7 +3165,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3194,7 +3194,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3223,7 +3223,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3252,7 +3252,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3281,7 +3281,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3310,7 +3310,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3339,7 +3339,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3368,7 +3368,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3397,7 +3397,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
used(p0)
used(p1)
used(p2)
@@ -3439,7 +3439,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(<unsafe-coerce>(value))) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
used(value)
} else {
%composer.skipToGroupEnd()
@@ -3525,7 +3525,7 @@
fun onCreate() {
setContent(composableLambdaInstance(<>, true) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C<B(a)>,<B(a)>:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
B(a, %composer, 0)
B(a, %composer, 0)
} else {
@@ -3540,7 +3540,7 @@
var a = "Test"
setContent(composableLambdaInstance(<>, true) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C<B(a)>,<B(a)>:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
B(a, %composer, 0)
B(a, %composer, 0)
} else {
@@ -3594,11 +3594,11 @@
internal object ComposableSingletons%TestKt {
val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C<IW>:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
IW({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<T(2)>,<T(4)>:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
T(2, %composer, 0b0110)
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "*<T(3)>")
@@ -3673,7 +3673,7 @@
Layout({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<Text("...>:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Text("%c %cl", %composer, 0)
} else {
%composer.skipToGroupEnd()
@@ -3782,7 +3782,7 @@
Layout({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<Text("...>:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Text("%c %cl", %composer, 0)
} else {
%composer.skipToGroupEnd()
@@ -3863,7 +3863,7 @@
Wrapper({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C*<Leaf(0...>:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
repeat(1) { it: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "*<Leaf(0...>")
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
index e907da4..750f428 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
@@ -100,7 +100,7 @@
}
internal object ComposableSingletons%TestKt {
val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
} else {
%composer.skipToGroupEnd()
@@ -127,7 +127,7 @@
sourceInformation(%composer, "C(Test)")
if (%changed !== 0 || !%composer.skipping) {
IW({ %composer: Composer?, %changed: Int ->
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
} else {
%composer.skipToGroupEnd()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
index 65c1383..becc6b8 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
@@ -105,7 +105,7 @@
} else if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(<unsafe-coerce>(foo))) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
foo = Foo(0)
}
@@ -175,7 +175,7 @@
fun Bar(unused: Function2<Composer, Int, Unit> = { %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -210,7 +210,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%default and 0b0001 === 0 && %composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
%composer.startDefaults()
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
if (%default and 0b0001 !== 0) {
@@ -260,7 +260,7 @@
if (%changed and 0b01110000 === 0) {
%dirty = %dirty or if (%default and 0b0010 === 0 && %composer.changed(b)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
%composer.startDefaults()
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
if (%default and 0b0001 !== 0) {
@@ -525,7 +525,7 @@
} else if (%changed3 and 0b1110 === 0) {
%dirty3 = %dirty3 or if (%composer.changed(a30)) 0b0100 else 0b0010
}
- if (%dirty and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty1 and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty2 and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty3 and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty1 and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty2 and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty3 and 0b1011 !== 0b0010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
a00 = 0
}
@@ -903,7 +903,7 @@
} else if (%changed3 and 0b01110000 === 0) {
%dirty3 = %dirty3 or if (%composer.changed(a31)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty1 and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty2 and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty3 and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty1 and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty2 and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty3 and 0b01011011 !== 0b00010010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
a00 = 0
}
@@ -1282,7 +1282,7 @@
if (%changed3 and 0b01110000 === 0) {
%dirty3 = %dirty3 or if (%default1 and 0b0001 === 0 && %composer.changed(a31)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty1 and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty2 and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty3 and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty1 and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty2 and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty3 and 0b01011011 !== 0b00010010 || !%composer.skipping) {
%composer.startDefaults()
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
if (%default and 0b0001 !== 0) {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
index 970a2ed..9953eff 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
@@ -85,7 +85,7 @@
} else if (%changed and 0b01110000 === 0) {
%dirty = %dirty or if (%composer.changed(y)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
x = 0
}
@@ -95,7 +95,7 @@
used(y)
Wrap(composableLambda(%composer, <>, true) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
if (x > 0) {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "<A(x)>")
@@ -162,7 +162,7 @@
} else if (%changed and 0b001110000000 === 0) {
%dirty = %dirty or if (%composer.changed(<unsafe-coerce>(overflow))) 0b000100000000 else 0b10000000
}
- if (%dirty and 0b001011011011 xor 0b10010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b001011011011 !== 0b10010010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
style = Companion.Default
}
@@ -212,7 +212,7 @@
} else if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(arrangement)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
arrangement = Arrangement.Top
}
@@ -250,7 +250,7 @@
} else if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(content)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
content = ComposableSingletons%TestKt.lambda-1
}
@@ -265,7 +265,7 @@
internal object ComposableSingletons%TestKt {
val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -300,7 +300,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(it)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
a.compute(it, %composer, 0b1110 and %dirty)
} else {
%composer.skipToGroupEnd()
@@ -351,7 +351,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(colors)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
Text("hello world", null, colors.getColor(%composer, 0b1110 and %dirty), <unsafe-coerce>(0L), null, null, null, <unsafe-coerce>(0L), null, null, <unsafe-coerce>(0L), <unsafe-coerce>(0), false, 0, null, null, %composer, 0b0110, 0, 0b1111111111111010)
} else {
%composer.skipToGroupEnd()
@@ -503,7 +503,7 @@
} else if (%changed and 0b01110000000000000000 === 0) {
%dirty = %dirty or if (%composer.changed(content)) 0b00100000000000000000 else 0b00010000000000000000
}
- if (%dirty and 0b01011011011011011011 xor 0b00010010010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011011011011011 !== 0b00010010010010010010 || !%composer.skipping) {
if (%default and 0b0010 !== 0) {
modifier = Companion
}
@@ -554,7 +554,7 @@
} else if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(content)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
modifier = Companion
}
@@ -604,7 +604,7 @@
} else if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(modifier)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
modifier = Companion
}
@@ -639,7 +639,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%default and 0b0001 === 0 && %composer.changed(a)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
%composer.startDefaults()
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
if (%default and 0b0001 !== 0) {
@@ -687,7 +687,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
@Composable
fun Inner(%composer: Composer?, %changed: Int) {
%composer.startReplaceableGroup(<>)
@@ -786,7 +786,7 @@
if (%changed and 0b01110000 === 0) {
%dirty = %dirty or if (%default and 0b0010 === 0 && %composer.changed(shape)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
%composer.startDefaults()
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
if (%default and 0b0001 !== 0) {
@@ -848,7 +848,7 @@
} else if (%changed and 0b01110000 === 0) {
%dirty = %dirty or if (%composer.changed(content)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
modifier = Companion
}
@@ -867,7 +867,7 @@
internal object ComposableSingletons%TestKt {
val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -904,7 +904,7 @@
if (%changed and 0b01110000 === 0) {
%dirty = %dirty or if (%composer.changed(y)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b001011011011 xor 0b10010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b001011011011 !== 0b10010010 || !%composer.skipping) {
A(x, %composer, 0b1110 and %dirty)
B(y, %composer, 0b1110 and %dirty shr 0b0011)
} else {
@@ -963,7 +963,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(content)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
content(%composer, 0b1110 and %dirty)
} else {
%composer.skipToGroupEnd()
@@ -988,7 +988,7 @@
internal object ComposableSingletons%TestKt {
val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
val id = object
} else {
%composer.skipToGroupEnd()
@@ -1024,7 +1024,7 @@
if (%dirty and 0b1110 === 0) {
%dirty = %dirty or 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
print(values)
} else {
%composer.skipToGroupEnd()
@@ -1064,7 +1064,7 @@
if (%dirty and 0b1110 === 0) {
%dirty = %dirty or 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
print(values)
} else {
%composer.skipToGroupEnd()
@@ -1180,7 +1180,7 @@
} else if (%changed and 0b001110000000 === 0) {
%dirty = %dirty or if (%composer.changed(c)) 0b000100000000 else 0b10000000
}
- if (%dirty and 0b001011011011 xor 0b10010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b001011011011 !== 0b10010010 || !%composer.skipping) {
%composer.startDefaults()
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
if (%default and 0b0001 !== 0) {
@@ -1243,7 +1243,7 @@
if (%changed and 0b01110000 === 0) {
%dirty = %dirty or if (%composer.changed(content)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
content(y, %composer, 0b1110 and %dirty or 0b01110000 and %dirty)
} else {
%composer.skipToGroupEnd()
@@ -1267,7 +1267,7 @@
} else if (%changed and 0b01110000 === 0) {
%dirty = %dirty or if (%composer.changed(y)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
x = 0
}
@@ -1281,7 +1281,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(it)) 0b0100 else 0b0010
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
used(it)
A(x, 0, %composer, 0b1110 and %dirty, 0b0010)
} else {
@@ -1348,7 +1348,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
A(x, 0, %composer, 0b1110 and %dirty, 0b0010)
} else {
%composer.skipToGroupEnd()
@@ -1400,7 +1400,7 @@
if (%changed and 0b01110000 === 0) {
%dirty = %dirty or if (%composer.changed(y)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
A(x, y, %composer, 0b1110 and %dirty or 0b01110000 and %dirty, 0)
} else {
%composer.skipToGroupEnd()
@@ -1446,7 +1446,7 @@
if (%default and 0b0010 !== 0) {
%dirty = %dirty or 0b00010000
}
- if (%default.inv() and 0b0010 !== 0 || %dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%default and 0b0010 !== 0b0010 || %dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
%composer.startDefaults()
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
if (%default and 0b0001 !== 0) {
@@ -1517,7 +1517,7 @@
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(CanSkip):Test.kt")
val %dirty = %changed
- if (%default.inv() and 0b0001 !== 0 || %dirty and 0b0001 !== 0 || !%composer.skipping) {
+ if (%default and 0b0001 !== 0b0001 || %dirty and 0b0001 !== 0 || !%composer.skipping) {
%composer.startDefaults()
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
if (%default and 0b0001 !== 0) {
@@ -1591,7 +1591,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
A(x, %composer, 0b1110 and %dirty)
} else {
%composer.skipToGroupEnd()
@@ -1633,7 +1633,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(text)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
B(text, <unsafe-coerce>(0), %composer, 0b1110 and %dirty, 0b0010)
} else {
%composer.skipToGroupEnd()
@@ -1657,7 +1657,7 @@
} else if (%changed and 0b01110000 === 0) {
%dirty = %dirty or if (%composer.changed(<unsafe-coerce>(color))) 0b00100000 else 0b00010000
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
if (%default and 0b0010 !== 0) {
color = Companion.Unset
}
@@ -1774,7 +1774,7 @@
internal object ComposableSingletons%TestKt {
val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -1811,7 +1811,7 @@
internal object ComposableSingletons%TestKt {
val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -1844,7 +1844,7 @@
} else if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
x = 0
}
@@ -1880,7 +1880,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%default and 0b0001 === 0 && %composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
%composer.startDefaults()
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
if (%default and 0b0001 !== 0) {
@@ -1951,7 +1951,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%default and 0b0001 === 0 && %composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
%composer.startDefaults()
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
if (%default and 0b0001 !== 0) {
@@ -2015,7 +2015,7 @@
if (%default and 0b00010000 !== 0) {
%dirty = %dirty or 0b0010000000000000
}
- if (%default.inv() and 0b00010000 !== 0 || %dirty and 0b1011011011011011 xor 0b0010010010010010 !== 0 || !%composer.skipping) {
+ if (%default and 0b00010000 !== 0b00010000 || %dirty and 0b1011011011011011 !== 0b0010010010010010 || !%composer.skipping) {
%composer.startDefaults()
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
if (%default and 0b0100 !== 0) {
@@ -2070,7 +2070,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
X(x + 1, %composer, 0)
X(x, %composer, 0b1110 and %dirty)
} else {
@@ -2128,7 +2128,7 @@
internal object ComposableSingletons%TestKt {
val lambda-1: @[ExtensionFunctionType] Function5<LazyItemScope, Int, User?, Composer, Int, Unit> = composableLambdaInstance(<>, false) { index: Int, user: User?, %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b0001010000000001 xor 0b010000000000 !== 0 || !%composer.skipping) {
+ if (%changed and 0b0001010000000001 !== 0b010000000000 || !%composer.skipping) {
print("Hello World")
} else {
%composer.skipToGroupEnd()
@@ -2159,7 +2159,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(<this>)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
doSomething(<this>, %composer, 0b1110 and %dirty)
} else {
%composer.skipToGroupEnd()
@@ -2261,7 +2261,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
B(x, x + 1, 123, fooGlobal, %composer, 0b110110000000 or 0b1110 and %dirty)
} else {
%composer.skipToGroupEnd()
@@ -2302,7 +2302,7 @@
internal object ComposableSingletons%TestKt {
val lambda-1: @[ExtensionFunctionType] Function3<Foo, Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b01010001 xor 0b00010000 !== 0 || !%composer.skipping) {
+ if (%changed and 0b01010001 !== 0b00010000 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -2314,7 +2314,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(%this%null)) 0b0100 else 0b0010
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
used(%this%null.x)
} else {
%composer.skipToGroupEnd()
@@ -2322,7 +2322,7 @@
}
val lambda-3: @[ExtensionFunctionType] Function3<StableFoo, Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b01010001 xor 0b00010000 !== 0 || !%composer.skipping) {
+ if (%changed and 0b01010001 !== 0b00010000 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -2334,7 +2334,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(%this%null)) 0b0100 else 0b0010
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
used(%this%null.x)
} else {
%composer.skipToGroupEnd()
@@ -2371,21 +2371,21 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
Provide(composableLambda(%composer, <>, true) { y: Int, %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C<Provid...>,<B(x,>:Test.kt")
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(y)) 0b0100 else 0b0010
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
Provide(composableLambda(%composer, <>, true) { z: Int, %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C<B(x,>:Test.kt")
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(z)) 0b0100 else 0b0010
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
B(x, y, z, %composer, 0b1110 and %dirty or 0b01110000 and %dirty shl 0b0011 or 0b001110000000 and %dirty shl 0b0110, 0)
} else {
%composer.skipToGroupEnd()
@@ -2430,7 +2430,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
@Composable
fun foo(y: Int, %composer: Composer?, %changed: Int) {
%composer.startReplaceableGroup(<>)
@@ -2592,7 +2592,7 @@
} else if (%changed1 and 0b1110000000000000 === 0) {
%dirty1 = %dirty1 or if (%composer.changed(a14)) 0b0100000000000000 else 0b0010000000000000
}
- if (%dirty and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty1 and 0b1011011011011011 xor 0b0010010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty1 and 0b1011011011011011 !== 0b0010010010010010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
a00 = 0
}
@@ -2801,7 +2801,7 @@
} else if (%changed1 and 0b01110000000000000000 === 0) {
%dirty1 = %dirty1 or if (%composer.changed(a15)) 0b00100000000000000000 else 0b00010000000000000000
}
- if (%dirty and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty1 and 0b01011011011011011011 xor 0b00010010010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty1 and 0b01011011011011011011 !== 0b00010010010010010010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
a00 = 0
}
@@ -2947,7 +2947,7 @@
if (%changed and 0b01110000 === 0) {
%dirty = %dirty or if (%default and 0b0010 === 0 && %composer.changed(mightChange)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
%composer.startDefaults()
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
if (%default and 0b0001 !== 0) {
@@ -2994,7 +2994,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(content)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
content(%composer, 0b1110 and %dirty)
} else {
%composer.skipToGroupEnd()
@@ -3067,7 +3067,7 @@
} else if (%changed and 0b001110000000 === 0) {
%dirty = %dirty or if (%composer.changed(content)) 0b000100000000 else 0b10000000
}
- if (%dirty and 0b001011011011 xor 0b10010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b001011011011 !== 0b10010010 || !%composer.skipping) {
if (%default and 0b0001 !== 0) {
modifier = Companion
}
@@ -3090,7 +3090,7 @@
internal object ComposableSingletons%TestKt {
val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -3126,7 +3126,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(cond)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "<A()>")
if (cond) {
@@ -3187,7 +3187,7 @@
if (%changed and 0b01110000 === 0) {
%dirty = %dirty or if (%composer.changed(b)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b01010001 xor 0b00010000 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01010001 !== 0b00010000 || !%composer.skipping) {
used(b)
} else {
%composer.skipToGroupEnd()
@@ -3204,7 +3204,7 @@
if (%changed and 0b001110000000 === 0) {
%dirty = %dirty or if (%composer.changed(c)) 0b000100000000 else 0b10000000
}
- if (%dirty and 0b001010000001 xor 0b10000000 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b001010000001 !== 0b10000000 || !%composer.skipping) {
used(c)
} else {
%composer.skipToGroupEnd()
@@ -3255,7 +3255,7 @@
if (%changed and 0b01110000 === 0) {
%dirty = %dirty or if (%composer.changed(x)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
used(<this>)
used(x)
} else {
@@ -3276,7 +3276,7 @@
if (%changed and 0b01110000 === 0) {
%dirty = %dirty or if (%composer.changed(it)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b001011011011 xor 0b10010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b001011011011 !== 0b10010010 || !%composer.skipping) {
used(%this%null)
used(it)
} else {
@@ -3320,7 +3320,7 @@
if (%dirty and 0b01110000 === 0) {
%dirty = %dirty or 0b00010000
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
%composer.startDefaults()
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
if (%default and 0b0010 !== 0) {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
index d37af66..77ca5c9 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
@@ -38,7 +38,7 @@
val b: String = ""
val c: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, true) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
print(b)
} else {
%composer.skipToGroupEnd()
@@ -89,7 +89,7 @@
sourceInformation(%composer, "C(C)<B>:Test.kt")
B(composableLambda(%composer, <>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C<A()>:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
} else {
%composer.skipToGroupEnd()
@@ -297,7 +297,7 @@
<<LOCALDELPROP>>
B(composableLambda(%composer, <>, true) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
print(<get-x>())
} else {
%composer.skipToGroupEnd()
@@ -332,7 +332,7 @@
internal object ComposableSingletons%TestKt {
val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -340,7 +340,7 @@
}
val lambda-2: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -384,7 +384,7 @@
internal object ComposableSingletons%TestKt {
val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -392,7 +392,7 @@
}
val lambda-2: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -432,7 +432,7 @@
internal object ComposableSingletons%TestKt {
val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -475,11 +475,11 @@
} else if (%changed and 0b01110000 === 0) {
%dirty = %dirty or if (%composer.changed(content)) 0b00100000 else 0b00010000
}
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
if (%default and 0b0010 !== 0) {
content = composableLambda(%composer, <>, true) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C<Displa...>:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Display("%enabled", %composer, 0)
} else {
%composer.skipToGroupEnd()
@@ -525,10 +525,10 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(enabled)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
val content = composableLambda(%composer, <>, true) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C<Displa...>:Test.kt")
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Display("%enabled", %composer, 0)
} else {
%composer.skipToGroupEnd()
@@ -578,7 +578,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(content)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
content()
} else {
%composer.skipToGroupEnd()
@@ -632,7 +632,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(content)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
content()
} else {
%composer.skipToGroupEnd()
@@ -649,7 +649,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
TestLambda(remember(a, {
{
println("Captures a" + a)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
index 89aa6c0..b6b2d95 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
@@ -153,7 +153,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
%composer.cache(%dirty and 0b1110 === 0b0100) {
val tmp0_return = 1
tmp0_return
@@ -185,7 +185,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
%composer.cache(%dirty and 0b1110 === 0b0100 || %dirty and 0b1000 !== 0 && %composer.changed(x)) {
val tmp0_return = 1
tmp0_return
@@ -529,7 +529,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
if (condition) {
val foo = %composer.cache(false) {
@@ -571,7 +571,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
if (condition) {
A(%composer, 0)
val foo = remember({
@@ -725,7 +725,7 @@
if (%changed and 0b0001110000000000 === 0) {
%dirty = %dirty or if (%composer.changed(d)) 0b100000000000 else 0b010000000000
}
- if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
val foo = %composer.cache(%dirty and 0b1110 === 0b0100 or %dirty and 0b01110000 === 0b00100000 or %dirty and 0b001110000000 === 0b000100000000 or %dirty and 0b0001110000000000 === 0b100000000000) {
val tmp0_return = Foo()
tmp0_return
@@ -790,7 +790,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(<unsafe-coerce>(inlineInt))) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
val a = InlineInt(123)
val foo = %composer.cache(%dirty and 0b1110 === 0b0100 or false) {
val tmp0_return = Foo()
@@ -873,7 +873,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
val b = someInt()
val foo = %composer.cache(%dirty and 0b1110 === 0b0100 or %composer.changed(b)) {
val tmp0_return = Foo(a, b)
@@ -941,7 +941,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%default and 0b0001 === 0 && %composer.changed(a)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
%composer.startDefaults()
if (%default and 0b0001 !== 0) {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StabilityPropagationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StabilityPropagationTransformTests.kt
index a056dcd..7ec98da 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StabilityPropagationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StabilityPropagationTransformTests.kt
@@ -67,7 +67,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
}
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
A(x, %composer, 0b1110 and %dirty)
A(Foo(0), %composer, 0)
A(remember({
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index fd485fd..64584940 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -204,7 +204,7 @@
project: Project,
configuration: CompilerConfiguration
) {
- val KOTLIN_VERSION_EXPECTATION = "1.6.0"
+ val KOTLIN_VERSION_EXPECTATION = "1.6.10"
KotlinCompilerVersion.getVersion()?.let { version ->
val suppressKotlinVersionCheck = configuration.get(
ComposeConfiguration.SUPPRESS_KOTLIN_VERSION_COMPATIBILITY_CHECK,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index c23d8691..909607f 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -364,7 +364,7 @@
* if ($changed and 0b0110 === 0) {
* $dirty = $dirty or if ($composer.changed(x)) 0b0010 else 0b0100
* }
- * if (%dirty and 0b1011 xor 0b1010 !== 0 || !$composer.skipping) {
+ * if (%dirty and 0b1011 !== 0b1010 || !$composer.skipping) {
* f(x)
* } else {
* $composer.skipToGroupEnd()
@@ -3055,7 +3055,7 @@
// (the shift amount represented here by `x`, `y`, and `z`).
// TODO: we could make some small optimization here if we have multiple values passed
- // from one function into another in the same order. This may not happen commonly eugh
+ // from one function into another in the same order. This may not happen commonly enough
// to be worth the complication though.
// NOTE: we start with 0b0 because it is important that the low bit is always 0
@@ -3392,12 +3392,13 @@
}
}
- return if (resultsWithCalls == 1) {
- transformed.asCoalescableGroup(resultScopes.single { it.hasComposableCalls })
- } else if (needsWrappingGroup) {
- transformed.asCoalescableGroup(whenScope)
- } else {
- transformed
+ return when {
+ resultsWithCalls == 1 ->
+ transformed.asCoalescableGroup(resultScopes.single { it.hasComposableCalls })
+ needsWrappingGroup ->
+ transformed.asCoalescableGroup(whenScope)
+ else ->
+ transformed
}
}
@@ -3916,13 +3917,13 @@
val end = min(start + BITS_PER_INT, count)
val unstableMask = bitMask(*unstable.sliceArray(start until end))
irNotEqual(
- // ~$default and unstableMask will be non-zero if any parameters were
- // *provided* AND *unstable*
+ // $default and unstableMask will be different from unstableMask
+ // iff any parameters were *provided* AND *unstable*
irAnd(
- irInv(irGet(param)),
+ irGet(param),
irConst(unstableMask)
),
- irConst(0)
+ irConst(unstableMask)
)
}
return if (expressions.size == 1)
@@ -4042,16 +4043,13 @@
irConst(0)
)
} else {
- // $dirty and (0b 101 ... 101 1) xor (0b 001 ... 001 0)
+ // $dirty and (0b 101 ... 101 1) != (0b 001 ... 001 0)
irNotEqual(
- irXor(
- irAnd(
- irGet(param),
- irConst(lhs or 0b1)
- ),
- irConst(rhs or 0b0)
+ irAnd(
+ irGet(param),
+ irConst(lhs or 0b1)
),
- irConst(0) // anything non-zero means we have differences
+ irConst(rhs or 0b0)
)
}
}
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 61bdcde..efedd5d 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -373,8 +373,10 @@
package androidx.compose.foundation.lazy {
public final class LazyDslKt {
- method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,? extends kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,? extends kotlin.Unit> content);
method public static inline <T> void items(androidx.compose.foundation.lazy.LazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void items(androidx.compose.foundation.lazy.LazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 5a7a948..707477a 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -428,8 +428,10 @@
}
public final class LazyDslKt {
- method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,? extends kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,? extends kotlin.Unit> content);
method public static inline <T> void items(androidx.compose.foundation.lazy.LazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void items(androidx.compose.foundation.lazy.LazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
@@ -446,7 +448,7 @@
}
public final class LazyGridKt {
- method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyVerticalGrid(androidx.compose.foundation.lazy.GridCells cells, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyGridScope,kotlin.Unit> content);
+ method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyVerticalGrid(androidx.compose.foundation.lazy.GridCells cells, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyGridScope,kotlin.Unit> content);
method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void items(androidx.compose.foundation.lazy.LazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyGridItemSpanScope,? super T,androidx.compose.foundation.lazy.GridItemSpan>? spans, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void items(androidx.compose.foundation.lazy.LazyGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyGridItemSpanScope,? super T,androidx.compose.foundation.lazy.GridItemSpan>? spans, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.compose.foundation.lazy.GridItemSpan>? spans, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 61bdcde..efedd5d 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -373,8 +373,10 @@
package androidx.compose.foundation.lazy {
public final class LazyDslKt {
- method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,? extends kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,? extends kotlin.Unit> content);
method public static inline <T> void items(androidx.compose.foundation.lazy.LazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void items(androidx.compose.foundation.lazy.LazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
new file mode 100644
index 0000000..838e3f6
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2021 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 androidx.compose.foundation.demos
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyItemScope
+import androidx.compose.foundation.lazy.LazyListItemInfo
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.Card
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.consumeAllChanges
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun LazyColumnDragAndDropDemo() {
+ var list by remember { mutableStateOf(List(50) { it }) }
+
+ val listState = rememberLazyListState()
+ val dragDropState = rememberDragDropState(listState) { fromIndex, toIndex ->
+ list = list.toMutableList().apply {
+ add(toIndex, removeAt(fromIndex))
+ }
+ }
+
+ LazyColumn(
+ modifier = Modifier.dragContainer(dragDropState),
+ state = listState,
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ itemsIndexed(list, key = { _, item -> item }) { index, item ->
+ DraggableItem(dragDropState, index) { isDragging ->
+ val elevation by animateDpAsState(if (isDragging) 4.dp else 1.dp)
+ Card(elevation = elevation) {
+ Text("Item $item", Modifier.fillMaxWidth().padding(20.dp))
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun rememberDragDropState(
+ lazyListState: LazyListState,
+ onMove: (Int, Int) -> Unit
+): DragDropState {
+ val scope = rememberCoroutineScope()
+ val state = remember(lazyListState) {
+ DragDropState(
+ state = lazyListState,
+ onMove = onMove,
+ scope = scope
+ )
+ }
+ LaunchedEffect(state) {
+ while (true) {
+ val diff = state.scrollChannel.receive()
+ lazyListState.scrollBy(diff)
+ }
+ }
+ return state
+}
+
+class DragDropState internal constructor(
+ private val state: LazyListState,
+ private val scope: CoroutineScope,
+ private val onMove: (Int, Int) -> Unit
+) {
+ var draggingItemIndex by mutableStateOf<Int?>(null)
+ private set
+
+ internal val scrollChannel = Channel<Float>()
+
+ private var draggingItemDraggedDelta by mutableStateOf(0f)
+ private var draggingItemInitialOffset by mutableStateOf(0)
+ internal val draggingItemOffset: Float
+ get() = draggingItemLayoutInfo?.let { item ->
+ draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
+ } ?: 0f
+
+ private val draggingItemLayoutInfo: LazyListItemInfo?
+ get() = state.layoutInfo.visibleItemsInfo
+ .firstOrNull { it.index == draggingItemIndex }
+
+ internal var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
+ private set
+ internal var previousItemOffset = Animatable(0f)
+ private set
+
+ internal fun onDragStart(offset: Offset) {
+ state.layoutInfo.visibleItemsInfo
+ .firstOrNull { item ->
+ offset.y.toInt() in item.offset..(item.offset + item.size)
+ }?.also {
+ draggingItemIndex = it.index
+ draggingItemInitialOffset = it.offset
+ }
+ }
+
+ internal fun onDragInterrupted() {
+ if (draggingItemIndex != null) {
+ previousIndexOfDraggedItem = draggingItemIndex
+ val startOffset = draggingItemOffset
+ scope.launch {
+ previousItemOffset.snapTo(startOffset)
+ previousItemOffset.animateTo(
+ 0f,
+ spring(
+ stiffness = Spring.StiffnessMediumLow,
+ visibilityThreshold = 1f
+ )
+ )
+ previousIndexOfDraggedItem = null
+ }
+ }
+ draggingItemDraggedDelta = 0f
+ draggingItemIndex = null
+ draggingItemInitialOffset = 0
+ }
+
+ internal fun onDrag(offset: Offset) {
+ draggingItemDraggedDelta += offset.y
+
+ val draggingItem = draggingItemLayoutInfo ?: return
+ val startOffset = draggingItem.offset + draggingItemOffset
+ val endOffset = startOffset + draggingItem.size
+
+ val targetItem = state.layoutInfo.visibleItemsInfo.find { item ->
+ if (item.offsetEnd > startOffset && item.offset < endOffset &&
+ draggingItem.index != item.index
+ ) {
+ val delta = startOffset - draggingItem.offset
+ when {
+ delta > 0 -> (endOffset > item.offsetEnd)
+ else -> (startOffset < item.offset)
+ }
+ } else {
+ false
+ }
+ }
+ if (targetItem != null) {
+ val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) {
+ draggingItem.index
+ } else if (draggingItem.index == state.firstVisibleItemIndex) {
+ targetItem.index
+ } else {
+ null
+ }
+ if (scrollToIndex != null) {
+ scope.launch {
+ // this is needed to neutralize automatic keeping the first item first.
+ state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
+ onMove.invoke(draggingItem.index, targetItem.index)
+ }
+ } else {
+ onMove.invoke(draggingItem.index, targetItem.index)
+ }
+ draggingItemIndex = targetItem.index
+ } else {
+ val overscroll = when {
+ draggingItemDraggedDelta > 0 ->
+ (endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
+ draggingItemDraggedDelta < 0 ->
+ (startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
+ else -> 0f
+ }
+ if (overscroll != 0f) {
+ scrollChannel.trySend(overscroll)
+ }
+ }
+ }
+
+ private val LazyListItemInfo.offsetEnd: Int
+ get() = this.offset + this.size
+}
+
+fun Modifier.dragContainer(dragDropState: DragDropState): Modifier {
+ return pointerInput(dragDropState) {
+ detectDragGesturesAfterLongPress(
+ onDrag = { change, offset ->
+ change.consumeAllChanges()
+ dragDropState.onDrag(offset = offset)
+ },
+ onDragStart = { offset -> dragDropState.onDragStart(offset) },
+ onDragEnd = { dragDropState.onDragInterrupted() },
+ onDragCancel = { dragDropState.onDragInterrupted() }
+ )
+ }
+}
+
+@ExperimentalFoundationApi
+@Composable
+fun LazyItemScope.DraggableItem(
+ dragDropState: DragDropState,
+ index: Int,
+ modifier: Modifier = Modifier,
+ content: @Composable ColumnScope.(isDragging: Boolean) -> Unit
+) {
+ val dragging = index == dragDropState.draggingItemIndex
+ val draggingModifier = if (dragging) {
+ Modifier
+ .zIndex(1f)
+ .graphicsLayer {
+ translationY = dragDropState.draggingItemOffset
+ }
+ } else if (index == dragDropState.previousIndexOfDraggedItem) {
+ Modifier.zIndex(1f)
+ .graphicsLayer {
+ translationY = dragDropState.previousItemOffset.value
+ }
+ } else {
+ Modifier.animateItemPlacement()
+ }
+ Column(modifier = modifier.then(draggingModifier)) {
+ content(dragging)
+ }
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
index 3e5ef66..eca4e16 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
@@ -111,6 +111,7 @@
ComposableDemo("Custom keys") { ReorderWithCustomKeys() },
ComposableDemo("Fling Config") { LazyWithFlingConfig() },
ComposableDemo("Item reordering") { PopularBooksDemo() },
+ ComposableDemo("Drag and drop") { LazyColumnDragAndDropDemo() },
PagingDemos
)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
index 92e395d..0d83981 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
@@ -18,6 +18,7 @@
import androidx.compose.foundation.AutoTestFrameClock
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
@@ -36,6 +37,11 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
+import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertHeightIsAtLeast
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
@@ -866,5 +872,97 @@
.assertWidthIsEqualTo(columnWidth)
}
+ @Test
+ fun pointerInputScrollingIsAllowedWhenUserScrollingIsEnabled() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ rule.setContentWithTestViewConfiguration {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ Modifier.size(itemSize * 3).testTag(LazyGridTag),
+ userScrollEnabled = true,
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyGridTag).scrollBy(y = itemSize, density = rule.density)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun pointerInputScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ rule.setContentWithTestViewConfiguration {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ Modifier.size(itemSize * 3).testTag(LazyGridTag),
+ userScrollEnabled = false,
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyGridTag).scrollBy(y = itemSize, density = rule.density)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun programmaticScrollingIsAllowedWhenUserScrollingIsDisabled() {
+ val itemSizePx = 30f
+ val itemSize = with(rule.density) { itemSizePx.toDp() }
+ lateinit var state: LazyListState
+ rule.setContentWithTestViewConfiguration {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ Modifier.size(itemSize * 3),
+ state = rememberLazyListState().also { state = it },
+ userScrollEnabled = false,
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(itemSizePx)
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun semanticScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ rule.setContentWithTestViewConfiguration {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ Modifier.size(itemSize * 3).testTag(LazyGridTag),
+ userScrollEnabled = false,
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyGridTag)
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.ScrollBy))
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.ScrollToIndex))
+ // but we still have a read only scroll range property
+ .assert(keyIsDefined(SemanticsProperties.VerticalScrollAxisRange))
+ }
+
// TODO: add tests for the cache logic
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
index fe8ea1e..93cdcc9 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
@@ -173,6 +173,12 @@
}
}
+ fun SemanticsNodeInteraction.scrollBy(offset: Dp) = scrollBy(
+ x = if (vertical) 0.dp else offset,
+ y = if (!vertical) 0.dp else offset,
+ density = rule.density
+ )
+
@Composable
fun LazyColumnOrRow(
modifier: Modifier = Modifier,
@@ -180,6 +186,7 @@
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+ userScrollEnabled: Boolean = true,
content: LazyListScope.() -> Unit
) {
if (vertical) {
@@ -189,6 +196,7 @@
contentPadding = contentPadding,
reverseLayout = reverseLayout,
flingBehavior = flingBehavior,
+ userScrollEnabled = userScrollEnabled,
content = content
)
} else {
@@ -198,6 +206,7 @@
contentPadding = contentPadding,
reverseLayout = reverseLayout,
flingBehavior = flingBehavior,
+ userScrollEnabled = userScrollEnabled,
content = content
)
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
index 2fa0d60..a676d5a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
@@ -50,7 +50,11 @@
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyNotDefined
import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
@@ -69,7 +73,6 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import com.google.common.collect.Range
@@ -83,8 +86,8 @@
import java.util.concurrent.CountDownLatch
@LargeTest
-@RunWith(AndroidJUnit4::class)
-class LazyListTest() : BaseLazyListTestWithOrientation(Orientation.Vertical) {
+@RunWith(Parameterized::class)
+class LazyListTest(orientation: Orientation) : BaseLazyListTestWithOrientation(orientation) {
private val LazyListTag = "LazyListTag"
private val firstItemTag = "firstItemTag"
@@ -1520,6 +1523,97 @@
}
}
+ @Test
+ fun pointerInputScrollingIsAllowedWhenUserScrollingIsEnabled() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
+ userScrollEnabled = true,
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag).scrollBy(itemSize)
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun pointerInputScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
+ userScrollEnabled = false,
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag).scrollBy(itemSize)
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun programmaticScrollingIsAllowedWhenUserScrollingIsDisabled() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ lateinit var state: LazyListState
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(itemSize * 3),
+ state = rememberLazyListState().also { state = it },
+ userScrollEnabled = false,
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun semanticScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
+ userScrollEnabled = false,
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .assert(keyNotDefined(SemanticsActions.ScrollBy))
+ .assert(keyNotDefined(SemanticsActions.ScrollToIndex))
+ // but we still have a read only scroll range property
+ .assert(
+ keyIsDefined(
+ if (vertical) {
+ SemanticsProperties.VerticalScrollAxisRange
+ } else {
+ SemanticsProperties.HorizontalScrollAxisRange
+ }
+ )
+ )
+ }
+
// ********************* END OF TESTS *********************
// Helper functions, etc. live below here
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
index b78cfa9..43c41d6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
@@ -183,6 +183,8 @@
* of them to fill the whole minimum size.
* @param verticalAlignment the vertical alignment applied to the items
* @param flingBehavior logic describing fling behavior.
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
* @param content a block which describes the content. Inside this block you can use methods like
* [LazyListScope.item] to add a single item or [LazyListScope.items] to add a list of items.
*/
@@ -196,6 +198,7 @@
if (!reverseLayout) Arrangement.Start else Arrangement.End,
verticalAlignment: Alignment.Vertical = Alignment.Top,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+ userScrollEnabled: Boolean = true,
content: LazyListScope.() -> Unit
) {
LazyList(
@@ -207,6 +210,7 @@
isVertical = false,
flingBehavior = flingBehavior,
reverseLayout = reverseLayout,
+ userScrollEnabled = userScrollEnabled,
content = content
)
}
@@ -233,6 +237,8 @@
* of them to fill the whole minimum size.
* @param horizontalAlignment the horizontal alignment applied to the items.
* @param flingBehavior logic describing fling behavior.
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
* @param content a block which describes the content. Inside this block you can use methods like
* [LazyListScope.item] to add a single item or [LazyListScope.items] to add a list of items.
*/
@@ -246,6 +252,7 @@
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+ userScrollEnabled: Boolean = true,
content: LazyListScope.() -> Unit
) {
LazyList(
@@ -257,6 +264,59 @@
verticalArrangement = verticalArrangement,
isVertical = true,
reverseLayout = reverseLayout,
+ userScrollEnabled = userScrollEnabled,
+ content = content
+ )
+}
+
+@Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN)
+@Composable
+fun LazyColumn(
+ modifier: Modifier = Modifier,
+ state: LazyListState = rememberLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ verticalArrangement: Arrangement.Vertical =
+ if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
+ horizontalAlignment: Alignment.Horizontal = Alignment.Start,
+ flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+ content: LazyListScope.() -> Unit
+) {
+ LazyColumn(
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ verticalArrangement = verticalArrangement,
+ horizontalAlignment = horizontalAlignment,
+ flingBehavior = flingBehavior,
+ userScrollEnabled = true,
+ content = content
+ )
+}
+
+@Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN)
+@Composable
+fun LazyRow(
+ modifier: Modifier = Modifier,
+ state: LazyListState = rememberLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ horizontalArrangement: Arrangement.Horizontal =
+ if (!reverseLayout) Arrangement.Start else Arrangement.End,
+ verticalAlignment: Alignment.Vertical = Alignment.Top,
+ flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+ content: LazyListScope.() -> Unit
+) {
+ LazyRow(
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ horizontalArrangement = horizontalArrangement,
+ verticalAlignment = verticalAlignment,
+ flingBehavior = flingBehavior,
+ userScrollEnabled = true,
content = content
)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
index 505a344..608824d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
@@ -48,6 +48,8 @@
* @param contentPadding specify a padding around the whole content
* @param verticalArrangement The vertical arrangement of the layout's children
* @param horizontalArrangement The horizontal arrangement of the layout's children
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
* @param content the [LazyListScope] which describes the content
*/
@ExperimentalFoundationApi
@@ -59,6 +61,7 @@
contentPadding: PaddingValues = PaddingValues(0.dp),
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
+ userScrollEnabled: Boolean = true,
content: LazyGridScope.() -> Unit
) {
when (cells) {
@@ -70,6 +73,7 @@
horizontalArrangement = horizontalArrangement,
verticalArrangement = verticalArrangement,
contentPadding = contentPadding,
+ userScrollEnabled = userScrollEnabled,
content = content
)
is GridCells.Adaptive ->
@@ -83,6 +87,7 @@
horizontalArrangement = horizontalArrangement,
verticalArrangement = verticalArrangement,
contentPadding = contentPadding,
+ userScrollEnabled = userScrollEnabled,
content = content
)
}
@@ -243,13 +248,15 @@
contentPadding: PaddingValues,
verticalArrangement: Arrangement.Vertical,
horizontalArrangement: Arrangement.Horizontal,
+ userScrollEnabled: Boolean,
content: LazyGridScope.() -> Unit
) {
LazyColumn(
modifier = modifier,
state = state,
verticalArrangement = verticalArrangement,
- contentPadding = contentPadding
+ contentPadding = contentPadding,
+ userScrollEnabled = userScrollEnabled
) {
val scope = LazyGridScopeImpl(nColumns)
scope.apply(content)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazyList.kt
index 0d1af28..ee35422 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazyList.kt
@@ -64,6 +64,8 @@
isVertical: Boolean,
/** fling behavior to be used for flinging */
flingBehavior: FlingBehavior,
+ /** Whether scrolling via the user gestures is allowed. */
+ userScrollEnabled: Boolean,
/** The alignment to align items horizontally. Required when isVertical is true */
horizontalAlignment: Alignment.Horizontal? = null,
/** The vertical arrangement for items. Required when isVertical is true */
@@ -117,7 +119,8 @@
state = state,
coroutineScope = scope,
isVertical = isVertical,
- reverseScrolling = reverseLayout
+ reverseScrolling = reverseLayout,
+ userScrollEnabled = userScrollEnabled
)
.clipScrollableContainer(isVertical)
.scrollable(
@@ -136,7 +139,8 @@
interactionSource = state.internalInteractionSource,
flingBehavior = flingBehavior,
state = state,
- overScrollController = overScrollController
+ overScrollController = overScrollController,
+ enabled = userScrollEnabled
),
state = innerState,
prefetchPolicy = state.prefetchPolicy,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazySemantics.kt
index 7ecbf46..2a3753c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazySemantics.kt
@@ -38,7 +38,8 @@
state: LazyListState,
coroutineScope: CoroutineScope,
isVertical: Boolean,
- reverseScrolling: Boolean
+ reverseScrolling: Boolean,
+ userScrollEnabled: Boolean
): Modifier {
return semantics {
indexForKey { needle ->
@@ -77,24 +78,30 @@
horizontalScrollAxisRange = accessibilityScrollState
}
- scrollBy { x, y ->
- val delta = if (isVertical) { y } else { x }
- coroutineScope.launch {
- (state as ScrollableState).animateScrollBy(delta)
+ if (userScrollEnabled) {
+ scrollBy { x, y ->
+ val delta = if (isVertical) {
+ y
+ } else {
+ x
+ }
+ coroutineScope.launch {
+ (state as ScrollableState).animateScrollBy(delta)
+ }
+ // TODO(aelias): is it important to return false if we know in advance we cannot scroll?
+ true
}
- // TODO(aelias): is it important to return false if we know in advance we cannot scroll?
- true
- }
- scrollToIndex { index ->
- require(index >= 0 && index < state.layoutInfo.totalItemsCount) {
- "Can't scroll to index $index, it is out of " +
- "bounds [0, ${state.layoutInfo.totalItemsCount})"
+ scrollToIndex { index ->
+ require(index >= 0 && index < state.layoutInfo.totalItemsCount) {
+ "Can't scroll to index $index, it is out of " +
+ "bounds [0, ${state.layoutInfo.totalItemsCount})"
+ }
+ coroutineScope.launch {
+ state.scrollToItem(index)
+ }
+ true
}
- coroutineScope.launch {
- state.scrollToItem(index)
- }
- true
}
collectionInfo = CollectionInfo(
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index fb9cc2d..a6917bb 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -479,10 +479,12 @@
public final class ModalBottomSheetKt {
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void ModalBottomSheetLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> sheetContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.ModalBottomSheetState sheetState, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.ModalBottomSheetState rememberModalBottomSheetState(androidx.compose.material.ModalBottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, boolean skipHalfExpanded, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmStateChange);
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.ModalBottomSheetState rememberModalBottomSheetState(androidx.compose.material.ModalBottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmStateChange);
}
@androidx.compose.material.ExperimentalMaterialApi public final class ModalBottomSheetState extends androidx.compose.material.SwipeableState<androidx.compose.material.ModalBottomSheetValue> {
+ ctor public ModalBottomSheetState(androidx.compose.material.ModalBottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, boolean isSkipHalfExpanded, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmStateChange);
ctor public ModalBottomSheetState(androidx.compose.material.ModalBottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmStateChange);
method public suspend Object? hide(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public boolean isVisible();
@@ -492,7 +494,8 @@
}
public static final class ModalBottomSheetState.Companion {
- method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.ModalBottomSheetState,?> Saver(androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmStateChange);
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.ModalBottomSheetState,?> Saver(androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, boolean skipHalfExpanded, kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmStateChange);
+ method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.compose.material.ModalBottomSheetState,?> Saver(androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmStateChange);
}
@androidx.compose.material.ExperimentalMaterialApi public enum ModalBottomSheetValue {
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
index f754340..18462f3 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
@@ -18,12 +18,16 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.Button
+import androidx.compose.material.Checkbox
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.ListItem
@@ -34,9 +38,14 @@
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@@ -44,7 +53,11 @@
@Composable
@OptIn(ExperimentalMaterialApi::class)
fun ModalBottomSheetSample() {
- val state = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
+ var skipHalfExpanded by remember { mutableStateOf(false) }
+ val state = rememberModalBottomSheetState(
+ initialValue = ModalBottomSheetValue.Hidden,
+ skipHalfExpanded = skipHalfExpanded
+ )
val scope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetState = state,
@@ -68,11 +81,21 @@
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
- Text("Rest of the UI")
+ Row(
+ Modifier.toggleable(
+ value = skipHalfExpanded,
+ role = Role.Checkbox,
+ onValueChange = { checked -> skipHalfExpanded = checked }
+ )
+ ) {
+ Checkbox(checked = skipHalfExpanded, onCheckedChange = null)
+ Spacer(Modifier.width(16.dp))
+ Text("Skip Half Expanded State")
+ }
Spacer(Modifier.height(20.dp))
Button(onClick = { scope.launch { state.show() } }) {
Text("Click to show sheet")
}
}
}
-}
\ No newline at end of file
+}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetStateTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetStateTest.kt
new file mode 100644
index 0000000..ca971e5
--- /dev/null
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetStateTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2021 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 androidx.compose.material
+
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalMaterialApi::class)
+class ModalBottomSheetStateTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+ private val restorationTester = StateRestorationTester(rule)
+
+ @Test
+ fun test_stateSavedAndRestored() {
+ val initialValue = ModalBottomSheetValue.Hidden
+ val skipHalfExpanded = true
+ val animationSpec = SpringSpec<Float>(visibilityThreshold = 10F)
+ lateinit var state: ModalBottomSheetState
+ restorationTester.setContent {
+ state = rememberModalBottomSheetState(
+ initialValue = initialValue,
+ skipHalfExpanded = skipHalfExpanded,
+ animationSpec = animationSpec
+ )
+ }
+
+ assertThat(state.animationSpec).isEqualTo(animationSpec)
+ assertThat(state.currentValue).isEqualTo(initialValue)
+ assertThat(state.isSkipHalfExpanded).isEqualTo(skipHalfExpanded)
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ assertThat(state.animationSpec).isEqualTo(animationSpec)
+ assertThat(state.currentValue).isEqualTo(initialValue)
+ assertThat(state.isSkipHalfExpanded).isEqualTo(skipHalfExpanded)
+ }
+
+ @Test
+ fun test_halfExpandDisabled_initialValueHalfExpanded_throws() {
+ try {
+ ModalBottomSheetState(
+ initialValue = ModalBottomSheetValue.HalfExpanded,
+ isSkipHalfExpanded = true
+ )
+ fail("ModalBottomSheetState didn't throw an exception")
+ } catch (exception: IllegalArgumentException) {
+ assertThat(exception)
+ .hasMessageThat()
+ .isNotEmpty()
+ }
+ }
+
+ @Test
+ fun test_halfExpandDisabled_initialValueHidden_doesntThrow() {
+ ModalBottomSheetState(
+ initialValue = ModalBottomSheetValue.Hidden,
+ isSkipHalfExpanded = true
+ )
+ }
+
+ @Test
+ fun test_halfExpandDisabled_initialValueExpanded_doesntThrow() {
+ ModalBottomSheetState(
+ initialValue = ModalBottomSheetValue.Expanded,
+ isSkipHalfExpanded = true
+ )
+ }
+}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
index 46cde1e..6d31620 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
@@ -399,6 +399,42 @@
}
@Test
+ fun modalBottomSheet_showAndHide_manually_skipHalfExpanded(): Unit = runBlocking(
+ AutoTestFrameClock()
+ ) {
+ lateinit var sheetState: ModalBottomSheetState
+ rule.setMaterialContent {
+ sheetState = rememberModalBottomSheetState(
+ ModalBottomSheetValue.Hidden,
+ skipHalfExpanded = true
+ )
+ ModalBottomSheetLayout(
+ sheetState = sheetState,
+ content = {},
+ sheetContent = {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag(sheetTag)
+ )
+ }
+ )
+ }
+
+ assertThat(sheetState.currentValue == ModalBottomSheetValue.Hidden)
+
+ sheetState.show()
+
+ advanceClock()
+
+ assertThat(sheetState.currentValue == ModalBottomSheetValue.Expanded)
+
+ sheetState.hide()
+
+ assertThat(sheetState.currentValue == ModalBottomSheetValue.Hidden)
+ }
+
+ @Test
fun modalBottomSheet_hideBySwiping() {
lateinit var sheetState: ModalBottomSheetState
rule.setMaterialContent {
@@ -415,6 +451,56 @@
sheetContent = {
Box(
Modifier
+ .fillMaxSize()
+ .testTag(sheetTag)
+ )
+ }
+ )
+ }
+
+ rule.runOnIdle {
+ assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Expanded)
+ }
+
+ rule.onNodeWithTag(sheetTag)
+ .performTouchInput { swipeDown(endY = rule.rootHeight().toPx() / 2) }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.HalfExpanded)
+ }
+
+ rule.onNodeWithTag(sheetTag)
+ .performTouchInput { swipeDown() }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Hidden)
+ }
+ }
+
+ @Test
+ fun modalBottomSheet_hideBySwiping_skipHalfExpanded() {
+ lateinit var sheetState: ModalBottomSheetState
+ rule.setMaterialContent {
+ sheetState = rememberModalBottomSheetState(
+ ModalBottomSheetValue.Expanded,
+ skipHalfExpanded = true
+ )
+ ModalBottomSheetLayout(
+ sheetState = sheetState,
+ content = {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag(contentTag)
+ )
+ },
+ sheetContent = {
+ Box(
+ Modifier
.fillMaxWidth()
.height(sheetHeight)
.testTag(sheetTag)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
index 759a753..3ba169b 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
@@ -86,14 +86,22 @@
/**
* State of the [ModalBottomSheetLayout] composable.
*
- * @param initialValue The initial value of the state.
+ * @param initialValue The initial value of the state. <b>Must not be set to
+ * [ModalBottomSheetValue.HalfExpanded] if [isSkipHalfExpanded] is set to true.</b>
* @param animationSpec The default animation that will be used to animate to a new state.
+ * @param isSkipHalfExpanded Whether the half expanded state, if the sheet is tall enough, should
+ * be skipped. If true, the sheet will always expand to the [Expanded] state and move to the
+ * [Hidden] state when hiding the sheet, either programmatically or by user interaction.
+ * <b>Must not be set to true if the [initialValue] is [ModalBottomSheetValue.HalfExpanded].</b>
+ * If supplied with [ModalBottomSheetValue.HalfExpanded] for the [initialValue], an
+ * [IllegalArgumentException] will be thrown.
* @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
*/
@ExperimentalMaterialApi
class ModalBottomSheetState(
initialValue: ModalBottomSheetValue,
animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
+ internal val isSkipHalfExpanded: Boolean,
confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
) : SwipeableState<ModalBottomSheetValue>(
initialValue = initialValue,
@@ -106,19 +114,36 @@
val isVisible: Boolean
get() = currentValue != Hidden
- internal val isHalfExpandedEnabled: Boolean
+ internal val hasHalfExpandedState: Boolean
get() = anchors.values.contains(HalfExpanded)
+ constructor(
+ initialValue: ModalBottomSheetValue,
+ animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
+ confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
+ ) : this(initialValue, animationSpec, isSkipHalfExpanded = false, confirmStateChange)
+
+ init {
+ if (isSkipHalfExpanded) {
+ require(initialValue != HalfExpanded) {
+ "The initial value must not be set to HalfExpanded if skipHalfExpanded is set to" +
+ " true."
+ }
+ }
+ }
+
/**
- * Show the bottom sheet with animation and suspend until it's shown. If half expand is
- * enabled, the bottom sheet will be half expanded. Otherwise it will be fully expanded.
+ * Show the bottom sheet with animation and suspend until it's shown. If the sheet is taller
+ * than 50% of the parent's height, the bottom sheet will be half expanded. Otherwise it will be
+ * fully expanded.
*
* @throws [CancellationException] if the animation is interrupted
*/
suspend fun show() {
- val targetValue =
- if (isHalfExpandedEnabled) HalfExpanded
- else Expanded
+ val targetValue = when {
+ hasHalfExpandedState -> HalfExpanded
+ else -> Expanded
+ }
animateTo(targetValue = targetValue)
}
@@ -129,7 +154,7 @@
* @throws [CancellationException] if the animation is interrupted
*/
internal suspend fun halfExpand() {
- if (!isHalfExpandedEnabled) {
+ if (!hasHalfExpandedState) {
return
}
animateTo(HalfExpanded)
@@ -159,6 +184,7 @@
*/
fun Saver(
animationSpec: AnimationSpec<Float>,
+ skipHalfExpanded: Boolean,
confirmStateChange: (ModalBottomSheetValue) -> Boolean
): Saver<ModalBottomSheetState, *> = Saver(
save = { it.currentValue },
@@ -166,10 +192,71 @@
ModalBottomSheetState(
initialValue = it,
animationSpec = animationSpec,
+ isSkipHalfExpanded = skipHalfExpanded,
confirmStateChange = confirmStateChange
)
}
)
+
+ /**
+ * The default [Saver] implementation for [ModalBottomSheetState].
+ */
+ @Deprecated(
+ message = "Please specify the skipHalfExpanded parameter",
+ replaceWith = ReplaceWith(
+ "ModalBottomSheetState.Saver(" +
+ "animationSpec = animationSpec," +
+ "skipHalfExpanded = ," +
+ "confirmStateChange = confirmStateChange" +
+ ")"
+ )
+ )
+ fun Saver(
+ animationSpec: AnimationSpec<Float>,
+ confirmStateChange: (ModalBottomSheetValue) -> Boolean
+ ): Saver<ModalBottomSheetState, *> = Saver(
+ animationSpec = animationSpec,
+ skipHalfExpanded = false,
+ confirmStateChange = confirmStateChange
+ )
+ }
+}
+
+/**
+ * Create a [ModalBottomSheetState] and [remember] it.
+ *
+ * @param initialValue The initial value of the state.
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param skipHalfExpanded Whether the half expanded state, if the sheet is tall enough, should
+ * be skipped. If true, the sheet will always expand to the [Expanded] state and move to the
+ * [Hidden] state when hiding the sheet, either programmatically or by user interaction.
+ * <b>Must not be set to true if the [initialValue] is [ModalBottomSheetValue.HalfExpanded].</b>
+ * If supplied with [ModalBottomSheetValue.HalfExpanded] for the [initialValue], an
+ * [IllegalArgumentException] will be thrown.
+ * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ */
+@Composable
+@ExperimentalMaterialApi
+fun rememberModalBottomSheetState(
+ initialValue: ModalBottomSheetValue,
+ animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
+ skipHalfExpanded: Boolean,
+ confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
+): ModalBottomSheetState {
+ return rememberSaveable(
+ initialValue, animationSpec, skipHalfExpanded, confirmStateChange,
+ saver = ModalBottomSheetState.Saver(
+ animationSpec = animationSpec,
+ skipHalfExpanded = skipHalfExpanded,
+ confirmStateChange = confirmStateChange
+ )
+ ) {
+ ModalBottomSheetState(
+ initialValue = initialValue,
+ animationSpec = animationSpec,
+ isSkipHalfExpanded = skipHalfExpanded,
+ confirmStateChange = confirmStateChange
+ )
}
}
@@ -186,20 +273,12 @@
initialValue: ModalBottomSheetValue,
animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
-): ModalBottomSheetState {
- return rememberSaveable(
- saver = ModalBottomSheetState.Saver(
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
- )
- ) {
- ModalBottomSheetState(
- initialValue = initialValue,
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
- )
- }
-}
+): ModalBottomSheetState = rememberModalBottomSheetState(
+ initialValue = initialValue,
+ animationSpec = animationSpec,
+ skipHalfExpanded = false,
+ confirmStateChange = confirmStateChange
+)
/**
* <a href="https://material.io/components/sheets-bottom#modal-bottom-sheet" class="external" target="_blank">Material Design modal bottom sheet</a>.
@@ -292,7 +371,7 @@
}
true
}
- } else if (sheetState.isHalfExpandedEnabled) {
+ } else if (sheetState.hasHalfExpandedState) {
collapse {
if (sheetState.confirmStateChange(HalfExpanded)) {
scope.launch { sheetState.halfExpand() }
@@ -321,7 +400,7 @@
): Modifier {
val sheetHeight = sheetHeightState.value
val modifier = if (sheetHeight != null) {
- val anchors = if (sheetHeight < fullHeight / 2) {
+ val anchors = if (sheetHeight < fullHeight / 2 || sheetState.isSkipHalfExpanded) {
mapOf(
fullHeight to Hidden,
fullHeight - sheetHeight to Expanded
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 1eb7bd7..ecb9fc8 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -253,6 +253,51 @@
public final class ScaffoldKt {
}
+ @androidx.compose.runtime.Stable public interface SnackbarData {
+ method public void dismiss();
+ method public androidx.compose.material3.SnackbarVisuals getVisuals();
+ method public void performAction();
+ property public abstract androidx.compose.material3.SnackbarVisuals visuals;
+ }
+
+ public enum SnackbarDuration {
+ enum_constant public static final androidx.compose.material3.SnackbarDuration Indefinite;
+ enum_constant public static final androidx.compose.material3.SnackbarDuration Long;
+ enum_constant public static final androidx.compose.material3.SnackbarDuration Short;
+ }
+
+ public final class SnackbarHostKt {
+ method @androidx.compose.runtime.Composable public static void SnackbarHost(androidx.compose.material3.SnackbarHostState hostState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SnackbarData,kotlin.Unit> snackbar);
+ }
+
+ @androidx.compose.runtime.Stable public final class SnackbarHostState {
+ ctor public SnackbarHostState();
+ method public androidx.compose.material3.SnackbarData? getCurrentSnackbarData();
+ method public suspend Object? showSnackbar(String message, optional String? actionLabel, optional boolean withDismissAction, optional androidx.compose.material3.SnackbarDuration duration, optional kotlin.coroutines.Continuation<? super androidx.compose.material3.SnackbarResult> p);
+ property public final androidx.compose.material3.SnackbarData? currentSnackbarData;
+ }
+
+ public final class SnackbarKt {
+ method @androidx.compose.runtime.Composable public static void Snackbar(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissAction, optional boolean actionOnNewLine, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Snackbar(androidx.compose.material3.SnackbarData snackbarData, optional androidx.compose.ui.Modifier modifier, optional boolean actionOnNewLine, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional long actionColor);
+ }
+
+ public enum SnackbarResult {
+ enum_constant public static final androidx.compose.material3.SnackbarResult ActionPerformed;
+ enum_constant public static final androidx.compose.material3.SnackbarResult Dismissed;
+ }
+
+ @androidx.compose.runtime.Stable public interface SnackbarVisuals {
+ method public String? getActionLabel();
+ method public androidx.compose.material3.SnackbarDuration getDuration();
+ method public String getMessage();
+ method public boolean getWithDismissAction();
+ property public abstract String? actionLabel;
+ property public abstract androidx.compose.material3.SnackbarDuration duration;
+ property public abstract String message;
+ property public abstract boolean withDismissAction;
+ }
+
public final class Strings_androidKt {
}
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 5c90c17..5f8321f 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -305,7 +305,53 @@
}
public final class ScaffoldKt {
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+ }
+
+ @androidx.compose.runtime.Stable public interface SnackbarData {
+ method public void dismiss();
+ method public androidx.compose.material3.SnackbarVisuals getVisuals();
+ method public void performAction();
+ property public abstract androidx.compose.material3.SnackbarVisuals visuals;
+ }
+
+ public enum SnackbarDuration {
+ enum_constant public static final androidx.compose.material3.SnackbarDuration Indefinite;
+ enum_constant public static final androidx.compose.material3.SnackbarDuration Long;
+ enum_constant public static final androidx.compose.material3.SnackbarDuration Short;
+ }
+
+ public final class SnackbarHostKt {
+ method @androidx.compose.runtime.Composable public static void SnackbarHost(androidx.compose.material3.SnackbarHostState hostState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SnackbarData,kotlin.Unit> snackbar);
+ }
+
+ @androidx.compose.runtime.Stable public final class SnackbarHostState {
+ ctor public SnackbarHostState();
+ method public androidx.compose.material3.SnackbarData? getCurrentSnackbarData();
+ method public suspend Object? showSnackbar(String message, optional String? actionLabel, optional boolean withDismissAction, optional androidx.compose.material3.SnackbarDuration duration, optional kotlin.coroutines.Continuation<? super androidx.compose.material3.SnackbarResult> p);
+ method @androidx.compose.material3.ExperimentalMaterial3Api public suspend Object? showSnackbar(androidx.compose.material3.SnackbarVisuals visuals, kotlin.coroutines.Continuation<? super androidx.compose.material3.SnackbarResult> p);
+ property public final androidx.compose.material3.SnackbarData? currentSnackbarData;
+ }
+
+ public final class SnackbarKt {
+ method @androidx.compose.runtime.Composable public static void Snackbar(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissAction, optional boolean actionOnNewLine, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Snackbar(androidx.compose.material3.SnackbarData snackbarData, optional androidx.compose.ui.Modifier modifier, optional boolean actionOnNewLine, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional long actionColor);
+ }
+
+ public enum SnackbarResult {
+ enum_constant public static final androidx.compose.material3.SnackbarResult ActionPerformed;
+ enum_constant public static final androidx.compose.material3.SnackbarResult Dismissed;
+ }
+
+ @androidx.compose.runtime.Stable public interface SnackbarVisuals {
+ method public String? getActionLabel();
+ method public androidx.compose.material3.SnackbarDuration getDuration();
+ method public String getMessage();
+ method public boolean getWithDismissAction();
+ property public abstract String? actionLabel;
+ property public abstract androidx.compose.material3.SnackbarDuration duration;
+ property public abstract String message;
+ property public abstract boolean withDismissAction;
}
public final class Strings_androidKt {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 1eb7bd7..ecb9fc8 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -253,6 +253,51 @@
public final class ScaffoldKt {
}
+ @androidx.compose.runtime.Stable public interface SnackbarData {
+ method public void dismiss();
+ method public androidx.compose.material3.SnackbarVisuals getVisuals();
+ method public void performAction();
+ property public abstract androidx.compose.material3.SnackbarVisuals visuals;
+ }
+
+ public enum SnackbarDuration {
+ enum_constant public static final androidx.compose.material3.SnackbarDuration Indefinite;
+ enum_constant public static final androidx.compose.material3.SnackbarDuration Long;
+ enum_constant public static final androidx.compose.material3.SnackbarDuration Short;
+ }
+
+ public final class SnackbarHostKt {
+ method @androidx.compose.runtime.Composable public static void SnackbarHost(androidx.compose.material3.SnackbarHostState hostState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SnackbarData,kotlin.Unit> snackbar);
+ }
+
+ @androidx.compose.runtime.Stable public final class SnackbarHostState {
+ ctor public SnackbarHostState();
+ method public androidx.compose.material3.SnackbarData? getCurrentSnackbarData();
+ method public suspend Object? showSnackbar(String message, optional String? actionLabel, optional boolean withDismissAction, optional androidx.compose.material3.SnackbarDuration duration, optional kotlin.coroutines.Continuation<? super androidx.compose.material3.SnackbarResult> p);
+ property public final androidx.compose.material3.SnackbarData? currentSnackbarData;
+ }
+
+ public final class SnackbarKt {
+ method @androidx.compose.runtime.Composable public static void Snackbar(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissAction, optional boolean actionOnNewLine, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Snackbar(androidx.compose.material3.SnackbarData snackbarData, optional androidx.compose.ui.Modifier modifier, optional boolean actionOnNewLine, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional long actionColor);
+ }
+
+ public enum SnackbarResult {
+ enum_constant public static final androidx.compose.material3.SnackbarResult ActionPerformed;
+ enum_constant public static final androidx.compose.material3.SnackbarResult Dismissed;
+ }
+
+ @androidx.compose.runtime.Stable public interface SnackbarVisuals {
+ method public String? getActionLabel();
+ method public androidx.compose.material3.SnackbarDuration getDuration();
+ method public String getMessage();
+ method public boolean getWithDismissAction();
+ property public abstract String? actionLabel;
+ property public abstract androidx.compose.material3.SnackbarDuration duration;
+ property public abstract String message;
+ property public abstract boolean withDismissAction;
+ }
+
public final class Strings_androidKt {
}
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
index baab98a..96529d0 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
@@ -169,6 +169,19 @@
examples = RadioButtonsExamples
)
+private val Snackbars = Component(
+ id = nextId(),
+ name = "Snackbars",
+ description = "Snackbars provide brief messages about app processes at the bottom of the " +
+ "screen.",
+ // No snackbar icon
+ tintIcon = true,
+ guidelinesUrl = "$ComponentGuidelinesUrl/snackbars",
+ docsUrl = "$DocsUrl#snackbar",
+ sourceUrl = "$Material3SourceUrl/Snackbar.kt",
+ examples = SnackbarsExamples
+)
+
private val TopAppBar = Component(
id = nextId(),
name = "Top app bar",
@@ -193,5 +206,6 @@
NavigationDrawer,
NavigationRail,
RadioButtons,
+ Snackbars,
TopAppBar
)
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 681d22b..4db8088 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -25,8 +25,8 @@
import androidx.compose.material3.samples.ButtonWithIconSample
import androidx.compose.material3.samples.CheckboxSample
import androidx.compose.material3.samples.ColorSchemeSample
-import androidx.compose.material3.samples.EnterAlwaysSmallTopAppBar
import androidx.compose.material3.samples.ElevatedButtonSample
+import androidx.compose.material3.samples.EnterAlwaysSmallTopAppBar
import androidx.compose.material3.samples.ExitUntilCollapsedLargeTopAppBar
import androidx.compose.material3.samples.ExitUntilCollapsedMediumTopAppBar
import androidx.compose.material3.samples.ExtendedFloatingActionButtonSample
@@ -35,14 +35,18 @@
import androidx.compose.material3.samples.LargeFloatingActionButtonSample
import androidx.compose.material3.samples.NavigationBarSample
import androidx.compose.material3.samples.NavigationBarWithOnlySelectedLabelsSample
+import androidx.compose.material3.samples.NavigationDrawerSample
import androidx.compose.material3.samples.NavigationRailBottomAlignSample
import androidx.compose.material3.samples.NavigationRailSample
import androidx.compose.material3.samples.NavigationRailWithOnlySelectedLabelsSample
import androidx.compose.material3.samples.OutlinedButtonSample
-import androidx.compose.material3.samples.NavigationDrawerSample
import androidx.compose.material3.samples.PinnedSmallTopAppBar
import androidx.compose.material3.samples.RadioButtonSample
import androidx.compose.material3.samples.RadioGroupSample
+import androidx.compose.material3.samples.ScaffoldWithCoroutinesSnackbar
+import androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
+import androidx.compose.material3.samples.ScaffoldWithIndefiniteSnackbar
+import androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
import androidx.compose.material3.samples.SimpleCenterAlignedTopAppBar
import androidx.compose.material3.samples.SimpleSmallTopAppBar
import androidx.compose.material3.samples.SmallFloatingActionButtonSample
@@ -275,3 +279,36 @@
RadioGroupSample()
},
)
+
+private const val SnackbarsExampleDescription = "Snackbars examples"
+private const val SnackbarsExampleSourceUrl = "$SampleSourceUrl/ScaffoldSamples.kt"
+val SnackbarsExamples = listOf(
+ Example(
+ name = ::ScaffoldWithSimpleSnackbar.name,
+ description = SnackbarsExampleDescription,
+ sourceUrl = SnackbarsExampleSourceUrl
+ ) {
+ ScaffoldWithSimpleSnackbar()
+ },
+ Example(
+ name = ::ScaffoldWithIndefiniteSnackbar.name,
+ description = SnackbarsExampleDescription,
+ sourceUrl = SnackbarsExampleSourceUrl
+ ) {
+ ScaffoldWithIndefiniteSnackbar()
+ },
+ Example(
+ name = ::ScaffoldWithCustomSnackbar.name,
+ description = SnackbarsExampleDescription,
+ sourceUrl = SnackbarsExampleSourceUrl
+ ) {
+ ScaffoldWithCustomSnackbar()
+ },
+ Example(
+ name = ::ScaffoldWithCoroutinesSnackbar.name,
+ description = SnackbarsExampleDescription,
+ sourceUrl = SnackbarsExampleSourceUrl
+ ) {
+ ScaffoldWithCoroutinesSnackbar()
+ }
+)
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScaffoldSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScaffoldSamples.kt
index e0eb904..4bd58d6 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScaffoldSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScaffoldSamples.kt
@@ -18,24 +18,47 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallTopAppBar
+import androidx.compose.material3.Snackbar
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.SnackbarResult
+import androidx.compose.material3.SnackbarVisuals
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Sampled
@@ -83,3 +106,189 @@
}
)
}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun ScaffoldWithSimpleSnackbar() {
+ val snackbarHostState = remember { SnackbarHostState() }
+ val scope = rememberCoroutineScope()
+ Scaffold(
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ floatingActionButton = {
+ var clickCount by remember { mutableStateOf(0) }
+ ExtendedFloatingActionButton(
+ text = { Text("Show snackbar") },
+ onClick = {
+ // show snackbar as a suspend function
+ scope.launch {
+ snackbarHostState.showSnackbar(
+ "Snackbar # ${++clickCount}"
+ )
+ }
+ }
+ )
+ },
+ content = { innerPadding ->
+ Text(
+ text = "Body content",
+ modifier = Modifier.padding(innerPadding).fillMaxSize().wrapContentSize()
+ )
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun ScaffoldWithIndefiniteSnackbar() {
+ val snackbarHostState = remember { SnackbarHostState() }
+ val scope = rememberCoroutineScope()
+ Scaffold(
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ floatingActionButton = {
+ var clickCount by remember { mutableStateOf(0) }
+ ExtendedFloatingActionButton(
+ text = { Text("Show snackbar") },
+ onClick = {
+ // show snackbar as a suspend function
+ scope.launch {
+ snackbarHostState.showSnackbar(
+ message = "Snackbar # ${++clickCount}",
+ actionLabel = "Action",
+ withDismissAction = true,
+ duration = SnackbarDuration.Indefinite
+ )
+ }
+ }
+ )
+ },
+ content = { innerPadding ->
+ Text(
+ text = "Body content",
+ modifier = Modifier.padding(innerPadding).fillMaxSize().wrapContentSize()
+ )
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun ScaffoldWithCustomSnackbar() {
+ class SnackbarVisualsWithError(
+ override val message: String,
+ val isError: Boolean
+ ) : SnackbarVisuals {
+ override val actionLabel: String
+ get() = if (isError) "Error" else "OK"
+ override val withDismissAction: Boolean
+ get() = false
+ override val duration: SnackbarDuration
+ get() = SnackbarDuration.Long
+ }
+
+ val snackbarHostState = remember { SnackbarHostState() }
+ val scope = rememberCoroutineScope()
+ Scaffold(
+ snackbarHost = {
+ // reuse default SnackbarHost to have default animation and timing handling
+ SnackbarHost(snackbarHostState) { data ->
+ // custom snackbar with the custom action button color and border
+ val isError = (data.visuals as? SnackbarVisualsWithError)?.isError ?: false
+ val buttonColor = if (isError) {
+ ButtonDefaults.textButtonColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ contentColor = MaterialTheme.colorScheme.error
+ )
+ } else {
+ ButtonDefaults.textButtonColors(
+ contentColor = MaterialTheme.colorScheme.inversePrimary
+ )
+ }
+
+ Snackbar(
+ modifier = Modifier
+ .border(2.dp, MaterialTheme.colorScheme.secondary)
+ .padding(12.dp),
+ action = {
+ TextButton(
+ onClick = { if (isError) data.dismiss() else data.performAction() },
+ colors = buttonColor
+ ) { Text(data.visuals.actionLabel ?: "") }
+ }
+ ) {
+ Text(data.visuals.message)
+ }
+ }
+ },
+ floatingActionButton = {
+ var clickCount by remember { mutableStateOf(0) }
+ ExtendedFloatingActionButton(
+ text = { Text("Show snackbar") },
+ onClick = {
+ scope.launch {
+ snackbarHostState.showSnackbar(
+ SnackbarVisualsWithError(
+ "Snackbar # ${++clickCount}",
+ isError = clickCount % 2 != 0
+ )
+ )
+ }
+ }
+ )
+ },
+ content = { innerPadding ->
+ Text(
+ text = "Custom Snackbar Demo",
+ modifier = Modifier.padding(innerPadding).fillMaxSize().wrapContentSize()
+ )
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun ScaffoldWithCoroutinesSnackbar() {
+ // decouple snackbar host state from scaffold state for demo purposes
+ // this state, channel and flow is for demo purposes to demonstrate business logic layer
+ val snackbarHostState = remember { SnackbarHostState() }
+ // we allow only one snackbar to be in the queue here, hence conflated
+ val channel = remember { Channel<Int>(Channel.CONFLATED) }
+ LaunchedEffect(channel) {
+ channel.receiveAsFlow().collect { index ->
+ val result = snackbarHostState.showSnackbar(
+ message = "Snackbar # $index",
+ actionLabel = "Action on $index"
+ )
+ when (result) {
+ SnackbarResult.ActionPerformed -> {
+ /* action has been performed */
+ }
+ SnackbarResult.Dismissed -> {
+ /* dismissed, no action needed */
+ }
+ }
+ }
+ }
+ Scaffold(
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ floatingActionButton = {
+ var clickCount by remember { mutableStateOf(0) }
+ ExtendedFloatingActionButton(
+ text = { Text("Show snackbar") },
+ onClick = {
+ // offset snackbar data to the business logic
+ channel.trySend(++clickCount)
+ }
+ )
+ },
+ content = { innerPadding ->
+ Text(
+ "Snackbar demo",
+ modifier = Modifier.padding(innerPadding).fillMaxSize().wrapContentSize()
+ )
+ }
+ )
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogScreenshotTest.kt
index 7078717..f6b22f5 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogScreenshotTest.kt
@@ -46,31 +46,29 @@
@Test
fun alertDialog_lightTheme() {
- composeTestRule.setContent {
- MaterialTheme {
- AlertDialog(
- onDismissRequest = {},
- title = {
- Text(text = "Title")
- },
- text = {
- Text(
- "This area typically contains the supportive text " +
- "which presents the details regarding the Dialog's purpose."
- )
- },
- confirmButton = {
- TextButton(onClick = { /* doSomething() */ }) {
- Text("Confirm")
- }
- },
- dismissButton = {
- TextButton(onClick = { /* doSomething() */ }) {
- Text("Dismiss")
- }
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ AlertDialog(
+ onDismissRequest = {},
+ title = {
+ Text(text = "Title")
+ },
+ text = {
+ Text(
+ "This area typically contains the supportive text " +
+ "which presents the details regarding the Dialog's purpose."
+ )
+ },
+ confirmButton = {
+ TextButton(onClick = { /* doSomething() */ }) {
+ Text("Confirm")
}
- )
- }
+ },
+ dismissButton = {
+ TextButton(onClick = { /* doSomething() */ }) {
+ Text("Dismiss")
+ }
+ }
+ )
}
assertAppBarAgainstGolden(goldenIdentifier = "alertDialog_lightTheme")
@@ -78,31 +76,29 @@
@Test
fun alertDialog_darkTheme() {
- composeTestRule.setContent {
- MaterialTheme(colorScheme = darkColorScheme()) {
- AlertDialog(
- onDismissRequest = {},
- title = {
- Text(text = "Title")
- },
- text = {
- Text(
- "This area typically contains the supportive text " +
- "which presents the details regarding the Dialog's purpose."
- )
- },
- confirmButton = {
- TextButton(onClick = { /* doSomething() */ }) {
- Text("Confirm")
- }
- },
- dismissButton = {
- TextButton(onClick = { /* doSomething() */ }) {
- Text("Dismiss")
- }
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ AlertDialog(
+ onDismissRequest = {},
+ title = {
+ Text(text = "Title")
+ },
+ text = {
+ Text(
+ "This area typically contains the supportive text " +
+ "which presents the details regarding the Dialog's purpose."
+ )
+ },
+ confirmButton = {
+ TextButton(onClick = { /* doSomething() */ }) {
+ Text("Confirm")
}
- )
- }
+ },
+ dismissButton = {
+ TextButton(onClick = { /* doSomething() */ }) {
+ Text("Dismiss")
+ }
+ }
+ )
}
assertAppBarAgainstGolden(goldenIdentifier = "alertDialog_darkTheme")
@@ -110,32 +106,30 @@
@Test
fun alertDialog_withIcon_lightTheme() {
- composeTestRule.setContent {
- MaterialTheme {
- AlertDialog(
- onDismissRequest = {},
- icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
- title = {
- Text(text = "Title")
- },
- text = {
- Text(
- "This area typically contains the supportive text " +
- "which presents the details regarding the Dialog's purpose."
- )
- },
- confirmButton = {
- TextButton(onClick = { /* doSomething() */ }) {
- Text("Confirm")
- }
- },
- dismissButton = {
- TextButton(onClick = { /* doSomething() */ }) {
- Text("Dismiss")
- }
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ AlertDialog(
+ onDismissRequest = {},
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
+ title = {
+ Text(text = "Title")
+ },
+ text = {
+ Text(
+ "This area typically contains the supportive text " +
+ "which presents the details regarding the Dialog's purpose."
+ )
+ },
+ confirmButton = {
+ TextButton(onClick = { /* doSomething() */ }) {
+ Text("Confirm")
}
- )
- }
+ },
+ dismissButton = {
+ TextButton(onClick = { /* doSomething() */ }) {
+ Text("Dismiss")
+ }
+ }
+ )
}
assertAppBarAgainstGolden(goldenIdentifier = "alertDialog_withIcon_lightTheme")
@@ -143,32 +137,30 @@
@Test
fun alertDialog_withIcon_darkTheme() {
- composeTestRule.setContent {
- MaterialTheme(colorScheme = darkColorScheme()) {
- AlertDialog(
- onDismissRequest = {},
- icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
- title = {
- Text(text = "Title")
- },
- text = {
- Text(
- "This area typically contains the supportive text " +
- "which presents the details regarding the Dialog's purpose."
- )
- },
- confirmButton = {
- TextButton(onClick = { /* doSomething() */ }) {
- Text("Confirm")
- }
- },
- dismissButton = {
- TextButton(onClick = { /* doSomething() */ }) {
- Text("Dismiss")
- }
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ AlertDialog(
+ onDismissRequest = {},
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
+ title = {
+ Text(text = "Title")
+ },
+ text = {
+ Text(
+ "This area typically contains the supportive text " +
+ "which presents the details regarding the Dialog's purpose."
+ )
+ },
+ confirmButton = {
+ TextButton(onClick = { /* doSomething() */ }) {
+ Text("Confirm")
}
- )
- }
+ },
+ dismissButton = {
+ TextButton(onClick = { /* doSomething() */ }) {
+ Text("Dismiss")
+ }
+ }
+ )
}
assertAppBarAgainstGolden(goldenIdentifier = "alertDialog_withIcon_darkTheme")
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
index 616e103..3221de1 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
@@ -193,7 +193,7 @@
@Test
fun alertDialog_withIcon_positioning() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
AlertDialog(
onDismissRequest = {},
icon = {
@@ -273,7 +273,7 @@
@Test
fun alertDialog_positioning() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
AlertDialog(
onDismissRequest = {},
title = { Text(text = "Title", modifier = Modifier.testTag(TitleTestTag)) },
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
index 215cae5..2dc6046 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
@@ -50,31 +50,29 @@
@Test
fun smallAppBar_lightTheme() {
- composeTestRule.setContent {
- MaterialTheme {
- Box(Modifier.testTag(TestTag)) {
- SmallTopAppBar(
- navigationIcon = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.ArrowBack,
- contentDescription = "Back"
- )
- }
- },
- title = {
- Text("Title")
- },
- actions = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.Favorite,
- contentDescription = "Like"
- )
- }
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ Box(Modifier.testTag(TestTag)) {
+ SmallTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
}
- )
- }
+ },
+ title = {
+ Text("Title")
+ },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = "Like"
+ )
+ }
+ }
+ )
}
}
@@ -83,31 +81,29 @@
@Test
fun smallAppBar_darkTheme() {
- composeTestRule.setContent {
- MaterialTheme(colorScheme = darkColorScheme()) {
- Box(Modifier.testTag(TestTag)) {
- SmallTopAppBar(
- navigationIcon = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.ArrowBack,
- contentDescription = "Back"
- )
- }
- },
- title = {
- Text("Title")
- },
- actions = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.Favorite,
- contentDescription = "Like"
- )
- }
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ Box(Modifier.testTag(TestTag)) {
+ SmallTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
}
- )
- }
+ },
+ title = {
+ Text("Title")
+ },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = "Like"
+ )
+ }
+ }
+ )
}
}
@@ -116,31 +112,29 @@
@Test
fun centerAlignedAppBar_lightTheme() {
- composeTestRule.setContent {
- MaterialTheme {
- Box(Modifier.testTag(TestTag)) {
- CenterAlignedTopAppBar(
- navigationIcon = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.ArrowBack,
- contentDescription = "Back"
- )
- }
- },
- title = {
- Text("Title")
- },
- actions = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.Favorite,
- contentDescription = "Like"
- )
- }
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ Box(Modifier.testTag(TestTag)) {
+ CenterAlignedTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
}
- )
- }
+ },
+ title = {
+ Text("Title")
+ },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = "Like"
+ )
+ }
+ }
+ )
}
}
@@ -149,31 +143,29 @@
@Test
fun centerAlignedAppBar_darkTheme() {
- composeTestRule.setContent {
- MaterialTheme(colorScheme = darkColorScheme()) {
- Box(Modifier.testTag(TestTag)) {
- CenterAlignedTopAppBar(
- navigationIcon = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.ArrowBack,
- contentDescription = "Back"
- )
- }
- },
- title = {
- Text("Title")
- },
- actions = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.Favorite,
- contentDescription = "Like"
- )
- }
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ Box(Modifier.testTag(TestTag)) {
+ CenterAlignedTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
}
- )
- }
+ },
+ title = {
+ Text("Title")
+ },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = "Like"
+ )
+ }
+ }
+ )
}
}
@@ -182,31 +174,29 @@
@Test
fun mediumAppBar_lightTheme() {
- composeTestRule.setContent {
- MaterialTheme {
- Box(Modifier.testTag(TestTag)) {
- MediumTopAppBar(
- navigationIcon = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.ArrowBack,
- contentDescription = "Back"
- )
- }
- },
- title = {
- Text("Title")
- },
- actions = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.Favorite,
- contentDescription = "Like"
- )
- }
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ Box(Modifier.testTag(TestTag)) {
+ MediumTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
}
- )
- }
+ },
+ title = {
+ Text("Title")
+ },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = "Like"
+ )
+ }
+ }
+ )
}
}
@@ -215,31 +205,29 @@
@Test
fun mediumAppBar_darkTheme() {
- composeTestRule.setContent {
- MaterialTheme(colorScheme = darkColorScheme()) {
- Box(Modifier.testTag(TestTag)) {
- MediumTopAppBar(
- navigationIcon = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.ArrowBack,
- contentDescription = "Back"
- )
- }
- },
- title = {
- Text("Title")
- },
- actions = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.Favorite,
- contentDescription = "Like"
- )
- }
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ Box(Modifier.testTag(TestTag)) {
+ MediumTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
}
- )
- }
+ },
+ title = {
+ Text("Title")
+ },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = "Like"
+ )
+ }
+ }
+ )
}
}
@@ -248,31 +236,29 @@
@Test
fun largeAppBar_lightTheme() {
- composeTestRule.setContent {
- MaterialTheme {
- Box(Modifier.testTag(TestTag)) {
- LargeTopAppBar(
- navigationIcon = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.ArrowBack,
- contentDescription = "Back"
- )
- }
- },
- title = {
- Text("Title")
- },
- actions = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.Favorite,
- contentDescription = "Like"
- )
- }
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ Box(Modifier.testTag(TestTag)) {
+ LargeTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
}
- )
- }
+ },
+ title = {
+ Text("Title")
+ },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = "Like"
+ )
+ }
+ }
+ )
}
}
@@ -281,31 +267,29 @@
@Test
fun largeAppBar_darkTheme() {
- composeTestRule.setContent {
- MaterialTheme(colorScheme = darkColorScheme()) {
- Box(Modifier.testTag(TestTag)) {
- LargeTopAppBar(
- navigationIcon = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.ArrowBack,
- contentDescription = "Back"
- )
- }
- },
- title = {
- Text("Title")
- },
- actions = {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- imageVector = Icons.Filled.Favorite,
- contentDescription = "Like"
- )
- }
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ Box(Modifier.testTag(TestTag)) {
+ LargeTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
}
- )
- }
+ },
+ title = {
+ Text("Title")
+ },
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = "Like"
+ )
+ }
+ }
+ )
}
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
index dd71a66..eb9b182 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
@@ -19,6 +19,11 @@
import android.os.Build
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.tokens.TopAppBarLarge
import androidx.compose.material3.tokens.TopAppBarMedium
import androidx.compose.material3.tokens.TopAppBarSmall
@@ -29,6 +34,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
@@ -47,6 +53,9 @@
import androidx.compose.ui.test.onLast
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -80,7 +89,7 @@
@Test
fun smallTopAppBar_withTitle() {
val title = "Title"
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
SmallTopAppBar(title = { Text(title) })
}
@@ -90,7 +99,7 @@
@Test
fun smallTopAppBar_default_positioning() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
SmallTopAppBar(
navigationIcon = {
@@ -110,7 +119,7 @@
@Test
fun smallTopAppBar_noNavigationIcon_positioning() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
SmallTopAppBar(
title = {
@@ -129,7 +138,7 @@
fun smallTopAppBar_titleDefaultStyle() {
var textStyle: TextStyle? = null
var expectedTextStyle: TextStyle? = null
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
SmallTopAppBar(title = {
Text("Title")
textStyle = LocalTextStyle.current
@@ -153,7 +162,7 @@
var expectedActionsColor: Color = Color.Unspecified
var expectedContainerColor: Color = Color.Unspecified
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
SmallTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
navigationIcon = {
@@ -204,7 +213,7 @@
var expectedScrolledContainerColor: Color = Color.Unspecified
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
SmallTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
title = {
@@ -234,7 +243,7 @@
val scrollOffsetDp = 20.dp
var scrollOffsetPx = 0f
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
scrollOffsetPx = with(LocalDensity.current) { scrollOffsetDp.toPx() }
SmallTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
@@ -265,7 +274,7 @@
@Test
fun centerAlignedTopAppBar_withTitle() {
val title = "Title"
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
CenterAlignedTopAppBar(title = { Text(title) })
}
@@ -275,7 +284,7 @@
@Test
fun centerAlignedTopAppBar_default_positioning() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
CenterAlignedTopAppBar(
navigationIcon = {
@@ -295,7 +304,7 @@
@Test
fun centerAlignedTopAppBar_noNavigationIcon_positioning() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
CenterAlignedTopAppBar(
title = {
@@ -314,7 +323,7 @@
fun centerAlignedTopAppBar_titleDefaultStyle() {
var textStyle: TextStyle? = null
var expectedTextStyle: TextStyle? = null
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
CenterAlignedTopAppBar(
title = {
Text("Title")
@@ -341,7 +350,7 @@
var expectedActionsColor: Color = Color.Unspecified
var expectedContainerColor: Color = Color.Unspecified
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
CenterAlignedTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
navigationIcon = {
@@ -389,7 +398,7 @@
var expectedScrolledContainerColor: Color = Color.Unspecified
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
CenterAlignedTopAppBar(
modifier = Modifier.testTag(TopAppBarTestTag),
title = {
@@ -423,7 +432,7 @@
@Test
fun mediumTopAppBar_expanded_positioning() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
MediumTopAppBar(
navigationIcon = {
@@ -502,7 +511,7 @@
@Test
fun largeTopAppBar_expanded_positioning() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(TopAppBarTestTag)) {
LargeTopAppBar(
navigationIcon = {
@@ -569,6 +578,101 @@
)
}
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun topAppBar_enterAlways_allowHorizontalScroll() {
+ lateinit var state: LazyListState
+ rule.setMaterialContent(lightColorScheme()) {
+ state = rememberLazyListState()
+ MultiPageContent(TopAppBarDefaults.enterAlwaysScrollBehavior(), state)
+ }
+
+ rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+ }
+
+ rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun topAppBar_exitUntilCollapsed_allowHorizontalScroll() {
+ lateinit var state: LazyListState
+ rule.setMaterialContent(lightColorScheme()) {
+ state = rememberLazyListState()
+ MultiPageContent(
+ TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
+ rememberSplineBasedDecay()
+ ), state
+ )
+ }
+
+ rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+ }
+
+ rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
+ fun topAppBar_pinned_allowHorizontalScroll() {
+ lateinit var state: LazyListState
+ rule.setMaterialContent(lightColorScheme()) {
+ state = rememberLazyListState()
+ MultiPageContent(TopAppBarDefaults.pinnedScrollBehavior(), state)
+ }
+
+ rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+ }
+
+ rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ private fun MultiPageContent(scrollBehavior: TopAppBarScrollBehavior, state: LazyListState) {
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ topBar = {
+ SmallTopAppBar(
+ modifier = Modifier.testTag(TopAppBarTestTag),
+ title = { Text(text = "Title") },
+ scrollBehavior = scrollBehavior,
+ )
+ }
+ ) { contentPadding ->
+ LazyRow(Modifier.fillMaxSize().testTag(LazyListTag), state) {
+ items(2) { page ->
+ LazyColumn(
+ modifier = Modifier.fillParentMaxSize(),
+ contentPadding = contentPadding
+ ) {
+ items(50) {
+ Text(
+ modifier = Modifier.fillParentMaxWidth(),
+ text = "Item #$page x $it"
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
/**
* Checks the app bar's components positioning when it's a [SmallTopAppBar], a
* [CenterAlignedTopAppBar], or a larger app bar that is scrolled up and collapsed into a small
@@ -717,7 +821,7 @@
var partiallyCollapsedOffsetPx = 0f
var fullyCollapsedOffsetPx = 0f
var scrollBehavior: TopAppBarScrollBehavior? = null
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
scrollBehavior =
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberSplineBasedDecay())
with(LocalDensity.current) {
@@ -773,7 +877,7 @@
var oneThirdCollapsedContainerColor: Color = Color.Unspecified
var titleContentColor: Color = Color.Unspecified
var scrollBehavior: TopAppBarScrollBehavior? = null
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
scrollBehavior =
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberSplineBasedDecay())
// Using the mediumTopAppBarColors for both Medium and Large top app bars, as the
@@ -861,6 +965,7 @@
private val AppBarStartAndEndPadding = 4.dp
private val AppBarTopAndBottomPadding = (TopAppBarSmall.SmallContainerHeight - FakeIconSize) / 2
+ private val LazyListTag = "lazyList"
private val TopAppBarTestTag = "bar"
private val NavigationIconTestTag = "navigationIcon"
private val TitleTestTag = "title"
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeScreenshotTest.kt
index dc2f4c2..a70e83b 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeScreenshotTest.kt
@@ -53,15 +53,13 @@
@Test
fun lightTheme_noContent() {
- composeTestRule.setContent {
- MaterialTheme(lightColorScheme()) {
- Box(
- Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
- contentAlignment = Alignment.Center
- ) {
- BadgedBox(badge = { Badge() }) {
- Icon(Icons.Filled.Favorite, null)
- }
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ Box(
+ Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
+ contentAlignment = Alignment.Center
+ ) {
+ BadgedBox(badge = { Badge() }) {
+ Icon(Icons.Filled.Favorite, null)
}
}
}
@@ -73,15 +71,13 @@
@Test
fun darkTheme_noContent() {
- composeTestRule.setContent {
- MaterialTheme(darkColorScheme()) {
- Box(
- Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
- contentAlignment = Alignment.Center
- ) {
- BadgedBox(badge = { Badge() }) {
- Icon(Icons.Filled.Favorite, null)
- }
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ Box(
+ Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
+ contentAlignment = Alignment.Center
+ ) {
+ BadgedBox(badge = { Badge() }) {
+ Icon(Icons.Filled.Favorite, null)
}
}
}
@@ -93,15 +89,13 @@
@Test
fun lightTheme_withContent() {
- composeTestRule.setContent {
- MaterialTheme(lightColorScheme()) {
- Box(
- Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
- contentAlignment = Alignment.Center
- ) {
- BadgedBox(badge = { Badge { Text("8") } }) {
- Icon(Icons.Filled.Favorite, null)
- }
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ Box(
+ Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
+ contentAlignment = Alignment.Center
+ ) {
+ BadgedBox(badge = { Badge { Text("8") } }) {
+ Icon(Icons.Filled.Favorite, null)
}
}
}
@@ -113,15 +107,13 @@
@Test
fun darkTheme_withContent() {
- composeTestRule.setContent {
- MaterialTheme(darkColorScheme()) {
- Box(
- Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
- contentAlignment = Alignment.Center
- ) {
- BadgedBox(badge = { Badge { Text("8") } }) {
- Icon(Icons.Filled.Favorite, null)
- }
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ Box(
+ Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
+ contentAlignment = Alignment.Center
+ ) {
+ BadgedBox(badge = { Badge { Text("8") } }) {
+ Icon(Icons.Filled.Favorite, null)
}
}
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeTest.kt
index 4714915..76b9ac0 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeTest.kt
@@ -104,7 +104,7 @@
@Test
fun badge_noContent_shape() {
var errorColor = Color.Unspecified
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
errorColor = MaterialTheme.colorScheme.fromToken(NavigationBar.BadgeColor)
Badge(modifier = Modifier.testTag(TestBadgeTag))
}
@@ -123,7 +123,7 @@
@Test
fun badgeBox_noContent_position() {
rule
- .setMaterialContent {
+ .setMaterialContent(lightColorScheme()) {
BadgedBox(badge = { Badge(Modifier.testTag(TestBadgeTag)) }) {
Icon(
icon,
@@ -137,8 +137,8 @@
val badgeBounds = badge.getUnclippedBoundsInRoot()
badge.assertPositionInRootIsEqualTo(
expectedLeft =
- anchorBounds.right + BadgeOffset +
- max((NavigationBar.BadgeSize - badgeBounds.width) / 2, 0.dp),
+ anchorBounds.right + BadgeOffset +
+ max((NavigationBar.BadgeSize - badgeBounds.width) / 2, 0.dp),
expectedTop = -badgeBounds.height / 2
)
}
@@ -146,7 +146,7 @@
@Test
fun badgeBox_shortContent_position() {
rule
- .setMaterialContent {
+ .setMaterialContent(lightColorScheme()) {
BadgedBox(badge = { Badge { Text("8") } }) {
Icon(
icon,
@@ -160,7 +160,7 @@
val badgeBounds = badge.getUnclippedBoundsInRoot()
badge.assertPositionInRootIsEqualTo(
expectedLeft = anchorBounds.right + BadgeWithContentHorizontalOffset + max
- (
+ (
(
NavigationBar.LargeBadgeSize - badgeBounds.width
) / 2,
@@ -173,7 +173,7 @@
@Test
fun badgeBox_longContent_position() {
rule
- .setMaterialContent {
+ .setMaterialContent(lightColorScheme()) {
BadgedBox(badge = { Badge { Text("999+") } }) {
Icon(
icon,
@@ -196,7 +196,7 @@
@Test
fun badge_notMergingDescendants_withOwnContentDescription() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
BadgedBox(
badge = {
Badge { Text("99+") }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ButtonTest.kt
index be7cd16..5403fe4 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ButtonTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ButtonTest.kt
@@ -49,7 +49,7 @@
@Test
fun defaultSemantics() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box {
Button(modifier = Modifier.testTag("myButton"), onClick = {}) {
Text("myButton")
@@ -64,7 +64,7 @@
@Test
fun disabledSemantics() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box {
Button(modifier = Modifier.testTag("myButton"), onClick = {}, enabled = false) {
Text("myButton")
@@ -83,7 +83,7 @@
val onClick: () -> Unit = { ++counter }
val text = "myButton"
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box {
Button(onClick = onClick, modifier = Modifier.testTag("myButton")) {
Text(text)
@@ -107,7 +107,7 @@
fun canBeDisabled() {
val tag = "myButton"
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
var enabled by remember { mutableStateOf(true) }
val onClick = { enabled = false }
Box {
@@ -138,7 +138,7 @@
val text = "myButton"
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Column {
Button(modifier = Modifier.testTag(button1Tag), onClick = button1OnClick) {
Text(text)
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxScreenshotTest.kt
index 4dd811e..3d6aa94 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxScreenshotTest.kt
@@ -62,11 +62,9 @@
@Test
fun checkBox_checked() {
- rule.setContent {
- MaterialTheme(scheme.colorScheme) {
- Box(wrap.testTag(wrapperTestTag)) {
- Checkbox(checked = true, onCheckedChange = { })
- }
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ Checkbox(checked = true, onCheckedChange = { })
}
}
assertToggeableAgainstGolden("checkBox_${scheme.name}_checked")
@@ -74,11 +72,9 @@
@Test
fun checkBox_unchecked() {
- rule.setContent {
- MaterialTheme(scheme.colorScheme) {
- Box(wrap.testTag(wrapperTestTag)) {
- Checkbox(modifier = wrap, checked = false, onCheckedChange = { })
- }
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ Checkbox(modifier = wrap, checked = false, onCheckedChange = { })
}
}
assertToggeableAgainstGolden("checkBox_${scheme.name}_unchecked")
@@ -86,11 +82,9 @@
@Test
fun checkBox_pressed() {
- rule.setContent {
- MaterialTheme(scheme.colorScheme) {
- Box(wrap.testTag(wrapperTestTag)) {
- Checkbox(modifier = wrap, checked = false, onCheckedChange = { })
- }
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ Checkbox(modifier = wrap, checked = false, onCheckedChange = { })
}
}
@@ -111,15 +105,13 @@
@Test
fun checkBox_indeterminate() {
- rule.setContent {
- MaterialTheme(scheme.colorScheme) {
- Box(wrap.testTag(wrapperTestTag)) {
- TriStateCheckbox(
- state = ToggleableState.Indeterminate,
- modifier = wrap,
- onClick = {}
- )
- }
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ TriStateCheckbox(
+ state = ToggleableState.Indeterminate,
+ modifier = wrap,
+ onClick = {}
+ )
}
}
assertToggeableAgainstGolden("checkBox_${scheme.name}_indeterminate")
@@ -127,15 +119,13 @@
@Test
fun checkBox_disabled_checked() {
- rule.setContent {
- MaterialTheme(scheme.colorScheme) {
- Box(wrap.testTag(wrapperTestTag)) {
- Checkbox(
- modifier = wrap,
- checked = true,
- enabled = false,
- onCheckedChange = { })
- }
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ Checkbox(
+ modifier = wrap,
+ checked = true,
+ enabled = false,
+ onCheckedChange = { })
}
}
assertToggeableAgainstGolden("checkBox_${scheme.name}_disabled_checked")
@@ -143,15 +133,13 @@
@Test
fun checkBox_disabled_unchecked() {
- rule.setContent {
- MaterialTheme(scheme.colorScheme) {
- Box(wrap.testTag(wrapperTestTag)) {
- Checkbox(
- modifier = wrap,
- checked = false,
- enabled = false,
- onCheckedChange = { })
- }
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ Checkbox(
+ modifier = wrap,
+ checked = false,
+ enabled = false,
+ onCheckedChange = { })
}
}
assertToggeableAgainstGolden("checkBox_${scheme.name}_disabled_unchecked")
@@ -159,16 +147,14 @@
@Test
fun checkBox_disabled_indeterminate() {
- rule.setContent {
- MaterialTheme(scheme.colorScheme) {
- Box(wrap.testTag(wrapperTestTag)) {
- TriStateCheckbox(
- state = ToggleableState.Indeterminate,
- enabled = false,
- modifier = wrap,
- onClick = {}
- )
- }
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ TriStateCheckbox(
+ state = ToggleableState.Indeterminate,
+ enabled = false,
+ modifier = wrap,
+ onClick = {}
+ )
}
}
assertToggeableAgainstGolden("checkBox_${scheme.name}_disabled_indeterminate")
@@ -177,15 +163,13 @@
@Test
fun checkBox_unchecked_animateToChecked() {
val isChecked = mutableStateOf(false)
- rule.setContent {
- MaterialTheme(scheme.colorScheme) {
- Box(wrap.testTag(wrapperTestTag)) {
- Checkbox(
- modifier = wrap,
- checked = isChecked.value,
- onCheckedChange = { isChecked.value = it }
- )
- }
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ Checkbox(
+ modifier = wrap,
+ checked = isChecked.value,
+ onCheckedChange = { isChecked.value = it }
+ )
}
}
@@ -208,15 +192,13 @@
@Test
fun checkBox_checked_animateToUnchecked() {
val isChecked = mutableStateOf(true)
- rule.setContent {
- MaterialTheme(scheme.colorScheme) {
- Box(wrap.testTag(wrapperTestTag)) {
- Checkbox(
- modifier = wrap,
- checked = isChecked.value,
- onCheckedChange = { isChecked.value = it }
- )
- }
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ Checkbox(
+ modifier = wrap,
+ checked = isChecked.value,
+ onCheckedChange = { isChecked.value = it }
+ )
}
}
@@ -238,15 +220,13 @@
@Test
fun checkBox_hover() {
- rule.setContent {
- MaterialTheme(scheme.colorScheme) {
- Box(wrap.testTag(wrapperTestTag)) {
- Checkbox(
- modifier = wrap,
- checked = true,
- onCheckedChange = { }
- )
- }
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ Checkbox(
+ modifier = wrap,
+ checked = true,
+ onCheckedChange = { }
+ )
}
}
@@ -262,19 +242,17 @@
fun checkBox_focus() {
val focusRequester = FocusRequester()
- rule.setContent {
- MaterialTheme(scheme.colorScheme) {
- Box(wrap.testTag(wrapperTestTag)) {
- Checkbox(
- modifier = wrap
- // Normally this is only focusable in non-touch mode, so let's force it to
- // always be focusable so we can test how it appears
- .focusProperties { canFocus = true }
- .focusRequester(focusRequester),
- checked = true,
- onCheckedChange = { }
- )
- }
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ Checkbox(
+ modifier = wrap
+ // Normally this is only focusable in non-touch mode, so let's force it to
+ // always be focusable so we can test how it appears
+ .focusProperties { canFocus = true }
+ .focusRequester(focusRequester),
+ checked = true,
+ onCheckedChange = { }
+ )
}
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxTest.kt
index 0ee07c6..d252a0d 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxTest.kt
@@ -28,18 +28,18 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.focused
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.state.ToggleableState.Indeterminate
import androidx.compose.ui.state.ToggleableState.Off
import androidx.compose.ui.state.ToggleableState.On
-import androidx.compose.ui.test.assertHasClickAction
-import androidx.compose.ui.test.assertHasNoClickAction
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertHasNoClickAction
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsOff
@@ -72,7 +72,7 @@
@Test
fun checkBoxTest_defaultSemantics() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Column {
Checkbox(false, {}, modifier = Modifier.testTag(tag = "checkboxUnchecked"))
Checkbox(true, {}, modifier = Modifier.testTag("checkboxChecked"))
@@ -92,7 +92,7 @@
@Test
fun checkBoxTest_toggle() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
val (checked, onCheckedChange) = remember { mutableStateOf(false) }
Checkbox(checked, onCheckedChange, modifier = Modifier.testTag(defaultTag))
}
@@ -105,7 +105,7 @@
@Test
fun checkBoxTest_toggle_twice() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
val (checked, onCheckedChange) = remember { mutableStateOf(false) }
Checkbox(checked, onCheckedChange, modifier = Modifier.testTag(defaultTag))
}
@@ -122,7 +122,7 @@
fun checkBoxTest_untoggleable_whenEmptyLambda() {
val parentTag = "parent"
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
val (checked, _) = remember { mutableStateOf(false) }
Box(Modifier.semantics(mergeDescendants = true) {}.testTag(parentTag)) {
Checkbox(
@@ -144,7 +144,7 @@
@Test
fun checkBoxTest_untoggleableAndMergeable_whenNullLambda() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
val (checked, _) = remember { mutableStateOf(false) }
Box(Modifier.semantics(mergeDescendants = true) {}.testTag(defaultTag)) {
Checkbox(
@@ -281,7 +281,9 @@
) {
TriStateCheckbox(
state = checkboxValue,
- onClick = if (clickable) { {} } else null,
+ onClick = if (clickable) {
+ {}
+ } else null,
enabled = false
)
}
@@ -299,7 +301,7 @@
fun checkBoxTest_clickInMinimumTouchTarget(): Unit = with(rule.density) {
val tag = "switch"
var state by mutableStateOf(Off)
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
// Box is needed because otherwise the control will be expanded to fill its parent
Box(Modifier.fillMaxSize()) {
TriStateCheckbox(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
index 67f6d1e..aac9d78 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
@@ -61,7 +61,7 @@
@Test
fun iconButton_lightTheme() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
@@ -73,16 +73,14 @@
@Test
fun iconButton_darkTheme() {
- rule.setContent {
- MaterialTheme(darkColorScheme()) {
- Surface(modifier = Modifier.fillMaxSize()) {
- Box(wrap.testTag(wrapperTestTag)) {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Localized description"
- )
- }
+ rule.setMaterialContent(darkColorScheme()) {
+ Surface(modifier = Modifier.fillMaxSize()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ Icons.Filled.Favorite,
+ contentDescription = "Localized description"
+ )
}
}
}
@@ -92,8 +90,7 @@
@Test
fun iconButton_lightTheme_disabled() {
-
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
IconButton(onClick = { /* doSomething() */ }, enabled = false) {
Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
@@ -105,7 +102,7 @@
@Test
fun iconButton_lightTheme_pressed() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
@@ -130,7 +127,7 @@
@Test
fun iconButton_lightTheme_hovered() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
@@ -148,7 +145,7 @@
fun iconButton_lightTheme_focused() {
val focusRequester = FocusRequester()
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
IconButton(onClick = { /* doSomething() */ },
modifier = Modifier
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt
index 0e7fa45..1454beb 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt
@@ -95,7 +95,7 @@
@Test
fun iconButton_defaultSemantics() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
IconButtonContent()
}
rule.onNode(hasClickAction()).apply {
@@ -105,7 +105,7 @@
@Test
fun iconButton_disabledSemantics() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
IconButton(onClick = {}, enabled = false) {}
}
rule.onNode(hasClickAction()).apply {
@@ -116,7 +116,7 @@
@Test
fun iconButton_materialIconSize_iconPositioning() {
val diameter = 24.dp
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box {
IconButton(onClick = {}) {
Box(Modifier.size(diameter).testTag("icon"))
@@ -134,7 +134,7 @@
fun iconButton_customIconSize_iconPositioning() {
val width = 36.dp
val height = 14.dp
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box {
IconButton(onClick = {}) {
Box(Modifier.size(width, height).testTag("icon"))
@@ -177,7 +177,7 @@
@Test
fun iconToggleButton_defaultSemantics() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
IconToggleButtonContent()
}
rule.onNode(isToggleable()).apply {
@@ -190,7 +190,7 @@
@Test
fun iconToggleButton_disabledSemantics() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
IconToggleButton(checked = false, onCheckedChange = {}, enabled = false) {}
}
rule.onNode(isToggleable()).apply {
@@ -202,7 +202,7 @@
@Test
fun iconToggleButton_materialIconSize_iconPositioning() {
val diameter = 24.dp
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box {
IconToggleButton(checked = false, onCheckedChange = {}) {
Box(Modifier.size(diameter).testTag("icon"))
@@ -220,7 +220,7 @@
fun iconToggleButton_customIconSize_iconPositioning() {
val width = 36.dp
val height = 14.dp
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box {
IconToggleButton(checked = false, onCheckedChange = {}) {
Box(Modifier.size(width, height).testTag("icon"))
@@ -238,7 +238,7 @@
fun iconToggleButton_clickInMinimumTouchTarget(): Unit = with(rule.density) {
val tag = "iconToggleButton"
var checked by mutableStateOf(false)
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
// Box is needed because otherwise the control will be expanded to fill its parent
Box(Modifier.fillMaxSize()) {
IconToggleButton(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt
index ef8ae3e..7d7ddf3 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt
@@ -26,8 +26,8 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
-import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
@@ -159,7 +159,7 @@
val height = 24.dp
val testTag = "testTag"
var expectedIntSize: IntSize? = null
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
val image: ImageBitmap
with(LocalDensity.current) {
image = createBitmapWithColor(
@@ -196,7 +196,7 @@
val width = 35.dp
val height = 83.dp
val testTag = "testTag"
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
val image: ImageBitmap
with(LocalDensity.current) {
image = createBitmapWithColor(
@@ -219,7 +219,7 @@
val width = 35.dp
val height = 83.dp
val testTag = "testTag"
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
val image: ImageBitmap
with(LocalDensity.current) {
image = createBitmapWithColor(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialTest.kt
index 38d2f0c..e1319813e 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialTest.kt
@@ -38,12 +38,20 @@
import androidx.compose.ui.unit.height
import androidx.compose.ui.unit.width
+/**
+ * Wraps Compose content in a [MaterialTheme] and a [Surface].
+ *
+ * @param colorScheme a [ColorScheme] to provide to the theme. Usually a [lightColorScheme],
+ * [darkColorScheme], or a dynamic one
+ * @param modifier a [Modifier] to be applied at the [Surface] wrapper
+ */
fun ComposeContentTestRule.setMaterialContent(
+ colorScheme: ColorScheme,
modifier: Modifier = Modifier,
composable: @Composable () -> Unit
) {
setContent {
- MaterialTheme {
+ MaterialTheme(colorScheme = colorScheme) {
Surface(modifier = modifier, content = composable)
}
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
index 897a894..f181c0f 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
@@ -62,11 +62,9 @@
var scope: CoroutineScope? = null
- composeTestRule.setContent {
- MaterialTheme(lightColorScheme()) {
- scope = rememberCoroutineScope()
- DefaultNavigationBar(interactionSource)
- }
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ scope = rememberCoroutineScope()
+ DefaultNavigationBar(interactionSource)
}
assertNavigationBarMatches(
@@ -83,11 +81,9 @@
var scope: CoroutineScope? = null
- composeTestRule.setContent {
- MaterialTheme(lightColorScheme()) {
- scope = rememberCoroutineScope()
- DefaultNavigationBar(interactionSource)
- }
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ scope = rememberCoroutineScope()
+ DefaultNavigationBar(interactionSource)
}
assertNavigationBarMatches(
@@ -104,11 +100,9 @@
var scope: CoroutineScope? = null
- composeTestRule.setContent {
- MaterialTheme(darkColorScheme()) {
- scope = rememberCoroutineScope()
- DefaultNavigationBar(interactionSource)
- }
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ scope = rememberCoroutineScope()
+ DefaultNavigationBar(interactionSource)
}
assertNavigationBarMatches(
@@ -125,11 +119,9 @@
var scope: CoroutineScope? = null
- composeTestRule.setContent {
- MaterialTheme(darkColorScheme()) {
- scope = rememberCoroutineScope()
- DefaultNavigationBar(interactionSource)
- }
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ scope = rememberCoroutineScope()
+ DefaultNavigationBar(interactionSource)
}
assertNavigationBarMatches(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
index b6e78469..f0a898d 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
@@ -72,7 +72,7 @@
@Test
fun defaultSemantics() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
NavigationBar {
NavigationBarItem(
modifier = Modifier.testTag("item"),
@@ -101,7 +101,7 @@
@Test
fun disabledSemantics() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
NavigationBar {
NavigationBarItem(
enabled = false,
@@ -150,6 +150,7 @@
lateinit var parentCoords: LayoutCoordinates
val itemCoords = mutableMapOf<Int, LayoutCoordinates>()
rule.setMaterialContent(
+ lightColorScheme(),
Modifier.onGloballyPositioned { coords: LayoutCoordinates ->
parentCoords = coords
}
@@ -190,7 +191,7 @@
@Test
fun navigationBarItemContent_withLabel_sizeAndPosition() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box {
NavigationBar {
NavigationBarItem(
@@ -235,7 +236,7 @@
@Test
fun navigationBarItemContent_withLabel_unselected_sizeAndPosition() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box {
NavigationBar {
NavigationBarItem(
@@ -269,7 +270,7 @@
@Test
fun navigationBarItemContent_withoutLabel_sizeAndPosition() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box {
NavigationBar {
NavigationBarItem(
@@ -297,7 +298,7 @@
@Test
fun navigationBar_selectNewItem() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
var selectedItem by remember { mutableStateOf(0) }
val items = listOf("Songs", "Artists", "Playlists")
@@ -336,7 +337,7 @@
@Test
fun disabled_noClicks() {
var clicks = 0
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
NavigationBar {
NavigationBarItem(
enabled = false,
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerScreenshotTest.kt
index a2991a4..d4f5273 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerScreenshotTest.kt
@@ -21,7 +21,6 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.runtime.Composable
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
@@ -52,7 +51,7 @@
val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
private fun ComposeContentTestRule.setnavigationDrawer(drawerValue: DrawerValue) {
- setMaterialContent {
+ setMaterialContent(lightColorScheme()) {
Box(Modifier.requiredSize(400.dp, 32.dp).testTag(ContainerTestTag)) {
NavigationDrawer(
drawerState = rememberDrawerState(drawerValue),
@@ -67,30 +66,21 @@
}
}
- // TODO(b/196872589): Move to MaterialTest
- private fun ComposeContentTestRule.setMaterialContentWithDarkTheme(
- modifier: Modifier = Modifier,
- composable: @Composable () -> Unit,
- ) {
- setContent {
- MaterialTheme(colorScheme = darkColorScheme()) {
- Surface(modifier = modifier, content = composable)
- }
- }
- }
-
private fun ComposeContentTestRule.setDarknavigationDrawer(drawerValue: DrawerValue) {
- setMaterialContentWithDarkTheme {
- Box(Modifier.requiredSize(400.dp, 32.dp).testTag(ContainerTestTag)) {
- NavigationDrawer(
- drawerState = rememberDrawerState(drawerValue),
- drawerContent = {},
- content = {
- Box(
- Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)
- )
- }
- )
+ setMaterialContent(darkColorScheme()) {
+ Surface {
+ Box(Modifier.requiredSize(400.dp, 32.dp).testTag(ContainerTestTag)) {
+ NavigationDrawer(
+ drawerState = rememberDrawerState(drawerValue),
+ drawerContent = {},
+ content = {
+ Box(
+ Modifier.fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)
+ )
+ }
+ )
+ }
}
}
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt
index f2ac639..c65f154 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt
@@ -71,7 +71,7 @@
@Test
fun navigationDrawer_testOffset_whenOpen() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
val drawerState = rememberDrawerState(DrawerValue.Open)
NavigationDrawer(
drawerState = drawerState,
@@ -88,7 +88,7 @@
@Test
fun navigationDrawer_testOffset_whenClosed() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
val drawerState = rememberDrawerState(DrawerValue.Closed)
NavigationDrawer(
drawerState = drawerState,
@@ -106,7 +106,7 @@
@Test
fun navigationDrawer_testWidth_whenOpen() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
val drawerState = rememberDrawerState(DrawerValue.Open)
NavigationDrawer(
drawerState = drawerState,
@@ -125,7 +125,7 @@
@SmallTest
fun navigationDrawer_hasPaneTitle() {
lateinit var navigationMenu: String
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
NavigationDrawer(
drawerState = rememberDrawerState(DrawerValue.Open),
drawerContent = {
@@ -145,7 +145,7 @@
@LargeTest
fun navigationDrawer_openAndClose(): Unit = runBlocking(AutoTestFrameClock()) {
lateinit var drawerState: DrawerState
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
drawerState = rememberDrawerState(DrawerValue.Closed)
NavigationDrawer(
drawerState = drawerState,
@@ -176,7 +176,7 @@
@LargeTest
fun navigationDrawer_animateTo(): Unit = runBlocking(AutoTestFrameClock()) {
lateinit var drawerState: DrawerState
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
drawerState = rememberDrawerState(DrawerValue.Closed)
NavigationDrawer(
drawerState = drawerState,
@@ -207,7 +207,7 @@
@LargeTest
fun navigationDrawer_snapTo(): Unit = runBlocking(AutoTestFrameClock()) {
lateinit var drawerState: DrawerState
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
drawerState = rememberDrawerState(DrawerValue.Closed)
NavigationDrawer(
drawerState = drawerState,
@@ -238,7 +238,7 @@
@LargeTest
fun navigationDrawer_currentValue(): Unit = runBlocking(AutoTestFrameClock()) {
lateinit var drawerState: DrawerState
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
drawerState = rememberDrawerState(DrawerValue.Closed)
NavigationDrawer(
drawerState = drawerState,
@@ -269,7 +269,7 @@
var drawerClicks = 0
var bodyClicks = 0
lateinit var drawerState: DrawerState
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
drawerState = rememberDrawerState(DrawerValue.Closed)
// emulate click on the screen
NavigationDrawer(
@@ -310,7 +310,7 @@
) {
var bodyClicks = 0
lateinit var drawerState: DrawerState
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
drawerState = rememberDrawerState(DrawerValue.Closed)
NavigationDrawer(
drawerState = drawerState,
@@ -345,7 +345,7 @@
@LargeTest
fun navigationDrawer_openBySwipe() {
lateinit var drawerState: DrawerState
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
drawerState = rememberDrawerState(DrawerValue.Closed)
Box(Modifier.testTag(DrawerTestTag)) {
NavigationDrawer(
@@ -379,7 +379,7 @@
@LargeTest
fun navigationDrawer_confirmStateChangeRespect() {
lateinit var drawerState: DrawerState
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
drawerState = rememberDrawerState(
DrawerValue.Open,
confirmStateChange = {
@@ -424,7 +424,7 @@
@LargeTest
fun navigationDrawer_openBySwipe_rtl() {
lateinit var drawerState: DrawerState
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
drawerState = rememberDrawerState(DrawerValue.Closed)
// emulate click on the screen
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
@@ -463,7 +463,7 @@
AutoTestFrameClock()
) {
lateinit var drawerState: DrawerState
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
drawerState = rememberDrawerState(DrawerValue.Closed)
NavigationDrawer(
drawerState = drawerState,
@@ -500,7 +500,7 @@
fun navigationDrawer_scrimNode_reportToSemanticsWhenOpen_notReportToSemanticsWhenClosed() {
val topTag = "navigationDrawer"
lateinit var closeDrawer: String
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
NavigationDrawer(
modifier = Modifier.testTag(topTag),
drawerState = rememberDrawerState(DrawerValue.Open),
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailScreenshotTest.kt
index 7fe8197..3395ae0 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailScreenshotTest.kt
@@ -64,11 +64,9 @@
var scope: CoroutineScope? = null
- composeTestRule.setContent {
- MaterialTheme(lightColorScheme()) {
- scope = rememberCoroutineScope()
- DefaultNavigationRail(interactionSource)
- }
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ scope = rememberCoroutineScope()
+ DefaultNavigationRail(interactionSource)
}
assertNavigationRailMatches(
@@ -85,11 +83,9 @@
var scope: CoroutineScope? = null
- composeTestRule.setContent {
- MaterialTheme(lightColorScheme()) {
- scope = rememberCoroutineScope()
- DefaultNavigationRail(interactionSource)
- }
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ scope = rememberCoroutineScope()
+ DefaultNavigationRail(interactionSource)
}
assertNavigationRailMatches(
@@ -106,11 +102,9 @@
var scope: CoroutineScope? = null
- composeTestRule.setContent {
- MaterialTheme(darkColorScheme()) {
- scope = rememberCoroutineScope()
- DefaultNavigationRail(interactionSource)
- }
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ scope = rememberCoroutineScope()
+ DefaultNavigationRail(interactionSource)
}
assertNavigationRailMatches(
@@ -127,11 +121,9 @@
var scope: CoroutineScope? = null
- composeTestRule.setContent {
- MaterialTheme(darkColorScheme()) {
- scope = rememberCoroutineScope()
- DefaultNavigationRail(interactionSource)
- }
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ scope = rememberCoroutineScope()
+ DefaultNavigationRail(interactionSource)
}
assertNavigationRailMatches(
@@ -148,11 +140,9 @@
var scope: CoroutineScope? = null
- composeTestRule.setContent {
- MaterialTheme(lightColorScheme()) {
- scope = rememberCoroutineScope()
- DefaultNavigationRail(interactionSource, withHeaderFab = true)
- }
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ scope = rememberCoroutineScope()
+ DefaultNavigationRail(interactionSource, withHeaderFab = true)
}
assertNavigationRailMatches(
@@ -169,11 +159,9 @@
var scope: CoroutineScope? = null
- composeTestRule.setContent {
- MaterialTheme(lightColorScheme()) {
- scope = rememberCoroutineScope()
- DefaultNavigationRail(interactionSource, withHeaderFab = true)
- }
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ scope = rememberCoroutineScope()
+ DefaultNavigationRail(interactionSource, withHeaderFab = true)
}
assertNavigationRailMatches(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailTest.kt
index e1ed22a..99b4032 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailTest.kt
@@ -74,7 +74,7 @@
@Test
fun defaultSemantics() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
NavigationRail {
NavigationRailItem(
modifier = Modifier.testTag("item"),
@@ -103,7 +103,7 @@
@Test
fun disabledSemantics() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
NavigationRail {
NavigationRailItem(
enabled = false,
@@ -149,7 +149,7 @@
@Test
fun navigationRailItem_sizeAndPositions() {
val itemCoords = mutableMapOf<Int, LayoutCoordinates>()
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box {
NavigationRail {
repeat(4) { index ->
@@ -194,7 +194,7 @@
@Test
fun navigationRailItemContent_withLabel_sizeAndPosition() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box {
NavigationRail {
NavigationRailItem(
@@ -237,7 +237,7 @@
@Test
fun navigationRailItemContent_withLabel_unselected_sizeAndPosition() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box {
NavigationRail {
NavigationRailItem(
@@ -274,7 +274,7 @@
@Test
fun navigationRailItemContent_withoutLabel_sizeAndPosition() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box {
NavigationRail {
NavigationRailItem(
@@ -305,7 +305,7 @@
@Test
fun navigationRail_selectNewItem() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
var selectedItem by remember { mutableStateOf(0) }
val items = listOf("Home", "Search", "Settings")
val icons = listOf(Icons.Filled.Home, Icons.Filled.Search, Icons.Filled.Settings)
@@ -344,7 +344,7 @@
@Test
fun disabled_noClicks() {
var clicks = 0
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
NavigationRail {
NavigationRailItem(
enabled = false,
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt
index 2bb97e1..06f365b 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt
@@ -60,7 +60,7 @@
@Test
fun radioButton_lightTheme_selected() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
RadioButton(selected = true, onClick = {})
}
@@ -70,11 +70,9 @@
@Test
fun radioButton_darkTheme_selected() {
- rule.setContent {
- MaterialTheme(darkColorScheme()) {
- Box(wrap.testTag(wrapperTestTag)) {
- RadioButton(selected = true, onClick = {})
- }
+ rule.setMaterialContent(darkColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ RadioButton(selected = true, onClick = {})
}
}
assertSelectableAgainstGolden("radioButton_darkTheme_selected")
@@ -82,7 +80,7 @@
@Test
fun radioButton_lightTheme_notSelected() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
RadioButton(selected = false, onClick = {})
}
@@ -92,11 +90,9 @@
@Test
fun radioButton_darkTheme_notSelected() {
- rule.setContent {
- MaterialTheme(darkColorScheme()) {
- Box(wrap.testTag(wrapperTestTag)) {
- RadioButton(selected = false, onClick = {})
- }
+ rule.setMaterialContent(darkColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ RadioButton(selected = false, onClick = {})
}
}
assertSelectableAgainstGolden("radioButton_darkTheme_notSelected")
@@ -104,7 +100,7 @@
@Test
fun radioButton_lightTheme_pressed() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
RadioButton(selected = false, onClick = {})
}
@@ -127,11 +123,9 @@
@Test
fun radioButton_darkTheme_pressed() {
- rule.setContent {
- MaterialTheme(darkColorScheme()) {
- Box(wrap.testTag(wrapperTestTag)) {
- RadioButton(selected = false, onClick = {})
- }
+ rule.setMaterialContent(darkColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ RadioButton(selected = false, onClick = {})
}
}
@@ -152,7 +146,7 @@
@Test
fun radioButton_lightTheme_hovered() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
RadioButton(selected = false, onClick = {})
}
@@ -166,11 +160,9 @@
@Test
fun radioButton_darkTheme_hovered() {
- rule.setContent {
- MaterialTheme(darkColorScheme()) {
- Box(wrap.testTag(wrapperTestTag)) {
- RadioButton(selected = false, onClick = {})
- }
+ rule.setMaterialContent(darkColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ RadioButton(selected = false, onClick = {})
}
}
rule.onNodeWithTag(wrapperTestTag).performMouseInput {
@@ -184,7 +176,7 @@
fun radioButton_lightTheme_focused() {
val focusRequester = FocusRequester()
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
RadioButton(
selected = false,
@@ -209,19 +201,17 @@
fun radioButton_darkTheme_focused() {
val focusRequester = FocusRequester()
- rule.setContent {
- MaterialTheme(darkColorScheme()) {
- Box(wrap.testTag(wrapperTestTag)) {
- RadioButton(
- selected = false,
- onClick = {},
- modifier = Modifier
- // Normally this is only focusable in non-touch mode, so let's force it to
- // always be focusable so we can test how it appears
- .focusProperties { canFocus = true }
- .focusRequester(focusRequester)
- )
- }
+ rule.setMaterialContent(darkColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ RadioButton(
+ selected = false,
+ onClick = {},
+ modifier = Modifier
+ // Normally this is only focusable in non-touch mode, so let's force it to
+ // always be focusable so we can test how it appears
+ .focusProperties { canFocus = true }
+ .focusRequester(focusRequester)
+ )
}
}
@@ -234,7 +224,7 @@
@Test
fun radioButton_lightTheme_disabled_selected() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
RadioButton(selected = true, onClick = {}, enabled = false)
}
@@ -244,11 +234,9 @@
@Test
fun radioButton_darkTheme_disabled_selected() {
- rule.setContent {
- MaterialTheme(darkColorScheme()) {
- Box(wrap.testTag(wrapperTestTag)) {
- RadioButton(selected = true, onClick = {}, enabled = false)
- }
+ rule.setMaterialContent(darkColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ RadioButton(selected = true, onClick = {}, enabled = false)
}
}
assertSelectableAgainstGolden("radioButton_darkTheme_disabled_selected")
@@ -256,7 +244,7 @@
@Test
fun radioButton_lightTheme_disabled_notSelected() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
RadioButton(selected = false, onClick = {}, enabled = false)
}
@@ -266,11 +254,9 @@
@Test
fun radioButton_darkTheme_disabled_notSelected() {
- rule.setContent {
- MaterialTheme(darkColorScheme()) {
- Box(wrap.testTag(wrapperTestTag)) {
- RadioButton(selected = false, onClick = {}, enabled = false)
- }
+ rule.setMaterialContent(darkColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ RadioButton(selected = false, onClick = {}, enabled = false)
}
}
assertSelectableAgainstGolden("radioButton_darkTheme_disabled_notSelected")
@@ -278,7 +264,7 @@
@Test
fun radioButton_lightTheme_notSelected_animateToSelected() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
val isSelected = remember { mutableStateOf(false) }
Box(wrap.testTag(wrapperTestTag)) {
RadioButton(
@@ -308,15 +294,13 @@
@Test
fun radioButton_darkTheme_notSelected_animateToSelected() {
- rule.setContent {
- MaterialTheme(darkColorScheme()) {
- val isSelected = remember { mutableStateOf(false) }
- Box(wrap.testTag(wrapperTestTag)) {
- RadioButton(
- selected = isSelected.value,
- onClick = { isSelected.value = !isSelected.value }
- )
- }
+ rule.setMaterialContent(darkColorScheme()) {
+ val isSelected = remember { mutableStateOf(false) }
+ Box(wrap.testTag(wrapperTestTag)) {
+ RadioButton(
+ selected = isSelected.value,
+ onClick = { isSelected.value = !isSelected.value }
+ )
}
}
@@ -340,7 +324,7 @@
@Test
fun radioButton_lightTheme_selected_animateToNotSelected() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
val isSelected = remember { mutableStateOf(true) }
Box(wrap.testTag(wrapperTestTag)) {
RadioButton(
@@ -370,15 +354,13 @@
@Test
fun radioButton_darkTheme_selected_animateToNotSelected() {
- rule.setContent {
- MaterialTheme(darkColorScheme()) {
- val isSelected = remember { mutableStateOf(true) }
- Box(wrap.testTag(wrapperTestTag)) {
- RadioButton(
- selected = isSelected.value,
- onClick = { isSelected.value = !isSelected.value }
- )
- }
+ rule.setMaterialContent(darkColorScheme()) {
+ val isSelected = remember { mutableStateOf(true) }
+ Box(wrap.testTag(wrapperTestTag)) {
+ RadioButton(
+ selected = isSelected.value,
+ onClick = { isSelected.value = !isSelected.value }
+ )
}
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonTest.kt
index 5c00290c..a5909aa 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonTest.kt
@@ -66,7 +66,7 @@
fun radioGroupTest_defaultSemantics() {
val selected = mutableStateOf(itemOne)
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Column {
options.forEach { item ->
RadioButton(
@@ -99,7 +99,7 @@
fun radioGroupTest_ensureUnselectable() {
val selected = mutableStateOf(itemOne)
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Column {
options.forEach { item ->
RadioButton(
@@ -126,7 +126,7 @@
@Test
fun radioGroupTest_clickSelect() {
val selected = mutableStateOf(itemOne)
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Column {
options.forEach { item ->
RadioButton(
@@ -152,7 +152,7 @@
@Test
fun radioGroup_untoggleableAndMergeable_whenNullLambda() {
val parentTag = "parent"
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Column(Modifier.semantics(mergeDescendants = true) {}.testTag(parentTag)) {
RadioButton(
selected = true,
@@ -172,7 +172,7 @@
fun radioGroupTest_clickSelectTwoDifferentItems() {
val selected = mutableStateOf(itemOne)
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Column {
options.forEach { item ->
RadioButton(
@@ -287,7 +287,10 @@
CompositionLocalProvider(
LocalMinimumTouchTargetEnforcement provides minimumTouchTarget
) {
- RadioButton(selected = selected, onClick = if (clickable) { {} } else null)
+ RadioButton(
+ selected = selected, onClick = if (clickable) {
+ {}
+ } else null)
}
}
.run {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
index 1b7f6f7..1eb38c1 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
@@ -77,7 +77,7 @@
fun scaffold_onlyContent_stackSlot() {
var child1: Offset = Offset.Zero
var child2: Offset = Offset.Zero
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Scaffold {
Text(
"One",
@@ -98,7 +98,7 @@
var appbarPosition: Offset = Offset.Zero
var appbarSize: IntSize = IntSize.Zero
var contentPosition: Offset = Offset.Zero
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Scaffold(
topBar = {
Box(
@@ -132,7 +132,7 @@
var appbarSize: IntSize = IntSize.Zero
var contentPosition: Offset = Offset.Zero
var contentSize: IntSize = IntSize.Zero
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Scaffold(
bottomBar = {
Box(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarHostTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarHostTest.kt
new file mode 100644
index 0000000..1b1df83b
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarHostTest.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2021 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 androidx.compose.material3
+
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.platform.AccessibilityManager
+import androidx.compose.ui.semantics.LiveRegionMode
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onParent
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.doReturn
+import com.nhaarman.mockitokotlin2.eq
+import com.nhaarman.mockitokotlin2.mock
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.AdditionalMatchers.not
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class SnackbarHostTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun snackbarHost_observePushedData() {
+ var resultedInvocation = ""
+ val hostState = SnackbarHostState()
+ lateinit var scope: CoroutineScope
+ rule.setContent {
+ scope = rememberCoroutineScope()
+ SnackbarHost(hostState) { data ->
+ LaunchedEffect(data) {
+ resultedInvocation += data.visuals.message
+ data.dismiss()
+ }
+ }
+ }
+ val job = scope.launch {
+ hostState.showSnackbar("1")
+ Truth.assertThat(resultedInvocation).isEqualTo("1")
+ hostState.showSnackbar("2")
+ Truth.assertThat(resultedInvocation).isEqualTo("12")
+ hostState.showSnackbar("3")
+ Truth.assertThat(resultedInvocation).isEqualTo("123")
+ }
+
+ rule.waitUntil { job.isCompleted }
+ }
+
+ @Test
+ fun snackbarHost_fifoQueueContract() {
+ var resultedInvocation = ""
+ val hostState = SnackbarHostState()
+ lateinit var scope: CoroutineScope
+ rule.setContent {
+ scope = rememberCoroutineScope()
+ SnackbarHost(hostState) { data ->
+ LaunchedEffect(data) {
+ resultedInvocation += data.visuals.message
+ launch {
+ delay(30L)
+ data.dismiss()
+ }
+ }
+ }
+ }
+ val parent = SupervisorJob()
+ repeat(10) {
+ scope.launch(parent) {
+ delay(it * 10L)
+ hostState.showSnackbar(it.toString())
+ }
+ }
+
+ rule.waitUntil { parent.children.all { it.isCompleted } }
+ Truth.assertThat(resultedInvocation).isEqualTo("0123456789")
+ }
+
+ @Test
+ @LargeTest
+ fun snackbarHost_returnedResult() {
+ val hostState = SnackbarHostState()
+ lateinit var scope: CoroutineScope
+ rule.setContent {
+ scope = rememberCoroutineScope()
+ SnackbarHost(hostState) { data ->
+ Snackbar(data)
+ }
+ }
+ val job1 = scope.launch {
+ val result = hostState.showSnackbar("1", actionLabel = "press")
+ Truth.assertThat(result).isEqualTo(SnackbarResult.ActionPerformed)
+ }
+ rule.onNodeWithText("press")
+ .performClick()
+
+ rule.waitUntil { job1.isCompleted }
+
+ val job2 = scope.launch {
+ val result = hostState.showSnackbar(
+ message = "1",
+ actionLabel = "do not press"
+ )
+ Truth.assertThat(result).isEqualTo(SnackbarResult.Dismissed)
+ }
+
+ rule.waitUntil(timeoutMillis = 5_000) { job2.isCompleted }
+ }
+
+ @Test
+ fun snackbarHost_scopeLifecycleRespect() {
+ val switchState = mutableStateOf(true)
+ val hostState = SnackbarHostState()
+ lateinit var scope: CoroutineScope
+ rule.setContent {
+ if (switchState.value) {
+ scope = rememberCoroutineScope()
+ }
+ SnackbarHost(hostState) { data ->
+ Snackbar(data)
+ }
+ }
+ val job1 = scope.launch {
+ hostState.showSnackbar("1")
+ Truth.assertWithMessage("Result shouldn't happen due to cancellation").fail()
+ }
+ val job2 = scope.launch {
+ delay(10)
+ switchState.value = false
+ }
+
+ rule.waitUntil { job1.isCompleted && job2.isCompleted }
+ }
+
+ @Test
+ fun snackbarHost_semantics() {
+ val hostState = SnackbarHostState()
+ lateinit var scope: CoroutineScope
+ rule.setContent {
+ scope = rememberCoroutineScope()
+ SnackbarHost(hostState) { data ->
+ Snackbar(data)
+ }
+ }
+ val job1 = scope.launch {
+ val result = hostState.showSnackbar("1", actionLabel = "press")
+ Truth.assertThat(result).isEqualTo(SnackbarResult.Dismissed)
+ }
+ rule.onNodeWithText("1").onParent().onParent()
+ .assert(
+ SemanticsMatcher.expectValue(SemanticsProperties.LiveRegion, LiveRegionMode.Polite)
+ )
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+ .performSemanticsAction(SemanticsActions.Dismiss)
+
+ rule.waitUntil { job1.isCompleted }
+ }
+
+ @Test
+ fun snackbarDuration_toMillis_nonNullAccessibilityManager() {
+ val mockDurationControl = 10000L
+ val mockDurationNonControl = 5000L
+ val accessibilityManager: AccessibilityManager = mock {
+ on {
+ calculateRecommendedTimeoutMillis(eq(Long.MAX_VALUE), any(), any(), any())
+ } doReturn Long.MAX_VALUE
+ on {
+ calculateRecommendedTimeoutMillis(not(eq(Long.MAX_VALUE)), any(), any(), eq(true))
+ } doReturn mockDurationControl
+ on {
+ calculateRecommendedTimeoutMillis(not(eq(Long.MAX_VALUE)), any(), any(), eq(false))
+ } doReturn mockDurationNonControl
+ }
+ assertEquals(
+ Long.MAX_VALUE,
+ SnackbarDuration.Indefinite.toMillis(true, accessibilityManager)
+ )
+ assertEquals(
+ Long.MAX_VALUE,
+ SnackbarDuration.Indefinite.toMillis(false, accessibilityManager)
+ )
+ assertEquals(
+ mockDurationControl,
+ SnackbarDuration.Long.toMillis(true, accessibilityManager)
+ )
+ assertEquals(
+ mockDurationNonControl,
+ SnackbarDuration.Long.toMillis(false, accessibilityManager)
+ )
+ assertEquals(
+ mockDurationControl,
+ SnackbarDuration.Short.toMillis(true, accessibilityManager)
+ )
+ assertEquals(
+ mockDurationNonControl,
+ SnackbarDuration.Short.toMillis(false, accessibilityManager)
+ )
+ }
+
+ @Test
+ fun snackbarDuration_toMillis_nullAccessibilityManager() {
+ assertEquals(
+ Long.MAX_VALUE,
+ SnackbarDuration.Indefinite.toMillis(true, null)
+ )
+ assertEquals(
+ Long.MAX_VALUE,
+ SnackbarDuration.Indefinite.toMillis(false, null)
+ )
+ assertEquals(
+ 10000L,
+ SnackbarDuration.Long.toMillis(true, null)
+ )
+ assertEquals(
+ 10000L,
+ SnackbarDuration.Long.toMillis(false, null)
+ )
+ assertEquals(
+ 4000L,
+ SnackbarDuration.Short.toMillis(true, null)
+ )
+ assertEquals(
+ 4000L,
+ SnackbarDuration.Short.toMillis(false, null)
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarScreenshotTest.kt
new file mode 100644
index 0000000..893ac64
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarScreenshotTest.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2021 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 androidx.compose.material3
+
+import android.os.Build
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalTestApi::class)
+class SnackbarScreenshotTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @get:Rule
+ val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
+
+ private val snackbarTestTag = "snackbarTestTag"
+
+ @Test
+ fun snackbar_lightTheme() {
+ rule.setMaterialContent(lightColorScheme()) {
+ TestSnackbar()
+ }
+ assertAgainstGolden("snackbar_lightTheme")
+ }
+
+ @Test
+ fun snackbar_withAction_lightTheme() {
+ rule.setMaterialContent(lightColorScheme()) {
+ TestSnackbar(showAction = true)
+ }
+ assertAgainstGolden("snackbar_withAction_lightTheme")
+ }
+
+ @Test
+ fun snackbar_withDismiss_lightTheme() {
+ rule.setMaterialContent(lightColorScheme()) {
+ TestSnackbar(showAction = true, duration = SnackbarDuration.Indefinite)
+ }
+ assertAgainstGolden("snackbar_withDismiss_lightTheme")
+ }
+
+ @Test
+ fun snackbar_darkTheme() {
+ rule.setMaterialContent(darkColorScheme()) {
+ TestSnackbar()
+ }
+ assertAgainstGolden("snackbar_darkTheme")
+ }
+
+ @Test
+ fun snackbar_withAction_darkTheme() {
+ rule.setMaterialContent(darkColorScheme()) {
+ TestSnackbar(showAction = true)
+ }
+ assertAgainstGolden("snackbar_withAction_darkTheme")
+ }
+
+ @Test
+ fun snackbar_withDismiss_darkTheme() {
+ rule.setMaterialContent(darkColorScheme()) {
+ TestSnackbar(showAction = true, duration = SnackbarDuration.Indefinite)
+ }
+ assertAgainstGolden("snackbar_withDismiss_darkTheme")
+ }
+
+ @Composable
+ private fun TestSnackbar(
+ showAction: Boolean = false,
+ duration: SnackbarDuration = SnackbarDuration.Long
+ ) {
+ Snackbar(
+ snackbarData = object : SnackbarData {
+ override val visuals: SnackbarVisuals = object : SnackbarVisuals {
+ override val message: String = "Snackbar message"
+ override val actionLabel: String? = if (showAction) "Undo" else null
+ override val withDismissAction: Boolean =
+ duration == SnackbarDuration.Indefinite
+ override val duration: SnackbarDuration = duration
+ }
+
+ override fun performAction() {
+ // no-op
+ }
+
+ override fun dismiss() {
+ // no-op
+ }
+ },
+ modifier = Modifier.testTag(snackbarTestTag),
+ )
+ }
+
+ private fun assertAgainstGolden(goldenName: String) {
+ rule.onNodeWithTag(snackbarTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, goldenName)
+ }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarTest.kt
new file mode 100644
index 0000000..baca7ca
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarTest.kt
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2021 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 androidx.compose.material3
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.testutils.assertIsEqualTo
+import androidx.compose.testutils.assertIsNotEqualTo
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.layout.FirstBaseline
+import androidx.compose.ui.layout.LastBaseline
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getAlignmentLinePosition
+import androidx.compose.ui.test.getBoundsInRoot
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.max
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.unit.width
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class SnackbarTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val longText = "Message is very long and long and long and long and long " +
+ "and long and long and long and long and long and long"
+
+ @Test
+ fun defaultSnackbar_semantics() {
+ var clicked = false
+ rule.setMaterialContent(lightColorScheme()) {
+ Box {
+ Snackbar(
+ content = { Text("Message") },
+ action = {
+ TextButton(onClick = { clicked = true }) {
+ Text("UNDO")
+ }
+ }
+ )
+ }
+ }
+
+ rule.onNodeWithText("Message")
+ .assertExists()
+
+ assertThat(clicked).isFalse()
+
+ rule.onNodeWithText("UNDO")
+ .performClick()
+
+ assertThat(clicked).isTrue()
+ }
+
+ @Test
+ fun snackbar_shortTextOnly_defaultSizes() {
+ val snackbar = rule.setMaterialContentForSizeAssertions(
+ parentMaxWidth = 300.dp
+ ) {
+ Snackbar(
+ content = {
+ Text("Message")
+ }
+ )
+ }
+ .assertWidthIsEqualTo(300.dp)
+ .assertHeightIsEqualTo(48.dp)
+
+ val firstBaseLine = rule.onNodeWithText("Message").getAlignmentLinePosition(FirstBaseline)
+ val lastBaseLine = rule.onNodeWithText("Message").getAlignmentLinePosition(LastBaseline)
+ firstBaseLine.assertIsNotEqualTo(0.dp, "first baseline")
+ firstBaseLine.assertIsEqualTo(lastBaseLine, "first baseline")
+
+ val snackBounds = snackbar.getUnclippedBoundsInRoot()
+ val textBounds = rule.onNodeWithText("Message").getUnclippedBoundsInRoot()
+
+ val textTopOffset = textBounds.top - snackBounds.top
+ val textBottomOffset = textBounds.top - snackBounds.top
+
+ textTopOffset.assertIsEqualTo(textBottomOffset)
+ }
+
+ @Test
+ fun snackbar_shortTextOnly_bigFont_centered() {
+ val snackbar = rule.setMaterialContentForSizeAssertions(
+ parentMaxWidth = 300.dp
+ ) {
+ Snackbar(
+ content = {
+ Text("Message", fontSize = 30.sp)
+ }
+ )
+ }
+ .assertWidthIsEqualTo(300.dp)
+
+ val firstBaseLine = rule.onNodeWithText("Message").getAlignmentLinePosition(FirstBaseline)
+ val lastBaseLine = rule.onNodeWithText("Message").getAlignmentLinePosition(LastBaseline)
+ firstBaseLine.assertIsNotEqualTo(0.dp, "first baseline")
+ firstBaseLine.assertIsEqualTo(lastBaseLine, "first baseline")
+
+ val snackBounds = snackbar.getUnclippedBoundsInRoot()
+ val textBounds = rule.onNodeWithText("Message").getUnclippedBoundsInRoot()
+
+ val textTopOffset = textBounds.top - snackBounds.top
+ val textBottomOffset = textBounds.top - snackBounds.top
+
+ textTopOffset.assertIsEqualTo(textBottomOffset)
+ }
+
+ @Test
+ fun snackbar_shortTextAndButton_alignment() {
+ val snackbar = rule.setMaterialContentForSizeAssertions(
+ parentMaxWidth = 300.dp
+ ) {
+ Snackbar(
+ content = {
+ Text("Message")
+ },
+ action = {
+ TextButton(
+ onClick = {},
+ modifier = Modifier.clipToBounds().testTag("button")
+ ) {
+ Text("Undo")
+ }
+ }
+ )
+ }
+ .assertWidthIsEqualTo(300.dp)
+ .assertHeightIsEqualTo(48.dp)
+
+ val textBaseLine = rule.onNodeWithText("Message").getAlignmentLinePosition(FirstBaseline)
+ val buttonBaseLine = rule.onNodeWithTag("button").getAlignmentLinePosition(FirstBaseline)
+ textBaseLine.assertIsNotEqualTo(0.dp, "text baseline")
+ buttonBaseLine.assertIsNotEqualTo(0.dp, "button baseline")
+
+ val snackBounds = snackbar.getUnclippedBoundsInRoot()
+ val textBounds = rule.onNodeWithText("Message").getUnclippedBoundsInRoot()
+ val buttonBounds = rule.onNodeWithText("Undo").getBoundsInRoot()
+
+ val buttonTopOffset = buttonBounds.top - snackBounds.top
+ val textTopOffset = textBounds.top - snackBounds.top
+ val textBottomOffset = textBounds.top - snackBounds.top
+ textTopOffset.assertIsEqualTo(textBottomOffset)
+
+ (buttonBaseLine + buttonTopOffset).assertIsEqualTo(textBaseLine + textTopOffset)
+ }
+
+ @Test
+ fun snackbar_shortTextAndButton_bigFont_alignment() {
+ val snackbar = rule.setMaterialContentForSizeAssertions(
+ parentMaxWidth = 400.dp
+ ) {
+ val fontSize = 30.sp
+ Snackbar(
+ content = {
+ Text("Message", fontSize = fontSize)
+ },
+ action = {
+ TextButton(
+ onClick = {},
+ modifier = Modifier.testTag("button")
+ ) {
+ Text("Undo", fontSize = fontSize)
+ }
+ }
+ )
+ }
+
+ val textBaseLine = rule.onNodeWithText("Message").getAlignmentLinePosition(FirstBaseline)
+ val buttonBaseLine = rule.onNodeWithTag("button").getAlignmentLinePosition(FirstBaseline)
+ textBaseLine.assertIsNotEqualTo(0.dp, "text baseline")
+ buttonBaseLine.assertIsNotEqualTo(0.dp, "button baseline")
+
+ val snackBounds = snackbar.getUnclippedBoundsInRoot()
+ val textBounds = rule.onNodeWithText("Message").getUnclippedBoundsInRoot()
+ val buttonBounds = rule.onNodeWithText("Undo").getUnclippedBoundsInRoot()
+
+ val buttonTopOffset = buttonBounds.top - snackBounds.top
+ val textTopOffset = textBounds.top - snackBounds.top
+ val textBottomOffset = textBounds.top - snackBounds.top
+ textTopOffset.assertIsEqualTo(textBottomOffset)
+
+ (buttonBaseLine + buttonTopOffset).assertIsEqualTo(textBaseLine + textTopOffset)
+ }
+
+ @Test
+ fun snackbar_longText_sizes() {
+ val snackbar = rule.setMaterialContentForSizeAssertions(
+ parentMaxWidth = 300.dp
+ ) {
+ Snackbar(
+ content = {
+ Text(longText, Modifier.testTag("text"), maxLines = 2)
+ }
+ )
+ }
+ .assertWidthIsEqualTo(300.dp)
+ .assertHeightIsEqualTo(68.dp)
+
+ val firstBaseline = rule.onNodeWithTag("text").getFirstBaselinePosition()
+ val lastBaseline = rule.onNodeWithTag("text").getLastBaselinePosition()
+
+ firstBaseline.assertIsNotEqualTo(0.dp, "first baseline")
+ lastBaseline.assertIsNotEqualTo(0.dp, "last baseline")
+ firstBaseline.assertIsNotEqualTo(lastBaseline, "first baseline")
+
+ val snackBounds = snackbar.getUnclippedBoundsInRoot()
+ val textBounds = rule.onNodeWithTag("text").getUnclippedBoundsInRoot()
+
+ val textTopOffset = textBounds.top - snackBounds.top
+ val textBottomOffset = textBounds.top - snackBounds.top
+
+ textTopOffset.assertIsEqualTo(textBottomOffset)
+ }
+
+ @Test
+ fun snackbar_longTextAndButton_alignment() {
+ val snackbar = rule.setMaterialContentForSizeAssertions(
+ parentMaxWidth = 300.dp
+ ) {
+ Snackbar(
+ content = {
+ Text(longText, Modifier.testTag("text"), maxLines = 2)
+ },
+ action = {
+ TextButton(
+ modifier = Modifier.testTag("button"),
+ onClick = {}
+ ) {
+ Text("Undo")
+ }
+ }
+ )
+ }
+ .assertWidthIsEqualTo(300.dp)
+ .assertHeightIsEqualTo(68.dp)
+
+ val textFirstBaseLine = rule.onNodeWithTag("text").getFirstBaselinePosition()
+ val textLastBaseLine = rule.onNodeWithTag("text").getLastBaselinePosition()
+
+ textFirstBaseLine.assertIsNotEqualTo(0.dp, "first baseline")
+ textLastBaseLine.assertIsNotEqualTo(0.dp, "last baseline")
+ textFirstBaseLine.assertIsNotEqualTo(textLastBaseLine, "first baseline")
+
+ rule.onNodeWithTag("text")
+ .assertTopPositionInRootIsEqualTo(30.dp - textFirstBaseLine)
+
+ val buttonBounds = rule.onNodeWithTag("button").getUnclippedBoundsInRoot()
+ val snackBounds = snackbar.getUnclippedBoundsInRoot()
+
+ val buttonCenter = buttonBounds.top + (buttonBounds.height / 2)
+ buttonCenter.assertIsEqualTo(snackBounds.height / 2, "button center")
+ }
+
+ @Test
+ fun snackbar_textAndButtonOnSeparateLine_alignment() {
+ val snackbar = rule.setMaterialContentForSizeAssertions(
+ parentMaxWidth = 300.dp
+ ) {
+ Snackbar(
+ content = {
+ Text("Message", Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp))
+ },
+ action = {
+ TextButton(
+ onClick = {},
+ modifier = Modifier.testTag("button")
+ ) {
+ Text("Undo", Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp))
+ }
+ },
+ actionOnNewLine = true
+ )
+ }
+
+ val textFirstBaseLine = rule.onNodeWithText("Message").getFirstBaselinePosition()
+ val textLastBaseLine = rule.onNodeWithText("Message").getLastBaselinePosition()
+ val textBounds = rule.onNodeWithText("Message").getUnclippedBoundsInRoot()
+ val buttonBounds = rule.onNodeWithTag("button").getUnclippedBoundsInRoot()
+
+ rule.onNodeWithText("Message")
+ .assertTopPositionInRootIsEqualTo(30.dp - textFirstBaseLine)
+
+ val lastBaselineToBottom = max(18.dp, 48.dp - textLastBaseLine)
+
+ rule.onNodeWithTag("button").assertTopPositionInRootIsEqualTo(
+ lastBaselineToBottom + textBounds.top + textLastBaseLine
+ )
+
+ snackbar
+ .assertHeightIsEqualTo(2.dp + buttonBounds.top + buttonBounds.height)
+ .assertWidthIsEqualTo(8.dp + buttonBounds.left + buttonBounds.width)
+ }
+
+ @Test
+ fun defaultSnackbar_dataVersion_proxiesParameters() {
+ var clicked = false
+ val snackbarVisuals = object : SnackbarVisuals {
+ override val message: String = "Data message"
+ override val actionLabel: String = "UNDO"
+ override val withDismissAction: Boolean = false
+ override val duration: SnackbarDuration = SnackbarDuration.Short
+ }
+ val snackbarData = object : SnackbarData {
+ override val visuals: SnackbarVisuals = snackbarVisuals
+
+ override fun performAction() {
+ clicked = true
+ }
+
+ override fun dismiss() {}
+ }
+ rule.setMaterialContent(lightColorScheme()) {
+ Box {
+ Snackbar(snackbarData = snackbarData)
+ }
+ }
+
+ rule.onNodeWithText("Data message")
+ .assertExists()
+
+ assertThat(clicked).isFalse()
+
+ rule.onNodeWithText("UNDO")
+ .performClick()
+
+ assertThat(clicked).isTrue()
+ }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt
index 007999b..74d8699 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt
@@ -79,7 +79,7 @@
fun noTonalElevationColorIsSetOnNonElevatedSurfaceColor() {
var absoluteTonalElevation: Dp = 0.dp
var surfaceColor: Color = Color.Unspecified
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
surfaceColor = MaterialTheme.colorScheme.surface
Box(
Modifier
@@ -114,7 +114,7 @@
var absoluteTonalElevation: Dp = 0.dp
var surfaceTonalColor: Color = Color.Unspecified
var surfaceColor: Color
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
surfaceColor = MaterialTheme.colorScheme.surface
Box(
Modifier
@@ -149,7 +149,7 @@
@Test
fun tonalElevationColorIsNotSetOnNonSurfaceColor() {
var absoluteTonalElevation: Dp = 0.dp
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Box(
Modifier
.size(10.dp, 10.dp)
@@ -181,7 +181,7 @@
fun absoluteElevationCompositionLocalIsSet() {
var outerElevation: Dp? = null
var innerElevation: Dp? = null
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Surface(tonalElevation = 2.dp) {
outerElevation = LocalAbsoluteTonalElevation.current
Surface(tonalElevation = 4.dp) {
@@ -199,7 +199,7 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun absoluteElevationIsNotUsedForShadows() {
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Column {
Box(
Modifier
@@ -257,7 +257,7 @@
fun contentColorSetBeforeModifier() {
var contentColor: Color = Color.Unspecified
val expectedColor = Color.Blue
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalContentColor provides Color.Red) {
Surface(
Modifier.composed {
@@ -279,7 +279,7 @@
@Test
fun clickableOverload_semantics() {
val count = mutableStateOf(0)
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Surface(
modifier = Modifier.testTag("surface"),
role = Role.Checkbox,
@@ -302,7 +302,7 @@
@Test
fun clickableOverload_clickAction() {
val count = mutableStateOf(0f)
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Surface(
modifier = Modifier.testTag("surface"),
onClick = { count.value += 1 }
@@ -324,7 +324,7 @@
fun clickableOverload_enabled_disabled() {
val count = mutableStateOf(0f)
val enabled = mutableStateOf(true)
- rule.setMaterialContent {
+ rule.setMaterialContent(lightColorScheme()) {
Surface(
modifier = Modifier.testTag("surface"),
enabled = enabled.value,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index c355453..b095ebf 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -1057,7 +1057,8 @@
// Nothing coerced, meaning we're in the middle of top app bar collapse or
// expand.
offset = coerced
- available
+ // Consume only the scroll on the Y axis.
+ available.copy(x = 0f)
} else {
Offset.Zero
}
@@ -1122,7 +1123,8 @@
// Nothing coerced, meaning we're in the middle of top app bar collapse or
// expand.
offset = coerced
- available
+ // Consume only the scroll on the Y axis.
+ available.copy(x = 0f)
} else {
Offset.Zero
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
index 8c618ca..fed51df 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
@@ -40,9 +40,15 @@
*
* @sample androidx.compose.material3.samples.SimpleScaffoldWithTopBar
*
- * @param modifier optional Modifier for the root of the [Scaffold]
+ * To show a [Snackbar], use [SnackbarHostState.showSnackbar].
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
+ *
+ * @param modifier optional Modifier for the root of the [Scaffold].
* @param topBar top app bar of the screen. Consider using [SmallTopAppBar].
* @param bottomBar bottom bar of the screen. Consider using [NavigationBar].
+ * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
+ * [SnackbarHostState.showSnackbar]. Usually it's a [SnackbarHost].
* @param floatingActionButton Main action button of your screen. Consider using
* [FloatingActionButton] for this slot.
* @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition] for
@@ -62,19 +68,20 @@
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
+ snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
containerColor: Color = MaterialTheme.colorScheme.background,
contentColor: Color = contentColorFor(containerColor),
content: @Composable (PaddingValues) -> Unit
) {
-
Surface(modifier = modifier, color = containerColor, contentColor = contentColor) {
ScaffoldLayout(
fabPosition = floatingActionButtonPosition,
topBar = topBar,
bottomBar = bottomBar,
content = content,
+ snackbar = snackbarHost,
fab = floatingActionButton
)
}
@@ -86,7 +93,9 @@
* @param fabPosition [FabPosition] for the FAB (if present)
* @param topBar the content to place at the top of the [Scaffold], typically a [SmallTopAppBar]
* @param content the main 'body' of the [Scaffold]
- * @param fab the [FloatingActionButton] displayed on top of the [content]
+ * @param snackbar the [Snackbar] displayed on top of the [content]
+ * @param fab the [FloatingActionButton] displayed on top of the [content], below the [snackbar]
+ * and above the [bottomBar]
* @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
* [content], typically a [NavigationBar].
*/
@@ -96,6 +105,7 @@
fabPosition: FabPosition,
topBar: @Composable () -> Unit,
content: @Composable (PaddingValues) -> Unit,
+ snackbar: @Composable () -> Unit,
fab: @Composable () -> Unit,
bottomBar: @Composable () -> Unit
@@ -113,6 +123,13 @@
val topBarHeight = topBarPlaceables.maxByOrNull { it.height }?.height ?: 0
+ val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).map {
+ it.measure(looseConstraints)
+ }
+
+ val snackbarHeight = snackbarPlaceables.maxByOrNull { it.height }?.height ?: 0
+ val snackbarWidth = snackbarPlaceables.maxByOrNull { it.width }?.width ?: 0
+
val fabPlaceables =
subcompose(ScaffoldLayoutContent.Fab, fab).mapNotNull { measurable ->
measurable.measure(looseConstraints).takeIf { it.height != 0 && it.width != 0 }
@@ -159,6 +176,12 @@
}
}
+ val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
+ snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight)
+ } else {
+ 0
+ }
+
val bodyContentHeight = layoutHeight - topBarHeight
val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
@@ -174,6 +197,12 @@
topBarPlaceables.forEach {
it.place(0, 0)
}
+ snackbarPlaceables.forEach {
+ it.place(
+ (layoutWidth - snackbarWidth) / 2,
+ layoutHeight - snackbarOffsetFromBottom
+ )
+ }
// The bottom bar is always at the bottom of the layout
bottomBarPlaceables.forEach {
it.place(0, layoutHeight - bottomBarHeight)
@@ -240,4 +269,4 @@
// FAB spacing above the bottom bar / bottom of the Scaffold
private val FabSpacing = 16.dp
-private enum class ScaffoldLayoutContent { TopBar, MainContent, Fab, BottomBar }
+private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
new file mode 100644
index 0000000..fa20bb2
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2021 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 androidx.compose.material3
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material3.tokens.SnackbarTokens
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.FirstBaseline
+import androidx.compose.ui.layout.LastBaseline
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.unit.dp
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Material Design snackbar.
+ *
+ * Snackbars provide brief messages about app processes at the bottom of the screen.
+ *
+ * Snackbars inform users of a process that an app has performed or will perform. They appear
+ * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience,
+ * and they don’t require user input to disappear.
+ *
+ * A Snackbar can contain a single action. "Dismiss" or "cancel" actions are optional.
+ *
+ * Snackbars with an action should not timeout or self-dismiss until the user performs another
+ * action. Here, moving the keyboard focus indicator to navigate through interactive elements in a
+ * page is not considered an action.
+ *
+ * This component provides only the visuals of the Snackbar. If you need to show a Snackbar
+ * with defaults on the screen, use [SnackbarHostState.showSnackbar]:
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
+ *
+ * If you want to customize appearance of the Snackbar, you can pass your own version as a child
+ * of the [SnackbarHost] to the [Scaffold]:
+ * @sample androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
+ *
+ * @param modifier modifiers for the Snackbar layout
+ * @param action action / button component to add as an action to the snackbar. Consider using
+ * [ColorScheme.inversePrimary] as the color for the action, if you do not have a predefined color
+ * you wish to use instead.
+ * @param dismissAction action / button component to add as an additional close affordance action
+ * when a snackbar is non self-dismissive. Consider using [ColorScheme.inverseOnSurface] as the
+ * color for the action, if you do not have a predefined color you wish to use instead.
+ * @param actionOnNewLine whether or not action should be put on the separate line. Recommended
+ * for action with long action text
+ * @param shape defines the Snackbar's shape (as well as its shadow when using `shadowElevation`)
+ * @param containerColor background color of the Snackbar
+ * @param contentColor the preferred color for content inside this Snackbar. Also see
+ * [LocalContentColor] which is used by [Text] and [Icon] by default.
+ * @param content content to show information about a process that an app has performed or will
+ * perform
+ */
+@Composable
+fun Snackbar(
+ modifier: Modifier = Modifier,
+ action: @Composable (() -> Unit)? = null,
+ dismissAction: @Composable (() -> Unit)? = null,
+ actionOnNewLine: Boolean = false,
+ shape: Shape = SnackbarTokens.ContainerShape,
+ containerColor: Color = MaterialTheme.colorScheme.fromToken(SnackbarTokens.ContainerColor),
+ contentColor: Color = MaterialTheme.colorScheme.fromToken(SnackbarTokens.SupportingTextColor),
+ content: @Composable () -> Unit
+) {
+ Surface(
+ modifier = modifier,
+ shape = shape,
+ color = containerColor,
+ contentColor = contentColor,
+ shadowElevation = SnackbarTokens.ContainerElevation
+ ) {
+ val textStyle = MaterialTheme.typography.fromToken(SnackbarTokens.SupportingTextFont)
+ CompositionLocalProvider(LocalTextStyle provides textStyle) {
+ when {
+ action == null -> OneRowSnackbar(
+ text = content,
+ action = null,
+ dismissAction = dismissAction
+ )
+ actionOnNewLine -> NewLineButtonSnackbar(content, action, dismissAction)
+ else -> OneRowSnackbar(
+ text = content,
+ action = action,
+ dismissAction = dismissAction
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Material Design snackbar.
+ *
+ * Snackbars provide brief messages about app processes at the bottom of the screen.
+ *
+ * Snackbars inform users of a process that an app has performed or will perform. They appear
+ * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience,
+ * and they don’t require user input to disappear.
+ *
+ * A Snackbar can contain a single action. "Dismiss" or "cancel" actions are optional.
+ *
+ * Snackbars with an action should not timeout or self-dismiss until the user performs another
+ * action. Here, moving the keyboard focus indicator to navigate through interactive elements in a
+ * page is not considered an action.
+ *
+ * This version of snackbar is designed to work with [SnackbarData] provided by the
+ * [SnackbarHost], which is usually used inside of the [Scaffold].
+ *
+ * This components provides only the visuals of the Snackbar. If you need to show a Snackbar
+ * with defaults on the screen, use [SnackbarHostState.showSnackbar]:
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
+ *
+ * If you want to customize appearance of the Snackbar, you can pass your own version as a child
+ * of the [SnackbarHost] to the [Scaffold]:
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
+ *
+ * When a [SnackbarData.visuals] sets the Snackbar's duration as [SnackbarDuration.Indefinite], it's
+ * recommended to display an additional close affordance action.
+ * See [SnackbarVisuals.withDismissAction]:
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithIndefiniteSnackbar
+ *
+ * @param snackbarData data about the current snackbar showing via [SnackbarHostState]
+ * @param modifier modifiers for the Snackbar layout
+ * @param actionOnNewLine whether or not the Snackbar's action should be put on the separate line
+ * (recommended for action with long action text)
+ * @param shape defines the Snackbar's shape (as well as its shadow when using `shadowElevation`)
+ * @param containerColor background color of the Snackbar
+ * @param contentColor the preferred color for content inside this Snackbar. Also see
+ * [LocalContentColor] which is used by [Text] and [Icon] by default.
+ * @param actionColor color of the Snackbar's action
+ */
+@Composable
+fun Snackbar(
+ snackbarData: SnackbarData,
+ modifier: Modifier = Modifier,
+ actionOnNewLine: Boolean = false,
+ shape: Shape = SnackbarTokens.ContainerShape,
+ containerColor: Color = MaterialTheme.colorScheme.fromToken(SnackbarTokens.ContainerColor),
+ contentColor: Color = MaterialTheme.colorScheme.fromToken(SnackbarTokens.SupportingTextColor),
+ actionColor: Color = MaterialTheme.colorScheme.fromToken(SnackbarTokens.ActionLabelTextColor)
+) {
+ val actionLabel = snackbarData.visuals.actionLabel
+ val actionComposable: (@Composable () -> Unit)? = if (actionLabel != null) {
+ @Composable {
+ TextButton(
+ colors = ButtonDefaults.textButtonColors(contentColor = actionColor),
+ onClick = { snackbarData.performAction() },
+ content = { Text(actionLabel) }
+ )
+ }
+ } else {
+ null
+ }
+ val dismissActionComposable: (@Composable () -> Unit)? =
+ if (snackbarData.visuals.withDismissAction) {
+ @Composable {
+ IconButton(
+ onClick = { snackbarData.dismiss() },
+ content = {
+ Icon(
+ Icons.Filled.Close,
+ contentDescription = null, // TODO add "Dismiss Snackbar" to Strings.
+ )
+ }
+ )
+ }
+ } else {
+ null
+ }
+ Snackbar(
+ modifier = modifier.padding(12.dp),
+ action = actionComposable,
+ dismissAction = dismissActionComposable,
+ actionOnNewLine = actionOnNewLine,
+ shape = shape,
+ containerColor = containerColor,
+ contentColor = contentColor,
+ content = { Text(snackbarData.visuals.message) }
+ )
+}
+
+@Composable
+private fun NewLineButtonSnackbar(
+ text: @Composable () -> Unit,
+ action: @Composable () -> Unit,
+ dismissAction: @Composable (() -> Unit)?
+) {
+ Column(
+ modifier = Modifier
+ // Fill max width, up to ContainerMaxWidth.
+ .widthIn(max = ContainerMaxWidth)
+ .fillMaxWidth()
+ .padding(
+ start = HorizontalSpacing,
+ bottom = SeparateButtonExtraY
+ )
+ ) {
+ Box(
+ Modifier.paddingFromBaseline(HeightToFirstLine, LongButtonVerticalOffset)
+ .padding(end = HorizontalSpacingButtonSide)
+ ) { text() }
+
+ Box(
+ Modifier.align(Alignment.End)
+ .padding(end = if (dismissAction == null) HorizontalSpacingButtonSide else 0.dp)
+ ) {
+ Row {
+ val actionTextColor =
+ MaterialTheme.colorScheme.fromToken(SnackbarTokens.ActionLabelTextColor)
+ val actionTextStyle =
+ MaterialTheme.typography.fromToken(SnackbarTokens.ActionLabelTextFont)
+ CompositionLocalProvider(
+ LocalContentColor provides actionTextColor,
+ LocalTextStyle provides actionTextStyle,
+ content = action
+ )
+
+ if (dismissAction != null) {
+ val dismissActionColor =
+ MaterialTheme.colorScheme.fromToken(SnackbarTokens.IconColor)
+ CompositionLocalProvider(
+ LocalContentColor provides dismissActionColor,
+ content = dismissAction
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun OneRowSnackbar(
+ text: @Composable () -> Unit,
+ action: @Composable (() -> Unit)?,
+ dismissAction: @Composable (() -> Unit)?
+) {
+ val textTag = "text"
+ val actionTag = "action"
+ val dismissActionTag = "dismissAction"
+ Layout(
+ {
+ Box(Modifier.layoutId(textTag).padding(vertical = SnackbarVerticalPadding)) { text() }
+ if (action != null) {
+ Box(Modifier.layoutId(actionTag)) {
+ val actionTextColor =
+ MaterialTheme.colorScheme.fromToken(SnackbarTokens.ActionLabelTextColor)
+ val actionTextStyle =
+ MaterialTheme.typography.fromToken(SnackbarTokens.ActionLabelTextFont)
+ CompositionLocalProvider(
+ LocalContentColor provides actionTextColor,
+ LocalTextStyle provides actionTextStyle,
+ content = action
+ )
+ }
+ }
+ if (dismissAction != null) {
+ Box(Modifier.layoutId(dismissActionTag)) {
+ val dismissActionColor =
+ MaterialTheme.colorScheme.fromToken(SnackbarTokens.IconColor)
+ CompositionLocalProvider(
+ LocalContentColor provides dismissActionColor,
+ content = dismissAction
+ )
+ }
+ }
+ },
+ modifier = Modifier.padding(
+ start = HorizontalSpacing,
+ end = if (dismissAction == null) HorizontalSpacingButtonSide else 0.dp
+ )
+ ) { measurables, constraints ->
+ val containerWidth = min(constraints.maxWidth, ContainerMaxWidth.roundToPx())
+ val actionButtonPlaceable =
+ measurables.firstOrNull { it.layoutId == actionTag }?.measure(constraints)
+ val dismissButtonPlaceable =
+ measurables.firstOrNull { it.layoutId == dismissActionTag }?.measure(constraints)
+ val actionButtonWidth = actionButtonPlaceable?.width ?: 0
+ val actionButtonHeight = actionButtonPlaceable?.height ?: 0
+ val dismissButtonWidth = dismissButtonPlaceable?.width ?: 0
+ val dismissButtonHeight = dismissButtonPlaceable?.height ?: 0
+ val extraSpacingWidth = if (dismissButtonWidth == 0) TextEndExtraSpacing.roundToPx() else 0
+ val textMaxWidth =
+ (containerWidth - actionButtonWidth - dismissButtonWidth - extraSpacingWidth)
+ .coerceAtLeast(constraints.minWidth)
+ val textPlaceable = measurables.first { it.layoutId == textTag }.measure(
+ constraints.copy(minHeight = 0, maxWidth = textMaxWidth)
+ )
+
+ val firstTextBaseline = textPlaceable[FirstBaseline]
+ require(firstTextBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
+ val lastTextBaseline = textPlaceable[LastBaseline]
+ require(lastTextBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
+ val isOneLine = firstTextBaseline == lastTextBaseline
+ val dismissButtonPlaceX = containerWidth - dismissButtonWidth
+ val actionButtonPlaceX = dismissButtonPlaceX - actionButtonWidth
+
+ val textPlaceY: Int
+ val containerHeight: Int
+ val actionButtonPlaceY: Int
+ if (isOneLine) {
+ val minContainerHeight = SnackbarTokens.SingleLineContainerHeight.roundToPx()
+ val contentHeight = max(actionButtonHeight, dismissButtonHeight)
+ containerHeight = max(minContainerHeight, contentHeight)
+ textPlaceY = (containerHeight - textPlaceable.height) / 2
+ actionButtonPlaceY = if (actionButtonPlaceable != null) {
+ actionButtonPlaceable[FirstBaseline].let {
+ if (it != AlignmentLine.Unspecified) {
+ textPlaceY + firstTextBaseline - it
+ } else {
+ 0
+ }
+ }
+ } else {
+ 0
+ }
+ } else {
+ val baselineOffset = HeightToFirstLine.roundToPx()
+ textPlaceY = baselineOffset - firstTextBaseline
+ val minContainerHeight = SnackbarTokens.TwoLinesContainerHeight.roundToPx()
+ val contentHeight = textPlaceY + textPlaceable.height
+ containerHeight = max(minContainerHeight, contentHeight)
+ actionButtonPlaceY = if (actionButtonPlaceable != null) {
+ (containerHeight - actionButtonPlaceable.height) / 2
+ } else {
+ 0
+ }
+ }
+ val dismissButtonPlaceY = if (dismissButtonPlaceable != null) {
+ (containerHeight - dismissButtonPlaceable.height) / 2
+ } else {
+ 0
+ }
+
+ layout(containerWidth, containerHeight) {
+ textPlaceable.placeRelative(0, textPlaceY)
+ dismissButtonPlaceable?.placeRelative(dismissButtonPlaceX, dismissButtonPlaceY)
+ actionButtonPlaceable?.placeRelative(actionButtonPlaceX, actionButtonPlaceY)
+ }
+ }
+}
+
+private val ContainerMaxWidth = 600.dp
+private val HeightToFirstLine = 30.dp
+private val HorizontalSpacing = 16.dp
+private val HorizontalSpacingButtonSide = 8.dp
+private val SeparateButtonExtraY = 2.dp
+private val SnackbarVerticalPadding = 6.dp
+private val TextEndExtraSpacing = 8.dp
+private val LongButtonVerticalOffset = 12.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SnackbarHost.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SnackbarHost.kt
new file mode 100644
index 0000000..a382877
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SnackbarHost.kt
@@ -0,0 +1,457 @@
+/*
+ * Copyright 2021 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 androidx.compose.material3
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.RecomposeScope
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.currentRecomposeScope
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.AccessibilityManager
+import androidx.compose.ui.platform.LocalAccessibilityManager
+import androidx.compose.ui.semantics.LiveRegionMode
+import androidx.compose.ui.semantics.dismiss
+import androidx.compose.ui.semantics.liveRegion
+import androidx.compose.ui.semantics.semantics
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlin.coroutines.resume
+
+/**
+ * State of the [SnackbarHost], controls the queue and the current [Snackbar] being shown inside
+ * the [SnackbarHost].
+ *
+ * This state usually [remember]ed and used to provide a [SnackbarHost] to a [Scaffold].
+ */
+@Stable
+class SnackbarHostState {
+
+ /**
+ * Only one [Snackbar] can be shown at a time.
+ * Since a suspending Mutex is a fair queue, this manages our message queue
+ * and we don't have to maintain one.
+ */
+ private val mutex = Mutex()
+
+ /**
+ * The current [SnackbarData] being shown by the [SnackbarHost], of `null` if none.
+ */
+ var currentSnackbarData by mutableStateOf<SnackbarData?>(null)
+ private set
+
+ /**
+ * Shows or queues to be shown a [Snackbar] at the bottom of the [Scaffold] at
+ * which this state is attached and suspends until snackbar is disappeared.
+ *
+ * [SnackbarHostState] guarantees to show at most one snackbar at a time. If this function is
+ * called while another snackbar is already visible, it will be suspended until this snack
+ * bar is shown and subsequently addressed. If the caller is cancelled, the snackbar will be
+ * removed from display and/or the queue to be displayed.
+ *
+ * All of this allows for granular control over the snackbar queue from within:
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithCoroutinesSnackbar
+ *
+ * To change the Snackbar appearance, change it in 'snackbarHost' on the [Scaffold].
+ *
+ * @param message text to be shown in the Snackbar
+ * @param actionLabel optional action label to show as button in the Snackbar
+ * @param withDismissAction a boolean to show a dismiss action in the Snackbar. This is
+ * recommended to be set to true for better accessibility when a Snackbar is set with a
+ * [SnackbarDuration.Indefinite]
+ * @param duration duration to control how long snackbar will be shown in [SnackbarHost], either
+ * [SnackbarDuration.Short], [SnackbarDuration.Long] or [SnackbarDuration.Indefinite].
+ *
+ * @return [SnackbarResult.ActionPerformed] if option action has been clicked or
+ * [SnackbarResult.Dismissed] if snackbar has been dismissed via timeout or by the user
+ */
+ @OptIn(ExperimentalMaterial3Api::class)
+ suspend fun showSnackbar(
+ message: String,
+ actionLabel: String? = null,
+ withDismissAction: Boolean = false,
+ duration: SnackbarDuration = SnackbarDuration.Short
+ ): SnackbarResult =
+ showSnackbar(SnackbarVisualsImpl(message, actionLabel, withDismissAction, duration))
+
+ /**
+ * Shows or queues to be shown a [Snackbar] at the bottom of the [Scaffold] at
+ * which this state is attached and suspends until snackbar is disappeared.
+ *
+ * [SnackbarHostState] guarantees to show at most one snackbar at a time. If this function is
+ * called while another snackbar is already visible, it will be suspended until this snack
+ * bar is shown and subsequently addressed. If the caller is cancelled, the snackbar will be
+ * removed from display and/or the queue to be displayed.
+ *
+ * All of this allows for granular control over the snackbar queue from within:
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
+ *
+ * @param visuals [SnackbarVisuals] that are used to create a Snackbar
+ *
+ * @return [SnackbarResult.ActionPerformed] if option action has been clicked or
+ * [SnackbarResult.Dismissed] if snackbar has been dismissed via timeout or by the user
+ */
+ @ExperimentalMaterial3Api
+ suspend fun showSnackbar(visuals: SnackbarVisuals): SnackbarResult = mutex.withLock {
+ try {
+ return suspendCancellableCoroutine { continuation ->
+ currentSnackbarData = SnackbarDataImpl(visuals, continuation)
+ }
+ } finally {
+ currentSnackbarData = null
+ }
+ }
+
+ private class SnackbarVisualsImpl(
+ override val message: String,
+ override val actionLabel: String?,
+ override val withDismissAction: Boolean,
+ override val duration: SnackbarDuration
+ ) : SnackbarVisuals {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as SnackbarVisualsImpl
+
+ if (message != other.message) return false
+ if (actionLabel != other.actionLabel) return false
+ if (withDismissAction != other.withDismissAction) return false
+ if (duration != other.duration) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = message.hashCode()
+ result = 31 * result + actionLabel.hashCode()
+ result = 31 * result + withDismissAction.hashCode()
+ result = 31 * result + duration.hashCode()
+ return result
+ }
+ }
+
+ private class SnackbarDataImpl(
+ override val visuals: SnackbarVisuals,
+ private val continuation: CancellableContinuation<SnackbarResult>
+ ) : SnackbarData {
+
+ override fun performAction() {
+ if (continuation.isActive) continuation.resume(SnackbarResult.ActionPerformed)
+ }
+
+ override fun dismiss() {
+ if (continuation.isActive) continuation.resume(SnackbarResult.Dismissed)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as SnackbarDataImpl
+
+ if (visuals != other.visuals) return false
+ if (continuation != other.continuation) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = visuals.hashCode()
+ result = 31 * result + continuation.hashCode()
+ return result
+ }
+ }
+}
+
+/**
+ * Host for [Snackbar]s to be used in [Scaffold] to properly show, hide and dismiss items based
+ * on material specification and the [hostState].
+ *
+ * This component with default parameters comes build-in with [Scaffold], if you need to show a
+ * default [Snackbar], use [SnackbarHostState.showSnackbar].
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
+ *
+ * If you want to customize appearance of the [Snackbar], you can pass your own version as a child
+ * of the [SnackbarHost] to the [Scaffold]:
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
+ *
+ * @param hostState state of this component to read and show [Snackbar]s accordingly
+ * @param modifier optional modifier for this component
+ * @param snackbar the instance of the [Snackbar] to be shown at the appropriate time with
+ * appearance based on the [SnackbarData] provided as a param
+ */
+@Composable
+fun SnackbarHost(
+ hostState: SnackbarHostState,
+ modifier: Modifier = Modifier,
+ snackbar: @Composable (SnackbarData) -> Unit = { Snackbar(it) }
+) {
+ val currentSnackbarData = hostState.currentSnackbarData
+ val accessibilityManager = LocalAccessibilityManager.current
+ LaunchedEffect(currentSnackbarData) {
+ if (currentSnackbarData != null) {
+ val duration = currentSnackbarData.visuals.duration.toMillis(
+ currentSnackbarData.visuals.actionLabel != null,
+ accessibilityManager
+ )
+ delay(duration)
+ currentSnackbarData.dismiss()
+ }
+ }
+ FadeInFadeOutWithScale(
+ current = hostState.currentSnackbarData,
+ modifier = modifier,
+ content = snackbar
+ )
+}
+
+/**
+ * Interface to represent the visuals of one particular [Snackbar] as a piece of the [SnackbarData].
+ *
+ * @property message text to be shown in the Snackbar
+ * @property actionLabel optional action label to show as button in the Snackbar
+ * @property withDismissAction a boolean to show a dismiss action in the Snackbar. This is
+ * recommended to be set to true better accessibility when a Snackbar is set with a
+ * [SnackbarDuration.Indefinite]
+ * @property duration duration of the Snackbar
+ */
+@Stable
+interface SnackbarVisuals {
+ val message: String
+ val actionLabel: String?
+ val withDismissAction: Boolean
+ val duration: SnackbarDuration
+}
+
+/**
+ * Interface to represent the data of one particular [Snackbar] as a piece of the
+ * [SnackbarHostState].
+ *
+ * @property visuals Holds the visual representation for a particular [Snackbar].
+ */
+@Stable
+interface SnackbarData {
+ val visuals: SnackbarVisuals
+
+ /**
+ * Function to be called when Snackbar action has been performed to notify the listeners.
+ */
+ fun performAction()
+
+ /**
+ * Function to be called when Snackbar is dismissed either by timeout or by the user.
+ */
+ fun dismiss()
+}
+
+/**
+ * Possible results of the [SnackbarHostState.showSnackbar] call
+ */
+enum class SnackbarResult {
+ /**
+ * [Snackbar] that is shown has been dismissed either by timeout of by user
+ */
+ Dismissed,
+
+ /**
+ * Action on the [Snackbar] has been clicked before the time out passed
+ */
+ ActionPerformed,
+}
+
+/**
+ * Possible durations of the [Snackbar] in [SnackbarHost]
+ */
+enum class SnackbarDuration {
+ /**
+ * Show the Snackbar for a short period of time
+ */
+ Short,
+
+ /**
+ * Show the Snackbar for a long period of time
+ */
+ Long,
+
+ /**
+ * Show the Snackbar indefinitely until explicitly dismissed or action is clicked
+ */
+ Indefinite
+}
+
+// TODO: magic numbers adjustment
+internal fun SnackbarDuration.toMillis(
+ hasAction: Boolean,
+ accessibilityManager: AccessibilityManager?
+): Long {
+ val original = when (this) {
+ SnackbarDuration.Indefinite -> Long.MAX_VALUE
+ SnackbarDuration.Long -> 10000L
+ SnackbarDuration.Short -> 4000L
+ }
+ if (accessibilityManager == null) {
+ return original
+ }
+ return accessibilityManager.calculateRecommendedTimeoutMillis(
+ original,
+ containsIcons = true,
+ containsText = true,
+ containsControls = hasAction
+ )
+}
+
+// TODO: to be replaced with the public customizable implementation
+// it's basically tweaked nullable version of Crossfade
+@Composable
+private fun FadeInFadeOutWithScale(
+ current: SnackbarData?,
+ modifier: Modifier = Modifier,
+ content: @Composable (SnackbarData) -> Unit
+) {
+ val state = remember { FadeInFadeOutState<SnackbarData?>() }
+ if (current != state.current) {
+ state.current = current
+ val keys = state.items.map { it.key }.toMutableList()
+ if (!keys.contains(current)) {
+ keys.add(current)
+ }
+ state.items.clear()
+ keys.filterNotNull().mapTo(state.items) { key ->
+ FadeInFadeOutAnimationItem(key) { children ->
+ val isVisible = key == current
+ val duration = if (isVisible) SnackbarFadeInMillis else SnackbarFadeOutMillis
+ val delay = SnackbarFadeOutMillis + SnackbarInBetweenDelayMillis
+ val animationDelay = if (isVisible && keys.filterNotNull().size != 1) delay else 0
+ val opacity = animatedOpacity(
+ animation = tween(
+ easing = LinearEasing,
+ delayMillis = animationDelay,
+ durationMillis = duration
+ ),
+ visible = isVisible,
+ onAnimationFinish = {
+ if (key != state.current) {
+ // leave only the current in the list
+ state.items.removeAll { it.key == key }
+ state.scope?.invalidate()
+ }
+ }
+ )
+ val scale = animatedScale(
+ animation = tween(
+ easing = FastOutSlowInEasing,
+ delayMillis = animationDelay,
+ durationMillis = duration
+ ),
+ visible = isVisible
+ )
+ Box(
+ Modifier
+ .graphicsLayer(
+ scaleX = scale.value,
+ scaleY = scale.value,
+ alpha = opacity.value
+ )
+ .semantics {
+ liveRegion = LiveRegionMode.Polite
+ dismiss { key.dismiss(); true }
+ }
+ ) {
+ children()
+ }
+ }
+ }
+ }
+ Box(modifier) {
+ state.scope = currentRecomposeScope
+ state.items.forEach { (item, opacity) ->
+ key(item) {
+ opacity {
+ content(item!!)
+ }
+ }
+ }
+ }
+}
+
+private class FadeInFadeOutState<T> {
+ // we use Any here as something which will not be equals to the real initial value
+ var current: Any? = Any()
+ var items = mutableListOf<FadeInFadeOutAnimationItem<T>>()
+ var scope: RecomposeScope? = null
+}
+
+private data class FadeInFadeOutAnimationItem<T>(
+ val key: T,
+ val transition: FadeInFadeOutTransition
+)
+
+private typealias FadeInFadeOutTransition = @Composable (content: @Composable () -> Unit) -> Unit
+
+@Composable
+private fun animatedOpacity(
+ animation: AnimationSpec<Float>,
+ visible: Boolean,
+ onAnimationFinish: () -> Unit = {}
+): State<Float> {
+ val alpha = remember { Animatable(if (!visible) 1f else 0f) }
+ LaunchedEffect(visible) {
+ alpha.animateTo(
+ if (visible) 1f else 0f,
+ animationSpec = animation
+ )
+ onAnimationFinish()
+ }
+ return alpha.asState()
+}
+
+@Composable
+private fun animatedScale(animation: AnimationSpec<Float>, visible: Boolean): State<Float> {
+ val scale = remember { Animatable(if (!visible) 1f else 0.8f) }
+ LaunchedEffect(visible) {
+ scale.animateTo(
+ if (visible) 1f else 0.8f,
+ animationSpec = animation
+ )
+ }
+ return scale.asState()
+}
+
+private const val SnackbarFadeInMillis = 150
+private const val SnackbarFadeOutMillis = 75
+private const val SnackbarInBetweenDelayMillis = 0
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ShapeTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ShapeTokens.kt
new file mode 100644
index 0000000..f536558
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ShapeTokens.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 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.
+ */
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.ui.unit.dp
+
+// TODO(b/209589029): Remove once the shape subsystem is in place.
+internal object ShapeTokens {
+ val Large = RoundedCornerShape(8.0.dp)
+ val Medium = RoundedCornerShape(8.0.dp)
+ val Small = RoundedCornerShape(4.0.dp)
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SnackbarTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SnackbarTokens.kt
new file mode 100644
index 0000000..16b4b0c
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SnackbarTokens.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 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.
+ */
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object SnackbarTokens {
+ val ActionFocusLabelTextColor = ColorSchemeKey.InversePrimary
+ val ActionFocusStateLayerColor = ColorSchemeKey.InversePrimary
+ val ActionHoverLabelTextColor = ColorSchemeKey.InversePrimary
+ val ActionHoverStateLayerColor = ColorSchemeKey.InversePrimary
+ val ActionLabelTextColor = ColorSchemeKey.InversePrimary
+ val ActionLabelTextFont = TypographyKey.LabelLarge
+ val ActionPressedLabelTextColor = ColorSchemeKey.InversePrimary
+ val ActionPressedStateLayerColor = ColorSchemeKey.InversePrimary
+ val ContainerColor = ColorSchemeKey.InverseSurface
+ val ContainerElevation = Elevation.Level3
+ val ContainerShape = ShapeTokens.Small
+ val IconColor = ColorSchemeKey.InverseOnSurface
+ val IconSize = 24.0.dp
+ val SupportingTextColor = ColorSchemeKey.InverseOnSurface
+ val SupportingTextFont = TypographyKey.BodyMedium
+ val SingleLineContainerHeight = 48.0.dp
+ val TwoLinesContainerHeight = 68.0.dp
+}
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 27fddc4..bdbbd99 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -783,6 +783,9 @@
field public static final androidx.compose.runtime.snapshots.SnapshotApplyResult.Success INSTANCE;
}
+ public final class SnapshotDoubleIndexHeapKt {
+ }
+
public final class SnapshotIdSetKt {
}
@@ -896,10 +899,12 @@
public interface CompositionGroup extends androidx.compose.runtime.tooling.CompositionData {
method public Iterable<java.lang.Object> getData();
+ method public default Object? getIdentity();
method public Object getKey();
method public Object? getNode();
method public String? getSourceInfo();
property public abstract Iterable<java.lang.Object> data;
+ property public default Object? identity;
property public abstract Object key;
property public abstract Object? node;
property public abstract String? sourceInfo;
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 2f9a57e..f4eda00 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -818,6 +818,9 @@
field public static final androidx.compose.runtime.snapshots.SnapshotApplyResult.Success INSTANCE;
}
+ public final class SnapshotDoubleIndexHeapKt {
+ }
+
public final class SnapshotIdSetKt {
}
@@ -931,10 +934,12 @@
public interface CompositionGroup extends androidx.compose.runtime.tooling.CompositionData {
method public Iterable<java.lang.Object> getData();
+ method public default Object? getIdentity();
method public Object getKey();
method public Object? getNode();
method public String? getSourceInfo();
property public abstract Iterable<java.lang.Object> data;
+ property public default Object? identity;
property public abstract Object key;
property public abstract Object? node;
property public abstract String? sourceInfo;
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 5b450a1..e224229 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -815,6 +815,9 @@
field public static final androidx.compose.runtime.snapshots.SnapshotApplyResult.Success INSTANCE;
}
+ public final class SnapshotDoubleIndexHeapKt {
+ }
+
public final class SnapshotIdSetKt {
}
@@ -934,10 +937,12 @@
public interface CompositionGroup extends androidx.compose.runtime.tooling.CompositionData {
method public Iterable<java.lang.Object> getData();
+ method public default Object? getIdentity();
method public Object getKey();
method public Object? getNode();
method public String? getSourceInfo();
property public abstract Iterable<java.lang.Object> data;
+ property public default Object? identity;
property public abstract Object key;
property public abstract Object? node;
property public abstract String? sourceInfo;
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index adcb14c..3325134 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -21,9 +21,9 @@
import androidx.compose.runtime.snapshots.fastForEach
import androidx.compose.runtime.snapshots.fastMap
import androidx.compose.runtime.tooling.CompositionData
+import androidx.compose.runtime.tooling.CompositionGroup
import kotlin.math.max
import kotlin.math.min
-import androidx.compose.runtime.tooling.CompositionGroup
// Nomenclature -
// Address - an absolute offset into the array ignoring its gap. See Index below.
@@ -2580,6 +2580,12 @@
override val data: Iterable<Any?> get() = DataIterator(table, group)
+ override val identity: Any
+ get() {
+ validateRead()
+ return table.read { it.anchor(group) }
+ }
+
override val compositionGroups: Iterable<CompositionGroup> get() = this
override fun iterator(): Iterator<CompositionGroup> {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index fe17ea1..22cd1fc 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -75,6 +75,7 @@
*/
open fun dispose() {
disposed = true
+ releasePinnedSnapshot()
}
/**
@@ -133,6 +134,13 @@
internal var disposed = false
/*
+ *
+ */
+ @Suppress("LeakingThis")
+ private var pinningTrackingHandle =
+ if (id != INVALID_SNAPSHOT) trackPinning(id, invalid) else -1
+
+ /*
* The read observer for the snapshot if there is one.
*/
internal abstract val readObserver: ((Any) -> Unit)?
@@ -181,6 +189,7 @@
internal open fun close() {
sync {
openSnapshots = openSnapshots.clear(id)
+ releasePinnedSnapshot()
}
}
@@ -188,6 +197,16 @@
require(!disposed) { "Cannot use a disposed snapshot" }
}
+ internal fun releasePinnedSnapshot() {
+ if (pinningTrackingHandle >= 0) {
+ releasePinning(pinningTrackingHandle)
+ pinningTrackingHandle = -1
+ }
+ }
+
+ internal fun takeoverPinnedSnapshot(): Int =
+ pinningTrackingHandle.also { pinningTrackingHandle = -1 }
+
companion object {
/**
* Return the thread's active snapshot. If no thread snapshot is active then the current
@@ -479,6 +498,28 @@
}
/**
+ * Pin the snapshot and invalid set.
+ *
+ * @return returns a handle that should be passed to [releasePinning] when the snapshot closes or
+ * is disposed.
+ */
+internal fun trackPinning(id: Int, invalid: SnapshotIdSet): Int {
+ val pinned = invalid.lowest(id)
+ return sync {
+ pinningTable.add(pinned)
+ }
+}
+
+/**
+ * Release the [handle] returned by [trackPinning]
+ */
+internal fun releasePinning(handle: Int) {
+ sync {
+ pinningTable.remove(handle)
+ }
+}
+
+/**
* A snapshot of the values return by mutable states and other state objects. All state object
* will have the same value in the snapshot as they had when the snapshot was created unless they
* are explicitly changed in the snapshot.
@@ -681,6 +722,8 @@
sync {
// Remove itself and previous ids from the open set.
openSnapshots = openSnapshots.clear(id).andNot(previousIds)
+ releasePinnedSnapshot()
+ releasePreviouslyPinnedSnapshots()
}
}
@@ -825,6 +868,25 @@
}
}
+ internal fun recordPreviousPinnedSnapshot(id: Int) {
+ if (id >= 0)
+ previousPinnedSnapshots = previousPinnedSnapshots + id
+ }
+
+ internal fun recordPreviousPinnedSnapshots(handles: IntArray) {
+ // Avoid unnecessary copies implied by the `+` below.
+ if (handles.isEmpty()) return
+ val pinned = previousPinnedSnapshots
+ if (pinned.isEmpty()) previousPinnedSnapshots = handles
+ else previousPinnedSnapshots = pinned + handles
+ }
+
+ internal fun releasePreviouslyPinnedSnapshots() {
+ for (index in previousPinnedSnapshots.indices) {
+ releasePinning(previousPinnedSnapshots[index])
+ }
+ }
+
internal fun recordPreviousList(snapshots: SnapshotIdSet) {
sync {
previousIds = previousIds.or(snapshots)
@@ -844,6 +906,11 @@
internal var previousIds: SnapshotIdSet = SnapshotIdSet.EMPTY
/**
+ * A list of the pinned snapshots handles that must be released by this snapshot
+ */
+ internal var previousPinnedSnapshots: IntArray = IntArray(0)
+
+ /**
* The number of pending nested snapshots of this snapshot. To simplify the code, this
* snapshot it, itself, counted as its own nested snapshot.
*/
@@ -1165,10 +1232,7 @@
error("Cannot apply the global snapshot directly. Call Snapshot.advanceGlobalSnapshot")
override fun dispose() {
- // Disposing the global snapshot is a no-op.
-
- // The dispose behavior is performed by advancing the global snapshot. This method is
- // squelched so calling it from `currentSnapshot` doesn't cause incorrect behavior
+ releasePinnedSnapshot()
}
}
@@ -1236,7 +1300,9 @@
// Ensure the ids associated with this snapshot are also applied by the parent.
parent.recordPrevious(id)
+ parent.recordPreviousPinnedSnapshot(takeoverPinnedSnapshot())
parent.recordPreviousList(previousIds)
+ parent.recordPreviousPinnedSnapshots(previousPinnedSnapshots)
}
applied = true
@@ -1382,6 +1448,13 @@
/** The first snapshot created must be at least on more than the INVALID_SNAPSHOT */
private var nextSnapshotId = INVALID_SNAPSHOT + 1
+/**
+ * A tracking table for pinned snapshots. A pinned snapshot is the lowest snapshot id that the
+ * snapshot is ignoring by considering them invalid. This is used to calculate when a snapshot
+ * record can be reused.
+ */
+private val pinningTable = SnapshotDoubleIndexHeap()
+
/** A list of apply observers */
private val applyObservers = mutableListOf<(Set<Any>, Snapshot) -> Unit>()
@@ -1425,6 +1498,7 @@
invalid = openSnapshots
)
)
+ previousGlobalSnapshot.dispose()
openSnapshots = openSnapshots.set(globalId)
}
@@ -1534,10 +1608,11 @@
* record created in an abandoned snapshot. It is also true if the record is valid in the
* previous snapshot and is obscured by another record also valid in the previous state record.
*/
-private fun used(state: StateObject, id: Int, invalid: SnapshotIdSet): StateRecord? {
+private fun used(state: StateObject): StateRecord? {
var current: StateRecord? = state.firstStateRecord
var validRecord: StateRecord? = null
- val lowestOpen = invalid.lowest(id)
+ val reuseLimit = pinningTable.lowestOrDefault(nextSnapshotId) - 1
+ val invalid = SnapshotIdSet.EMPTY
while (current != null) {
val currentId = current.snapshotId
if (currentId == INVALID_SNAPSHOT) {
@@ -1545,7 +1620,7 @@
// immediately.
return current
}
- if (valid(current, lowestOpen, invalid)) {
+ if (valid(current, reuseLimit, invalid)) {
if (validRecord == null) {
validRecord = current
} else {
@@ -1593,7 +1668,7 @@
if (candidate.snapshotId == id) return candidate
- val newData = newOverwritableRecord(state, snapshot)
+ val newData = newOverwritableRecord(state)
newData.snapshotId = id
snapshot.recordModified(state)
@@ -1614,13 +1689,13 @@
// cache the result of readable() as the mutating thread calls to writable() can change the
// result of readable().
@Suppress("UNCHECKED_CAST")
- val newData = newOverwritableRecord(state, snapshot)
+ val newData = newOverwritableRecord(state)
newData.assign(this)
newData.snapshotId = snapshot.id
return newData
}
-internal fun <T : StateRecord> T.newOverwritableRecord(state: StateObject, snapshot: Snapshot): T {
+internal fun <T : StateRecord> T.newOverwritableRecord(state: StateObject): T {
// Calling used() on a state object might return the same record for each thread calling
// used() therefore selecting the record to reuse should be guarded.
@@ -1633,7 +1708,7 @@
// cache the result of readable() as the mutating thread calls to writable() can change the
// result of readable().
@Suppress("UNCHECKED_CAST")
- return (used(state, snapshot.id, openSnapshots) as T?)?.apply {
+ return (used(state) as T?)?.apply {
snapshotId = Int.MAX_VALUE
} ?: create().apply {
snapshotId = Int.MAX_VALUE
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotDoubleIndexHeap.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotDoubleIndexHeap.kt
new file mode 100644
index 0000000..12241e4
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotDoubleIndexHeap.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2021 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 androidx.compose.runtime.snapshots
+
+import androidx.compose.runtime.TestOnly
+
+/**
+ * This class maintains returns the lowest number of all the number it is given and can return that
+ * number in O(1) time. Adding a number is at worst O(log N). Adding a number returns a handled that
+ * can be later used to remove the number also with at worst O(log N).
+ *
+ * The data structure used is a heap, the first stage of a heap sort. As values are added and
+ * removed the heap invariants are reestablished for the new value by either shifting values up
+ * or down in the heap.
+ *
+ * This class is used to track the lowest pinning snapshot id. A pinning snapshot id is either the
+ * lowest snapshot in its invalid list or its own id if its invalid list is empty.
+ *
+ * If any snapshot object has two records below the lowest pinned snapshot then the lowest snapshot
+ * id can be reused as it will never be selected as the current record of the object because the
+ * record with the higher id will always be selected instead.
+ */
+internal class SnapshotDoubleIndexHeap {
+ var size = 0
+ private set
+ // An array of values which are the snapshot ids
+ private var values = IntArray(INITIAL_CAPACITY)
+
+ // An array of where the value's handle is in the handles array.
+ private var index = IntArray(INITIAL_CAPACITY)
+
+ // An array of handles which tracks where the value is the values array. Free handles are stored
+ // as a single linked list using the array value as the link to the next free handle location.
+ // It is initialized with 1, 2, 3, ... which produces a linked list of all handles free starting
+ // at 0.
+ private var handles = IntArray(INITIAL_CAPACITY) { it + 1 }
+
+ // The first free handle.
+ private var firstFreeHandle = 0
+
+ fun lowestOrDefault(default: Int = 0) = if (size > 0) values[0] else default
+
+ /**
+ * Add a value to the heap by adding it to the end of the heap and then shifting it up until
+ * it is either at the root or its parent is less or equal to it.
+ */
+ fun add(value: Int): Int {
+ ensure(size + 1)
+ val i = size++
+ val handle = allocateHandle()
+ values[i] = value
+ index[i] = handle
+ handles[handle] = i
+ shiftUp(i)
+ return handle
+ }
+
+ /**
+ * Remove a value by using the index to locate where it is in the heap then replacing its
+ * location with the last member of the heap and shifting it up or down depending to restore
+ * the heap invariants.
+ */
+ fun remove(handle: Int) {
+ val i = handles[handle]
+ swap(i, size - 1)
+ size--
+ shiftUp(i)
+ shiftDown(i)
+ freeHandle(handle)
+ }
+
+ /**
+ * Validate that the heap invariants hold.
+ */
+ @TestOnly
+ fun validate() {
+ for (index in 1 until size) {
+ val parent = ((index + 1) shr 1) - 1
+ if (values[parent] > values[index]) error("Index $index is out of place")
+ }
+ }
+
+ /**
+ * Validate that the handle refers to the expected value.
+ */
+ @TestOnly
+ fun validateHandle(handle: Int, value: Int) {
+ val i = handles[handle]
+ if (index[i] != handle) error("Index for handle $handle is corrupted")
+ if (values[i] != value)
+ error("Value for handle $handle was ${values[i]} but was supposed to be $value")
+ }
+
+ /**
+ * Shift a value at [index] until its parent is less than it is or it is at index 0.
+ */
+ private fun shiftUp(index: Int) {
+ val values = values
+ val value = values[index]
+ var current = index
+ while (current > 0) {
+ val parent = ((current + 1) shr 1) - 1
+ if (values[parent] > value) {
+ swap(parent, current)
+ current = parent
+ continue
+ }
+ break
+ }
+ }
+
+ /**
+ * Shift a value at [index] down by comparing it to the least of its children and swapping with
+ * it if the child is less than it is, continuing until the index has no children.
+ */
+ private fun shiftDown(index: Int) {
+ val values = values
+ val half = size shr 1
+ var current = index
+ while (current < half) {
+ val right = (current + 1) shl 1
+ val left = right - 1
+ if (right < size && values[right] < values[left]) {
+ if (values[right] < values[current]) {
+ swap(right, current)
+ current = right
+ } else
+ return
+ } else if (values[left] < values[current]) {
+ swap(left, current)
+ current = left
+ } else
+ return
+ }
+ }
+
+ /**
+ * Swap the values at index [a] and [b]. This is used to restore the heap invariants in
+ * [shiftUp] and [shiftDown]. It also ensures that the [index] and [handles] are updated to
+ * account for the swap.
+ */
+ private fun swap(a: Int, b: Int) {
+ val values = values
+ val index = index
+ val handles = handles
+ var t = values[a]
+ values[a] = values[b]
+ values[b] = t
+ t = index[a]
+ index[a] = index[b]
+ index[b] = t
+ handles[index[a]] = a
+ handles[index[b]] = b
+ }
+
+ /**
+ * Ensure that the heap can contain at least [atLeast] elements.
+ */
+ private fun ensure(atLeast: Int) {
+ val capacity = values.size
+ if (atLeast <= capacity) return
+ val newCapacity = capacity * 2
+ val newValues = IntArray(newCapacity)
+ val newIndex = IntArray(newCapacity)
+ values.copyInto(newValues)
+ index.copyInto(newIndex)
+ values = newValues
+ index = newIndex
+ }
+
+ /**
+ * Allocate a free handle, growing the list of handles if necessary.
+ */
+ private fun allocateHandle(): Int {
+ val capacity = handles.size
+ if (firstFreeHandle >= capacity) {
+ val newHandles = IntArray(capacity * 2) { it + 1 }
+ handles.copyInto(newHandles)
+ handles = newHandles
+ }
+ val handle = firstFreeHandle
+ firstFreeHandle = handles[firstFreeHandle]
+ return handle
+ }
+
+ /**
+ * Free a handle by adding it to the free list of handles which is a linked list of handles
+ * stored in the handles array as a linked list of indexes.
+ */
+ private fun freeHandle(handle: Int) {
+ handles[handle] = firstFreeHandle
+ firstFreeHandle = handle
+ }
+}
+
+private const val INITIAL_CAPACITY = 16
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt
index 19a2458..0d64142 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt
@@ -237,6 +237,33 @@
}
}
+ fun and(bits: SnapshotIdSet): SnapshotIdSet {
+ if (bits == EMPTY) return EMPTY
+ if (this == EMPTY) return EMPTY
+ return if (bits.lowerBound == this.lowerBound && bits.belowBound === this.belowBound) {
+ val newUpper = this.upperSet and bits.upperSet
+ val newLower = this.lowerSet and bits.lowerSet
+ if (newUpper == 0L && newLower == 0L && this.belowBound == null)
+ EMPTY
+ else
+ SnapshotIdSet(
+ this.upperSet and bits.upperSet,
+ this.lowerSet and bits.lowerSet,
+ this.lowerBound,
+ this.belowBound
+ )
+ } else {
+ if (this.belowBound == null)
+ this.fold(EMPTY) { previous, index ->
+ if (bits.get(index)) previous.set(index) else previous
+ }
+ else
+ bits.fold(EMPTY) { previous, index ->
+ if (this.get(index)) previous.set(index) else previous
+ }
+ }
+ }
+
/**
* Produce a set that if the value is set in this set or [bits] (`a | b`)
*/
@@ -283,6 +310,28 @@
}
}.iterator()
+ inline fun fastForEach(block: (Int) -> Unit) {
+ val belowBound = belowBound
+ if (belowBound != null)
+ for (element in belowBound) {
+ block(element)
+ }
+ if (lowerSet != 0L) {
+ for (index in 0 until Long.SIZE_BITS) {
+ if (lowerSet and (1L shl index) != 0L) {
+ block(index + lowerBound)
+ }
+ }
+ }
+ if (upperSet != 0L) {
+ for (index in 0 until Long.SIZE_BITS) {
+ if (upperSet and (1L shl index) != 0L) {
+ block(index + Long.SIZE_BITS + lowerBound)
+ }
+ }
+ }
+ }
+
fun lowest(default: Int): Int {
val belowBound = belowBound
if (belowBound != null) return belowBound[0]
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionData.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionData.kt
index 630f878..cc1edd6 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionData.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionData.kt
@@ -74,4 +74,10 @@
* [remember] and the last value returned by [remember], etc.
*/
val data: Iterable<Any?>
+
+ /**
+ * A value that identifies a Group independently of movement caused by recompositions.
+ */
+ val identity: Any?
+ get() = null
}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
index bb0dc99..2df65f4 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -3229,6 +3229,41 @@
threadException?.let { throw it }
}
+ @Test
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun avoidRaceConditionWhenApplyingSnapshotsInAThread() = compositionTest {
+ val count = mutableStateOf(0)
+ var threadException: Exception? = null
+
+ compose {
+ Text("Some text")
+ Text("Count ${count.value}")
+ }
+
+ val thread = thread {
+ try {
+ while (!Thread.interrupted()) {
+ Snapshot.withMutableSnapshot {
+ count.value++
+ }
+ }
+ } catch (e: Exception) {
+ threadException = e
+ }
+ }
+
+ repeat(200) {
+ advance(ignorePendingWork = true)
+ delay(1)
+ }
+
+ thread.interrupt()
+ @Suppress("BlockingMethodInNonBlockingContext")
+ thread.join()
+ delay(10)
+ threadException?.let { throw it }
+ }
+
@Test // b/197064250 and others
fun canInvalidateDuringApplyChanges() = compositionTest {
var value by mutableStateOf(0)
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotDoubleIndexHeapTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotDoubleIndexHeapTests.kt
new file mode 100644
index 0000000..03590c9
--- /dev/null
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotDoubleIndexHeapTests.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2021 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 androidx.compose.runtime.snapshots
+
+import kotlin.random.Random
+import kotlin.random.nextInt
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+class SnapshotDoubleIndexHeapTests {
+
+ @Test
+ fun canCreateADoubleIndexHeap() {
+ val heap = SnapshotDoubleIndexHeap()
+ assertNotNull(heap)
+ }
+
+ @Test
+ fun canAddAndRemoveNumbersInSequence() {
+ val heap = SnapshotDoubleIndexHeap()
+ val handles = IntArray(100)
+ repeat(100) {
+ handles[it] = heap.add(it)
+ }
+ repeat(100) {
+ assertEquals(it, heap.lowestOrDefault(-1))
+ heap.remove(handles[it])
+ }
+ assertEquals(0, heap.size)
+ }
+
+ @Test
+ fun canInsertAndRemoveRandomNumbersWithDuplicate() {
+ val heap = SnapshotDoubleIndexHeap()
+ val random = Random(1377)
+ val toAdd = IntArray(5000) { random.nextInt(0 until 300) }.toMutableList()
+ val toRemove = mutableListOf<Pair<Int, Int>>()
+
+ while (toAdd.size > 0 || toRemove.size > 0) {
+ val shouldAdd = random.nextInt(toAdd.size + toRemove.size) < toAdd.size
+ if (shouldAdd) {
+ val indexToAdd = random.nextInt(toAdd.size)
+ val value = toAdd[indexToAdd]
+ val handle = heap.add(value)
+ toRemove.add(value to handle)
+ toAdd.removeAt(indexToAdd)
+ } else {
+ val indexToRemove = random.nextInt(toRemove.size)
+ val (value, handle) = toRemove[indexToRemove]
+ assertTrue(heap.lowestOrDefault(-1) <= value)
+ heap.remove(handle)
+ toRemove.removeAt(indexToRemove)
+ }
+
+ heap.validate()
+ for ((value, handle) in toRemove) {
+ heap.validateHandle(handle, value)
+ }
+ val lowestAdded = toRemove.fold(400) { lowest, (value, _) ->
+ if (value < lowest) value else lowest
+ }
+ assertEquals(lowestAdded, heap.lowestOrDefault(400))
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
index f01ebb3..da8ca1a 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
@@ -28,6 +28,8 @@
import androidx.compose.runtime.referentialEqualityPolicy
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot.Companion.openSnapshotCount
+import androidx.compose.runtime.snapshots.Snapshot.Companion.takeMutableSnapshot
+import androidx.compose.runtime.snapshots.Snapshot.Companion.takeSnapshot
import androidx.compose.runtime.structuralEqualityPolicy
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
@@ -798,6 +800,21 @@
}
}
+ @Test
+ fun testRecordsAreReusedCorrectly() {
+ val value = mutableStateOf(0)
+ Snapshot.withMutableSnapshot { value.value++ }
+ val mutable1 = takeMutableSnapshot()
+ val readable1 = takeSnapshot()
+ mutable1.enter { value.value++ }
+ mutable1.apply().check()
+ Snapshot.withMutableSnapshot { value.value++ }
+ val v = readable1.enter { value.value }
+ assertEquals(v, 1)
+ readable1.dispose()
+ mutable1.dispose()
+ }
+
private var count = 0
@BeforeTest
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
index 46b3aab..e48c49a 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
@@ -800,23 +800,34 @@
val androidComposeView = findAndroidComposeView()
androidComposeView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
val builder = LayoutInspectorTree()
- builder.hideSystemNodes = false
- val first = builder.convert(androidComposeView)
- .flatMap { flatten(it) }
- .first { it.name == "First" }
+ val tree1 = builder.convert(androidComposeView)
+ val first = tree1.flatMap { flatten(it) }.single { it.name == "First" }
val hash = packageNameHash(this.javaClass.name.substringBeforeLast('.'))
assertThat(first.fileName).isEqualTo("LayoutInspectorTreeTest.kt")
assertThat(first.packageHash).isEqualTo(hash)
assertThat(first.parameters.map { it.name }).contains("p1")
+ val cross1 = tree1.flatMap { flatten(it) }.single { it.name == "Crossfade" }
+ val button1 = tree1.flatMap { flatten(it) }.single { it.name == "Button" }
+ val column1 = tree1.flatMap { flatten(it) }.single { it.name == "Column" }
+ assertThat(cross1.id < RESERVED_FOR_GENERATED_IDS)
+ assertThat(button1.id < RESERVED_FOR_GENERATED_IDS)
+ assertThat(column1.id < RESERVED_FOR_GENERATED_IDS)
+
composeTestRule.onNodeWithText("Button").performClick()
composeTestRule.runOnIdle {
- val second = builder.convert(androidComposeView)
- .flatMap { flatten(it) }
- .first { it.name == "Second" }
+ val tree2 = builder.convert(androidComposeView)
+ val second = tree2.flatMap { flatten(it) }.first { it.name == "Second" }
assertThat(second.fileName).isEqualTo("LayoutInspectorTreeTest.kt")
assertThat(second.packageHash).isEqualTo(hash)
assertThat(second.parameters.map { it.name }).contains("p2")
+
+ val cross2 = tree2.flatMap { flatten(it) }.first { it.name == "Crossfade" }
+ val button2 = tree2.flatMap { flatten(it) }.single { it.name == "Button" }
+ val column2 = tree2.flatMap { flatten(it) }.single { it.name == "Column" }
+ assertThat(cross2.id).isEqualTo(cross1.id)
+ assertThat(button2.id).isEqualTo(button1.id)
+ assertThat(column2.id).isEqualTo(column1.id)
}
}
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
index 5dab3dd..cb4481c 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -67,6 +67,17 @@
packageNameHash("androidx.compose.ui.window"),
)
+/**
+ * The [InspectorNode.id] will be populated with:
+ * - the layerId from a LayoutNode if this exists
+ * - an id generated from an Anchor instance from the SlotTree if this exists
+ * - a generated id if none of the above ids are available
+ *
+ * The interval -10000..-2 is reserved for the generated ids.
+ */
+@VisibleForTesting
+const val RESERVED_FOR_GENERATED_IDS = -10000L
+
private val unwantedCalls = setOf(
"CompositionLocalProvider",
"Content",
@@ -387,7 +398,7 @@
input.forEach { node ->
if (node.name.isEmpty() && !(buildFakeChildNodes && node.layoutNodes.isNotEmpty())) {
parentNode.children.addAll(node.children)
- if (node.id != UNDEFINED_ID) {
+ if (node.id > UNDEFINED_ID) {
// If multiple siblings with a render ids are dropped:
// Ignore them all. And delegate the drawing to a parent in the inspector.
id = if (id == null) node.id else UNDEFINED_ID
@@ -414,13 +425,13 @@
}
val nodeId = id
parentNode.id =
- if (parentNode.id == UNDEFINED_ID && nodeId != null) nodeId else parentNode.id
+ if (parentNode.id <= UNDEFINED_ID && nodeId != null) nodeId else parentNode.id
}
@OptIn(UiToolingDataApi::class)
private fun parse(group: Group, overrideBox: IntRect? = null): MutableInspectorNode {
val node = newNode()
- node.id = getRenderNode(group)
+ node.id = parseId(group)
node.name = group.name ?: ""
parsePosition(group, node, overrideBox)
parseLayoutInfo(group, node)
@@ -518,12 +529,19 @@
}
@OptIn(UiToolingDataApi::class)
- private fun getRenderNode(group: Group): Long =
+ private fun parseId(group: Group): Long =
group.modifierInfo.asSequence()
.map { it.extra }
.filterIsInstance<GraphicLayerInfo>()
.map { it.layerId }
- .firstOrNull() ?: 0
+ .firstOrNull() ?: syntheticId(group)
+
+ @OptIn(UiToolingDataApi::class)
+ private fun syntheticId(group: Group): Long {
+ val id = group.identity?.hashCode() ?: return UNDEFINED_ID
+ // The hashCode is an Int
+ return id.toLong() - Int.MAX_VALUE.toLong() + RESERVED_FOR_GENERATED_IDS
+ }
private fun belongsToView(layoutNodes: List<LayoutInfo>, view: View): Boolean =
layoutNodes.asSequence().flatMap { node ->
diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.skiko.kt b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.skiko.kt
index dd3e69f..ed859db 100644
--- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.skiko.kt
+++ b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.skiko.kt
@@ -270,19 +270,14 @@
val prevBox = getBoxBackwardByOffset(offset)
val nextBox = getBoxForwardByOffset(offset)
return when {
- prevBox == null -> {
- val line = lineMetricsForOffset(offset)!!
- return when (getParagraphDirection(offset)) {
- ResolvedTextDirection.Ltr -> line.left.toFloat()
- ResolvedTextDirection.Rtl -> line.right.toFloat()
- }
- }
-
- nextBox == null || usePrimaryDirection || nextBox.direction == prevBox.direction ->
- prevBox.cursorHorizontalPosition()
-
- else ->
- nextBox.cursorHorizontalPosition(true)
+ prevBox == null && nextBox == null -> 0f
+ prevBox == null -> nextBox!!.cursorHorizontalPosition(true)
+ nextBox == null -> prevBox.cursorHorizontalPosition()
+ nextBox.direction == prevBox.direction -> nextBox.cursorHorizontalPosition(true)
+ // BiDi transition offset, we need to resolve ambiguity with usePrimaryDirection
+ // for details see comment for MultiParagraph.getHorizontalPosition
+ usePrimaryDirection -> prevBox.cursorHorizontalPosition()
+ else -> nextBox.cursorHorizontalPosition(true)
}
}
diff --git a/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt b/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt
index df276cc..f8fb257 100644
--- a/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt
@@ -2,7 +2,7 @@
package androidx.compose.ui.tooling.data {
@androidx.compose.ui.tooling.data.UiToolingDataApi public final class CallGroup extends androidx.compose.ui.tooling.data.Group {
- ctor public CallGroup(Object? key, String? name, androidx.compose.ui.unit.IntRect box, androidx.compose.ui.tooling.data.SourceLocation? location, java.util.List<androidx.compose.ui.tooling.data.ParameterInformation> parameters, java.util.Collection<?> data, java.util.Collection<? extends androidx.compose.ui.tooling.data.Group> children);
+ ctor public CallGroup(Object? key, String? name, androidx.compose.ui.unit.IntRect box, androidx.compose.ui.tooling.data.SourceLocation? location, Object? identity, java.util.List<androidx.compose.ui.tooling.data.ParameterInformation> parameters, java.util.Collection<?> data, java.util.Collection<? extends androidx.compose.ui.tooling.data.Group> children);
property public java.util.List<androidx.compose.ui.tooling.data.ParameterInformation> parameters;
}
@@ -10,6 +10,7 @@
method public final androidx.compose.ui.unit.IntRect getBox();
method public final java.util.Collection<androidx.compose.ui.tooling.data.Group> getChildren();
method public final java.util.Collection<java.lang.Object> getData();
+ method public final Object? getIdentity();
method public final Object? getKey();
method public final androidx.compose.ui.tooling.data.SourceLocation? getLocation();
method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
@@ -18,6 +19,7 @@
property public final androidx.compose.ui.unit.IntRect box;
property public final java.util.Collection<androidx.compose.ui.tooling.data.Group> children;
property public final java.util.Collection<java.lang.Object> data;
+ property public final Object? identity;
property public final Object? key;
property public final androidx.compose.ui.tooling.data.SourceLocation? location;
property public java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo;
diff --git a/compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/SlotTree.kt b/compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/SlotTree.kt
index d117e47..1ef1358 100644
--- a/compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/SlotTree.kt
+++ b/compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/SlotTree.kt
@@ -48,6 +48,11 @@
val location: SourceLocation?,
/**
+ * An optional value that identifies a Group independently of movement caused by recompositions.
+ */
+ val identity: Any?,
+
+ /**
* The bounding layout box for the group.
*/
val box: IntRect,
@@ -134,10 +139,11 @@
name: String?,
box: IntRect,
location: SourceLocation?,
+ identity: Any?,
override val parameters: List<ParameterInformation>,
data: Collection<Any?>,
children: Collection<Group>
-) : Group(key, name, location, box, data, children)
+) : Group(key, name, location, identity, box, data, children)
/**
* A group that represents an emitted node
@@ -154,13 +160,14 @@
data: Collection<Any?>,
override val modifierInfo: List<ModifierInfo>,
children: Collection<Group>
-) : Group(key, null, null, box, data, children)
+) : Group(key, null, null, null, box, data, children)
@UiToolingDataApi
private object EmptyGroup : Group(
key = null,
name = null,
location = null,
+ identity = null,
box = emptyBox,
data = emptyList(),
children = emptyList()
@@ -466,6 +473,8 @@
if (children.isEmpty()) emptyBox else
children.map { g -> g.box }.reduce { acc, box -> box.union(acc) }
}
+ val location =
+ if (context?.isCall == true) { parentContext?.nextSourceLocation() } else { null }
return if (node != null) NodeGroup(
key,
node,
@@ -478,8 +487,11 @@
key,
context?.name,
box,
- if (context != null && context.isCall) {
- parentContext?.nextSourceLocation()
+ location,
+ identity = if (!context?.name.isNullOrEmpty() &&
+ location?.sourceFile != null &&
+ (box.bottom - box.top > 0 || box.right - box.left > 0)) {
+ this.identity
} else {
null
},
diff --git a/compose/ui/ui-tooling-preview/api/current.txt b/compose/ui/ui-tooling-preview/api/current.txt
index 241d9b7..5acb5df 100644
--- a/compose/ui/ui-tooling-preview/api/current.txt
+++ b/compose/ui/ui-tooling-preview/api/current.txt
@@ -24,6 +24,10 @@
field public static final String PIXEL_4_XL = "id:pixel_4_xl";
field public static final String PIXEL_C = "id:pixel_c";
field public static final String PIXEL_XL = "id:pixel_xl";
+ field public static final String WEAR_OS_LARGE_ROUND = "id:wearos_large_round";
+ field public static final String WEAR_OS_RECT = "id:wearos_rect";
+ field public static final String WEAR_OS_SMALL_ROUND = "id:wearos_small_round";
+ field public static final String WEAR_OS_SQUARE = "id:wearos_square";
}
@kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Preview {
diff --git a/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt b/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt
index 241d9b7..5acb5df 100644
--- a/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt
@@ -24,6 +24,10 @@
field public static final String PIXEL_4_XL = "id:pixel_4_xl";
field public static final String PIXEL_C = "id:pixel_c";
field public static final String PIXEL_XL = "id:pixel_xl";
+ field public static final String WEAR_OS_LARGE_ROUND = "id:wearos_large_round";
+ field public static final String WEAR_OS_RECT = "id:wearos_rect";
+ field public static final String WEAR_OS_SMALL_ROUND = "id:wearos_small_round";
+ field public static final String WEAR_OS_SQUARE = "id:wearos_square";
}
@kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Preview {
diff --git a/compose/ui/ui-tooling-preview/api/restricted_current.txt b/compose/ui/ui-tooling-preview/api/restricted_current.txt
index 241d9b7..5acb5df 100644
--- a/compose/ui/ui-tooling-preview/api/restricted_current.txt
+++ b/compose/ui/ui-tooling-preview/api/restricted_current.txt
@@ -24,6 +24,10 @@
field public static final String PIXEL_4_XL = "id:pixel_4_xl";
field public static final String PIXEL_C = "id:pixel_c";
field public static final String PIXEL_XL = "id:pixel_xl";
+ field public static final String WEAR_OS_LARGE_ROUND = "id:wearos_large_round";
+ field public static final String WEAR_OS_RECT = "id:wearos_rect";
+ field public static final String WEAR_OS_SMALL_ROUND = "id:wearos_small_round";
+ field public static final String WEAR_OS_SQUARE = "id:wearos_square";
}
@kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Preview {
diff --git a/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt
index 74939e4b..254acdd 100644
--- a/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt
+++ b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt
@@ -45,6 +45,11 @@
const val PIXEL_4_XL = "id:pixel_4_xl"
const val AUTOMOTIVE_1024p = "id:automotive_1024p_landscape"
+
+ const val WEAR_OS_LARGE_ROUND = "id:wearos_large_round"
+ const val WEAR_OS_SMALL_ROUND = "id:wearos_small_round"
+ const val WEAR_OS_SQUARE = "id:wearos_square"
+ const val WEAR_OS_RECT = "id:wearos_rect"
}
/**
@@ -77,7 +82,12 @@
Devices.PIXEL_4,
Devices.PIXEL_4_XL,
- Devices.AUTOMOTIVE_1024p
+ Devices.AUTOMOTIVE_1024p,
+
+ Devices.WEAR_OS_LARGE_ROUND,
+ Devices.WEAR_OS_SMALL_ROUND,
+ Devices.WEAR_OS_SQUARE,
+ Devices.WEAR_OS_RECT,
]
)
annotation class Device
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/AndroidMouse.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/AndroidMouse.kt
deleted file mode 100644
index 8b13789..0000000
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/AndroidMouse.kt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/layout/GraphicLayerInfo.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/layout/GraphicLayerInfo.android.kt
similarity index 100%
rename from compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/layout/GraphicLayerInfo.kt
rename to compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/layout/GraphicLayerInfo.android.kt
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAccessibilityManager.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAccessibilityManager.android.kt
similarity index 100%
rename from compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAccessibilityManager.kt
rename to compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAccessibilityManager.android.kt
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 5c2d443..fbec0bf 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -2504,14 +2504,16 @@
.getAllUncoveredSemanticsNodesToMap(): Map<Int, SemanticsNodeWithAdjustedBounds> {
val root = unmergedRootSemanticsNode
val nodes = mutableMapOf<Int, SemanticsNodeWithAdjustedBounds>()
- if (!root.layoutNode.isPlaced) {
+ if (!root.layoutNode.isPlaced || !root.layoutNode.isAttached) {
return nodes
}
val unaccountedSpace = Region().also { it.set(root.boundsInRoot.toAndroidRect()) }
fun findAllSemanticNodesRecursive(currentNode: SemanticsNode) {
+ val notAttachedOrPlaced =
+ !currentNode.layoutNode.isPlaced || !currentNode.layoutNode.isAttached
if ((unaccountedSpace.isEmpty && currentNode.id != root.id) ||
- (!currentNode.layoutNode.isPlaced && !currentNode.isFake)
+ (notAttachedOrPlaced && !currentNode.isFake)
) {
return
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/LayerMatrixCache.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/LayerMatrixCache.android.kt
similarity index 100%
rename from compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/LayerMatrixCache.kt
rename to compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/LayerMatrixCache.android.kt
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt
index b82b38c..4a870b3 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt
@@ -120,7 +120,7 @@
get() = layer.component.transparency
set(value) {
if (value != layer.component.transparency) {
- check(isUndecorated) { "Window should be undecorated!" }
+ check(isUndecorated) { "Transparent window should be undecorated!" }
check(!isDisplayable) {
"Cannot change transparency if window is already displayable."
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt
index e0b8e92..0cc292e 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt
@@ -117,7 +117,7 @@
get() = layer.component.transparency
set(value) {
if (value != layer.component.transparency) {
- check(isUndecorated) { "Window should be undecorated!" }
+ check(isUndecorated) { "Transparent window should be undecorated!" }
check(!isDisplayable) {
"Cannot change transparency if window is already displayable."
}
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactoryTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactoryTest.java
index b11e60c..aa06da9 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactoryTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactoryTest.java
@@ -30,7 +30,7 @@
@SmallTest
public void testGetConverter_registeredConverter_returnsRegisteredConverter() {
AppSearchDocumentConverter converter = AppSearchDocumentConverterFactory
- .getConverter("Timer");
+ .getConverter("builtin:Timer");
assertThat(converter).isNotNull();
assertThat(converter).isInstanceOf(TimerConverter.class);
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/TimerConverterTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/TimerConverterTest.java
index 22b8a02..8607bcf 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/TimerConverterTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/TimerConverterTest.java
@@ -18,6 +18,8 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
import android.content.Context;
import android.provider.AlarmClock;
@@ -29,39 +31,52 @@
import com.google.firebase.appindexing.Indexable;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
import java.util.TimeZone;
@RunWith(AndroidJUnit4.class)
public class TimerConverterTest {
- private final TimerConverter mTimerConverter = new TimerConverter();
+ @Mock private TimeModel mTimeModelMock;
+ private TimerConverter mTimerConverter;
private final Context mContext = ApplicationProvider.getApplicationContext();
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mTimerConverter = new TimerConverter(mTimeModelMock);
+ }
+
@Test
@SmallTest
public void testConvert_returnsIndexable() throws Exception {
// Expire time is timezone sensitive. Force the default timezone to be GMT here.
TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
+ // mock 5 seconds passed since the timer startTime
+ when(mTimeModelMock.getSystemCurrentTimeMillis()).thenReturn(8000L);
+ when(mTimeModelMock.getSystemClockElapsedRealtime()).thenReturn(10000L);
+
Timer timer = new Timer.Builder("namespace", "id")
- .setDurationMillis(1000)
- .setTimerStatus(Timer.STATUS_STARTED)
- .setExpireTimeMillis(3000)
- .setName("my timer")
- .setRemainingTimeMillis(500)
- .setRingtone(AlarmClock.VALUE_RINGTONE_SILENT)
.setScore(10)
.setTtlMillis(60000)
+ .setCreationTimestampMillis(1)
+ .setName("my timer")
+ .setDurationMillis(1000)
+ .setRemainingTimeMillis(10000)
+ .setRingtone(AlarmClock.VALUE_RINGTONE_SILENT)
+ .setStatus(Timer.STATUS_STARTED)
.setVibrate(true)
+ .setStartTimeMillis(3000)
+ .setStartTimeMillisInElapsedRealtime(5000)
.build();
- Indexable result = mTimerConverter.convertGenericDocument(mContext,
- GenericDocument.fromDocumentClass(timer))
- // Override creation timestamp to a constant instead of System.currentTimeMillis.
- // TODO: add creation timestamp to timer.
- .put(IndexableKeys.CREATION_TIMESTAMP_MILLIS, 1)
+ Indexable result = mTimerConverter.convertGenericDocument(
+ mContext, GenericDocument.fromDocumentClass(timer))
.build();
Indexable expectedResult = new Indexable.Builder("Timer")
.setMetadata(new Indexable.Metadata.Builder().setScore(10))
@@ -73,11 +88,69 @@
.put(IndexableKeys.NAMESPACE, "namespace")
.put(IndexableKeys.TTL_MILLIS, 60000)
.put(IndexableKeys.CREATION_TIMESTAMP_MILLIS, 1)
+ .put("message", "my timer")
.put("length", 1000)
.put("timerStatus", "Started")
- .put("expireTime", "1970-01-01T00:00:03+0000")
+ // Calculation: 3000 + 10000 = 13000 = 13 seconds since epoch
+ .put("expireTime", "1970-01-01T00:00:13+0000")
+ // Calculation: 8000 - (10000 - 5000) + 10000 = 13000 = 13 seconds since epoch
+ .put("expireTimeCorrectedByStartTimeInElapsedRealtime",
+ "1970-01-01T00:00:13+0000")
+ .put("remainingTime", 10000)
+ .put("ringtone", "silent")
+ .put("vibrate", true)
+ .build();
+ assertThat(result).isEqualTo(expectedResult);
+ }
+
+ @Test
+ @SmallTest
+ public void testConvert_differentSystemCurrentTime_returnsIndexable() throws Exception {
+ // Expire time is timezone sensitive. Force the default timezone to be GMT here.
+ TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
+
+ // 10 seconds has passed for System.CurrentTimeMillis
+ when(mTimeModelMock.getSystemCurrentTimeMillis()).thenReturn(13000L);
+ // 5 seconds has passed for SystemClock.elapsedRealtime
+ when(mTimeModelMock.getSystemClockElapsedRealtime()).thenReturn(10000L);
+
+ Timer timer = new Timer.Builder("namespace", "id")
+ .setScore(10)
+ .setTtlMillis(60000)
+ .setCreationTimestampMillis(1)
+ .setName("my timer")
+ .setDurationMillis(1000)
+ .setRemainingTimeMillis(10000)
+ .setRingtone(AlarmClock.VALUE_RINGTONE_SILENT)
+ .setStatus(Timer.STATUS_STARTED)
+ .setVibrate(true)
+ .setStartTimeMillis(3000)
+ .setStartTimeMillisInElapsedRealtime(5000)
+ .build();
+
+ Indexable result = mTimerConverter.convertGenericDocument(
+ mContext, GenericDocument.fromDocumentClass(timer))
+ .build();
+ Indexable expectedResult = new Indexable.Builder("Timer")
+ .setMetadata(new Indexable.Metadata.Builder().setScore(10))
+ .setId("id")
+ .setName("namespace")
+ .setUrl("intent:#Intent;action=androidx.core.content.pm.SHORTCUT_LISTENER;"
+ + "component=androidx.core.google.shortcuts.test/androidx.core.google."
+ + "shortcuts.TrampolineActivity;S.id=id;end")
+ .put(IndexableKeys.NAMESPACE, "namespace")
+ .put(IndexableKeys.TTL_MILLIS, 60000)
+ .put(IndexableKeys.CREATION_TIMESTAMP_MILLIS, 1)
.put("message", "my timer")
- .put("remainingTime", 500)
+ .put("length", 1000)
+ .put("timerStatus", "Started")
+ // Calculation: 3000 + 10000 = 13000 = 13 seconds since epoch
+ .put("expireTime", "1970-01-01T00:00:13+0000")
+ // Calculation: 13000 - (10000 - 5000) + 10000 = 18000 = 18 seconds since epoch
+ // This is the more correct expire time
+ .put("expireTimeCorrectedByStartTimeInElapsedRealtime",
+ "1970-01-01T00:00:18+0000")
+ .put("remainingTime", 10000)
.put("ringtone", "silent")
.put("vibrate", true)
.build();
@@ -88,13 +161,12 @@
@SmallTest
public void testConvert_withoutOptionalFields_returnsIndexable() throws Exception {
Timer timer = new Timer.Builder("namespace", "id")
+ // need to override to a value, otherwise it will use current time
+ .setCreationTimestampMillis(0)
.build();
Indexable result = mTimerConverter.convertGenericDocument(mContext,
GenericDocument.fromDocumentClass(timer))
- // Override creation timestamp to a constant instead of System.currentTimeMillis.
- // TODO: add creation timestamp to timer.
- .put(IndexableKeys.CREATION_TIMESTAMP_MILLIS, 1)
.build();
Indexable expectedResult = new Indexable.Builder("Timer")
.setMetadata(new Indexable.Metadata.Builder().setScore(0))
@@ -105,7 +177,7 @@
+ "shortcuts.TrampolineActivity;S.id=id;end")
.put(IndexableKeys.NAMESPACE, "namespace")
.put(IndexableKeys.TTL_MILLIS, 0)
- .put(IndexableKeys.CREATION_TIMESTAMP_MILLIS, 1)
+ .put(IndexableKeys.CREATION_TIMESTAMP_MILLIS, 0)
.put("length", 0)
.put("timerStatus", "Unknown")
.put("remainingTime", 0)
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/utils/ConverterUtilsTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/utils/ConverterUtilsTest.java
index e5cc3b4..ad5592f 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/utils/ConverterUtilsTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/utils/ConverterUtilsTest.java
@@ -55,6 +55,7 @@
.put(IndexableKeys.TTL_MILLIS, 0)
.put(IndexableKeys.CREATION_TIMESTAMP_MILLIS, 1);
assertThat(ConverterUtils.buildBaseIndexableFromGenericDocument(
- mContext, genericDocument).build()).isEqualTo(expectedIndexableBuilder.build());
+ mContext, "schema", genericDocument).build())
+ .isEqualTo(expectedIndexableBuilder.build());
}
}
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactory.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactory.java
index c4136ad..32b5af0 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactory.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactory.java
@@ -35,6 +35,8 @@
public class AppSearchDocumentConverterFactory {
private static final String TAG = "AppSearchDocumentConver"; // NOTYPO
+ private static final String TIMER_SCHEMA_TYPE = "builtin:Timer";
+
/**
* Returns a {@link AppSearchDocumentConverter} given a schema type. If the schema type is not
* supported, then the {@link GenericDocumentConverter} will be returned.
@@ -43,7 +45,7 @@
public static AppSearchDocumentConverter getConverter(@NonNull String schemaType) {
Preconditions.checkNotNull(schemaType);
- if ("Timer".equals(schemaType)) {
+ if (TIMER_SCHEMA_TYPE.equals(schemaType)) {
return new TimerConverter();
}
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/GenericDocumentConverter.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/GenericDocumentConverter.java
index 0e15087..0c25217 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/GenericDocumentConverter.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/GenericDocumentConverter.java
@@ -50,7 +50,7 @@
Preconditions.checkNotNull(genericDocument);
Indexable.Builder indexableBuilder = ConverterUtils.buildBaseIndexableFromGenericDocument(
- context, genericDocument);
+ context, genericDocument.getSchemaType(), genericDocument);
for (String property : genericDocument.getPropertyNames()) {
Object rawProperty = genericDocument.getProperty(property);
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimeModel.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimeModel.java
new file mode 100644
index 0000000..12256a3
--- /dev/null
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimeModel.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 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 androidx.core.google.shortcuts.converters;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.os.SystemClock;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Wrapper around time related system methods. This makes unit testing against system times
+ * easier.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class TimeModel {
+ /** Returns {@link System#currentTimeMillis()}. */
+ public long getSystemCurrentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ /** Returns {@link SystemClock#elapsedRealtime()}. */
+ public long getSystemClockElapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+}
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimerConverter.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimerConverter.java
index b09f04a..608ec9d 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimerConverter.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimerConverter.java
@@ -23,6 +23,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.builtintypes.Timer;
import androidx.core.google.shortcuts.utils.ConverterUtils;
@@ -49,15 +50,20 @@
private static final String DURATION_MILLIS_KEY = "durationMillis";
private static final String REMAINING_TIME_MILLIS_KEY = "remainingTimeMillis";
private static final String RINGTONE_KEY = "ringtone";
+ private static final String STATUS_KEY = "status";
private static final String VIBRATE_KEY = "vibrate";
- private static final String TIMER_STATUS_KEY = "timerStatus";
- private static final String EXPIRE_TIME_MILLIS_KEY = "expireTimeMillis";
+ private static final String START_TIME_MILLIS_KEY = "startTimeMillis";
+ private static final String START_TIME_MILLIS_IN_ELAPSED_REALTIME_KEY =
+ "startTimeMillisInElapsedRealtime";
// Keys for Indexables
private static final String MESSAGE_KEY = "message";
private static final String LENGTH_KEY = "length";
private static final String REMAINING_TIME_KEY = "remainingTime";
private static final String EXPIRE_TIME_KEY = "expireTime";
+ private static final String EXPIRE_TIME_CORRECTED_BY_START_TIME_IN_ELAPSED_REALTIME_KEY =
+ "expireTimeCorrectedByStartTimeInElapsedRealtime";
+ private static final String TIMER_STATUS_KEY = "timerStatus";
// Enums for TimerStatus
private static final String STARTED = "Started";
@@ -67,6 +73,8 @@
private static final String RESET = "Reset";
private static final String UNKNOWN = "Unknown";
+ private static final String TIMER_INDEXABLE_TYPE = "Timer";
+
private static final ThreadLocal<DateFormat> ISO8601_DATE_TIME_FORMAT =
new ThreadLocal<DateFormat>() {
@Override
@@ -75,6 +83,17 @@
}
};
+ private final TimeModel mTimeModel;
+
+ public TimerConverter() {
+ this(new TimeModel());
+ }
+
+ @VisibleForTesting
+ TimerConverter(TimeModel timeModel) {
+ mTimeModel = timeModel;
+ }
+
@Override
@NonNull
public Indexable.Builder convertGenericDocument(@NonNull Context context,
@@ -83,7 +102,7 @@
Preconditions.checkNotNull(timer);
Indexable.Builder indexableBuilder = ConverterUtils.buildBaseIndexableFromGenericDocument(
- context, timer);
+ context, TIMER_INDEXABLE_TYPE, timer);
indexableBuilder
.put(MESSAGE_KEY, timer.getPropertyString(NAME_KEY))
@@ -92,7 +111,7 @@
.put(RINGTONE_KEY, timer.getPropertyString(RINGTONE_KEY))
.put(VIBRATE_KEY, timer.getPropertyBoolean(VIBRATE_KEY));
- int timerStatus = (int) timer.getPropertyLong(TIMER_STATUS_KEY);
+ int timerStatus = (int) timer.getPropertyLong(STATUS_KEY);
switch (timerStatus) {
case Timer.STATUS_UNKNOWN:
indexableBuilder.put(TIMER_STATUS_KEY, UNKNOWN);
@@ -114,17 +133,35 @@
break;
default:
indexableBuilder.put(TIMER_STATUS_KEY, UNKNOWN);
- Log.w(TAG, "Invalud time status: " + timerStatus + ", defaulting to "
+ Log.w(TAG, "Invalid time status: " + timerStatus + ", defaulting to "
+ "Timer.STATUS_UNKNOWN");
}
- // 0 means never expire.
- long expireTime = timer.getPropertyLong(EXPIRE_TIME_MILLIS_KEY);
- if (expireTime > 0) {
- Date date = new Date(expireTime);
- indexableBuilder.put(EXPIRE_TIME_KEY,
- Preconditions.checkNotNull(ISO8601_DATE_TIME_FORMAT.get()).format(date));
+ if (timerStatus == Timer.STATUS_STARTED) {
+ long startTime = timer.getPropertyLong(START_TIME_MILLIS_KEY);
+ long remainingTime = timer.getPropertyLong(REMAINING_TIME_MILLIS_KEY);
+
+ long expireTime = remainingTime + startTime;
+ indexableBuilder.put(EXPIRE_TIME_KEY, convertTimeToISOFormat(expireTime));
+
+ long startTimeInElapsedRealtime =
+ timer.getPropertyLong(START_TIME_MILLIS_IN_ELAPSED_REALTIME_KEY);
+ if (startTimeInElapsedRealtime >= 0) {
+ // If startTime in elapsed realtime is set, use that to calculate expire time as
+ // well.
+ long elapsedTime = mTimeModel.getSystemClockElapsedRealtime()
+ - startTimeInElapsedRealtime;
+ long expireTimeFromElapsedRealtime =
+ mTimeModel.getSystemCurrentTimeMillis() - elapsedTime + remainingTime;
+ indexableBuilder.put(EXPIRE_TIME_CORRECTED_BY_START_TIME_IN_ELAPSED_REALTIME_KEY,
+ convertTimeToISOFormat(expireTimeFromElapsedRealtime));
+ }
}
return indexableBuilder;
}
+
+ private String convertTimeToISOFormat(long time) {
+ Date date = new Date(time);
+ return Preconditions.checkNotNull(ISO8601_DATE_TIME_FORMAT.get()).format(date);
+ }
}
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ConverterUtils.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ConverterUtils.java
index 35a05d9..f67c5a9 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ConverterUtils.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ConverterUtils.java
@@ -40,11 +40,12 @@
@NonNull
public static Indexable.Builder buildBaseIndexableFromGenericDocument(
@NonNull Context context,
+ @NonNull String indexableType,
@NonNull GenericDocument genericDocument) {
Preconditions.checkNotNull(context);
Preconditions.checkNotNull(genericDocument);
- return new Indexable.Builder(genericDocument.getSchemaType())
+ return new Indexable.Builder(indexableType)
.setId(genericDocument.getId())
// TODO (b/206020715): remove name when it's no longer a required field.
.setName(genericDocument.getNamespace())
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 9284f90..910aa2e 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -223,7 +223,7 @@
\^
symbol\: static FLAG_MUTABLE
location\: class PendingIntent
-\$OUT_DIR\/androidx\/docs\-public\/build\/unzippedDocsSources\/androidx\/work\/impl\/utils\/ForceStopRunnable\.java\:[0-9]+\: error\: cannot find symbol
+\$OUT_DIR\/androidx\/docs\-public\/build\/srcs\/androidx\/work\/impl\/utils\/ForceStopRunnable\.java\:[0-9]+\: error\: cannot find symbol
# > Task :buildOnServer
[0-9]+ problems were found reusing the configuration cache\.
[0-9]+ problems were found reusing the configuration cache, [0-9]+ of which seem unique\.
@@ -417,7 +417,7 @@
# > Task :compose:ui:ui-graphics:ui-graphics-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/compose/ui/ui\-graphics/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Task :docs-public:doclavaDocs
-\$OUT_DIR\/androidx\/docs\-public\/build\/unzippedDocsSources\/androidx\/work\/impl\/WorkManagerImpl\.java\:[0-9]+\: error\: cannot find symbol
+\$OUT_DIR\/androidx\/docs\-public\/build\/srcs\/androidx\/work\/impl\/WorkManagerImpl\.java\:[0-9]+\: error\: cannot find symbol
import static android\.app\.PendingIntent\.FLAG_MUTABLE\;
# > Task :docs-public:dackkaDocs
PROGRESS: Initializing plugins
@@ -612,4 +612,4 @@
Information in locals\-table is invalid with respect to the stack map table\. Local refers to non\-present stack map type for register: [0-9]+ with constraint OBJECT\.
Info: Some warnings are typically a sign of using an outdated Java toolchain\. To fix, recompile the source with an updated toolchain\.
# > Task :compose:ui:ui-inspection:buildCMakeRelWithDebInfo[arm64-v8a]
-C/C\+\+: ninja: warning: bad deps log signature or version; starting over
\ No newline at end of file
+C/C\+\+: ninja: warning: bad deps log signature or version; starting over
diff --git a/development/update_studio.sh b/development/update_studio.sh
index 1a40e5b..235344c 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -1,7 +1,7 @@
#!/bin/bash
# Get versions
-AGP_VERSION=${1:-7.1.0-beta03}
-STUDIO_VERSION_STRING=${2:-"Android Studio Bumblebee (2021.1.1) Beta 3"}
+AGP_VERSION=${1:-7.2.0-alpha06}
+STUDIO_VERSION_STRING=${2:-"Android Studio Chipmunk (2021.2.1) Canary 6"}
STUDIO_IFRAME_LINK=`curl "https://developer.android.com/studio/archive.html" | grep iframe | sed "s/.*src=\"\([a-zA-Z0-9\/\._]*\)\".*/https:\/\/android-dot-devsite-v2-prod.appspot.com\1/g"`
STUDIO_LINK=`curl -s $STUDIO_IFRAME_LINK | grep -C30 "$STUDIO_VERSION_STRING" | grep Linux | tail -n 1 | sed 's/.*a href="\(.*\).*"/\1/g'`
STUDIO_VERSION=`echo $STUDIO_LINK | sed "s/.*ide-zips\/\(.*\)\/android-studio-.*/\1/g"`
@@ -39,7 +39,7 @@
./development/importMaven/import_maven_artifacts.py -n "com.android.tools.utp:$ARTIFACT:$ADT_VERSION"
done
-ATP_VERSION=${4:-0.0.8-alpha06}
+ATP_VERSION=${4:-0.0.8-alpha07}
./development/importMaven/import_maven_artifacts.py -n "com.google.testing.platform:android-test-plugin:$ATP_VERSION"
./development/importMaven/import_maven_artifacts.py -n "com.google.testing.platform:launcher:$ATP_VERSION"
./development/importMaven/import_maven_artifacts.py -n "com.google.testing.platform:android-driver-instrumentation:$ATP_VERSION"
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index ee404c5..5032588 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -23,10 +23,10 @@
docs("androidx.arch.core:core-testing:2.1.0")
docs("androidx.asynclayoutinflater:asynclayoutinflater:1.0.0")
docs("androidx.autofill:autofill:1.2.0-beta01")
- docs("androidx.benchmark:benchmark-common:1.1.0-beta01")
- docs("androidx.benchmark:benchmark-junit4:1.1.0-beta01")
- docs("androidx.benchmark:benchmark-macro:1.1.0-beta01")
- docs("androidx.benchmark:benchmark-macro-junit4:1.1.0-beta01")
+ docs("androidx.benchmark:benchmark-common:1.1.0-alpha13")
+ docs("androidx.benchmark:benchmark-junit4:1.1.0-alpha13")
+ docs("androidx.benchmark:benchmark-macro:1.1.0-alpha13")
+ docs("androidx.benchmark:benchmark-macro-junit4:1.1.0-alpha13")
docs("androidx.biometric:biometric:1.2.0-alpha04")
docs("androidx.biometric:biometric-ktx:1.2.0-alpha04")
samples("androidx.biometric:biometric-ktx-samples:1.2.0-alpha04")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index bf20035..a22fca0 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -265,6 +265,8 @@
samples(project(":wear:compose:compose-foundation-samples"))
docs(project(":wear:compose:compose-material"))
samples(project(":wear:compose:compose-material-samples"))
+ docs(project(":wear:compose:compose-navigation"))
+ samples(project(":wear:compose:compose-navigation-samples"))
docs(project(":wear:wear-input"))
docs(project(":wear:wear-input-testing"))
samples(project(":wear:wear-input-samples"))
@@ -279,6 +281,7 @@
docs(project(":wear:watchface:watchface"))
docs(project(":wear:watchface:watchface-complications"))
docs(project(":wear:watchface:watchface-complications-data"))
+ docs(project(":wear:watchface:watchface-complications-data-core"))
docs(project(":wear:watchface:watchface-complications-data-source"))
samples(project(":wear:watchface:watchface-complications-data-source-samples"))
docs(project(":wear:watchface:watchface-complications-rendering"))
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6bcb125..c5b17de 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,20 +2,20 @@
# -----------------------------------------------------------------------------
# All of the following should be updated in sync.
# -----------------------------------------------------------------------------
-androidGradlePlugin = "7.1.0-beta03"
+androidGradlePlugin = "7.2.0-alpha06"
# NOTE: When updating the lint version we also need to update the `api` version
# supported by `IssueRegistry`'s.' For e.g. r.android.com/1331903
-androidLint = "30.1.0-beta03"
+androidLint = "30.2.0-alpha06"
# Once you have a chosen version of AGP to upgrade to, go to
# https://developer.android.com/studio/archive and find the matching version of Studio.
-androidStudio = "2021.1.1.16"
+androidStudio = "2021.2.1.6"
# -----------------------------------------------------------------------------
androidLintMin = "27.2.1"
androidLintMinCompose = "30.0.0"
androidxTestRunner = "1.4.0"
androidxTestRules = "1.4.0"
-androidxTestMonitor = "1.5.0-rc01"
+androidxTestMonitor = "1.5.0"
androidxTestCore = "1.4.0"
androidxTestExtJunit = "1.1.3"
androidxTestExtTruth = "1.4.0"
@@ -28,10 +28,10 @@
guavaJre = "29.0-jre"
hilt = "2.40.1"
incap = "0.2"
-kotlin = "1.6.0"
+kotlin = "1.6.10"
kotlinCompileTesting = "1.4.1"
kotlinCoroutines = "1.5.2"
-ksp = "1.6.0-1.0.1"
+ksp = "1.6.10-1.0.2"
ktlint = "0.43.0"
leakcanary = "2.7"
mockito = "2.25.0"
diff --git a/jetifier/jetifier/migration.config b/jetifier/jetifier/migration.config
index ae9f741..35cac7a 100644
--- a/jetifier/jetifier/migration.config
+++ b/jetifier/jetifier/migration.config
@@ -39,10 +39,6 @@
"to": "ignore"
},
{
- "from": "android/support/compat/R(.*)",
- "to": "androidx/core/R{0}"
- },
- {
"from": "android/support/v7/recyclerview/R(.*)",
"to": "androidx/recyclerview/R{0}"
},
@@ -183,22 +179,6 @@
"to": "androidx/recyclerview/widget/{0}"
},
{
- "from": "android/support/v4/content/res/GrowingArrayUtils(.*)",
- "to": "androidx/core/content/res/GrowingArrayUtils{0}"
- },
- {
- "from": "android/support/v7/content/res/GrowingArrayUtils(.*)",
- "to": "androidx/core/content/res/GrowingArrayUtils{0}"
- },
- {
- "from": "android/support/v4/content/res/ColorStateListInflaterCompat(.*)",
- "to": "androidx/core/content/res/ColorStateListInflaterCompat{0}"
- },
- {
- "from": "android/support/v7/content/res/AppCompatColorStateListInflater(.*)",
- "to": "androidx/core/content/res/ColorStateListInflaterCompat{0}"
- },
- {
"from": "android/support/v7/(.*)",
"to": "androidx/appcompat/{0}"
},
@@ -207,14 +187,6 @@
"to": "androidx/annotation/{0}"
},
{
- "from": "android/support/v4/view/animation/PathInterpolatorApi14(.*)",
- "to": "androidx/core/view/animation/PathInterpolatorApi14{0}"
- },
- {
- "from": "android/support/v4/view/animation/PathInterpolatorCompat(.*)",
- "to": "androidx/core/view/animation/PathInterpolatorCompat{0}"
- },
- {
"from": "android/support/v4/app/SupportActivity(.*)",
"to": "androidx/core/app/ComponentActivity{0}"
},
@@ -305,15 +277,15 @@
"to": "ignore"
},
{
- "from": "androidx/core/content/ContextCompat(.*)",
+ "from": "androidx/core/database/(.*)",
"to": "ignore"
},
{
- "from": "androidx/core/content/UnusedAppRestrictionsBackportCallback(.*)",
+ "from": "androidx/core/internal/view/(.*)",
"to": "ignore"
},
{
- "from": "androidx/core/content/UnusedAppRestrictionsBackportService(.*)",
+ "from": "androidx/core/content/(.*)",
"to": "ignore"
},
{
@@ -349,6 +321,10 @@
"to": "ignore"
},
{
+ "from": "androidx/core/view/(.*)",
+ "to": "ignore"
+ },
+ {
"from": "androidx/core/accessibilityservice/AccessibilityServiceInfoCompat(.*)",
"to": "ignore"
},
@@ -389,6 +365,94 @@
"to": "ignore"
},
{
+ "from": "androidx/core/app/NavUtils(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/NotificationChannelCompat(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/NotificationCompatSideChannelService(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/NotificationCompat(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/NotificationManagerCompat(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/RemoteInput(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/RemoteActionCompat(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/NotificationBuilderWithBuilderAccessor(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/NotificationCompatBuilder(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/NotificationCompatExtras(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/NotificationCompatJellybean(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/FrameMetricsAggregator(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/telephony/(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/R(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/DialogCompat(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/BundleCompat(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/ServiceCompat(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/ShareCompat(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/SharedElementCallback(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/app/TaskStackBuilder(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/hardware/(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/location/(.*)",
+ "to": "ignore"
+ },
+ {
"from": "androidx/core/os/BuildCompat(.*)",
"to": "ignore"
},
@@ -448,11 +512,35 @@
"from": "androidx/core/os/TraceCompat(.*)",
"to": "ignore"
},
+ {
+ "from": "androidx/core/os/ExecutorCompat(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/os/UserHandleCompat(.*)",
+ "to": "ignore"
+ },
{
"from": "androidx/core/os/UserManagerCompat(.*)",
"to": "ignore"
},
{
+ "from": "androidx/core/splashscreen(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/graphics/(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/util/(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/core/widget/(.*)",
+ "to": "ignore"
+ },
+ {
"from": "androidx/viewpager2/(.*)",
"to": "ignore"
},
@@ -1227,7 +1315,7 @@
"to": "androidx/leanback/tab"
},
{
- "from": "android/support/compat",
+ "from": "androidx/core",
"to": "androidx/core"
},
{
@@ -2130,9 +2218,9 @@
},
{
"from": {
- "groupId": "com.android.support",
- "artifactId": "support-compat",
- "version": "{oldSlVersion}"
+ "groupId": "androidx.core",
+ "artifactId": "core",
+ "version": "{newSlVersion}"
},
"to": {
"groupId": "androidx.core",
@@ -3986,7 +4074,6 @@
"android/support/annotation/VisibleForTesting": "androidx/annotation/VisibleForTesting",
"android/support/annotation/WorkerThread": "androidx/annotation/WorkerThread",
"android/support/annotation/XmlRes": "androidx/annotation/XmlRes",
- "android/support/compat/R": "androidx/core/R",
"android/support/constraint/Barrier": "androidx/constraintlayout/widget/Barrier",
"android/support/constraint/ConstraintHelper": "androidx/constraintlayout/widget/ConstraintHelper",
"android/support/constraint/ConstraintLayout": "androidx/constraintlayout/widget/ConstraintLayout",
@@ -4029,7 +4116,6 @@
"android/support/multidex/ZipUtil": "androidx/multidex/ZipUtil",
"android/support/v4/app/BackStackRecord": "androidx/fragment/app/BackStackRecord",
"android/support/v4/app/BackStackState": "androidx/fragment/app/BackStackState",
- "android/support/v4/app/BundleCompat": "androidx/core/app/BundleCompat",
"android/support/v4/app/DialogFragment": "androidx/fragment/app/DialogFragment",
"android/support/v4/app/Fragment": "androidx/fragment/app/Fragment",
"android/support/v4/app/FragmentActivity": "androidx/fragment/app/FragmentActivity",
@@ -4048,146 +4134,14 @@
"android/support/v4/app/FragmentTransition": "androidx/fragment/app/FragmentTransition",
"android/support/v4/app/FragmentTransitionCompat21": "androidx/fragment/app/FragmentTransitionCompat21",
"android/support/v4/app/FragmentTransitionImpl": "androidx/fragment/app/FragmentTransitionImpl",
- "android/support/v4/app/FrameMetricsAggregator": "androidx/core/app/FrameMetricsAggregator",
"android/support/v4/app/INotificationSideChannel": "androidx/core/app/INotificationSideChannel",
"android/support/v4/app/ListFragment": "androidx/fragment/app/ListFragment",
- "android/support/v4/app/NavUtils": "androidx/core/app/NavUtils",
- "android/support/v4/app/NotificationBuilderWithBuilderAccessor": "androidx/core/app/NotificationBuilderWithBuilderAccessor",
- "android/support/v4/app/NotificationCompat": "androidx/core/app/NotificationCompat",
- "android/support/v4/app/NotificationCompatBuilder": "androidx/core/app/NotificationCompatBuilder",
- "android/support/v4/app/NotificationCompatExtras": "androidx/core/app/NotificationCompatExtras",
- "android/support/v4/app/NotificationCompatJellybean": "androidx/core/app/NotificationCompatJellybean",
- "android/support/v4/app/NotificationCompatSideChannelService": "androidx/core/app/NotificationCompatSideChannelService",
- "android/support/v4/app/NotificationManagerCompat": "androidx/core/app/NotificationManagerCompat",
"android/support/v4/app/OneShotPreDrawListener": "androidx/fragment/app/OneShotPreDrawListener",
- "android/support/v4/app/RemoteInput": "androidx/core/app/RemoteInput",
- "android/support/v4/app/ServiceCompat": "androidx/core/app/ServiceCompat",
- "android/support/v4/app/ShareCompat": "androidx/core/app/ShareCompat",
- "android/support/v4/app/SharedElementCallback": "androidx/core/app/SharedElementCallback",
"android/support/v4/app/SuperNotCalledException": "androidx/fragment/app/SuperNotCalledException",
"android/support/v4/app/SupportActivity": "androidx/core/app/ComponentActivity",
- "android/support/v4/app/TaskStackBuilder": "androidx/core/app/TaskStackBuilder",
- "android/support/v4/content/ContentResolverCompat": "androidx/core/content/ContentResolverCompat",
- "android/support/v4/content/FileProvider": "androidx/core/content/FileProvider",
- "android/support/v4/content/IntentCompat": "androidx/core/content/IntentCompat",
- "android/support/v4/content/MimeTypeFilter": "androidx/core/content/MimeTypeFilter",
- "android/support/v4/content/PermissionChecker": "androidx/core/content/PermissionChecker",
- "android/support/v4/content/SharedPreferencesCompat": "androidx/core/content/SharedPreferencesCompat",
- "android/support/v4/content/pm/ActivityInfoCompat": "androidx/core/content/pm/ActivityInfoCompat",
- "android/support/v4/content/pm/PackageInfoCompat": "androidx/core/content/pm/PackageInfoCompat",
- "android/support/v4/content/pm/PermissionInfoCompat": "androidx/core/content/pm/PermissionInfoCompat",
- "android/support/v4/content/pm/ShortcutInfoCompat": "androidx/core/content/pm/ShortcutInfoCompat",
- "android/support/v4/content/pm/ShortcutManagerCompat": "androidx/core/content/pm/ShortcutManagerCompat",
- "android/support/v4/content/res/ColorStateListInflaterCompat": "androidx/core/content/res/ColorStateListInflaterCompat",
- "android/support/v4/content/res/ComplexColorCompat": "androidx/core/content/res/ComplexColorCompat",
- "android/support/v4/content/res/ConfigurationHelper": "androidx/core/content/res/ConfigurationHelper",
- "android/support/v4/content/res/FontResourcesParserCompat": "androidx/core/content/res/FontResourcesParserCompat",
- "android/support/v4/content/res/GradientColorInflaterCompat": "androidx/core/content/res/GradientColorInflaterCompat",
- "android/support/v4/content/res/GrowingArrayUtils": "androidx/core/content/res/GrowingArrayUtils",
- "android/support/v4/content/res/ResourcesCompat": "androidx/core/content/res/ResourcesCompat",
- "android/support/v4/content/res/TypedArrayUtils": "androidx/core/content/res/TypedArrayUtils",
- "android/support/v4/database/CursorWindowCompat": "androidx/core/database/CursorWindowCompat",
- "android/support/v4/database/DatabaseUtilsCompat": "androidx/core/database/DatabaseUtilsCompat",
- "android/support/v4/database/sqlite/SQLiteCursorCompat": "androidx/core/database/sqlite/SQLiteCursorCompat",
- "android/support/v4/graphics/BitmapCompat": "androidx/core/graphics/BitmapCompat",
- "android/support/v4/graphics/ColorUtils": "androidx/core/graphics/ColorUtils",
- "android/support/v4/graphics/PaintCompat": "androidx/core/graphics/PaintCompat",
- "android/support/v4/graphics/PathParser": "androidx/core/graphics/PathParser",
- "android/support/v4/graphics/PathSegment": "androidx/core/graphics/PathSegment",
- "android/support/v4/graphics/PathUtils": "androidx/core/graphics/PathUtils",
- "android/support/v4/graphics/TypefaceCompat": "androidx/core/graphics/TypefaceCompat",
- "android/support/v4/graphics/TypefaceCompatApi21Impl": "androidx/core/graphics/TypefaceCompatApi21Impl",
- "android/support/v4/graphics/TypefaceCompatApi24Impl": "androidx/core/graphics/TypefaceCompatApi24Impl",
- "android/support/v4/graphics/TypefaceCompatApi26Impl": "androidx/core/graphics/TypefaceCompatApi26Impl",
- "android/support/v4/graphics/TypefaceCompatApi28Impl": "androidx/core/graphics/TypefaceCompatApi28Impl",
- "android/support/v4/graphics/TypefaceCompatBaseImpl": "androidx/core/graphics/TypefaceCompatBaseImpl",
- "android/support/v4/graphics/TypefaceCompatUtil": "androidx/core/graphics/TypefaceCompatUtil",
- "android/support/v4/graphics/drawable/DrawableCompat": "androidx/core/graphics/drawable/DrawableCompat",
- "android/support/v4/graphics/drawable/IconCompat": "androidx/core/graphics/drawable/IconCompat",
"android/support/v4/graphics/drawable/IconCompatParcelizer": "android/support/v4/graphics/drawable/IconCompatParcelizer",
- "android/support/v4/graphics/drawable/RoundedBitmapDrawable": "androidx/core/graphics/drawable/RoundedBitmapDrawable",
- "android/support/v4/graphics/drawable/RoundedBitmapDrawable21": "androidx/core/graphics/drawable/RoundedBitmapDrawable21",
- "android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory": "androidx/core/graphics/drawable/RoundedBitmapDrawableFactory",
- "android/support/v4/graphics/drawable/TintAwareDrawable": "androidx/core/graphics/drawable/TintAwareDrawable",
- "android/support/v4/graphics/drawable/WrappedDrawable": "androidx/core/graphics/drawable/WrappedDrawable",
- "android/support/v4/graphics/drawable/WrappedDrawableApi14": "androidx/core/graphics/drawable/WrappedDrawableApi14",
- "android/support/v4/graphics/drawable/WrappedDrawableApi21": "androidx/core/graphics/drawable/WrappedDrawableApi21",
- "android/support/v4/hardware/display/DisplayManagerCompat": "androidx/core/hardware/display/DisplayManagerCompat",
- "android/support/v4/hardware/fingerprint/FingerprintManagerCompat": "androidx/core/hardware/fingerprint/FingerprintManagerCompat",
- "android/support/v4/internal/view/SupportMenu": "androidx/core/internal/view/SupportMenu",
- "android/support/v4/internal/view/SupportMenuItem": "androidx/core/internal/view/SupportMenuItem",
- "android/support/v4/internal/view/SupportSubMenu": "androidx/core/internal/view/SupportSubMenu",
"android/support/v4/os/IResultReceiver": "androidx/core/os/IResultReceiver",
"android/support/v4/os/ResultReceiver": "androidx/core/os/ResultReceiver",
- "android/support/v4/util/AtomicFile": "androidx/core/util/AtomicFile",
- "android/support/v4/util/Consumer": "androidx/core/util/Consumer",
- "android/support/v4/util/DebugUtils": "androidx/core/util/DebugUtils",
- "android/support/v4/util/LogWriter": "androidx/core/util/LogWriter",
- "android/support/v4/util/ObjectsCompat": "androidx/core/util/ObjectsCompat",
- "android/support/v4/util/Pair": "androidx/core/util/Pair",
- "android/support/v4/util/PatternsCompat": "androidx/core/util/PatternsCompat",
- "android/support/v4/util/Pools": "androidx/core/util/Pools",
- "android/support/v4/util/Preconditions": "androidx/core/util/Preconditions",
- "android/support/v4/util/TimeUtils": "androidx/core/util/TimeUtils",
- "android/support/v4/view/AccessibilityDelegateCompat": "androidx/core/view/AccessibilityDelegateCompat",
- "android/support/v4/view/ActionProvider": "androidx/core/view/ActionProvider",
- "android/support/v4/view/DisplayCutoutCompat": "androidx/core/view/DisplayCutoutCompat",
- "android/support/v4/view/GestureDetectorCompat": "androidx/core/view/GestureDetectorCompat",
- "android/support/v4/view/GravityCompat": "androidx/core/view/GravityCompat",
- "android/support/v4/view/InputDeviceCompat": "androidx/core/view/InputDeviceCompat",
- "android/support/v4/view/KeyEventDispatcher": "androidx/core/view/KeyEventDispatcher",
- "android/support/v4/view/LayoutInflaterCompat": "androidx/core/view/LayoutInflaterCompat",
- "android/support/v4/view/LayoutInflaterFactory": "androidx/core/view/LayoutInflaterFactory",
- "android/support/v4/view/MarginLayoutParamsCompat": "androidx/core/view/MarginLayoutParamsCompat",
- "android/support/v4/view/MenuCompat": "androidx/core/view/MenuCompat",
- "android/support/v4/view/MenuItemCompat": "androidx/core/view/MenuItemCompat",
- "android/support/v4/view/MotionEventCompat": "androidx/core/view/MotionEventCompat",
- "android/support/v4/view/NestedScrollingChild": "androidx/core/view/NestedScrollingChild",
- "android/support/v4/view/NestedScrollingChild2": "androidx/core/view/NestedScrollingChild2",
- "android/support/v4/view/NestedScrollingChildHelper": "androidx/core/view/NestedScrollingChildHelper",
- "android/support/v4/view/NestedScrollingParent": "androidx/core/view/NestedScrollingParent",
- "android/support/v4/view/NestedScrollingParent2": "androidx/core/view/NestedScrollingParent2",
- "android/support/v4/view/NestedScrollingParentHelper": "androidx/core/view/NestedScrollingParentHelper",
- "android/support/v4/view/OnApplyWindowInsetsListener": "androidx/core/view/OnApplyWindowInsetsListener",
- "android/support/v4/view/PointerIconCompat": "androidx/core/view/PointerIconCompat",
- "android/support/v4/view/ScaleGestureDetectorCompat": "androidx/core/view/ScaleGestureDetectorCompat",
- "android/support/v4/view/ScrollingView": "androidx/core/view/ScrollingView",
- "android/support/v4/view/TintableBackgroundView": "androidx/core/view/TintableBackgroundView",
- "android/support/v4/view/VelocityTrackerCompat": "androidx/core/view/VelocityTrackerCompat",
- "android/support/v4/view/ViewCompat": "androidx/core/view/ViewCompat",
- "android/support/v4/view/ViewConfigurationCompat": "androidx/core/view/ViewConfigurationCompat",
- "android/support/v4/view/ViewGroupCompat": "androidx/core/view/ViewGroupCompat",
- "android/support/v4/view/ViewParentCompat": "androidx/core/view/ViewParentCompat",
- "android/support/v4/view/ViewPropertyAnimatorCompat": "androidx/core/view/ViewPropertyAnimatorCompat",
- "android/support/v4/view/ViewPropertyAnimatorListener": "androidx/core/view/ViewPropertyAnimatorListener",
- "android/support/v4/view/ViewPropertyAnimatorListenerAdapter": "androidx/core/view/ViewPropertyAnimatorListenerAdapter",
- "android/support/v4/view/ViewPropertyAnimatorUpdateListener": "androidx/core/view/ViewPropertyAnimatorUpdateListener",
- "android/support/v4/view/WindowCompat": "androidx/core/view/WindowCompat",
- "android/support/v4/view/WindowInsetsCompat": "androidx/core/view/WindowInsetsCompat",
- "android/support/v4/view/accessibility/AccessibilityEventCompat": "androidx/core/view/accessibility/AccessibilityEventCompat",
- "android/support/v4/view/accessibility/AccessibilityManagerCompat": "androidx/core/view/accessibility/AccessibilityManagerCompat",
- "android/support/v4/view/accessibility/AccessibilityNodeInfoCompat": "androidx/core/view/accessibility/AccessibilityNodeInfoCompat",
- "android/support/v4/view/accessibility/AccessibilityNodeProviderCompat": "androidx/core/view/accessibility/AccessibilityNodeProviderCompat",
- "android/support/v4/view/accessibility/AccessibilityRecordCompat": "androidx/core/view/accessibility/AccessibilityRecordCompat",
- "android/support/v4/view/accessibility/AccessibilityWindowInfoCompat": "androidx/core/view/accessibility/AccessibilityWindowInfoCompat",
- "android/support/v4/view/animation/PathInterpolatorApi14": "androidx/core/view/animation/PathInterpolatorApi14",
- "android/support/v4/view/animation/PathInterpolatorCompat": "androidx/core/view/animation/PathInterpolatorCompat",
- "android/support/v4/widget/AutoScrollHelper": "androidx/core/widget/AutoScrollHelper",
- "android/support/v4/widget/AutoSizeableTextView": "androidx/core/widget/AutoSizeableTextView",
- "android/support/v4/widget/CompoundButtonCompat": "androidx/core/widget/CompoundButtonCompat",
- "android/support/v4/widget/ContentLoadingProgressBar": "androidx/core/widget/ContentLoadingProgressBar",
- "android/support/v4/widget/EdgeEffectCompat": "androidx/core/widget/EdgeEffectCompat",
- "android/support/v4/widget/ImageViewCompat": "androidx/core/widget/ImageViewCompat",
- "android/support/v4/widget/ListPopupWindowCompat": "androidx/core/widget/ListPopupWindowCompat",
- "android/support/v4/widget/ListViewAutoScrollHelper": "androidx/core/widget/ListViewAutoScrollHelper",
- "android/support/v4/widget/ListViewCompat": "androidx/core/widget/ListViewCompat",
- "android/support/v4/widget/NestedScrollView": "androidx/core/widget/NestedScrollView",
- "android/support/v4/widget/PopupMenuCompat": "androidx/core/widget/PopupMenuCompat",
- "android/support/v4/widget/PopupWindowCompat": "androidx/core/widget/PopupWindowCompat",
- "android/support/v4/widget/ScrollerCompat": "androidx/core/widget/ScrollerCompat",
- "android/support/v4/widget/TextViewCompat": "androidx/core/widget/TextViewCompat",
- "android/support/v4/widget/TintableCompoundButton": "androidx/core/widget/TintableCompoundButton",
- "android/support/v4/widget/TintableImageSourceView": "androidx/core/widget/TintableImageSourceView",
"android/support/v7/app/ActionBar": "androidx/appcompat/app/ActionBar",
"android/support/v7/app/ActionBarDrawerToggle": "androidx/appcompat/app/ActionBarDrawerToggle",
"android/support/v7/app/ActionBarDrawerToggleHoneycomb": "androidx/appcompat/app/ActionBarDrawerToggleHoneycomb",
@@ -4382,9 +4336,6 @@
"android/support/v4/{any}",
"androidx/{any}"
],
- "android/support/v4/view/{any}": [
- "androidx/core/view/{any}"
- ],
"android/support/v7/{any}": [
"androidx/appcompat/{any}"
],
@@ -4398,9 +4349,6 @@
"android/support/v7/internal/widget/ActionBarView${any}": [
"androidx/appcompat/widget/AbsActionBarView${any}"
],
- "android/support/v4/view/MenuItemCompat/*": [
- "androidx/core/view/MenuItemCompat/*"
- ],
"Android{any}": [
"Android{any}"
]
diff --git a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
index 088ea33..b8da452 100644
--- a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
@@ -25,7 +25,7 @@
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.android.tools.lint.checks.ApiLookup
import com.android.tools.lint.checks.ApiLookup.equivalentName
-import com.android.tools.lint.checks.ApiLookup.startsWithEquivalentPrefix
+import com.android.tools.lint.checks.DesugaredMethodLookup
import com.android.tools.lint.checks.VersionChecks.Companion.codeNameToApi
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Context
@@ -415,48 +415,19 @@
return
}
}
-
- // If it's a method we have source for, obviously it shouldn't be a
- // violation. (This happens for example when compiling the support library.)
- if (method !is PsiCompiledElement) {
- return
- }
}
- // Desugar rewrites compare calls (see b/36390874)
- if (name == "compare" &&
- api == 19 &&
- startsWithEquivalentPrefix(owner, "java/lang/") &&
- desc.length == 4 &&
- (
- desc == "(JJ)" ||
- desc == "(ZZ)" ||
- desc == "(BB)" ||
- desc == "(CC)" ||
- desc == "(II)" ||
- desc == "(SS)"
- )
- ) {
- if (context.project.isDesugaring(Desugaring.LONG_COMPARE)) {
- return
- }
+ // Builtin R8 desugaring, such as rewriting compare calls (see b/36390874)
+ if (owner.startsWith("java.") &&
+ DesugaredMethodLookup.isDesugared(owner, name, desc)) {
+ return
}
- // Desugar rewrites Objects.requireNonNull calls (see b/32446315)
- if (name == "requireNonNull" &&
- api == 19 &&
- owner == "java.util.Objects" &&
- desc == "(Ljava.lang.Object;)"
- ) {
- if (context.project.isDesugaring(Desugaring.OBJECTS_REQUIRE_NON_NULL)) {
- return
- }
- }
-
- if (name == "addSuppressed" &&
- api == 19 &&
- owner == "java.lang.Throwable" &&
- desc == "(Ljava.lang.Throwable;)"
+ // These methods are not included in the R8 backported list so handle them manually
+ // the way R8 seems to
+ if (api == 19 && owner == "java.lang.Throwable" &&
+ (name == "addSuppressed" && desc == "(Ljava.lang.Throwable;)" ||
+ name == "getSuppressed" && desc == "()")
) {
if (context.project.isDesugaring(Desugaring.TRY_WITH_RESOURCES)) {
return
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index ec2d6c4..260ecfd 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -25,7 +25,7 @@
kotlin.code.style=official
# Disable docs
androidx.enableDocumentation=false
-androidx.playground.snapshotBuildId=7990267
+androidx.playground.snapshotBuildId=8005530
androidx.playground.metalavaBuildId=7856580
androidx.playground.dokkaBuildId=7472101
androidx.studio.type=playground
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DiagnosticsMessageCollector.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DiagnosticsMessageCollector.kt
index ac2eca84..dfd0add 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DiagnosticsMessageCollector.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DiagnosticsMessageCollector.kt
@@ -57,6 +57,10 @@
message: String,
location: CompilerMessageSourceLocation?
) {
+ if (message == KSP_ADDITIONAL_ERROR_MESSAGE) {
+ // ignore this as it will impact error counts.
+ return
+ }
// Both KSP and KAPT reports null location but instead put the location into the message.
// We parse it back here to recover the location.
val (strippedMessage, rawLocation) = if (location == null) {
@@ -150,5 +154,10 @@
private val KIND_REGEX = """^\w+: """.toRegex()
// example: "[ksp] the real message"
private val KSP_PREFIX_REGEX = """^\[ksp] """.toRegex()
+
+ // KSP always prints an additional error if any other error occurred.
+ // We drop that additional message to provide a more consistent error count with KAPT/javac.
+ private const val KSP_ADDITIONAL_ERROR_MESSAGE =
+ "Error occurred in KSP, check log for detail"
}
}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt
index 233cb41..b4104d3 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt
@@ -83,6 +83,7 @@
}
val logger = MessageCollectorBasedKSPLogger(
messageCollector = messageCollector,
+ wrappedMessageCollector = messageCollector,
allWarningsAsErrors = baseOptions.allWarningsAsErrors
)
val options = baseOptions.build()
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
index c7dd2e7..9807eef 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
@@ -25,6 +25,7 @@
import com.google.devtools.ksp.symbol.KSTypeArgument
import com.google.devtools.ksp.symbol.KSTypeParameter
import com.google.devtools.ksp.symbol.KSTypeReference
+import com.google.devtools.ksp.symbol.Modifier
import com.google.devtools.ksp.symbol.Variance
/**
@@ -133,7 +134,8 @@
param.variance == Variance.CONTRAVARIANT ||
when (val decl = myType.declaration) {
is KSClassDeclaration -> {
- decl.isOpen() || decl.classKind == ClassKind.ENUM_CLASS
+ decl.isOpen() || decl.classKind == ClassKind.ENUM_CLASS ||
+ decl.modifiers.contains(Modifier.SEALED)
}
else -> true
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
index a090664..43002a4 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
@@ -132,7 +132,11 @@
// turn them into string, provides better failure reports
fun Map<String, List<TypeName>>.signatures(): List<Pair<String, String>> {
return this.entries.map {
- it.key to it.value.joinToString(" ")
+ it.key to it.value.joinToString(" ") {
+ // javapoet doesn't always read enclosing class property from classpath
+ // we don't care about it here.
+ it.toString().replace('$', '.')
+ }
}.sortedBy {
it.first
}
@@ -155,6 +159,13 @@
VAL1,
VAL2;
}
+ sealed class GrandParentSealed {
+ class Parent1: GrandParentSealed()
+ sealed class Parent2: GrandParentSealed() {
+ class Child1: Parent2()
+ class Child2: Parent2()
+ }
+ }
""".trimIndent()
)
return listOf(
@@ -239,6 +250,9 @@
numberList: List<Number>,
stringList: List<String>,
enumList: List<MyEnum>,
+ sealedListGrandParent: List<GrandParentSealed>,
+ sealedListParent: List<GrandParentSealed.Parent1>,
+ sealedListChild: List<GrandParentSealed.Parent2.Child1>,
jvmWildcard: List<@JvmWildcard String>,
suppressJvmWildcard: List<@JvmSuppressWildcards Number>
) {
@@ -248,6 +262,9 @@
var propWithOpenGeneric: List<Number> = TODO()
var propWithTypeArg: R = TODO()
var propWithTypeArgGeneric: List<R> = TODO()
+ var propSealedListGrandParent: List<GrandParentSealed> = TODO()
+ var propSealedListParent: List<GrandParentSealed.Parent1> = TODO()
+ var propSealedListChild: List<GrandParentSealed.Parent2.Child1> = TODO()
@JvmSuppressWildcards
var propWithOpenTypeButSuppressAnnotation: Number = 3
fun list(list: List<*>): List<*> { TODO() }
@@ -255,6 +272,9 @@
fun listTypeArgNumber(list: List<Number>): List<Number> { TODO() }
fun listTypeArgString(list: List<String>): List<String> { TODO() }
fun listTypeArgEnum(list: List<MyEnum>): List<MyEnum> { TODO() }
+ fun listSealedListGrandParent(list: List<GrandParentSealed>): List<GrandParentSealed> { TODO() }
+ fun listSealedListParent(list: List<GrandParentSealed.Parent1>): List<GrandParentSealed.Parent1> { TODO() }
+ fun listSealedListChild(list: List<GrandParentSealed.Parent2.Child1>): List<GrandParentSealed.Parent2.Child1> { TODO() }
fun explicitJvmWildcard(
list: List<@JvmWildcard String>
): List<@JvmWildcard String> { TODO() }
@@ -271,6 +291,9 @@
fun suspendListTypeArgNumber(list: List<Number>): List<Number> { TODO() }
fun suspendListTypeArgString(list: List<String>): List<String> { TODO() }
fun suspendListTypeArgEnum(list: List<MyEnum>): List<MyEnum> { TODO() }
+ fun suspendListSealedListGrandParent(list: List<GrandParentSealed>): List<GrandParentSealed> { TODO() }
+ fun suspendListSealedListParent(list: List<GrandParentSealed.Parent1>): List<GrandParentSealed.Parent1> { TODO() }
+ fun suspendListSealedListChild(list: List<GrandParentSealed.Parent2.Child1>): List<GrandParentSealed.Parent2.Child1> { TODO() }
fun suspendExplicitJvmWildcard(
list: List<@JvmWildcard String>
): List<@JvmWildcard String> { TODO() }
diff --git a/settings.gradle b/settings.gradle
index afb72af..abab84a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -341,6 +341,9 @@
includeProject(":compose:compiler:compiler-hosted", "compose/compiler/compiler-hosted", [BuildType.COMPOSE, BuildType.MAIN])
includeProject(":compose:compiler:compiler-hosted:integration-tests", "compose/compiler/compiler-hosted/integration-tests", [BuildType.COMPOSE])
includeProject(":compose:compiler:compiler-hosted:integration-tests:kotlin-compiler-repackaged", "compose/compiler/compiler-hosted/integration-tests/kotlin-compiler-repackaged", [BuildType.COMPOSE])
+includeProject(":compose:compiler:compiler-daemon", "compose/compiler/compiler-daemon", [BuildType.COMPOSE])
+includeProject(":compose:compiler:compiler-daemon:integration-tests", "compose/compiler/compiler-daemon/integration-tests", [BuildType.COMPOSE])
+
if (isMultiplatformEnabled()) {
includeProject(":compose:desktop", "compose/desktop", [BuildType.COMPOSE])
includeProject(":compose:desktop:desktop", "compose/desktop/desktop", [BuildType.COMPOSE])
@@ -724,6 +727,7 @@
includeProject(":wear:watchface:watchface-complications", "wear/watchface/watchface-complications", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:watchface:watchface-complications-permission-dialogs-sample", "wear/watchface/watchface-complications-permission-dialogs-sample", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:watchface:watchface-complications-data", "wear/watchface/watchface-complications-data", [BuildType.MAIN, BuildType.WEAR])
+includeProject(":wear:watchface:watchface-complications-data-core", "wear/watchface/watchface-complications-data-core", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:watchface:watchface-complications-data-source", "wear/watchface/watchface-complications-data-source", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:watchface:watchface-complications-data-source-ktx", "wear/watchface/watchface-complications-data-source-ktx", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:watchface:watchface-complications-data-source-samples", "wear/watchface/watchface-complications-data-source-samples", [BuildType.MAIN, BuildType.WEAR])
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index 55b5e40..707c7ff 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -261,7 +261,7 @@
}
public final class ScalingLazyColumnKt {
- method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyListState state, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyListState state, optional int anchorType, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
@@ -271,6 +271,17 @@
public final class ScalingLazyColumnMeasureKt {
}
+ @androidx.compose.runtime.Immutable public final inline class ScalingLazyListAnchorType {
+ ctor public ScalingLazyListAnchorType();
+ }
+
+ public static final class ScalingLazyListAnchorType.Companion {
+ method public int getItemCenter();
+ method public int getItemStart();
+ property public final int ItemCenter;
+ property public final int ItemStart;
+ }
+
public interface ScalingLazyListItemInfo {
method public float getAlpha();
method public int getIndex();
@@ -295,12 +306,10 @@
}
public interface ScalingLazyListLayoutInfo {
- method public int getCentralItemIndex();
method public int getTotalItemsCount();
method public int getViewportEndOffset();
method public int getViewportStartOffset();
method public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
- property public abstract int centralItemIndex;
property public abstract int totalItemsCount;
property public abstract int viewportEndOffset;
property public abstract int viewportStartOffset;
@@ -313,11 +322,17 @@
}
@androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
- ctor public ScalingLazyListState();
+ ctor public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+ method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public float dispatchRawDelta(float delta);
+ method public int getCenterItemIndex();
+ method public int getCenterItemScrollOffset();
method public androidx.wear.compose.material.ScalingLazyListLayoutInfo getLayoutInfo();
method public boolean isScrollInProgress();
method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ property public final int centerItemIndex;
+ property public final int centerItemScrollOffset;
property public boolean isScrollInProgress;
property public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
field public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
@@ -329,7 +344,7 @@
}
public final class ScalingLazyListStateKt {
- method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState();
+ method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
}
@kotlin.DslMarker public @interface ScalingLazyScopeMarker {
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index 9294509..8625dce 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -287,7 +287,7 @@
}
public final class ScalingLazyColumnKt {
- method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyListState state, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyListState state, optional int anchorType, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
@@ -297,6 +297,17 @@
public final class ScalingLazyColumnMeasureKt {
}
+ @androidx.compose.runtime.Immutable public final inline class ScalingLazyListAnchorType {
+ ctor public ScalingLazyListAnchorType();
+ }
+
+ public static final class ScalingLazyListAnchorType.Companion {
+ method public int getItemCenter();
+ method public int getItemStart();
+ property public final int ItemCenter;
+ property public final int ItemStart;
+ }
+
public interface ScalingLazyListItemInfo {
method public float getAlpha();
method public int getIndex();
@@ -321,12 +332,10 @@
}
public interface ScalingLazyListLayoutInfo {
- method public int getCentralItemIndex();
method public int getTotalItemsCount();
method public int getViewportEndOffset();
method public int getViewportStartOffset();
method public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
- property public abstract int centralItemIndex;
property public abstract int totalItemsCount;
property public abstract int viewportEndOffset;
property public abstract int viewportStartOffset;
@@ -339,11 +348,17 @@
}
@androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
- ctor public ScalingLazyListState();
+ ctor public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+ method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public float dispatchRawDelta(float delta);
+ method public int getCenterItemIndex();
+ method public int getCenterItemScrollOffset();
method public androidx.wear.compose.material.ScalingLazyListLayoutInfo getLayoutInfo();
method public boolean isScrollInProgress();
method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ property public final int centerItemIndex;
+ property public final int centerItemScrollOffset;
property public boolean isScrollInProgress;
property public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
field public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
@@ -355,7 +370,7 @@
}
public final class ScalingLazyListStateKt {
- method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState();
+ method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
}
@kotlin.DslMarker public @interface ScalingLazyScopeMarker {
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index 55b5e40..707c7ff 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -261,7 +261,7 @@
}
public final class ScalingLazyColumnKt {
- method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyListState state, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyListState state, optional int anchorType, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
@@ -271,6 +271,17 @@
public final class ScalingLazyColumnMeasureKt {
}
+ @androidx.compose.runtime.Immutable public final inline class ScalingLazyListAnchorType {
+ ctor public ScalingLazyListAnchorType();
+ }
+
+ public static final class ScalingLazyListAnchorType.Companion {
+ method public int getItemCenter();
+ method public int getItemStart();
+ property public final int ItemCenter;
+ property public final int ItemStart;
+ }
+
public interface ScalingLazyListItemInfo {
method public float getAlpha();
method public int getIndex();
@@ -295,12 +306,10 @@
}
public interface ScalingLazyListLayoutInfo {
- method public int getCentralItemIndex();
method public int getTotalItemsCount();
method public int getViewportEndOffset();
method public int getViewportStartOffset();
method public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
- property public abstract int centralItemIndex;
property public abstract int totalItemsCount;
property public abstract int viewportEndOffset;
property public abstract int viewportStartOffset;
@@ -313,11 +322,17 @@
}
@androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
- ctor public ScalingLazyListState();
+ ctor public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+ method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public float dispatchRawDelta(float delta);
+ method public int getCenterItemIndex();
+ method public int getCenterItemScrollOffset();
method public androidx.wear.compose.material.ScalingLazyListLayoutInfo getLayoutInfo();
method public boolean isScrollInProgress();
method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ property public final int centerItemIndex;
+ property public final int centerItemScrollOffset;
property public boolean isScrollInProgress;
property public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
field public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
@@ -329,7 +344,7 @@
}
public final class ScalingLazyListStateKt {
- method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState();
+ method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
}
@kotlin.DslMarker public @interface ScalingLazyScopeMarker {
diff --git a/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt
index 214ca1c..afa486d 100644
--- a/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt
+++ b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt
@@ -19,14 +19,19 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.assertNoPendingChanges
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.benchmarkDrawPerf
import androidx.compose.testutils.benchmark.benchmarkFirstCompose
-import androidx.compose.testutils.benchmark.benchmarkFirstDraw
-import androidx.compose.testutils.benchmark.benchmarkFirstLayout
import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
import androidx.compose.testutils.benchmark.benchmarkLayoutPerf
+import androidx.compose.testutils.benchmark.recomposeUntilNoChangesPending
+import androidx.compose.testutils.doFramesUntilNoChangesPending
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -37,6 +42,7 @@
import androidx.wear.compose.material.ScalingLazyColumn
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.rememberScalingLazyListState
+import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -65,12 +71,12 @@
@Test
fun first_layout() {
- benchmarkRule.benchmarkFirstLayout(scalingLazyColumnCaseFactory)
+ benchmarkRule.benchmarkFirstScalingLazyColumnLayout(scalingLazyColumnCaseFactory)
}
@Test
fun first_draw() {
- benchmarkRule.benchmarkFirstDraw(scalingLazyColumnCaseFactory)
+ benchmarkRule.benchmarkFirstScalingLazyColumnDraw(scalingLazyColumnCaseFactory)
}
@Test
@@ -111,4 +117,88 @@
content()
}
}
+}
+
+// TODO (b/210654937): Should be able to get rid of this workaround in the future once able to call
+// LaunchedEffect directly on underlying LazyColumn rather than via a 2-stage initialization via
+// onGloballyPositioned().
+fun ComposeBenchmarkRule.benchmarkFirstScalingLazyColumnLayout(
+ caseFactory: () -> LayeredComposeTestCase
+) {
+ runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
+ measureRepeated {
+ runWithTimingDisabled {
+ doFramesUntilNoChangesPending()
+ // Add the content to benchmark
+ getTestCase().addMeasuredContent()
+ recomposeUntilNoChangesPending()
+ requestLayout()
+ measure()
+ }
+
+ layout()
+ recomposeUntilNoChangesPending()
+
+ runWithTimingDisabled {
+ assertNoPendingChanges()
+ disposeContent()
+ }
+ }
+ }
+}
+
+// TODO (b/210654937): Should be able to get rid of this workaround in the future once able to call
+// LaunchedEffect directly on underlying LazyColumn rather than via a 2-stage initialization via
+// onGloballyPositioned().
+fun ComposeBenchmarkRule.benchmarkFirstScalingLazyColumnDraw(
+ caseFactory: () -> LayeredComposeTestCase
+) {
+ runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
+ measureRepeated {
+ runWithTimingDisabled {
+ doFramesUntilNoChangesPending()
+ // Add the content to benchmark
+ getTestCase().addMeasuredContent()
+ recomposeUntilNoChangesPending()
+ requestLayout()
+ measure()
+ layout()
+ drawPrepare()
+ }
+
+ draw()
+ drawFinish()
+ recomposeUntilNoChangesPending()
+
+ runWithTimingDisabled {
+ assertNoPendingChanges()
+ disposeContent()
+ }
+ }
+ }
+}
+
+private class LayeredCaseAdapter(private val innerCase: LayeredComposeTestCase) : ComposeTestCase {
+
+ companion object {
+ fun of(caseFactory: () -> LayeredComposeTestCase): () -> LayeredCaseAdapter = {
+ LayeredCaseAdapter(caseFactory())
+ }
+ }
+
+ var isComposed by mutableStateOf(false)
+
+ @Composable
+ override fun Content() {
+ innerCase.ContentWrappers {
+ if (isComposed) {
+ innerCase.MeasuredContent()
+ }
+ }
+ }
+
+ fun addMeasuredContent() {
+ Assert.assertTrue(!isComposed)
+ isComposed = true
+ }
}
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
index 89276dd..939306f 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
@@ -104,12 +104,13 @@
}
@Test
- fun can_scroll_picker_down_on_start() {
+ fun can_scroll_picker_down_on_start() {
+ val numberOfOptions = 5
lateinit var state: PickerState
rule.setContent {
WithTouchSlop(0f) {
Picker(
- 5,
+ numberOfOptions,
state = rememberPickerState().also { state = it },
modifier = Modifier.testTag(TEST_TAG)
.requiredSize(itemSizeDp * 3),
@@ -130,7 +131,9 @@
}
rule.waitForIdle()
- assertThat(state.selectedOption).isEqualTo(initiallySelectedItem - 1)
+ val targetValue =
+ if (initiallySelectedItem == 0) numberOfOptions - 1 else initiallySelectedItem - 1
+ assertThat(state.selectedOption).isEqualTo(targetValue)
}
@Test
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
index dd701f6..a8daba8 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
@@ -929,16 +929,12 @@
private fun ScalingLazyListLayoutInfo.assertWhollyVisibleItems(
firstItemIndex: Int,
- firstItemNotVisible: Int = 0,
lastItemIndex: Int,
- lastItemNotVisible: Int = 0,
viewPortHeight: Int
) {
assertThat(visibleItemsInfo.first().index).isEqualTo(firstItemIndex)
- assertThat(visibleItemsInfo.first().offset).isEqualTo(firstItemNotVisible)
assertThat(visibleItemsInfo.last().index).isEqualTo(lastItemIndex)
- assertThat(
- viewPortHeight - (visibleItemsInfo.last().offset + visibleItemsInfo.last().size)
- ).isEqualTo(lastItemNotVisible)
+ assertThat((viewPortHeight / 2f) >=
+ (visibleItemsInfo.last().offset + (visibleItemsInfo.last().size / 2)))
}
}
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
index 5b36b6a..5422045 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
@@ -136,7 +136,8 @@
modifier = Modifier.testTag(TEST_TAG).requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
+ scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+ contentPadding = PaddingValues(vertical = 100.dp)
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
@@ -154,8 +155,8 @@
)
}
rule.waitForIdle()
- state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
- assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+ state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+ assertThat(state.centerItemIndex).isEqualTo(1)
}
@Test
@@ -164,14 +165,14 @@
rule.setContent {
WithTouchSlop(0f) {
ScalingLazyColumn(
- state = rememberScalingLazyListState().also { state = it },
+ state = rememberScalingLazyListState(8).also { state = it },
modifier = Modifier.testTag(TEST_TAG).requiredSize(
- itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+ itemSizeDp * 4f + defaultItemSpacingDp * 3f
),
scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
reverseLayout = true
) {
- items(5) {
+ items(15) {
Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
}
}
@@ -187,8 +188,9 @@
)
}
rule.waitForIdle()
- state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
- assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+ state.layoutInfo.assertVisibleItems(count = 5, startIndex = 7)
+ assertThat(state.centerItemIndex).isEqualTo(9)
+ assertThat(state.centerItemScrollOffset).isEqualTo(0)
}
@Composable
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
index c5b3156..605c1a6 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
@@ -29,26 +29,23 @@
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
import kotlin.math.roundToInt
-import kotlin.properties.Delegates
@MediumTest
@RunWith(AndroidJUnit4::class)
@@ -166,13 +163,13 @@
@Test
fun itemStraddlingCenterLineDoesNotGetScaled() {
lateinit var state: ScalingLazyListState
- var viewPortHeight by Delegates.notNull<Int>()
+ val centerItemIndex = 2
rule.setContent {
ScalingLazyColumn(
- state = rememberScalingLazyListState().also { state = it },
+ state = rememberScalingLazyListState(centerItemIndex).also { state = it },
modifier = Modifier.requiredSize(
itemSizeDp * 3
- ).onSizeChanged { viewPortHeight = it.height },
+ ),
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -182,14 +179,12 @@
rule.runOnIdle {
// Get the middle item on the screen
- val secondItem = state.layoutInfo.visibleItemsInfo[1]
- // Confirm it's the second item in the list
- assertThat(secondItem.index).isEqualTo(1)
- // And that it is located either side of the center line
- assertThat(secondItem.offset).isLessThan(viewPortHeight / 2)
- assertThat(secondItem.offset + secondItem.size).isGreaterThan(viewPortHeight / 2)
+ val centerScreenItem =
+ state.layoutInfo.visibleItemsInfo.find { it.index == centerItemIndex }
+ // and confirm its offset is 0
+ assertThat(centerScreenItem!!.offset).isEqualTo(0)
// And that it is not scaled
- assertThat(secondItem.scale).isEqualTo(1.0f)
+ assertThat(centerScreenItem.scale).isEqualTo(1.0f)
}
}
@@ -219,13 +214,13 @@
}
@Test
- fun visibleItemsAreCorrectNoScaling() {
+ fun visibleItemsAreCorrectCenterPivotNoOffset() {
lateinit var state: ScalingLazyListState
rule.setContent {
ScalingLazyColumn(
- state = rememberScalingLazyListState().also { state = it },
+ state = rememberScalingLazyListState(2).also { state = it },
modifier = Modifier.requiredSize(
- itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+ itemSizeDp * 2f + defaultItemSpacingDp * 1f
),
scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
) {
@@ -236,8 +231,83 @@
}
rule.runOnIdle {
- state.layoutInfo.assertVisibleItems(count = 4)
- assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+ state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
+ assertThat(state.centerItemIndex).isEqualTo(2)
+ assertThat(state.centerItemScrollOffset).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrectCenterPivotWithOffset() {
+ lateinit var state: ScalingLazyListState
+ rule.setContent {
+ ScalingLazyColumn(
+ state = rememberScalingLazyListState(2, -5).also { state = it },
+ modifier = Modifier.requiredSize(
+ itemSizeDp * 2f + defaultItemSpacingDp * 1f
+ ),
+ scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
+ ) {
+ items(5) {
+ Box(Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
+ assertThat(state.centerItemIndex).isEqualTo(2)
+ assertThat(state.centerItemScrollOffset).isEqualTo(-5)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrectCenterPivotNoOffsetReverseLayout() {
+ lateinit var state: ScalingLazyListState
+ rule.setContent {
+ ScalingLazyColumn(
+ state = rememberScalingLazyListState(2).also { state = it },
+ modifier = Modifier.requiredSize(
+ itemSizeDp * 2f + defaultItemSpacingDp * 1f
+ ),
+ scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+ reverseLayout = true
+ ) {
+ items(5) {
+ Box(Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
+ assertThat(state.centerItemIndex).isEqualTo(2)
+ assertThat(state.centerItemScrollOffset).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrectCenterPivotWithOffsetReverseLayout() {
+ lateinit var state: ScalingLazyListState
+ rule.setContent {
+ ScalingLazyColumn(
+ state = rememberScalingLazyListState(2, -5).also { state = it },
+ modifier = Modifier.requiredSize(
+ itemSizeDp * 2f + defaultItemSpacingDp * 1f
+ ),
+ scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+ reverseLayout = true
+ ) {
+ items(5) {
+ Box(Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
+ assertThat(state.centerItemIndex).isEqualTo(2)
+ assertThat(state.centerItemScrollOffset).isEqualTo(-5)
}
}
@@ -246,15 +316,15 @@
lateinit var state: ScalingLazyListState
rule.setContent {
ScalingLazyColumn(
- state = rememberScalingLazyListState().also { state = it },
+ state = rememberScalingLazyListState(8).also { state = it },
modifier = Modifier.requiredSize(
- itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+ itemSizeDp * 4f + defaultItemSpacingDp * 3f
),
scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
reverseLayout = true
) {
- items(5) {
- Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
+ items(15) {
+ Box(Modifier.requiredSize(itemSizeDp).testTag("Item:$it"))
}
}
}
@@ -262,13 +332,10 @@
rule.waitForIdle()
// Assert that items are being shown at the end of the parent as this is reverseLayout
- rule.onNodeWithTag(testTag = "Item:0").assertIsDisplayed()
- rule.onNodeWithTag(testTag = "Item:0")
- .assertTopPositionInRootIsEqualTo(itemSizeDp * 2.5f + defaultItemSpacingDp * 2.5f)
+ rule.onNodeWithTag(testTag = "Item:8").assertIsDisplayed()
rule.runOnIdle {
- state.layoutInfo.assertVisibleItems(count = 4)
- assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+ state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6)
}
}
@@ -281,10 +348,14 @@
modifier = Modifier.requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
+ scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+ contentPadding = PaddingValues(vertical = 100.dp),
) {
items(5) {
- Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
+ Box(
+ Modifier
+ .requiredSize(itemSizeDp)
+ .testTag("Item:$it"))
}
}
}
@@ -292,22 +363,24 @@
rule.waitForIdle()
rule.onNodeWithTag(testTag = "Item:0").assertIsDisplayed()
- // Assert that the 0th item is displayed at the end of the parent for reversedLayout
- rule.onNodeWithTag(testTag = "Item:0").assertTopPositionInRootIsEqualTo(0.dp)
+ val scrollAmount = (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()).roundToInt()
rule.runOnIdle {
+ assertThat(state.centerItemIndex).isEqualTo(0)
+ assertThat(state.centerItemScrollOffset).isEqualTo(0)
+
runBlocking {
- state.scrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+ state.scrollBy(scrollAmount.toFloat())
}
- state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
- assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+ state.layoutInfo.assertVisibleItems(count = 4)
+ assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(-scrollAmount)
}
rule.runOnIdle {
runBlocking {
- state.scrollBy(-(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
+ state.scrollBy(-scrollAmount.toFloat())
}
- state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+ state.layoutInfo.assertVisibleItems(count = 3)
assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
}
}
@@ -317,40 +390,40 @@
lateinit var state: ScalingLazyListState
rule.setContent {
ScalingLazyColumn(
- state = rememberScalingLazyListState().also { state = it },
+ state = rememberScalingLazyListState(8).also { state = it },
modifier = Modifier.requiredSize(
- itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+ itemSizeDp * 4f + defaultItemSpacingDp * 3f
),
scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
reverseLayout = true
) {
- items(5) {
- Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
+ items(15) {
+ Box(Modifier.requiredSize(itemSizeDp).testTag("Item:$it"))
}
}
}
rule.waitForIdle()
- rule.onNodeWithTag(testTag = "Item:0").assertIsDisplayed()
- // Assert that the 0th item is displayed at the end of the parent for reversedLayout
- rule.onNodeWithTag(testTag = "Item:0")
- .assertTopPositionInRootIsEqualTo(itemSizeDp * 2.5f + defaultItemSpacingDp * 2.5f)
+ rule.onNodeWithTag(testTag = "Item:8").assertIsDisplayed()
+ val scrollAmount = (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()).roundToInt()
rule.runOnIdle {
+ state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6)
+ assertThat(state.centerItemIndex).isEqualTo(8)
+ assertThat(state.centerItemScrollOffset).isEqualTo(0)
+
runBlocking {
- state.scrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+ state.scrollBy(scrollAmount.toFloat())
}
- state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
- assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+ state.layoutInfo.assertVisibleItems(count = 5, startIndex = 7)
}
rule.runOnIdle {
runBlocking {
- state.scrollBy(-(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
+ state.scrollBy(-scrollAmount.toFloat())
}
- state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
- assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+ state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6)
}
}
@@ -363,7 +436,8 @@
modifier = Modifier.requiredSize(
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
- scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
+ scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+ contentPadding = PaddingValues(vertical = 100.dp)
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -371,19 +445,21 @@
}
}
+ val scrollAmount = itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()
rule.runOnIdle {
runBlocking {
- state.dispatchRawDelta(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+ state.dispatchRawDelta(scrollAmount)
}
- state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
- assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+ state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+ assertThat(state.layoutInfo.visibleItemsInfo.first().offset)
+ .isEqualTo(-scrollAmount.roundToInt())
}
rule.runOnIdle {
runBlocking {
- state.dispatchRawDelta(-(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
+ state.dispatchRawDelta(-scrollAmount)
}
- state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+ state.layoutInfo.assertVisibleItems(count = 3, startIndex = 0)
assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
}
}
@@ -405,13 +481,13 @@
}
}
}
-
+ val firstItemOffset = state.layoutInfo.visibleItemsInfo.first().offset
rule.runOnIdle {
runBlocking {
state.dispatchRawDelta(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
}
state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
- assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+ assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(firstItemOffset)
}
rule.runOnIdle {
@@ -419,7 +495,7 @@
state.dispatchRawDelta(-(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
}
state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
- assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+ assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(firstItemOffset)
}
}
@@ -559,6 +635,44 @@
}
}
+ @Composable
+ fun ObservingCentralItemIndexFun(
+ state: ScalingLazyListState,
+ currentInfo: StableRef<Int?>
+ ) {
+ currentInfo.value = state.centerItemIndex
+ }
+
+ @Test
+ fun isCentralListItemIndexObservableWhenWeScroll() {
+ lateinit var state: ScalingLazyListState
+ var scope: CoroutineScope? = null
+ val currentInfo = StableRef<Int?>(null)
+ rule.setContent {
+ scope = rememberCoroutineScope()
+ ScalingLazyColumn(
+ state = rememberScalingLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f)
+ ) {
+ items(6) {
+ Box(Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ ObservingCentralItemIndexFun(state, currentInfo)
+ }
+
+ scope!!.launch {
+ // empty it here and scrolling should invoke observingFun again
+ currentInfo.value = null
+ state.animateScrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo.value).isNotNull()
+ assertThat(currentInfo.value).isEqualTo(2)
+ }
+ }
+
@Test
fun visibleItemsAreObservableWhenResize() {
lateinit var state: ScalingLazyListState
@@ -665,7 +779,7 @@
}
}
- fun ScalingLazyListLayoutInfo.assertVisibleItems(
+ private fun ScalingLazyListLayoutInfo.assertVisibleItems(
count: Int,
startIndex: Int = 0,
unscaledSize: Int = itemSizePx,
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
index 4a28d8e..7ccfcf2 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
@@ -72,10 +72,11 @@
}
val repeatTarget = if (repeatItems) 100_000_000 / numberOfOptions else 1
- if (repeatItems) {
+ // TODO: Remove this and pass into the initial position when creating the LazyListState.
+ if (repeatItems && state.scalingLazyListState.initialized.value) {
LaunchedEffect(state, numberOfOptions) {
// Scroll to the middle block.
- state.scalingLazyListState.lazyListState.scrollToItem(
+ state.scalingLazyListState.scrollToItem(
numberOfOptions * (repeatTarget / 2),
0
)
@@ -123,7 +124,7 @@
get() = if (itemCount == 0) {
0
} else {
- scalingLazyListState.layoutInfo.centralItemIndex % itemCount
+ scalingLazyListState.centerItemIndex % itemCount
}
/**
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
index 1625ff2..a3254f6 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
@@ -421,20 +421,23 @@
private fun decimalLastItemIndex(): Float {
if (state.layoutInfo.visibleItemsInfo.isEmpty()) return 0f
val lastItem = state.layoutInfo.visibleItemsInfo.last()
- val lastItemVisibleSize = state.layoutInfo.viewportEndOffset - lastItem.offset
- val decimalLastItemIndex = lastItem.index.toFloat() +
- lastItemVisibleSize.toFloat() / lastItem.size.toFloat()
+ val lastItemConvertedOffset: Float = lastItem.offset - (lastItem.size / 2f) +
+ state.viewportHeightPx.value?.div(2f)!! + state.layoutInfo.viewportStartOffset
+ val lastItemVisibleSize = state.layoutInfo.viewportEndOffset - lastItemConvertedOffset
+ val decimalLastItemIndex = lastItem.index.toFloat() + lastItemVisibleSize /
+ lastItem.size.toFloat()
return decimalLastItemIndex
}
private fun decimalFirstItemIndex(): Float {
if (state.layoutInfo.visibleItemsInfo.isEmpty()) return 0f
val firstItem = state.layoutInfo.visibleItemsInfo.first()
- val firstItemOffset = firstItem.offset - state.layoutInfo.viewportStartOffset
+ val firstItemConvertedOffset: Float = firstItem.offset - (firstItem.size / 2f) +
+ state.viewportHeightPx.value?.div(2f)!! + state.layoutInfo.viewportStartOffset
val decimalFirstItemIndex =
- if (firstItemOffset < 0)
+ if (firstItemConvertedOffset < 0)
firstItem.index.toFloat() +
- abs(firstItemOffset.toFloat()) / firstItem.size.toFloat()
+ abs(firstItemConvertedOffset) / firstItem.size.toFloat()
else firstItem.index.toFloat()
return decimalFirstItemIndex
}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
index 8378453..2baa159 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
@@ -29,13 +29,19 @@
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
@@ -161,6 +167,47 @@
itemContent(it, items[it])
}
+@Suppress("INLINE_CLASS_DEPRECATED")
+@Immutable
+public inline class ScalingLazyListAnchorType internal constructor(internal val type: Int) {
+
+ companion object {
+ /**
+ * Place the center of the item on (or as close to) the center line of the viewport
+ */
+ val ItemCenter = ScalingLazyListAnchorType(0)
+
+ /**
+ * Place the start (edge) of the item on, or as close to as possible, the center line of the
+ * viewport. For normal layout this will be the top edge of the item, for reverseLayout it
+ * will be the bottom edge.
+ */
+ val ItemStart = ScalingLazyListAnchorType(1)
+ }
+
+ override fun toString(): String {
+ return when (this) {
+ ItemStart -> "ScalingLazyListAnchorType.ItemStart"
+ else -> "ScalingLazyListAnchorType.ItemCenter"
+ }
+ }
+}
+
+internal fun convertToCenterOffset(
+ anchorType: ScalingLazyListAnchorType,
+ itemScrollOffset: Int,
+ viewPortSizeInPx: Int,
+ beforeContentPaddingInPx: Int,
+ itemSizeInPx: Int
+): Int {
+ if (anchorType == ScalingLazyListAnchorType.ItemStart) {
+ return itemScrollOffset - (viewPortSizeInPx / 2) + beforeContentPaddingInPx
+ } else {
+ return itemScrollOffset + (itemSizeInPx / 2) -
+ (viewPortSizeInPx / 2) + beforeContentPaddingInPx
+ }
+}
+
/**
* A scrolling scaling/fisheye list component that forms a key part of the Wear Material Design
* language. Provides scaling and transparency effects to the content items.
@@ -204,24 +251,19 @@
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp),
state: ScalingLazyListState = rememberScalingLazyListState(),
+ anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter,
content: ScalingLazyListScope.() -> Unit
) {
- require(scalingParams.minElementHeight <= scalingParams.maxElementHeight) {
- "minElementHeight must be less than or equal to maxElementHeight"
- }
- require(scalingParams.minTransitionArea <= scalingParams.maxTransitionArea) {
- "minTransitionArea must be less than or equal to maxTransitionArea"
- }
- require(scalingParams.minElementHeight != scalingParams.maxElementHeight ||
- scalingParams.minTransitionArea == scalingParams.maxTransitionArea) {
- "when minElementHeight and maxElementHeight are equal, " +
- "so should be minTransitionArea and maxTransitionArea"
- }
+ var initialized by remember { mutableStateOf(false) }
BoxWithConstraints(modifier = modifier) {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val extraPaddingInPixels = scalingParams.resolveViewportVerticalOffset(constraints)
val extraPadding = with(density) { extraPaddingInPixels.toDp() }
+ val beforeContentPaddingInPx = with(density) {
+ if (reverseLayout) contentPadding.calculateBottomPadding().roundToPx()
+ else contentPadding.calculateTopPadding().roundToPx()
+ }
val itemScope = with(density) {
ScalingLazyListItemScopeImpl(
density = density,
@@ -239,11 +281,13 @@
}
// Set up transient state
state.scalingParams.value = scalingParams
- state.extraPaddingInPixels.value = extraPaddingInPixels
+ state.extraPaddingPx.value = extraPaddingInPixels
+ state.beforeContentPaddingPx.value = beforeContentPaddingInPx
state.viewportHeightPx.value = constraints.maxHeight
state.gapBetweenItemsPx.value = with(density) {
verticalArrangement.spacing.roundToPx()
}
+ state.anchorType.value = anchorType
state.reverseLayout.value = reverseLayout
val combinedPaddingValues = CombinedPaddingValues(
@@ -251,10 +295,13 @@
extraPadding = extraPadding
)
LazyColumn(
- Modifier
+ modifier = Modifier
.fillMaxSize()
.clipToBounds()
- .verticalNegativePadding(extraPadding),
+ .verticalNegativePadding(extraPadding)
+ .onGloballyPositioned {
+ initialized = true
+ },
horizontalAlignment = horizontalAlignment,
contentPadding = combinedPaddingValues,
reverseLayout = reverseLayout,
@@ -268,6 +315,11 @@
)
scope.content()
}
+ if (initialized) {
+ LaunchedEffect(state) {
+ state.scrollToInitialItem()
+ }
+ }
}
}
@@ -449,15 +501,15 @@
) {
Box(
modifier = Modifier.graphicsLayer {
- val items = state.layoutInfo.visibleItemsInfo
val reverseLayout = state.reverseLayout.value!!
+ val items = state.layoutInfo.visibleItemsInfo
val currentItem = items.find { it.index == index }
if (currentItem != null) {
alpha = currentItem.alpha
scaleX = currentItem.scale
scaleY = currentItem.scale
val offsetAdjust = (currentItem.offset - currentItem.unadjustedOffset).toFloat()
- translationY = if (reverseLayout) - offsetAdjust else offsetAdjust
+ translationY = if (reverseLayout) -offsetAdjust else offsetAdjust
transformOrigin = TransformOrigin(
pivotFractionX = 0.5f,
pivotFractionY = if (reverseLayout) 1.0f else 0.0f
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
index 3466825..764c34a 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
@@ -188,13 +188,11 @@
init {
check(
- minElementHeight <= maxElementHeight,
- { "minElementHeight must be less than or equal to maxElementHeight" }
- )
+ minElementHeight <= maxElementHeight
+ ) { "minElementHeight must be less than or equal to maxElementHeight" }
check(
- minTransitionArea <= maxTransitionArea,
- { "minTransitionArea must be less than or equal to maxTransitionArea" }
- )
+ minTransitionArea <= maxTransitionArea
+ ) { "minTransitionArea must be less than or equal to maxTransitionArea" }
}
override fun resolveViewportVerticalOffset(viewportConstraints: Constraints): Int {
@@ -288,20 +286,17 @@
// TODO(b/202164558) - double check the height calculations with UX
val heightAsFractionOfHalfViewPort = itemHeightPx / viewPortEdgeToCenterPx
if (itemEdgeAsFractionOfHalfViewport > 0.0f && itemEdgeAsFractionOfHalfViewport < 1.0f) {
- val scalingLineAsFractionOfViewPort =
- if (scalingParams.minTransitionArea == scalingParams.maxTransitionArea) {
- scalingParams.minTransitionArea
- } else {
- // Work out the scaling line based on size, this is a value between 0.0..1.0
- val sizeRatio: Float = (
- (heightAsFractionOfHalfViewPort - scalingParams.minElementHeight) /
- (scalingParams.maxElementHeight - scalingParams.minElementHeight)
- ).coerceIn(0f, 1f)
+ // Work out the scaling line based on size, this is a value between 0.0..1.0
+ val sizeRatio: Float =
+ (
+ (heightAsFractionOfHalfViewPort - scalingParams.minElementHeight) /
+ (scalingParams.maxElementHeight - scalingParams.minElementHeight)
+ ).coerceIn(0f, 1f)
- scalingParams.minTransitionArea +
- (scalingParams.maxTransitionArea - scalingParams.minTransitionArea) *
- sizeRatio
- }
+ val scalingLineAsFractionOfViewPort =
+ scalingParams.minTransitionArea +
+ (scalingParams.maxTransitionArea - scalingParams.minTransitionArea) *
+ sizeRatio
if (itemEdgeAsFractionOfHalfViewport < scalingLineAsFractionOfViewPort) {
// We are scaling
@@ -313,12 +308,12 @@
scaleToApply =
scalingParams.edgeScale +
- (1.0f - scalingParams.edgeScale) *
- (1.0f - fractionOfDiffToApplyInterpolated)
+ (1.0f - scalingParams.edgeScale) *
+ (1.0f - fractionOfDiffToApplyInterpolated)
alphaToApply =
scalingParams.edgeAlpha +
- (1.0f - scalingParams.edgeAlpha) *
- (1.0f - fractionOfDiffToApplyInterpolated)
+ (1.0f - scalingParams.edgeAlpha) *
+ (1.0f - fractionOfDiffToApplyInterpolated)
}
} else {
scaleToApply = scalingParams.edgeScale
@@ -340,14 +335,22 @@
* viewport in order to correctly calculate the scaling to apply.
* @param viewportHeightPx the height of the viewport in pixels
* @param scalingParams the scaling params to use for determining the scaled size of the item
+ * @param beforeContentPaddingPx the number of pixels of padding before the first item
+ * @param anchorType the type of pivot to use for the center item when calculating position and
+ * offset
+ * @param initialized a flag to determine whether the ScalingLazyColumn is initialized or not, if
+ * not then set the item to be transparent.
*/
-internal fun createItemInfo(
+internal fun calculateItemInfo(
itemStart: Int,
item: LazyListItemInfo,
verticalAdjustment: Int,
viewportHeightPx: Int,
scalingParams: ScalingParams,
-): ScalingLazyListItemInfo {
+ beforeContentPaddingPx: Int,
+ anchorType: ScalingLazyListAnchorType,
+ initialized: Boolean
+): ItemInfoAndOffsetDelta {
val adjustedItemStart = itemStart - verticalAdjustment
val adjustedItemEnd = itemStart + item.size - verticalAdjustment
@@ -364,14 +367,32 @@
itemStart + item.size - scaledHeight
}
- return DefaultScalingLazyListItemInfo(
- index = item.index,
- key = item.key,
- unadjustedOffset = item.offset,
- offset = scaledItemTop,
- size = scaledHeight,
- scale = scaleAndAlpha.scale,
- alpha = scaleAndAlpha.alpha
+ val offset = convertToCenterOffset(
+ anchorType = anchorType,
+ itemScrollOffset = scaledItemTop,
+ viewPortSizeInPx = viewportHeightPx,
+ beforeContentPaddingInPx = beforeContentPaddingPx,
+ itemSizeInPx = item.size
+ )
+ val offsetDelta = scaledItemTop - offset
+ val unadjustedOffset = convertToCenterOffset(
+ anchorType = anchorType,
+ itemScrollOffset = item.offset,
+ viewPortSizeInPx = viewportHeightPx,
+ beforeContentPaddingInPx = beforeContentPaddingPx,
+ itemSizeInPx = item.size
+ )
+ return ItemInfoAndOffsetDelta(
+ offsetDelta,
+ DefaultScalingLazyListItemInfo(
+ index = item.index,
+ key = item.key,
+ unadjustedOffset = unadjustedOffset,
+ offset = offset,
+ size = scaledHeight,
+ scale = scaleAndAlpha.scale,
+ alpha = if (initialized) scaleAndAlpha.alpha else 0f
+ )
)
}
@@ -380,7 +401,8 @@
override val viewportStartOffset: Int,
override val viewportEndOffset: Int,
override val totalItemsCount: Int,
- override val centralItemIndex: Int
+ val centerItemIndex: Int,
+ val centerItemScrollOffset: Int
) : ScalingLazyListLayoutInfo
internal class DefaultScalingLazyListItemInfo(
@@ -391,7 +413,13 @@
override val size: Int,
override val scale: Float,
override val alpha: Float
-) : ScalingLazyListItemInfo
+) : ScalingLazyListItemInfo {
+ override fun toString(): String {
+ return "DefaultScalingLazyListItemInfo(index=$index, key=$key, " +
+ "unadjustedOffset=$unadjustedOffset, offset=$offset, size=$size, " +
+ "scale=$scale, alpha=$alpha)"
+ }
+}
@Immutable
internal data class ScaleAndAlpha(
@@ -403,3 +431,13 @@
internal val noScaling = ScaleAndAlpha(1.0f, 1.0f)
}
}
+
+@Immutable
+internal data class ItemInfoAndOffsetDelta(
+ val offsetDelta: Int,
+ val itemInfo: ScalingLazyListItemInfo
+) {
+ fun offsetAdjusted(): Int {
+ return itemInfo.offset + offsetDelta
+ }
+}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt
index 18e07cf..83ef1a8 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt
@@ -48,10 +48,4 @@
* The total count of items passed to [ScalingLazyColumn].
*/
val totalItemsCount: Int
-
- /**
- * The index of the item on to the center of the view, if there are two items around the center
- * line, the second one (higher index) is used. It's -1 if the list is empty.
- */
- val centralItemIndex: Int
}
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
index f8fe40a..03a4fd9 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.lazy.LazyListItemInfo
+import androidx.compose.foundation.lazy.LazyListLayoutInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@@ -29,121 +30,179 @@
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
+import kotlin.math.roundToInt
/**
* Creates a [ScalingLazyListState] that is remembered across compositions.
+ *
+ * @param initialCenterItemIndex the initial value for [ScalingLazyListState.centerItemIndex]
+ * @param initialCenterItemScrollOffset the initial value for
+ * [ScalingLazyListState.centerItemScrollOffset] in pixels
*/
@Composable
-public fun rememberScalingLazyListState(): ScalingLazyListState {
+public fun rememberScalingLazyListState(
+ initialCenterItemIndex: Int = 0,
+ initialCenterItemScrollOffset: Int = 0
+): ScalingLazyListState {
return rememberSaveable(saver = ScalingLazyListState.Saver) {
- ScalingLazyListState()
+ ScalingLazyListState(
+ initialCenterItemIndex,
+ initialCenterItemScrollOffset
+ )
}
}
/**
* A state object that can be hoisted to control and observe scrolling.
- * TODO (b/193792848): Add scrolling and snap support.
*
* In most cases, this will be created via [rememberScalingLazyListState].
+ *
+ * @param initialCenterItemIndex the initial value for [ScalingLazyListState.centerItemIndex]
+ * @param initialCenterItemScrollOffset the initial value for
+ * [ScalingLazyListState.centerItemScrollOffset]
*/
+// TODO (b/193792848): Add snap support.
@Stable
-public class ScalingLazyListState : ScrollableState {
+class ScalingLazyListState constructor(
+ private val initialCenterItemIndex: Int = 0,
+ private val initialCenterItemScrollOffset: Int = 0
+) : ScrollableState {
+
internal var lazyListState: LazyListState = LazyListState(0, 0)
- internal val extraPaddingInPixels = mutableStateOf<Int?>(null)
+ internal val extraPaddingPx = mutableStateOf<Int?>(null)
+ internal val beforeContentPaddingPx = mutableStateOf<Int?>(null)
internal val scalingParams = mutableStateOf<ScalingParams?>(null)
internal val gapBetweenItemsPx = mutableStateOf<Int?>(null)
internal val viewportHeightPx = mutableStateOf<Int?>(null)
internal val reverseLayout = mutableStateOf<Boolean?>(null)
+ internal val anchorType = mutableStateOf<ScalingLazyListAnchorType?>(null)
+ internal val initialized = mutableStateOf<Boolean>(false)
+
+ /**
+ * The index of the item positioned closest to the viewport center
+ */
+ public val centerItemIndex: Int
+ get() = (layoutInfo as? DefaultScalingLazyListLayoutInfo)?.centerItemIndex ?: 0
+
+ /**
+ * The offset of the item closest to the viewport center. Depending on the [ScalingLazyListAnchorType] of the
+ * [ScalingLazyColumn] the offset will be relative to either items Edge or Center.
+ */
+ public val centerItemScrollOffset: Int
+ get() = (layoutInfo as? DefaultScalingLazyListLayoutInfo)?.centerItemScrollOffset ?: 0
/**
* The object of [ScalingLazyListLayoutInfo] calculated during the last layout pass. For
* example, you can use it to calculate what items are currently visible.
*/
public val layoutInfo: ScalingLazyListLayoutInfo by derivedStateOf {
- if (extraPaddingInPixels.value == null || scalingParams.value == null ||
+ if (extraPaddingPx.value == null || scalingParams.value == null ||
gapBetweenItemsPx.value == null || viewportHeightPx.value == null ||
- reverseLayout.value == null
+ anchorType.value == null || reverseLayout.value == null ||
+ beforeContentPaddingPx.value == null
) {
EmptyScalingLazyListLayoutInfo
} else {
val visibleItemsInfo = mutableListOf<ScalingLazyListItemInfo>()
- var centralItemIndex = -1
+ var newCenterItemIndex = -1
+ var newCenterItemScrollOffset = 0
if (lazyListState.layoutInfo.visibleItemsInfo.isNotEmpty()) {
val verticalAdjustment =
- lazyListState.layoutInfo.viewportStartOffset + extraPaddingInPixels.value!!
+ lazyListState.layoutInfo.viewportStartOffset + extraPaddingPx.value!!
// Find the item in the middle of the viewport
val centralItem =
findItemNearestCenter(viewportHeightPx.value!!, verticalAdjustment)!!
// Place the center item
- val centerItemInfo = createItemInfo(
+ val centerItemInfoAndOffsetDelta = calculateItemInfo(
centralItem.offset,
centralItem,
verticalAdjustment,
viewportHeightPx.value!!,
scalingParams.value!!,
+ beforeContentPaddingPx.value!!,
+ anchorType.value!!,
+ initialized.value
)
visibleItemsInfo.add(
- centerItemInfo
+ centerItemInfoAndOffsetDelta.itemInfo
)
+
+ newCenterItemIndex = centralItem.index
+ newCenterItemScrollOffset = - centerItemInfoAndOffsetDelta.itemInfo.offset
+
// Go Up
- centralItemIndex = centralItem.index
- var nextItemBottomNoPadding = centerItemInfo.offset - gapBetweenItemsPx.value!!
+ var nextItemBottomNoPadding =
+ centerItemInfoAndOffsetDelta.offsetAdjusted() - gapBetweenItemsPx.value!!
val minIndex =
lazyListState.layoutInfo.visibleItemsInfo.minOf { it.index }
- (centralItemIndex - 1 downTo minIndex).forEach { ix ->
+ (newCenterItemIndex - 1 downTo minIndex).forEach { ix ->
val currentItem =
- lazyListState.layoutInfo.visibleItemsInfo.find { it.index == ix }!!
- val itemInfo = createItemInfo(
+ lazyListState.layoutInfo.findItemInfoWithIndex(ix)!!
+ val itemInfoAndOffset = calculateItemInfo(
nextItemBottomNoPadding - currentItem.size,
currentItem,
verticalAdjustment,
viewportHeightPx.value!!,
scalingParams.value!!,
+ beforeContentPaddingPx.value!!,
+ anchorType.value!!,
+ initialized.value
)
// If the item is visible in the viewport insert it at the start of the
// list
- if ((itemInfo.offset + itemInfo.size) > verticalAdjustment) {
+ if (
+ (itemInfoAndOffset.offsetAdjusted() + itemInfoAndOffset.itemInfo.size) >
+ verticalAdjustment) {
// Insert the item info at the front of the list
- visibleItemsInfo.add(0, itemInfo)
+ visibleItemsInfo.add(0, itemInfoAndOffset.itemInfo)
}
- nextItemBottomNoPadding = itemInfo.offset - gapBetweenItemsPx.value!!
+ nextItemBottomNoPadding =
+ itemInfoAndOffset.offsetAdjusted() - gapBetweenItemsPx.value!!
}
// Go Down
var nextItemTopNoPadding =
- centerItemInfo.offset + centerItemInfo.size +
+ centerItemInfoAndOffsetDelta.offsetAdjusted() +
+ centerItemInfoAndOffsetDelta.itemInfo.size +
gapBetweenItemsPx.value!!
val maxIndex =
lazyListState.layoutInfo.visibleItemsInfo.maxOf { it.index }
- (centralItemIndex + 1..maxIndex).forEach { ix ->
+ (newCenterItemIndex + 1..maxIndex).forEach { ix ->
val currentItem =
- lazyListState.layoutInfo.visibleItemsInfo.find { it.index == ix }!!
- val itemInfo = createItemInfo(
+ lazyListState.layoutInfo.findItemInfoWithIndex(ix)!!
+ val itemInfoAndOffset = calculateItemInfo(
nextItemTopNoPadding,
currentItem,
verticalAdjustment,
viewportHeightPx.value!!,
scalingParams.value!!,
+ beforeContentPaddingPx.value!!,
+ anchorType.value!!,
+ initialized.value
)
// If the item is visible in the viewport insert it at the end of the
// list
- if ((itemInfo.offset - verticalAdjustment) < viewportHeightPx.value!!) {
- visibleItemsInfo.add(itemInfo)
+ if ((itemInfoAndOffset.offsetAdjusted() - verticalAdjustment) <
+ viewportHeightPx.value!!) {
+ visibleItemsInfo.add(itemInfoAndOffset.itemInfo)
}
nextItemTopNoPadding =
- itemInfo.offset + itemInfo.size + gapBetweenItemsPx.value!!
+ itemInfoAndOffset.offsetAdjusted() + itemInfoAndOffset.itemInfo.size +
+ gapBetweenItemsPx.value!!
}
}
+
DefaultScalingLazyListLayoutInfo(
visibleItemsInfo = visibleItemsInfo,
totalItemsCount = lazyListState.layoutInfo.totalItemsCount,
viewportStartOffset = lazyListState.layoutInfo.viewportStartOffset +
- extraPaddingInPixels.value!!,
+ extraPaddingPx.value!!,
viewportEndOffset = lazyListState.layoutInfo.viewportEndOffset -
- extraPaddingInPixels.value!!,
- centralItemIndex = centralItemIndex
+ extraPaddingPx.value!!,
+ centerItemIndex = if (initialized.value) newCenterItemIndex else 0,
+ centerItemScrollOffset = if (initialized.value) newCenterItemScrollOffset else 0
)
}
}
@@ -173,16 +232,12 @@
val Saver = listSaver<ScalingLazyListState, Int>(
save = {
listOf(
- it.lazyListState.firstVisibleItemIndex,
- it.lazyListState.firstVisibleItemScrollOffset,
+ it.centerItemIndex,
+ it.centerItemScrollOffset,
)
},
restore = {
- val scalingLazyColumnState = ScalingLazyListState()
- scalingLazyColumnState.lazyListState = LazyListState(
- firstVisibleItemIndex = it[0],
- firstVisibleItemScrollOffset = it[1],
- )
+ val scalingLazyColumnState = ScalingLazyListState(it[0], it[1])
scalingLazyColumnState
}
)
@@ -203,6 +258,109 @@
) {
lazyListState.scroll(scrollPriority = scrollPriority, block = block)
}
+
+ /**
+ * Instantly brings the item at [index] to the center of the viewport and positions it based on
+ * the [anchorType] and applies the [scrollOffset] pixels.
+ *
+ * @param index the index to which to scroll. Must be non-negative.
+ * @param scrollOffset the offset that the item should end up after the scroll. Note that
+ * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
+ * scroll the item further upward (taking it partly offscreen).
+ */
+ public suspend fun scrollToItem(
+ /*@IntRange(from = 0)*/
+ index: Int,
+ /*@IntRange(from = 0)*/
+ scrollOffset: Int = 0
+ ) {
+ val offsetToCenterOfViewport =
+ beforeContentPaddingPx.value!! - (viewportHeightPx.value!! / 2)
+ if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
+ val offset = offsetToCenterOfViewport + scrollOffset
+ return lazyListState.scrollToItem(index, offset)
+ } else {
+ var item = lazyListState.layoutInfo.findItemInfoWithIndex(index)
+ if (item == null) {
+ // Scroll the item into the middle of the viewport so that we know it is visible
+ lazyListState.scrollToItem(
+ index,
+ offsetToCenterOfViewport
+ )
+ // Now we know that the item is visible find it and fine tune our position
+ item = lazyListState.layoutInfo.findItemInfoWithIndex(index)
+ }
+ if (item != null) {
+ val offset = offsetToCenterOfViewport + (item.size / 2) + scrollOffset
+ return lazyListState.scrollToItem(index, offset)
+ }
+ }
+ return
+ }
+
+ internal suspend fun scrollToInitialItem() {
+ if (!initialized.value) {
+ initialized.value = true
+ scrollToItem(initialCenterItemIndex, initialCenterItemScrollOffset)
+ }
+ return
+ }
+
+ /**
+ * Animate (smooth scroll) the given item at [index] to the center of the viewport and position
+ * it based on the [anchorType] and applies the [scrollOffset] pixels.
+ *
+ * @param index the index to which to scroll. Must be non-negative.
+ * @param scrollOffset the offset that the item should end up after the scroll (same as
+ * [scrollToItem]) - note that positive offset refers to forward scroll, so in a
+ * top-to-bottom list, positive offset will scroll the item further upward (taking it partly
+ * offscreen)
+ */
+ public suspend fun animateScrollToItem(
+ /*@IntRange(from = 0)*/
+ index: Int,
+ /*@IntRange(from = 0)*/
+ scrollOffset: Int = 0
+ ) {
+ val offsetToCenterOfViewport =
+ beforeContentPaddingPx.value!! - (viewportHeightPx.value!! / 2)
+ if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
+ val offset = offsetToCenterOfViewport + scrollOffset
+ return lazyListState.animateScrollToItem(index, offset)
+ } else {
+ var item = lazyListState.layoutInfo.findItemInfoWithIndex(index)
+ var sizeEstimate = 0
+ if (item == null) {
+ // Guess the size of the item so that we can try and position it correctly
+ sizeEstimate = lazyListState.layoutInfo.averageItemSize()
+ // Scroll the item towards the middle of the viewport so that we know it is visible
+ lazyListState.animateScrollToItem(
+ index,
+ offsetToCenterOfViewport + (sizeEstimate / 2) + scrollOffset
+ )
+ // Now we know that the item is visible find it and fine tune our position
+ item = lazyListState.layoutInfo.findItemInfoWithIndex(index)
+ }
+ // Determine if a second adjustment is needed
+ if (item != null && item.size != sizeEstimate) {
+ val offset = offsetToCenterOfViewport + (item.size / 2) + scrollOffset
+ return lazyListState.animateScrollToItem(index, offset)
+ }
+ }
+ return
+ }
+}
+
+private fun LazyListLayoutInfo.findItemInfoWithIndex(index: Int): LazyListItemInfo? {
+ return this.visibleItemsInfo.find { it.index == index }
+}
+
+private fun LazyListLayoutInfo.averageItemSize(): Int {
+ var totalSize = 0
+ visibleItemsInfo.forEach { totalSize += it.size }
+ return if (visibleItemsInfo.isNotEmpty())
+ (totalSize.toFloat() / visibleItemsInfo.size).roundToInt()
+ else 0
}
private object EmptyScalingLazyListLayoutInfo : ScalingLazyListLayoutInfo {
@@ -210,5 +368,4 @@
override val viewportStartOffset = 0
override val viewportEndOffset = 0
override val totalItemsCount = 0
- override val centralItemIndex = -1
}
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index cfc5ea3..b9cf22d 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -38,11 +38,11 @@
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(libs.testRunner)
androidTestImplementation(project(":wear:compose:compose-material"))
androidTestImplementation(project(":wear:compose:compose-navigation-samples"))
androidTestImplementation(libs.truth)
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
samples(project(":wear:compose:compose-navigation-samples"))
}
diff --git a/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt b/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
index 64f4b73..4169ba6 100644
--- a/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
+++ b/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
@@ -15,8 +15,12 @@
*/
package androidx.wear.compose.navigation
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.OnBackPressedDispatcherOwner
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSaveable
@@ -35,6 +39,7 @@
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeRight
import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.navigation.NavHostController
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.CompactChip
@@ -88,6 +93,36 @@
}
@Test
+ fun navigates_back_to_previous_level_with_back_button() {
+ val lifecycleOwner = TestLifecycleOwner()
+ val onBackPressedDispatcher = OnBackPressedDispatcher()
+ val dispatcherOwner = object : OnBackPressedDispatcherOwner {
+ override fun getLifecycle() = lifecycleOwner.lifecycle
+ override fun getOnBackPressedDispatcher() = onBackPressedDispatcher
+ }
+ lateinit var navController: NavHostController
+
+ rule.setContentWithTheme {
+ CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides dispatcherOwner) {
+ navController = rememberSwipeDismissableNavController()
+ SwipeDismissWithNavigation(navController)
+ }
+ }
+ // Move to next destination.
+ rule.onNodeWithText(START).performClick()
+
+ // Now trigger the back button
+ rule.runOnIdle {
+ onBackPressedDispatcher.onBackPressed()
+ }
+ rule.waitForIdle()
+
+ // Should now display "start".
+ rule.onNodeWithText(START).assertExists()
+ assertThat(navController.currentDestination?.route).isEqualTo(START)
+ }
+
+ @Test
fun hides_previous_level_when_not_swiping() {
rule.setContentWithTheme {
SwipeDismissWithNavigation()
diff --git a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
index 9d0cf94..ac2df15 100644
--- a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
+++ b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
@@ -16,6 +16,7 @@
package androidx.wear.compose.navigation
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -129,10 +130,23 @@
"SwipeDismissableNavHost requires a ViewModelStoreOwner to be provided " +
"via LocalViewModelStoreOwner"
}
+ val onBackPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
+ val onBackPressedDispatcher = onBackPressedDispatcherOwner?.onBackPressedDispatcher
// Setup the navController with proper owners
navController.setLifecycleOwner(lifecycleOwner)
navController.setViewModelStore(viewModelStoreOwner.viewModelStore)
+ if (onBackPressedDispatcher != null) {
+ navController.setOnBackPressedDispatcher(onBackPressedDispatcher)
+ }
+ // Ensure that the NavController only receives back events while
+ // the NavHost is in composition
+ DisposableEffect(navController) {
+ navController.enableOnBackPressed(true)
+ onDispose {
+ navController.enableOnBackPressed(false)
+ }
+ }
// Then set the graph
navController.graph = graph
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/CheckAccessibilityAvailable.kt b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/CheckAccessibilityAvailable.kt
index 55b0d70..b555540 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/CheckAccessibilityAvailable.kt
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/CheckAccessibilityAvailable.kt
@@ -16,7 +16,6 @@
package androidx.wear.tiles.checkers
-import androidx.annotation.RestrictTo
import androidx.wear.tiles.LayoutElementBuilders.Arc
import androidx.wear.tiles.LayoutElementBuilders.ArcAdapter
import androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement
@@ -41,11 +40,8 @@
*
* At least one element on each tile should have a machine-readable content description
* associated with it, which can be read out using screen readers.
- *
- * @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-class CheckAccessibilityAvailable : TimelineEntryChecker {
+internal class CheckAccessibilityAvailable : TimelineEntryChecker {
override val name: String
get() = "CheckAccessibilityAvailable"
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/TimelineChecker.kt b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/TimelineChecker.kt
index 072230d..8665a76 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/TimelineChecker.kt
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/TimelineChecker.kt
@@ -17,28 +17,21 @@
package androidx.wear.tiles.checkers
import android.util.Log
-import androidx.annotation.RestrictTo
import androidx.wear.tiles.TimelineBuilders.Timeline
import androidx.wear.tiles.TimelineBuilders.TimelineEntry
import kotlin.jvm.Throws
/**
* Exception thrown when a TimelineEntryChecker fails.
- *
- * @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class CheckerException(message: String) : Exception(message)
+internal class CheckerException(message: String) : Exception(message)
/**
* Checker for a Tile's TimelineEntries. Instances of this interface should check for a certain
* condition on the given [TimelineEntry], and throw an instance of [CheckerException] if there
* is a problem with that [TimelineEntry].
- *
- * @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public interface TimelineEntryChecker {
+internal interface TimelineEntryChecker {
/** The name of this TimelineEntryChecker. This will be printed in any error output. */
val name: String
@@ -56,11 +49,8 @@
* given [Timeline], and if any fail, log an error to logcat.
*
* @param entryCheckers The list of checkers to use. Defaults to all built in checks.
- *
- * @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class TimelineChecker(
+internal class TimelineChecker(
private val entryCheckers: List<TimelineEntryChecker> = listOf(CheckAccessibilityAvailable()),
) {
companion object {
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index ba67d7b..56d34c3 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -923,13 +923,19 @@
interactiveInstance.complicationSlotsState
// Add some additional ContentDescriptionLabels
+ val pendingIntent1 = PendingIntent.getActivity(context, 0, Intent("One"),
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ val pendingIntent2 = PendingIntent.getActivity(context, 0, Intent("Two"),
+ PendingIntent.FLAG_IMMUTABLE
+ )
wallpaperService.watchFace.renderer.additionalContentDescriptionLabels = listOf(
Pair(
0,
ContentDescriptionLabel(
PlainComplicationText.Builder("Before").build(),
Rect(10, 10, 20, 20),
- null
+ pendingIntent1
)
),
Pair(
@@ -937,7 +943,7 @@
ContentDescriptionLabel(
PlainComplicationText.Builder("After").build(),
Rect(30, 30, 40, 40),
- null
+ pendingIntent2
)
)
)
@@ -960,6 +966,7 @@
assertThat(
contentDescriptionLabels[1].getTextAt(context.resources, Instant.EPOCH)
).isEqualTo("Before")
+ assertThat(contentDescriptionLabels[1].tapAction).isEqualTo(pendingIntent1)
// Left complication.
assertThat(contentDescriptionLabels[2].bounds).isEqualTo(Rect(80, 160, 160, 240))
@@ -978,6 +985,7 @@
assertThat(
contentDescriptionLabels[4].getTextAt(context.resources, Instant.EPOCH)
).isEqualTo("After")
+ assertThat(contentDescriptionLabels[4].tapAction).isEqualTo(pendingIntent2)
}
@SuppressLint("NewApi") // renderWatchFaceToBitmap
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt
index 8ee7af8..9a2c34f 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt
@@ -17,7 +17,6 @@
package androidx.wear.watchface.client
import android.os.RemoteException
-import androidx.annotation.RestrictTo
import androidx.wear.watchface.editor.IEditorObserver
import androidx.wear.watchface.editor.IEditorService
import androidx.wear.watchface.editor.data.EditorStateWireFormat
@@ -50,9 +49,7 @@
public fun onEditorStateChanged(editorState: EditorState)
}
-/** @hide */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class EditorServiceClientImpl(
+internal class EditorServiceClientImpl(
private val iEditorService: IEditorService
) : EditorServiceClient {
private val lock = Any()
diff --git a/wear/watchface/watchface-complications-data-core/api/current.txt b/wear/watchface/watchface-complications-data-core/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/watchface/watchface-complications-data-core/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data-core/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/watchface/watchface-complications-data-core/api/res-current.txt b/wear/watchface/watchface-complications-data-core/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/api/res-current.txt
diff --git a/wear/watchface/watchface-complications-data-core/api/restricted_current.txt b/wear/watchface/watchface-complications-data-core/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/watchface/watchface-complications-data-core/build.gradle b/wear/watchface/watchface-complications-data-core/build.gradle
new file mode 100644
index 0000000..e3fc7b0
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/build.gradle
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api("androidx.annotation:annotation:1.1.0")
+ implementation("androidx.core:core:1.1.0")
+ implementation("androidx.preference:preference:1.1.0")
+ implementation("androidx.annotation:annotation:1.2.0")
+
+ constraints {
+ implementation(project(":wear:watchface:watchface-complications-data"))
+ }
+
+ testImplementation(libs.kotlinStdlib)
+ testImplementation(libs.kotlinCoroutinesAndroid)
+ testImplementation(libs.testCore)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.testRules)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.mockitoCore)
+ testImplementation(libs.truth)
+ testImplementation("junit:junit:4.13")
+}
+
+android {
+ buildFeatures {
+ aidl = true
+ }
+ defaultConfig {
+ minSdkVersion 26
+ }
+ buildTypes.all {
+ consumerProguardFiles "proguard-rules.pro"
+ }
+
+ // Use Robolectric 4.+
+ testOptions.unitTests.includeAndroidResources = true
+}
+
+androidx {
+ name = "Android Wear Complications Data Internal"
+ publish = Publish.SNAPSHOT_AND_RELEASE
+ mavenGroup = LibraryGroups.WEAR_WATCHFACE
+ inceptionYear = "2021"
+ description = "Android Wear Complications Data Internal"
+}
diff --git a/wear/watchface/watchface-complications-data/lint-baseline.xml b/wear/watchface/watchface-complications-data-core/lint-baseline.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/lint-baseline.xml
rename to wear/watchface/watchface-complications-data-core/lint-baseline.xml
diff --git a/wear/watchface/watchface-complications-data/proguard-rules.pro b/wear/watchface/watchface-complications-data-core/proguard-rules.pro
similarity index 100%
rename from wear/watchface/watchface-complications-data/proguard-rules.pro
rename to wear/watchface/watchface-complications-data-core/proguard-rules.pro
diff --git a/wear/watchface/watchface-complications-data-core/src/main/AndroidManifest.xml b/wear/watchface/watchface-complications-data-core/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5b067ac
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.wear.watchface.complications.data.internal">
+ <application>
+ <uses-library android:name="com.google.android.wearable" android:required="false" />
+ </application>
+</manifest>
diff --git a/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/ComplicationData.aidl b/wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/ComplicationData.aidl
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/ComplicationData.aidl
rename to wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/ComplicationData.aidl
diff --git a/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/ComplicationProviderInfo.aidl b/wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/ComplicationProviderInfo.aidl
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/ComplicationProviderInfo.aidl
rename to wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/ComplicationProviderInfo.aidl
diff --git a/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IComplicationManager.aidl b/wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IComplicationManager.aidl
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IComplicationManager.aidl
rename to wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IComplicationManager.aidl
diff --git a/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IComplicationProvider.aidl b/wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IComplicationProvider.aidl
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IComplicationProvider.aidl
rename to wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IComplicationProvider.aidl
diff --git a/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IPreviewComplicationDataCallback.aidl b/wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IPreviewComplicationDataCallback.aidl
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IPreviewComplicationDataCallback.aidl
rename to wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IPreviewComplicationDataCallback.aidl
diff --git a/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IProviderInfoService.aidl b/wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IProviderInfoService.aidl
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IProviderInfoService.aidl
rename to wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IProviderInfoService.aidl
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/CharSequenceSerializableHelper.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/CharSequenceSerializableHelper.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/CharSequenceSerializableHelper.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/CharSequenceSerializableHelper.java
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationData.java
similarity index 97%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationData.java
index 3a29289..226d0f1 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
+++ b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationData.java
@@ -40,7 +40,6 @@
import java.io.Serializable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -604,54 +603,54 @@
}
/**
- * For timeline entries. Returns the {@link Instant} at which this timeline entry becomes
+ * For timeline entries. Returns the epoch second at which this timeline entry becomes
* valid or `null` if it's not set.
*/
@Nullable
- public Instant getTimelineStartInstant() {
+ public Long getTimelineStartEpochSecond() {
long expiresAt = mFields.getLong(FIELD_TIMELINE_START_TIME, -1);
if (expiresAt == -1) {
return null;
} else {
- return Instant.ofEpochSecond(expiresAt);
+ return expiresAt;
}
}
/**
- * For timeline entries. Sets the {@link Instant} at which this timeline entry becomes invalid
+ * For timeline entries. Sets the epoch second at which this timeline entry becomes invalid
* or clears the field if instant is `null`.
*/
- public void setTimelineStartInstant(@Nullable Instant instant) {
- if (instant == null) {
+ public void setTimelineStartEpochSecond(@Nullable Long epochSecond) {
+ if (epochSecond == null) {
mFields.remove(FIELD_TIMELINE_START_TIME);
} else {
- mFields.putLong(FIELD_TIMELINE_START_TIME, instant.getEpochSecond());
+ mFields.putLong(FIELD_TIMELINE_START_TIME, epochSecond);
}
}
/**
- * For timeline entries. Returns the {@link Instant} at which this timeline entry becomes
- * invalid or `null` if it's not set.
+ * For timeline entries. Returns the epoch second at which this timeline entry becomes invalid
+ * or `null` if it's not set.
*/
@Nullable
- public Instant getTimelineEndInstant() {
+ public Long getTimelineEndEpochSecond() {
long expiresAt = mFields.getLong(FIELD_TIMELINE_END_TIME, -1);
if (expiresAt == -1) {
return null;
} else {
- return Instant.ofEpochSecond(expiresAt);
+ return expiresAt;
}
}
/**
- * For timeline entries. Sets the {@link Instant} at which this timeline entry becomes invalid,
+ * For timeline entries. Sets the epoch second at which this timeline entry becomes invalid,
* or clears the field if instant is `null`.
*/
- public void setTimelineEndInstant(@Nullable Instant instant) {
- if (instant == null) {
+ public void setTimelineEndEpochSecond(@Nullable Long epochSecond) {
+ if (epochSecond == null) {
mFields.remove(FIELD_TIMELINE_END_TIME);
} else {
- mFields.putLong(FIELD_TIMELINE_END_TIME, instant.getEpochSecond());
+ mFields.putLong(FIELD_TIMELINE_END_TIME, epochSecond);
}
}
@@ -674,9 +673,12 @@
if (timelineEntries == null) {
mFields.remove(FIELD_TIMELINE_ENTRIES);
} else {
- mFields.putParcelableArray(
- FIELD_TIMELINE_ENTRIES,
- timelineEntries.stream().map(e-> e.mFields).toArray(Parcelable[]::new));
+ Parcelable[] array = new Parcelable[timelineEntries.size()];
+ int index = 0;
+ for (ComplicationData timelineEntry : timelineEntries) {
+ array[index++] = timelineEntry.mFields;
+ }
+ mFields.putParcelableArray(FIELD_TIMELINE_ENTRIES, array);
}
}
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationProviderInfo.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationProviderInfo.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationProviderInfo.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationProviderInfo.java
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationText.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationText.java
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationTextTemplate.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationTextTemplate.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationTextTemplate.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationTextTemplate.java
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/IconSerializableHelper.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/IconSerializableHelper.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/IconSerializableHelper.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/IconSerializableHelper.java
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/TimeDependentText.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/TimeDependentText.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/TimeDependentText.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/TimeDependentText.java
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/TimeDifferenceText.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/TimeDifferenceText.java
similarity index 99%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/TimeDifferenceText.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/TimeDifferenceText.java
index 60d5669..ba7122b 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/TimeDifferenceText.java
+++ b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/TimeDifferenceText.java
@@ -23,7 +23,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import androidx.wear.watchface.complications.data.R;
+import androidx.wear.watchface.complications.data.internal.R;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/TimeFormatText.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/TimeFormatText.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/TimeFormatText.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/TimeFormatText.java
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/package-info.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/package-info.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/package-info.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/package-info.java
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-af/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-af/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-af/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-af/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-am/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-am/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-am/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-am/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ar/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ar/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ar/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ar/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-as/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-as/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-as/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-as/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-az/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-az/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-az/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-az/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-b+sr+Latn/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-b+sr+Latn/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-b+sr+Latn/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-b+sr+Latn/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-be/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-be/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-be/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-be/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-bg/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-bg/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-bg/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-bg/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-bn/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-bn/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-bn/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-bn/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-bs/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-bs/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-bs/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-bs/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ca/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ca/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ca/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ca/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-cs/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-cs/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-cs/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-cs/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-da/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-da/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-da/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-da/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-de/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-de/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-de/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-de/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-el/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-el/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-el/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-el/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-en-rAU/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-en-rAU/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-en-rAU/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-en-rAU/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-en-rCA/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-en-rCA/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-en-rCA/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-en-rCA/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-en-rGB/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-en-rGB/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-en-rGB/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-en-rGB/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-en-rIN/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-en-rIN/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-en-rIN/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-en-rIN/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-en-rXC/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-en-rXC/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-en-rXC/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-en-rXC/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-es-rUS/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-es-rUS/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-es-rUS/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-es-rUS/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-es/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-es/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-es/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-es/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-et/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-et/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-et/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-et/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-eu/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-eu/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-eu/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-eu/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-fa/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-fa/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-fa/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-fa/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-fi/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-fi/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-fi/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-fi/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-fr-rCA/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-fr-rCA/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-fr-rCA/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-fr-rCA/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-fr/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-fr/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-fr/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-fr/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-gl/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-gl/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-gl/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-gl/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-gu/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-gu/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-gu/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-gu/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-hi/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-hi/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-hi/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-hi/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-hr/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-hr/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-hr/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-hr/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-hu/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-hu/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-hu/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-hu/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-hy/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-hy/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-hy/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-hy/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-in/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-in/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-in/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-in/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-is/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-is/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-is/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-is/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-it/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-it/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-it/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-it/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-iw/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-iw/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-iw/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-iw/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ja/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ja/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ja/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ja/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ka/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ka/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ka/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ka/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-kk/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-kk/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-kk/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-kk/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-km/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-km/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-km/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-km/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-kn/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-kn/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-kn/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-kn/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ko/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ko/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ko/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ko/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ky/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ky/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ky/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ky/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-lo/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-lo/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-lo/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-lo/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-lt/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-lt/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-lt/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-lt/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-lv/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-lv/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-lv/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-lv/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-mk/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-mk/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-mk/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-mk/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ml/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ml/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ml/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ml/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-mn/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-mn/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-mn/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-mn/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-mr/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-mr/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-mr/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-mr/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ms/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ms/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ms/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ms/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-my/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-my/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-my/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-my/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-nb/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-nb/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-nb/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-nb/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ne/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ne/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ne/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ne/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-nl/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-nl/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-nl/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-nl/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-or/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-or/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-or/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-or/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-pa/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-pa/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-pa/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-pa/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-pl/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-pl/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-pl/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-pl/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-pt-rBR/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-pt-rBR/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-pt-rBR/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-pt-rBR/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-pt-rPT/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-pt-rPT/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-pt-rPT/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-pt-rPT/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-pt/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-pt/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-pt/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-pt/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ro/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ro/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ro/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ro/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ru/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ru/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ru/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ru/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-si/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-si/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-si/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-si/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-sk/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-sk/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-sk/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-sk/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-sl/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-sl/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-sl/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-sl/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-sq/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-sq/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-sq/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-sq/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-sr/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-sr/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-sr/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-sr/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-sv/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-sv/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-sv/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-sv/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-sw/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-sw/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-sw/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-sw/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ta/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ta/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ta/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ta/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-te/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-te/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-te/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-te/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-th/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-th/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-th/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-th/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-tl/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-tl/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-tl/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-tl/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-tr/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-tr/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-tr/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-tr/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-uk/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-uk/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-uk/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-uk/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ur/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ur/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ur/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ur/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-uz/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-uz/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-uz/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-uz/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-vi/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-vi/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-vi/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-vi/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-zh-rCN/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-zh-rCN/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-zh-rCN/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-zh-rCN/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-zh-rHK/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-zh-rHK/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-zh-rHK/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-zh-rHK/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-zh-rTW/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-zh-rTW/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-zh-rTW/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-zh-rTW/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-zu/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-zu/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-zu/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-zu/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
similarity index 99%
rename from wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
rename to wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
index dce3876..85a9927 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
+++ b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
@@ -23,7 +23,6 @@
import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
import android.support.wearable.complications.ComplicationText.TimeFormatBuilder
import androidx.test.core.app.ApplicationProvider
-import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
import com.google.common.truth.Truth
import org.junit.Assert
import org.junit.Assert.assertThrows
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTemplateTest.kt b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationTextTemplateTest.kt
similarity index 98%
rename from wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTemplateTest.kt
rename to wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationTextTemplateTest.kt
index 1ceb670..9853608 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTemplateTest.kt
+++ b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationTextTemplateTest.kt
@@ -20,7 +20,6 @@
import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
import android.support.wearable.complications.ComplicationText.TimeFormatBuilder
import androidx.test.core.app.ApplicationProvider
-import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
import com.google.common.truth.Truth
import org.junit.Assert
import org.junit.Test
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
similarity index 99%
rename from wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
rename to wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
index 2a85fef..4e662a5 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
+++ b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
@@ -21,7 +21,6 @@
import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
import android.support.wearable.complications.ComplicationText.TimeFormatBuilder
import androidx.test.core.app.ApplicationProvider
-import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
import com.google.common.truth.Truth
import org.junit.Assert
import org.junit.Test
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/Parcelables.kt b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/Parcelables.kt
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/Parcelables.kt
rename to wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/Parcelables.kt
diff --git a/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/SharedRobolectricTestRunner.kt b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/SharedRobolectricTestRunner.kt
new file mode 100644
index 0000000..17a5ffd
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/SharedRobolectricTestRunner.kt
@@ -0,0 +1,32 @@
+/*
+ * 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 android.support.wearable.complications
+
+import org.junit.runners.model.FrameworkMethod
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.internal.bytecode.InstrumentationConfiguration
+
+/** A test runner for all tests within this package. */
+public class SharedRobolectricTestRunner(private val testClass: Class<*>) :
+ RobolectricTestRunner(testClass) {
+
+ override fun createClassLoaderConfig(method: FrameworkMethod?): InstrumentationConfiguration =
+ InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method)).apply {
+ doNotInstrumentPackage("androidx.wear")
+ doNotInstrumentPackage("android.support.wearable")
+ }.build()
+}
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/TimeDifferenceTextTest.kt b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/TimeDifferenceTextTest.kt
similarity index 99%
rename from wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/TimeDifferenceTextTest.kt
rename to wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/TimeDifferenceTextTest.kt
index ef1d1a0..fae2cc6 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/TimeDifferenceTextTest.kt
+++ b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/TimeDifferenceTextTest.kt
@@ -18,7 +18,6 @@
import android.content.Context
import androidx.test.core.app.ApplicationProvider
-import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
import com.google.common.truth.Truth
import org.junit.Assert
import org.junit.Test
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/TimeFormatTextTest.kt b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/TimeFormatTextTest.kt
similarity index 99%
rename from wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/TimeFormatTextTest.kt
rename to wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/TimeFormatTextTest.kt
index 16ec776..cc83239 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/TimeFormatTextTest.kt
+++ b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/TimeFormatTextTest.kt
@@ -18,7 +18,6 @@
import android.content.Context
import androidx.test.core.app.ApplicationProvider
-import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
import com.google.common.truth.Truth
import org.junit.Assert
import org.junit.Test
diff --git a/wear/watchface/watchface-complications-data-core/src/test/resources/robolectric.properties b/wear/watchface/watchface-complications-data-core/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..ce87047
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# Robolectric currently doesn't support API 30, so we have to explicitly specify 29 as the target
+# sdk for now. Remove when no longer necessary.
+sdk=29
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
index 0c324d5..a246b2c 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
@@ -27,7 +27,6 @@
import android.support.wearable.complications.ComplicationProviderInfo
import android.support.wearable.complications.IComplicationManager
import android.support.wearable.complications.IComplicationProvider
-import androidx.annotation.RestrictTo
import androidx.annotation.UiThread
import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.ComplicationType
@@ -573,12 +572,9 @@
* complication data source chooser interface. If set to "true", users will not be able
* to select this complication data source. The complication data source may still be
* specified as a default complication data source by watch faces.
- *
- * @hide
*/
// TODO(b/192233205): Migrate value to androidx.
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- public const val METADATA_KEY_HIDDEN: String =
+ internal const val METADATA_KEY_HIDDEN: String =
"android.support.wearable.complications.HIDDEN"
/**
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
index 37a96a6..268dfc0 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
@@ -130,8 +130,8 @@
internal fun asWireComplicationData(): WireComplicationData {
val wireTimelineEntries = timelineEntries.map { timelineEntry ->
timelineEntry.complicationData.asWireComplicationData().apply {
- timelineStartInstant = timelineEntry.validity.start
- timelineEndInstant = timelineEntry.validity.end
+ timelineStartEpochSecond = timelineEntry.validity.start.epochSecond
+ timelineEndEpochSecond = timelineEntry.validity.end.epochSecond
}
}
return defaultComplicationData.asWireComplicationData().apply {
diff --git a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/ComplicationDataSourceServiceTest.java b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/ComplicationDataSourceServiceTest.java
index 9e3e54a..aefa3c6 100644
--- a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/ComplicationDataSourceServiceTest.java
+++ b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/ComplicationDataSourceServiceTest.java
@@ -331,22 +331,14 @@
data.getValue().getTimelineEntries();
assertThat(timeLineEntries).isNotNull();
assertThat(timeLineEntries.size()).isEqualTo(2);
- assertThat(timeLineEntries.get(0).getTimelineStartInstant()).isEqualTo(
- Instant.ofEpochSecond(1000)
- );
- assertThat(timeLineEntries.get(0).getTimelineEndInstant()).isEqualTo(
- Instant.ofEpochSecond(4000)
- );
+ assertThat(timeLineEntries.get(0).getTimelineStartEpochSecond()).isEqualTo(1000);
+ assertThat(timeLineEntries.get(0).getTimelineEndEpochSecond()).isEqualTo(4000);
assertThat(timeLineEntries.get(0).getLongText().getTextAt(null, 0)).isEqualTo(
"A"
);
- assertThat(timeLineEntries.get(1).getTimelineStartInstant()).isEqualTo(
- Instant.ofEpochSecond(6000)
- );
- assertThat(timeLineEntries.get(1).getTimelineEndInstant()).isEqualTo(
- Instant.ofEpochSecond(8000)
- );
+ assertThat(timeLineEntries.get(1).getTimelineStartEpochSecond()).isEqualTo(6000);
+ assertThat(timeLineEntries.get(1).getTimelineEndEpochSecond()).isEqualTo(8000);
assertThat(timeLineEntries.get(1).getLongText().getTextAt(null, 0)).isEqualTo(
"B"
);
diff --git a/wear/watchface/watchface-complications-data/build.gradle b/wear/watchface/watchface-complications-data/build.gradle
index d2e255d..1266b0f 100644
--- a/wear/watchface/watchface-complications-data/build.gradle
+++ b/wear/watchface/watchface-complications-data/build.gradle
@@ -14,10 +14,7 @@
* limitations under the License.
*/
-import androidx.build.RunApiTasks
-
import androidx.build.LibraryGroups
-import androidx.build.LibraryVersions
import androidx.build.Publish
plugins {
@@ -30,6 +27,7 @@
api("androidx.annotation:annotation:1.1.0")
api(libs.kotlinStdlib)
api(libs.kotlinCoroutinesAndroid)
+ api(project(":wear:watchface:watchface-complications-data-core"))
implementation("androidx.core:core:1.1.0")
implementation("androidx.preference:preference:1.1.0")
implementation("androidx.annotation:annotation:1.2.0")
@@ -43,15 +41,9 @@
}
android {
- buildFeatures {
- aidl = true
- }
defaultConfig {
minSdkVersion 26
}
- buildTypes.all {
- consumerProguardFiles "proguard-rules.pro"
- }
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
index 4f6ed04..ab7ecb0 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
@@ -57,7 +57,7 @@
*
* @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
public fun fromWireType(wireType: Int): ComplicationType =
when (wireType) {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index 8cbc383..b1828b1 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -688,8 +688,8 @@
timelineEntries?.let {
for (entry in it) {
val wireEntry = entry.asWireComplicationData()
- val start = wireEntry.timelineStartInstant?.epochSecond
- val end = wireEntry.timelineEndInstant?.epochSecond
+ val start = wireEntry.timelineStartEpochSecond
+ val end = wireEntry.timelineEndEpochSecond
if (start != null && end != null && time >= start && time < end) {
val duration = end - start
if (duration < previousShortest) {
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 6e57671..04d6be7 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -2620,8 +2620,8 @@
val b = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("B"))
.build()
- b.timelineStartInstant = Instant.ofEpochSecond(1000)
- b.timelineEndInstant = Instant.MAX
+ b.timelineStartEpochSecond = 1000
+ b.timelineEndEpochSecond = Long.MAX_VALUE
a.setTimelineEntryCollection(listOf(b))
// Set the ComplicationData.
@@ -3546,6 +3546,10 @@
@Test
public fun additionalContentDescriptionLabelsSetBeforeWatchFaceInitComplete() {
+ val pendingIntent = PendingIntent.getActivity(context, 0, Intent("Example"),
+ PendingIntent.FLAG_IMMUTABLE
+ )
+
testWatchFaceService = TestWatchFaceService(
WatchFaceType.ANALOG,
emptyList(),
@@ -3564,7 +3568,7 @@
ContentDescriptionLabel(
PlainComplicationText.Builder("Example").build(),
Rect(10, 10, 20, 20),
- null
+ pendingIntent
)
)
)
@@ -3624,6 +3628,8 @@
0
)
).isEqualTo("Example")
+
+ assertThat(engineWrapper.contentDescriptionLabels[1].tapAction).isEqualTo(pendingIntent)
}
@Test
@@ -4170,14 +4176,14 @@
val b = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("B"))
.build()
- b.timelineStartInstant = Instant.ofEpochSecond(1000)
- b.timelineEndInstant = Instant.ofEpochSecond(4000)
+ b.timelineStartEpochSecond = 1000
+ b.timelineEndEpochSecond = 4000
val c = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("C"))
.build()
- c.timelineStartInstant = Instant.ofEpochSecond(2000)
- c.timelineEndInstant = Instant.ofEpochSecond(3000)
+ c.timelineStartEpochSecond = 2000
+ c.timelineEndEpochSecond = 3000
a.setTimelineEntryCollection(listOf(b, c))
@@ -4222,14 +4228,14 @@
val b = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("B"))
.build()
- b.setTimelineStartInstant(Instant.ofEpochSecond(1000))
- b.setTimelineEndInstant(Instant.ofEpochSecond(2000))
+ b.timelineStartEpochSecond = 1000
+ b.timelineEndEpochSecond = 2000
val c = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("C"))
.build()
- c.setTimelineStartInstant(Instant.ofEpochSecond(3000))
- c.setTimelineEndInstant(Instant.ofEpochSecond(4000))
+ c.timelineStartEpochSecond = 3000
+ c.timelineEndEpochSecond = 4000
a.setTimelineEntryCollection(listOf(b, c))
diff --git a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
index 9dc5761..47c4577 100644
--- a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
+++ b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
@@ -22,7 +22,6 @@
import android.os.Parcel
import android.os.ResultReceiver
import androidx.annotation.IntDef
-import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.concurrent.futures.CallbackToFutureAdapter
import androidx.wear.remote.interactions.RemoteInteractionsUtil.isCurrentDeviceAWatch
@@ -283,13 +282,10 @@
/**
* Result code passed to [ResultReceiver.send] for the status of remote intent.
- *
- * @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY)
@IntDef(RESULT_OK, RESULT_FAILED)
@Retention(AnnotationRetention.SOURCE)
- public annotation class SendResult
+ internal annotation class SendResult
public class RemoteIntentException(message: String) : Exception(message)
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
index d9ebc25..48d2490 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
@@ -76,7 +76,6 @@
import androidx.work.worker.EchoingWorker;
import androidx.work.worker.ExceptionWorker;
import androidx.work.worker.FailureWorker;
-import androidx.work.worker.InfiniteTestWorker;
import androidx.work.worker.InterruptionAwareWorker;
import androidx.work.worker.LatchWorker;
import androidx.work.worker.RetryWorker;
@@ -100,6 +99,7 @@
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@@ -116,6 +116,7 @@
private ProgressUpdater mMockProgressUpdater;
private ForegroundUpdater mMockForegroundUpdater;
private Executor mSynchronousExecutor = new SynchronousExecutor();
+ private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
@Before
public void setUp() {
@@ -135,6 +136,12 @@
@After
public void tearDown() {
+ mExecutorService.shutdown();
+ try {
+ assertThat(mExecutorService.awaitTermination(3, TimeUnit.SECONDS), is(true));
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
mDatabase.close();
}
@@ -328,7 +335,7 @@
insertWork(work);
WorkerWrapper wrapper = createBuilder(work.getStringId()).build();
FutureListener listener = createAndAddFutureListener(wrapper);
- Executors.newSingleThreadExecutor().submit(wrapper);
+ mExecutorService.submit(wrapper);
Thread.sleep(2000L); // Async wait duration.
assertThat(mWorkSpecDao.getState(work.getStringId()), is(RUNNING));
Thread.sleep(SleepTestWorker.SLEEP_DURATION);
@@ -974,7 +981,7 @@
.withSchedulers(Collections.singletonList(mMockScheduler))
.build();
FutureListener listener = createAndAddFutureListener(workerWrapper);
- Executors.newSingleThreadExecutor().submit(workerWrapper);
+ mExecutorService.submit(workerWrapper);
workerWrapper.interrupt();
Thread.sleep(1000L);
assertThat(listener.mResult, is(true));
@@ -992,7 +999,7 @@
.withSchedulers(Collections.singletonList(mMockScheduler))
.build();
FutureListener listener = createAndAddFutureListener(workerWrapper);
- Executors.newSingleThreadExecutor().submit(workerWrapper);
+ mExecutorService.submit(workerWrapper);
Thread.sleep(200);
workerWrapper.interrupt();
Thread.sleep(1000L);
@@ -1007,6 +1014,7 @@
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(LatchWorker.class).build();
insertWork(work);
+ ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor();
LatchWorker latchWorker =
(LatchWorker) mConfiguration.getWorkerFactory().createWorkerWithDefaultFallback(
mContext.getApplicationContext(),
@@ -1017,7 +1025,7 @@
work.getTags(),
new WorkerParameters.RuntimeExtras(),
1,
- Executors.newSingleThreadExecutor(),
+ backgroundExecutor,
mWorkTaskExecutor,
mConfiguration.getWorkerFactory(),
mMockProgressUpdater,
@@ -1029,7 +1037,7 @@
.withWorker(latchWorker)
.build();
FutureListener listener = createAndAddFutureListener(workerWrapper);
- Executors.newSingleThreadExecutor().submit(workerWrapper);
+ mExecutorService.submit(workerWrapper);
Thread.sleep(1000L);
@@ -1042,6 +1050,8 @@
assertThat(listener.mResult, is(notNullValue()));
verify(mMockScheduler, times(1)).cancel(work.getStringId());
+ backgroundExecutor.shutdown();
+ assertThat(backgroundExecutor.awaitTermination(3, TimeUnit.SECONDS), is(true));
}
@Test
@@ -1073,7 +1083,7 @@
.withSchedulers(Collections.singletonList(mMockScheduler))
.withWorker(worker)
.build();
- Executors.newSingleThreadExecutor().submit(workerWrapper);
+ mExecutorService.submit(workerWrapper);
workerWrapper.interrupt();
assertThat(worker.isStopped(), is(true));
assertThat(mWorkSpecDao.getState(work.getStringId()), is(ENQUEUED));
@@ -1108,7 +1118,7 @@
.withSchedulers(Collections.singletonList(mMockScheduler))
.withWorker(worker)
.build();
- Executors.newSingleThreadExecutor().submit(workerWrapper);
+ mExecutorService.submit(workerWrapper);
workerWrapper.interrupt();
assertThat(worker.isStopped(), is(true));
assertThat(mWorkSpecDao.getState(work.getStringId()), is(ENQUEUED));
@@ -1139,7 +1149,7 @@
.withSchedulers(Collections.singletonList(mMockScheduler))
.build();
FutureListener listener = createAndAddFutureListener(workerWrapper);
- Executors.newSingleThreadExecutor().submit(workerWrapper);
+ mExecutorService.submit(workerWrapper);
mWorkSpecDao.delete(work.getStringId());
Thread.sleep(6000L);
assertThat(listener.mResult, is(false));
@@ -1148,7 +1158,7 @@
@Test
@LargeTest
public void testWorker_getsRunAttemptCount() throws InterruptedException {
- OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(InfiniteTestWorker.class)
+ OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(LatchWorker.class)
.setInitialRunAttemptCount(10)
.build();
insertWork(work);
@@ -1158,9 +1168,10 @@
.withSchedulers(Collections.singletonList(mMockScheduler))
.build();
- Executors.newSingleThreadExecutor().submit(workerWrapper);
+ mExecutorService.submit(workerWrapper);
Thread.sleep(1000L);
assertThat(workerWrapper.mWorker.getRunAttemptCount(), is(10));
+ ((LatchWorker) workerWrapper.mWorker).mLatch.countDown();
}
@Test
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
index 1ded228..d872142 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
@@ -49,10 +49,9 @@
import androidx.work.impl.Processor;
import androidx.work.impl.Scheduler;
import androidx.work.impl.WorkManagerImpl;
-import androidx.work.impl.constraints.trackers.BatteryChargingTracker;
+import androidx.work.impl.constraints.NetworkState;
import androidx.work.impl.constraints.trackers.BatteryNotLowTracker;
-import androidx.work.impl.constraints.trackers.NetworkStateTracker;
-import androidx.work.impl.constraints.trackers.StorageNotLowTracker;
+import androidx.work.impl.constraints.trackers.ConstraintTracker;
import androidx.work.impl.constraints.trackers.Trackers;
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.model.WorkSpecDao;
@@ -93,67 +92,74 @@
private static final int TEST_TIMEOUT = 6;
private Context mContext;
- private Scheduler mScheduler;
private WorkManagerImpl mWorkManager;
- private Configuration mConfiguration;
- private Processor mProcessor;
private Processor mSpyProcessor;
private CommandInterceptingSystemDispatcher mDispatcher;
private CommandInterceptingSystemDispatcher mSpyDispatcher;
- private SystemAlarmDispatcher.CommandsCompletedListener mCompletedListener;
private CountDownLatch mLatch;
- private Trackers mTracker;
- private BatteryChargingTracker mBatteryChargingTracker;
- private BatteryNotLowTracker mBatteryNotLowTracker;
- private NetworkStateTracker mNetworkStateTracker;
- private StorageNotLowTracker mStorageNotLowTracker;
+ private FakeConstraintTracker mBatteryChargingTracker;
+ private FakeConstraintTracker mStorageNotLowTracker;
@Before
+ @SuppressWarnings("unchecked")
public void setUp() {
mContext = ApplicationProvider.getApplicationContext().getApplicationContext();
- mScheduler = mock(Scheduler.class);
+ Scheduler scheduler = mock(Scheduler.class);
mWorkManager = mock(WorkManagerImpl.class);
mLatch = new CountDownLatch(1);
- mCompletedListener = new SystemAlarmDispatcher.CommandsCompletedListener() {
- @Override
- public void onAllCommandsCompleted() {
- mLatch.countDown();
- }
- };
- mTracker = mock(Trackers.class);
+ SystemAlarmDispatcher.CommandsCompletedListener completedListener =
+ new SystemAlarmDispatcher.CommandsCompletedListener() {
+ @Override
+ public void onAllCommandsCompleted() {
+ mLatch.countDown();
+ }
+ };
+
+ TaskExecutor instantTaskExecutor = new InstantWorkTaskExecutor();
+ mBatteryChargingTracker = new FakeConstraintTracker(mContext, instantTaskExecutor);
+ BatteryNotLowTracker batteryNotLowTracker =
+ new BatteryNotLowTracker(mContext, instantTaskExecutor);
+ // Requires API 24+ types.
+ ConstraintTracker<NetworkState> networkStateTracker =
+ new ConstraintTracker<NetworkState>(mContext, instantTaskExecutor) {
+ @Override
+ public NetworkState getInitialState() {
+ return new NetworkState(true, true, true, true);
+ }
+
+ @Override
+ public void startTracking() {
+ }
+
+ @Override
+ public void stopTracking() {
+ }
+ };
+ mStorageNotLowTracker = new FakeConstraintTracker(mContext, instantTaskExecutor);
+ Trackers trackers = new Trackers(mContext, instantTaskExecutor,
+ mBatteryChargingTracker, batteryNotLowTracker, networkStateTracker,
+ mStorageNotLowTracker);
Logger.setLogger(new Logger.LogcatLogger(Log.DEBUG));
- mConfiguration = new Configuration.Builder()
+ Configuration configuration = new Configuration.Builder()
.setExecutor(new SynchronousExecutor())
.build();
when(mWorkManager.getWorkDatabase()).thenReturn(mDatabase);
- when(mWorkManager.getConfiguration()).thenReturn(mConfiguration);
- TaskExecutor instantTaskExecutor = new InstantWorkTaskExecutor();
+ when(mWorkManager.getConfiguration()).thenReturn(configuration);
when(mWorkManager.getWorkTaskExecutor()).thenReturn(instantTaskExecutor);
- when(mWorkManager.getTrackers()).thenReturn(mTracker);
- mProcessor = new Processor(
+ when(mWorkManager.getTrackers()).thenReturn(trackers);
+ Processor processor = new Processor(
mContext,
- mConfiguration,
+ configuration,
instantTaskExecutor,
mDatabase,
- Collections.singletonList(mScheduler));
- mSpyProcessor = spy(mProcessor);
+ Collections.singletonList(scheduler));
+ mSpyProcessor = spy(processor);
mDispatcher =
new CommandInterceptingSystemDispatcher(mContext, mSpyProcessor, mWorkManager);
- mDispatcher.setCompletedListener(mCompletedListener);
+ mDispatcher.setCompletedListener(completedListener);
mSpyDispatcher = spy(mDispatcher);
-
- mBatteryChargingTracker = spy(new BatteryChargingTracker(mContext, instantTaskExecutor));
- mBatteryNotLowTracker = spy(new BatteryNotLowTracker(mContext, instantTaskExecutor));
- // Requires API 24+ types.
- mNetworkStateTracker = mock(NetworkStateTracker.class);
- mStorageNotLowTracker = spy(new StorageNotLowTracker(mContext, instantTaskExecutor));
-
- when(mTracker.getBatteryChargingTracker()).thenReturn(mBatteryChargingTracker);
- when(mTracker.getBatteryNotLowTracker()).thenReturn(mBatteryNotLowTracker);
- when(mTracker.getNetworkStateTracker()).thenReturn(mNetworkStateTracker);
- when(mTracker.getStorageNotLowTracker()).thenReturn(mStorageNotLowTracker);
}
@After
@@ -319,7 +325,7 @@
@Test
public void testSchedule_withConstraints() throws InterruptedException {
- when(mBatteryChargingTracker.getInitialState()).thenReturn(true);
+ mBatteryChargingTracker.setInitialState(true);
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(
System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1),
@@ -383,7 +389,7 @@
@LargeTest
@RepeatRule.Repeat(times = 1)
public void testDelayMet_withUnMetConstraint() throws InterruptedException {
- when(mBatteryChargingTracker.getInitialState()).thenReturn(false);
+ // fake BatteryCharging tracker says by default that it is not charging
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setConstraints(new Constraints.Builder()
@@ -420,12 +426,12 @@
@LargeTest
@RepeatRule.Repeat(times = 1)
public void testDelayMet_withPartiallyMetConstraint() throws InterruptedException {
- when(mStorageNotLowTracker.getInitialState()).thenReturn(true);
- when(mBatteryChargingTracker.getInitialState()).thenReturn(false);
+ mStorageNotLowTracker.setInitialState(true);
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setConstraints(new Constraints.Builder()
.setRequiresStorageNotLow(true)
+ // fake BatteryCharging tracker says by default that it is not charging
.setRequiresCharging(true)
.build())
.build();
@@ -457,7 +463,7 @@
@Test
public void testConstraintsChanged_withConstraint() throws InterruptedException {
- when(mBatteryChargingTracker.getInitialState()).thenReturn(true);
+ mBatteryChargingTracker.setInitialState(true);
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setConstraints(new Constraints.Builder()
@@ -475,7 +481,7 @@
@Test
public void testDelayMet_withMetConstraint() throws InterruptedException {
- when(mBatteryChargingTracker.getInitialState()).thenReturn(true);
+ mBatteryChargingTracker.setInitialState(true);
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setConstraints(new Constraints.Builder()
@@ -565,7 +571,7 @@
@Test
public void testConstraintsChanged_withFutureWork() throws InterruptedException {
- when(mBatteryChargingTracker.getInitialState()).thenReturn(true);
+ mBatteryChargingTracker.setInitialState(true);
// Use a mocked scheduler in this test.
Scheduler scheduler = mock(Scheduler.class);
doCallRealMethod().when(mWorkManager).rescheduleEligibleWork();
@@ -654,10 +660,10 @@
@RepeatRule.Repeat(times = 1)
public void testDelayMet_withUnMetConstraintShouldNotCrashOnDestroy()
throws InterruptedException {
- when(mBatteryChargingTracker.getInitialState()).thenReturn(false);
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setConstraints(new Constraints.Builder()
+ // fake BatteryCharging tracker says by default that it is not charging
.setRequiresCharging(true)
.build())
.build();
@@ -720,4 +726,30 @@
mCommands.add(intent);
}
}
+
+ private static final class FakeConstraintTracker extends ConstraintTracker<Boolean> {
+ private boolean mInitialState = false;
+
+ FakeConstraintTracker(@NonNull Context context,
+ @NonNull TaskExecutor taskExecutor) {
+ super(context, taskExecutor);
+ }
+
+ private void setInitialState(boolean initialState) {
+ mInitialState = initialState;
+ }
+
+ @Override
+ public Boolean getInitialState() {
+ return mInitialState;
+ }
+
+ @Override
+ public void startTracking() {
+ }
+
+ @Override
+ public void stopTracking() {
+ }
+ }
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.kt
index 540925e..2d0d124 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.kt
@@ -63,7 +63,7 @@
workConstraintsTracker.replace(TEST_WORKSPECS)
val (_, constrained) = capturingCallback.consumeCurrent()
assertThat(constrained).isEqualTo(TEST_WORKSPEC_IDS)
- tracker.setState(true)
+ tracker.state = true
val (unconstrained, _) = capturingCallback.consumeCurrent()
assertThat(unconstrained).isEqualTo(TEST_WORKSPEC_IDS)
}
@@ -80,7 +80,7 @@
)
workConstraintsTracker.replace(TEST_WORKSPECS)
capturingCallback.consumeCurrent()
- tracker1.setState(true)
+ tracker1.state = true
val (unconstrained, _) = capturingCallback.consumeCurrent()
assertThat(unconstrained).containsExactly(TEST_WORKSPEC_IDS[0], TEST_WORKSPEC_IDS[1])
}
@@ -92,7 +92,7 @@
val workConstraintsTracker = WorkConstraintsTracker(capturingCallback, tracker1, tracker2)
workConstraintsTracker.replace(TEST_WORKSPECS)
capturingCallback.consumeCurrent()
- tracker1.setState(true)
+ tracker1.state = true
val (unconstrained, _) = capturingCallback.consumeCurrent()
// only one constraint is resolved, so unconstrained is empty list
assertThat(unconstrained).isEqualTo(emptyList<String>())
@@ -106,7 +106,7 @@
workConstraintsTracker.replace(TEST_WORKSPECS)
val (unconstrained, _) = capturingCallback.consumeCurrent()
assertThat(unconstrained).isEqualTo(TEST_WORKSPEC_IDS)
- tracker1.setState(false)
+ tracker1.state = false
val (_, constrained) = capturingCallback.consumeCurrent()
assertThat(constrained).isEqualTo(TEST_WORKSPEC_IDS)
}
@@ -128,12 +128,11 @@
}
private class TestConstraintTracker(
- val initialState: Boolean = false,
+ override val initialState: Boolean = false,
context: Context = ApplicationProvider.getApplicationContext(),
taskExecutor: TaskExecutor = InstantWorkTaskExecutor(),
) : ConstraintTracker<Boolean>(context, taskExecutor) {
var isTracking = false
- override fun getInitialState() = initialState
override fun startTracking() {
isTracking = true
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java
index ffaa451..6b787c8 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java
@@ -19,12 +19,14 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
@@ -33,6 +35,7 @@
import androidx.work.WorkManagerTest;
import androidx.work.impl.constraints.trackers.ConstraintTracker;
import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor;
import androidx.work.worker.TestWorker;
import org.junit.Before;
@@ -46,14 +49,16 @@
@SdkSuppress(minSdkVersion = 23)
public class ConstraintControllerTest extends WorkManagerTest {
private TestDeviceIdleConstraintController mTestIdleController;
+
@SuppressWarnings("unchecked")
- private ConstraintTracker<Boolean> mMockTracker = mock(ConstraintTracker.class);
+ private final FakeConstraintTracker mTracker = new FakeConstraintTracker();
+
private ConstraintController.OnConstraintUpdatedCallback mCallback =
mock(ConstraintController.OnConstraintUpdatedCallback.class);
@Before
public void setUp() {
- mTestIdleController = new TestDeviceIdleConstraintController(mMockTracker);
+ mTestIdleController = new TestDeviceIdleConstraintController(mTracker);
mTestIdleController.setCallback(mCallback);
}
@@ -78,8 +83,8 @@
@Test
@SmallTest
public void testReplace_empty() {
- mTestIdleController.replace(Collections.<WorkSpec>emptyList());
- verify(mMockTracker).removeListener(mTestIdleController);
+ mTestIdleController.replace(Collections.emptyList());
+ assertThat(mTracker.mTracking, is(false));
verifyZeroInteractions(mCallback);
}
@@ -89,7 +94,7 @@
WorkSpec workSpecNoConstraints = createNoConstraintWorkSpec();
List<WorkSpec> workSpecs = Collections.singletonList(workSpecNoConstraints);
mTestIdleController.replace(workSpecs);
- verify(mMockTracker).removeListener(mTestIdleController);
+ assertThat(mTracker.mTracking, is(false));
verifyZeroInteractions(mCallback);
}
@@ -100,10 +105,10 @@
List<String> expectedWorkIds = Collections.singletonList(workSpecWithConstraint.id);
List<WorkSpec> workSpecs = Collections.singletonList(workSpecWithConstraint);
- mTestIdleController.setDeviceActive();
+ mTracker.setDeviceActive();
mTestIdleController.replace(workSpecs);
- verify(mMockTracker).addListener(mTestIdleController);
- verify(mCallback).onConstraintNotMet(eq(expectedWorkIds));
+ assertThat(mTracker.mTracking, is(true));
+ verify(mCallback, atLeastOnce()).onConstraintNotMet(eq(expectedWorkIds));
}
@Test
@@ -113,10 +118,13 @@
List<String> expectedWorkIds = Collections.singletonList(workSpecWithConstraint.id);
List<WorkSpec> workSpecs = Collections.singletonList(workSpecWithConstraint);
- mTestIdleController.setDeviceIdle();
+ mTracker.setDeviceIdle();
mTestIdleController.replace(workSpecs);
- verify(mMockTracker).addListener(mTestIdleController);
- verify(mCallback).onConstraintMet(eq(expectedWorkIds));
+ assertThat(mTracker.mTracking, is(true));
+ // called twice: replace calls updateCallback explicitly and
+ // tracker.addListener results in updateCallback too
+ // probably should be fixed eventually
+ verify(mCallback, atLeastOnce()).onConstraintMet(eq(expectedWorkIds));
}
@Test
@@ -127,14 +135,17 @@
List<WorkSpec> workSpecs = Collections.singletonList(workSpecWithConstraint);
mTestIdleController.replace(workSpecs);
- verify(mCallback).onConstraintNotMet(expectedWorkIds);
+ // called twice: replace calls updateCallback explictily and
+ // tracker.addListener results in updateCallback too
+ // probably should be fixed eventually
+ verify(mCallback, atLeastOnce()).onConstraintNotMet(expectedWorkIds);
}
@Test
@SmallTest
public void testReset_alreadyNoMatchingWorkSpecs() {
mTestIdleController.reset();
- verifyZeroInteractions(mMockTracker);
+ assertThat(mTracker.mTracking, is(false));
}
@Test
@@ -145,7 +156,7 @@
mTestIdleController.replace(workSpecs);
mTestIdleController.reset();
- verify(mMockTracker).removeListener(mTestIdleController);
+ assertThat(mTracker.mTracking, is(false));
}
@Test
@@ -162,11 +173,11 @@
List<String> expectedWorkIds = Collections.singletonList(workSpecWithConstraint.id);
List<WorkSpec> workSpecs = Collections.singletonList(workSpecWithConstraint);
mTestIdleController.replace(workSpecs);
- verify(mCallback).onConstraintNotMet(expectedWorkIds);
+ verify(mCallback, times(2)).onConstraintNotMet(expectedWorkIds);
final boolean deviceIdle = false;
mTestIdleController.onConstraintChanged(deviceIdle);
- verify(mCallback, times(2)).onConstraintNotMet(expectedWorkIds);
+ verify(mCallback, times(3)).onConstraintNotMet(expectedWorkIds);
}
@Test
@@ -193,17 +204,8 @@
@Test
@SmallTest
- public void testIsWorkSpecConstrained_constraintNotSet() {
- WorkSpec workSpecWithConstraint = createTestConstraintWorkSpec();
- mTestIdleController.replace(Collections.singletonList(workSpecWithConstraint));
- assertThat(mTestIdleController.isWorkSpecConstrained(workSpecWithConstraint.id),
- is(false));
- }
-
- @Test
- @SmallTest
public void testIsWorkSpecConstrained_constrained_withMatchingWorkSpecs() {
- mTestIdleController.setDeviceActive();
+ mTracker.setDeviceActive();
WorkSpec workSpecWithConstraint = createTestConstraintWorkSpec();
mTestIdleController.replace(Collections.singletonList(workSpecWithConstraint));
@@ -214,7 +216,7 @@
@Test
@SmallTest
public void testIsWorkSpecConstrained_constrained_noMatchingWorkSpecs() {
- mTestIdleController.setDeviceActive();
+ mTracker.setDeviceActive();
WorkSpec workSpecNoConstraints = createNoConstraintWorkSpec();
mTestIdleController.replace(Collections.singletonList(workSpecNoConstraints));
@@ -225,7 +227,7 @@
@Test
@SmallTest
public void testIsWorkSpecConstrained_unconstrained_withMatchingWorkSpecs() {
- mTestIdleController.setDeviceIdle();
+ mTracker.setDeviceIdle();
WorkSpec workSpecWithConstraint = createTestConstraintWorkSpec();
mTestIdleController.replace(Collections.singletonList(workSpecWithConstraint));
@@ -236,7 +238,7 @@
@Test
@SmallTest
public void testIsWorkSpecConstrained_unconstrained_noMatchingWorkSpecs() {
- mTestIdleController.setDeviceIdle();
+ mTracker.setDeviceIdle();
WorkSpec workSpecNoConstraints = createNoConstraintWorkSpec();
mTestIdleController.replace(Collections.singletonList(workSpecNoConstraints));
@@ -258,13 +260,37 @@
public boolean isConstrained(@NonNull Boolean isDeviceIdle) {
return !isDeviceIdle;
}
+ }
+
+ private static class FakeConstraintTracker extends ConstraintTracker<Boolean> {
+ public boolean mTracking;
+ private boolean mInitialState;
+
+ protected FakeConstraintTracker() {
+ super(ApplicationProvider.getApplicationContext(), new InstantWorkTaskExecutor());
+ }
+
+ @Override
+ public Boolean getInitialState() {
+ return mInitialState;
+ }
+
+ @Override
+ public void startTracking() {
+ mTracking = true;
+ }
+
+ @Override
+ public void stopTracking() {
+ mTracking = false;
+ }
void setDeviceActive() {
- onConstraintChanged(false);
+ mInitialState = false;
}
void setDeviceIdle() {
- onConstraintChanged(true);
+ mInitialState = true;
}
}
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryChargingTrackerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryChargingTrackerTest.java
index 6ac53c2..958e2ff 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryChargingTrackerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryChargingTrackerTest.java
@@ -33,7 +33,6 @@
import android.os.Build;
import androidx.annotation.RequiresApi;
-import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
@@ -137,9 +136,7 @@
mTracker.addListener(mListener);
verify(mListener).onConstraintChanged(true);
- mTracker.onBroadcastReceive(
- ApplicationProvider.getApplicationContext(),
- new Intent("INVALID"));
+ mTracker.onBroadcastReceive(new Intent("INVALID"));
verifyNoMoreInteractions(mListener);
}
@@ -151,9 +148,9 @@
mTracker.addListener(mListener);
verify(mListener).onConstraintChanged(false);
- mTracker.onBroadcastReceive(mMockContext, createChargingIntent(true));
+ mTracker.onBroadcastReceive(createChargingIntent(true));
verify(mListener).onConstraintChanged(true);
- mTracker.onBroadcastReceive(mMockContext, createChargingIntent(false));
+ mTracker.onBroadcastReceive(createChargingIntent(false));
verify(mListener, times(2)).onConstraintChanged(false);
}
@@ -165,9 +162,9 @@
mTracker.addListener(mListener);
verify(mListener).onConstraintChanged(false);
- mTracker.onBroadcastReceive(mMockContext, createChargingIntent_afterApi23(true));
+ mTracker.onBroadcastReceive(createChargingIntent_afterApi23(true));
verify(mListener).onConstraintChanged(true);
- mTracker.onBroadcastReceive(mMockContext, createChargingIntent_afterApi23(false));
+ mTracker.onBroadcastReceive(createChargingIntent_afterApi23(false));
// onConstraintChanged was called once more, in total, twice
verify(mListener, times(2)).onConstraintChanged(false);
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryNotLowTrackerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryNotLowTrackerTest.java
index d0c6333..cbf1e15 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryNotLowTrackerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryNotLowTrackerTest.java
@@ -46,7 +46,7 @@
private static final int STATUS_CHARGING = BatteryManager.BATTERY_STATUS_CHARGING;
private static final int UNKNOWN_STATUS = BatteryManager.BATTERY_STATUS_UNKNOWN;
- private static final float BELOW_THRESHOLD = BatteryNotLowTracker.BATTERY_LOW_THRESHOLD;
+ private static final float BELOW_THRESHOLD = BatteryNotLowTrackerKt.BATTERY_LOW_THRESHOLD;
private static final float ABOVE_THRESHOLD = BELOW_THRESHOLD + 0.01f;
private Context mMockContext;
@@ -132,7 +132,7 @@
mTracker.addListener(mListener);
verify(mListener).onConstraintChanged(true);
- mTracker.onBroadcastReceive(mMockContext, new Intent("INVALID"));
+ mTracker.onBroadcastReceive(new Intent("INVALID"));
verifyNoMoreInteractions(mListener);
}
@@ -144,9 +144,9 @@
mTracker.addListener(mListener);
verify(mListener).onConstraintChanged(false);
- mTracker.onBroadcastReceive(mMockContext, new Intent(Intent.ACTION_BATTERY_OKAY));
+ mTracker.onBroadcastReceive(new Intent(Intent.ACTION_BATTERY_OKAY));
verify(mListener).onConstraintChanged(true);
- mTracker.onBroadcastReceive(mMockContext, new Intent(Intent.ACTION_BATTERY_LOW));
+ mTracker.onBroadcastReceive(new Intent(Intent.ACTION_BATTERY_LOW));
verify(mListener, times(2)).onConstraintChanged(false);
}
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/NetworkStateTrackerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/NetworkStateTrackerTest.java
index 0313e43..c61c213 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/NetworkStateTrackerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/NetworkStateTrackerTest.java
@@ -15,6 +15,9 @@
*/
package androidx.work.impl.constraints.trackers;
+import static androidx.work.impl.constraints.trackers.NetworkStateTrackerKt.NetworkStateTracker;
+import static androidx.work.impl.constraints.trackers.NetworkStateTrackerKt.isActiveNetworkValidated;
+
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -44,7 +47,7 @@
@RunWith(AndroidJUnit4.class)
public class NetworkStateTrackerTest {
- private NetworkStateTracker mTracker;
+ private ConstraintTracker<NetworkState> mTracker;
private Context mMockContext;
private ConnectivityManager mMockConnectivityManager;
@@ -57,8 +60,7 @@
when(mMockContext.getApplicationContext()).thenReturn(mMockContext);
when(mMockContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE)))
.thenReturn(mMockConnectivityManager);
-
- mTracker = new NetworkStateTracker(mMockContext, new InstantWorkTaskExecutor());
+ mTracker = NetworkStateTracker(mMockContext, new InstantWorkTaskExecutor());
}
@Test
@@ -125,6 +127,6 @@
when(mMockConnectivityManager.getActiveNetwork()).thenReturn(activeNetwork);
when(mMockConnectivityManager.getNetworkCapabilities(activeNetwork))
.thenThrow(new SecurityException("Exception"));
- assertThat(mTracker.isActiveNetworkValidated(), is(false));
+ assertThat(isActiveNetworkValidated(mMockConnectivityManager), is(false));
}
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/StorageNotLowTrackerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/StorageNotLowTrackerTest.java
index 3f71de9..95d0d18 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/StorageNotLowTrackerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/StorageNotLowTrackerTest.java
@@ -98,7 +98,7 @@
mTracker.addListener(mListener);
verify(mListener).onConstraintChanged(true);
- mTracker.onBroadcastReceive(mMockContext, new Intent("INVALID"));
+ mTracker.onBroadcastReceive(new Intent("INVALID"));
verifyNoMoreInteractions(mListener);
}
@@ -109,9 +109,9 @@
mTracker.addListener(mListener);
verify(mListener).onConstraintChanged(false);
- mTracker.onBroadcastReceive(mMockContext, new Intent(Intent.ACTION_DEVICE_STORAGE_OK));
+ mTracker.onBroadcastReceive(new Intent(Intent.ACTION_DEVICE_STORAGE_OK));
verify(mListener).onConstraintChanged(true);
- mTracker.onBroadcastReceive(mMockContext, new Intent(Intent.ACTION_DEVICE_STORAGE_LOW));
+ mTracker.onBroadcastReceive(new Intent(Intent.ACTION_DEVICE_STORAGE_LOW));
// onConstraintChanged was called once more, in total, twice
verify(mListener, times(2)).onConstraintChanged(false);
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
index ae17700..532c261 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
@@ -52,9 +52,10 @@
import androidx.work.impl.Scheduler;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.WorkerWrapper;
+import androidx.work.impl.constraints.NetworkState;
import androidx.work.impl.constraints.trackers.BatteryChargingTracker;
import androidx.work.impl.constraints.trackers.BatteryNotLowTracker;
-import androidx.work.impl.constraints.trackers.NetworkStateTracker;
+import androidx.work.impl.constraints.trackers.ConstraintTracker;
import androidx.work.impl.constraints.trackers.StorageNotLowTracker;
import androidx.work.impl.constraints.trackers.Trackers;
import androidx.work.impl.foreground.ForegroundProcessor;
@@ -108,13 +109,14 @@
private Trackers mTracker;
private BatteryChargingTracker mBatteryChargingTracker;
private BatteryNotLowTracker mBatteryNotLowTracker;
- private NetworkStateTracker mNetworkStateTracker;
+ private ConstraintTracker<NetworkState> mNetworkStateTracker;
private StorageNotLowTracker mStorageNotLowTracker;
@Rule
public final RepeatRule mRepeatRule = new RepeatRule();
@Before
+ @SuppressWarnings("unchecked")
public void setUp() {
mContext = ApplicationProvider.getApplicationContext().getApplicationContext();
mHandlerThread = new HandlerThread("ConstraintTrackingHandler");
@@ -140,7 +142,7 @@
mBatteryChargingTracker = spy(new BatteryChargingTracker(mContext, mWorkTaskExecutor));
mBatteryNotLowTracker = spy(new BatteryNotLowTracker(mContext, mWorkTaskExecutor));
// Requires API 24+ types.
- mNetworkStateTracker = mock(NetworkStateTracker.class);
+ mNetworkStateTracker = mock(ConstraintTracker.class);
mStorageNotLowTracker = spy(new StorageNotLowTracker(mContext, mWorkTaskExecutor));
mTracker = mock(Trackers.class);
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
index 0853d9d..acb17785 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -269,7 +269,7 @@
@NonNull WorkDatabase database) {
Context applicationContext = context.getApplicationContext();
Logger.setLogger(new Logger.LogcatLogger(configuration.getMinimumLoggingLevel()));
- mTrackers = new Trackers(applicationContext, mWorkTaskExecutor);
+ mTrackers = new Trackers(applicationContext, workTaskExecutor);
List<Scheduler> schedulers =
createSchedulers(applicationContext, configuration, mTrackers);
Processor processor = new Processor(
@@ -300,7 +300,7 @@
@NonNull WorkDatabase workDatabase,
@NonNull List<Scheduler> schedulers,
@NonNull Processor processor) {
- mTrackers = new Trackers(context.getApplicationContext(), mWorkTaskExecutor);
+ mTrackers = new Trackers(context.getApplicationContext(), workTaskExecutor);
internalInit(context, configuration, workTaskExecutor, workDatabase, schedulers, processor);
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ContraintControllers.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ContraintControllers.kt
index c72820c..559a5b1 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ContraintControllers.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ContraintControllers.kt
@@ -22,16 +22,14 @@
import androidx.work.NetworkType.TEMPORARILY_UNMETERED
import androidx.work.NetworkType.UNMETERED
import androidx.work.impl.constraints.NetworkState
-import androidx.work.impl.constraints.trackers.BatteryChargingTracker
import androidx.work.impl.constraints.trackers.BatteryNotLowTracker
-import androidx.work.impl.constraints.trackers.NetworkStateTracker
-import androidx.work.impl.constraints.trackers.StorageNotLowTracker
+import androidx.work.impl.constraints.trackers.ConstraintTracker
import androidx.work.impl.model.WorkSpec
/**
* A [ConstraintController] for battery charging events.
*/
-class BatteryChargingController(tracker: BatteryChargingTracker) :
+class BatteryChargingController(tracker: ConstraintTracker<Boolean>) :
ConstraintController<Boolean>(tracker) {
override fun hasConstraint(workSpec: WorkSpec) = workSpec.constraints.requiresCharging()
@@ -51,7 +49,7 @@
/**
* A [ConstraintController] for monitoring that the network connection is unmetered.
*/
-class NetworkUnmeteredController(tracker: NetworkStateTracker) :
+class NetworkUnmeteredController(tracker: ConstraintTracker<NetworkState>) :
ConstraintController<NetworkState>(tracker) {
override fun hasConstraint(workSpec: WorkSpec): Boolean {
val requiredNetworkType = workSpec.constraints.requiredNetworkType
@@ -65,7 +63,7 @@
/**
* A [ConstraintController] for storage not low events.
*/
-class StorageNotLowController(tracker: StorageNotLowTracker) :
+class StorageNotLowController(tracker: ConstraintTracker<Boolean>) :
ConstraintController<Boolean>(tracker) {
override fun hasConstraint(workSpec: WorkSpec) = workSpec.constraints.requiresStorageNotLow()
@@ -76,7 +74,7 @@
/**
* A [ConstraintController] for monitoring that the network connection is not roaming.
*/
-class NetworkNotRoamingController(tracker: NetworkStateTracker) :
+class NetworkNotRoamingController(tracker: ConstraintTracker<NetworkState>) :
ConstraintController<NetworkState>(tracker) {
override fun hasConstraint(workSpec: WorkSpec): Boolean {
return workSpec.constraints.requiredNetworkType == NetworkType.NOT_ROAMING
@@ -111,7 +109,7 @@
*
* For API 25 and below, usable simply means that [NetworkState] is connected.
*/
-class NetworkConnectedController(tracker: NetworkStateTracker) :
+class NetworkConnectedController(tracker: ConstraintTracker<NetworkState>) :
ConstraintController<NetworkState>(tracker) {
override fun hasConstraint(workSpec: WorkSpec) =
workSpec.constraints.requiredNetworkType == NetworkType.CONNECTED
@@ -127,7 +125,7 @@
/**
* A [ConstraintController] for monitoring that the network connection is metered.
*/
-class NetworkMeteredController(tracker: NetworkStateTracker) :
+class NetworkMeteredController(tracker: ConstraintTracker<NetworkState>) :
ConstraintController<NetworkState>(tracker) {
override fun hasConstraint(workSpec: WorkSpec) =
workSpec.constraints.requiredNetworkType == NetworkType.METERED
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.java
deleted file mode 100644
index 1128d24..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2017 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 androidx.work.impl.constraints.trackers;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.BatteryManager;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.work.Logger;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-
-/**
- * Tracks whether or not the device's battery is charging.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class BatteryChargingTracker extends BroadcastReceiverConstraintTracker<Boolean> {
-
- private static final String TAG = Logger.tagWithPrefix("BatteryChrgTracker");
-
- /**
- * Create an instance of {@link BatteryChargingTracker}.
- * @param context The application {@link Context}
- * @param taskExecutor The internal {@link TaskExecutor} being used by WorkManager.
- */
- public BatteryChargingTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
- super(context, taskExecutor);
- }
-
- @Override
- public Boolean getInitialState() {
- // {@link ACTION_CHARGING} and {@link ACTION_DISCHARGING} are not sticky broadcasts, so
- // we use {@link ACTION_BATTERY_CHANGED} on all APIs to get the initial state.
- IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
- Intent intent = mAppContext.registerReceiver(null, intentFilter);
- if (intent == null) {
- Logger.get().error(TAG, "getInitialState - null intent received");
- return false;
- }
- return isBatteryChangedIntentCharging(intent);
- }
-
- @Override
- public IntentFilter getIntentFilter() {
- IntentFilter intentFilter = new IntentFilter();
- if (Build.VERSION.SDK_INT >= 23) {
- intentFilter.addAction(BatteryManager.ACTION_CHARGING);
- intentFilter.addAction(BatteryManager.ACTION_DISCHARGING);
- } else {
- intentFilter.addAction(Intent.ACTION_POWER_CONNECTED);
- intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);
- }
- return intentFilter;
- }
-
- @Override
- public void onBroadcastReceive(Context context, @NonNull Intent intent) {
- String action = intent.getAction();
- if (action == null) {
- return;
- }
-
- Logger.get().debug(TAG, "Received " + action);
- switch (action) {
- case BatteryManager.ACTION_CHARGING:
- setState(true);
- break;
-
- case BatteryManager.ACTION_DISCHARGING:
- setState(false);
- break;
-
- case Intent.ACTION_POWER_CONNECTED:
- setState(true);
- break;
-
- case Intent.ACTION_POWER_DISCONNECTED:
- setState(false);
- break;
- }
- }
-
- private boolean isBatteryChangedIntentCharging(Intent intent) {
- boolean charging;
- if (Build.VERSION.SDK_INT >= 23) {
- int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
- charging = (status == BatteryManager.BATTERY_STATUS_CHARGING
- || status == BatteryManager.BATTERY_STATUS_FULL);
- } else {
- int chargePlug = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
- charging = (chargePlug != 0);
- }
- return charging;
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.kt
new file mode 100644
index 0000000..9a59363
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 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 androidx.work.impl.constraints.trackers
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.BatteryManager
+import android.os.BatteryManager.BATTERY_STATUS_CHARGING
+import android.os.BatteryManager.BATTERY_STATUS_FULL
+import android.os.Build
+import androidx.annotation.RestrictTo
+import androidx.work.Logger
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+
+/**
+ * Tracks whether or not the device's battery is charging.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class BatteryChargingTracker(context: Context, taskExecutor: TaskExecutor) :
+ BroadcastReceiverConstraintTracker<Boolean>(context, taskExecutor) {
+
+ override val initialState: Boolean
+ get() {
+ // {@link ACTION_CHARGING} and {@link ACTION_DISCHARGING} are not sticky broadcasts, so
+ // we use {@link ACTION_BATTERY_CHANGED} on all APIs to get the initial state.
+ val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
+ val intent = appContext.registerReceiver(null, intentFilter)
+ if (intent == null) {
+ Logger.get().error(TAG, "getInitialState - null intent received")
+ return false
+ }
+ return isBatteryChangedIntentCharging(intent)
+ }
+
+ override val intentFilter: IntentFilter
+ get() {
+ val intentFilter = IntentFilter()
+ if (Build.VERSION.SDK_INT >= 23) {
+ intentFilter.addAction(BatteryManager.ACTION_CHARGING)
+ intentFilter.addAction(BatteryManager.ACTION_DISCHARGING)
+ } else {
+ intentFilter.addAction(Intent.ACTION_POWER_CONNECTED)
+ intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED)
+ }
+ return intentFilter
+ }
+
+ override fun onBroadcastReceive(intent: Intent) {
+ val action = intent.action ?: return
+ Logger.get().debug(TAG, "Received $action")
+ when (action) {
+ BatteryManager.ACTION_CHARGING -> state = true
+ BatteryManager.ACTION_DISCHARGING -> state = false
+ Intent.ACTION_POWER_CONNECTED -> state = true
+ Intent.ACTION_POWER_DISCONNECTED -> state = false
+ }
+ }
+
+ private fun isBatteryChangedIntentCharging(intent: Intent): Boolean {
+ return if (Build.VERSION.SDK_INT >= 23) {
+ val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
+ (status == BATTERY_STATUS_CHARGING || status == BATTERY_STATUS_FULL)
+ } else {
+ intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0
+ }
+ }
+}
+
+private val TAG = Logger.tagWithPrefix("BatteryChrgTracker")
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.java
deleted file mode 100644
index a8e4326..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2017 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 androidx.work.impl.constraints.trackers;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.BatteryManager;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.work.Logger;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-
-/**
- * Tracks whether or not the device's battery level is low.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class BatteryNotLowTracker extends BroadcastReceiverConstraintTracker<Boolean> {
-
- private static final String TAG = Logger.tagWithPrefix("BatteryNotLowTracker");
-
- /**
- * {@see https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/res/res/values/config.xml#986}
- */
- static final float BATTERY_LOW_THRESHOLD = 0.15f;
-
- /**
- * Create an instance of {@link BatteryNotLowTracker}.
- * @param context The application {@link Context}
- * @param taskExecutor The internal {@link TaskExecutor} being used by WorkManager.
- */
- public BatteryNotLowTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
- super(context, taskExecutor);
- }
-
- /**
- * Based on BatteryService#shouldSendBatteryLowLocked(), but this ignores the previous plugged
- * state - cannot guarantee the last plugged state because this isn't always tracking.
- *
- * {@see https://android.googlesource.com/platform/frameworks/base/+/oreo-release/services/core/java/com/android/server/BatteryService.java#268}
- */
- @Override
- public Boolean getInitialState() {
- IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
- Intent intent = mAppContext.registerReceiver(null, intentFilter);
- if (intent == null) {
- Logger.get().error(TAG, "getInitialState - null intent received");
- return false;
- }
-
- int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
- int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
- int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
- float batteryPercentage = level / (float) scale;
-
- // BATTERY_STATUS_UNKNOWN typically refers to devices without a battery.
- // So those kinds of devices must be allowed.
- return (status == BatteryManager.BATTERY_STATUS_UNKNOWN
- || batteryPercentage > BATTERY_LOW_THRESHOLD);
- }
-
- @Override
- public IntentFilter getIntentFilter() {
- IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(Intent.ACTION_BATTERY_OKAY);
- intentFilter.addAction(Intent.ACTION_BATTERY_LOW);
- return intentFilter;
- }
-
- @Override
- public void onBroadcastReceive(Context context, @NonNull Intent intent) {
- if (intent.getAction() == null) {
- return;
- }
-
- Logger.get().debug(TAG, "Received " + intent.getAction());
-
- switch (intent.getAction()) {
- case Intent.ACTION_BATTERY_OKAY:
- setState(true);
- break;
-
- case Intent.ACTION_BATTERY_LOW:
- setState(false);
- break;
- }
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.kt
new file mode 100644
index 0000000..7b98bc1
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017 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 androidx.work.impl.constraints.trackers
+
+import android.content.Context
+import android.content.IntentFilter
+import android.content.Intent
+import android.os.BatteryManager
+import androidx.annotation.RestrictTo
+import androidx.work.Logger
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+
+/**
+ * Tracks whether or not the device's battery level is low.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class BatteryNotLowTracker(context: Context, taskExecutor: TaskExecutor) :
+ BroadcastReceiverConstraintTracker<Boolean>(context, taskExecutor) {
+ /**
+ * Based on BatteryService#shouldSendBatteryLowLocked(), but this ignores the previous plugged
+ * state - cannot guarantee the last plugged state because this isn't always tracking.
+ *
+ * {@see https://android.googlesource.com/platform/frameworks/base/+/oreo-release/services/core/java/com/android/server/BatteryService.java#268}
+ */
+ override val initialState: Boolean
+ get() {
+ val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
+ val intent = appContext.registerReceiver(null, intentFilter)
+ if (intent == null) {
+ Logger.get().error(TAG, "getInitialState - null intent received")
+ return false
+ }
+ val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
+ val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
+ val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
+ val batteryPercentage = level / scale.toFloat()
+
+ // BATTERY_STATUS_UNKNOWN typically refers to devices without a battery.
+ // So those kinds of devices must be allowed.
+ return status == BatteryManager.BATTERY_STATUS_UNKNOWN ||
+ batteryPercentage > BATTERY_LOW_THRESHOLD
+ }
+
+ override val intentFilter: IntentFilter
+ get() {
+ val intentFilter = IntentFilter()
+ intentFilter.addAction(Intent.ACTION_BATTERY_OKAY)
+ intentFilter.addAction(Intent.ACTION_BATTERY_LOW)
+ return intentFilter
+ }
+
+ override fun onBroadcastReceive(intent: Intent) {
+ if (intent.action == null) {
+ return
+ }
+ Logger.get().debug(TAG, "Received ${intent.action}")
+ when (intent.action) {
+ Intent.ACTION_BATTERY_OKAY -> state = true
+ Intent.ACTION_BATTERY_LOW -> state = false
+ }
+ }
+}
+
+private val TAG = Logger.tagWithPrefix("BatteryNotLowTracker")
+
+/**
+ * {@see https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/res/res/values/config.xml#986}
+ */
+internal const val BATTERY_LOW_THRESHOLD = 0.15f
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.java
deleted file mode 100644
index ecfe937..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2017 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 androidx.work.impl.constraints.trackers;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.work.Logger;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-
-/**
- * A {@link ConstraintTracker} with a {@link BroadcastReceiver} for monitoring constraint changes.
- *
- * @param <T> the constraint data type observed by this tracker
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class BroadcastReceiverConstraintTracker<T> extends ConstraintTracker<T> {
- private static final String TAG = Logger.tagWithPrefix("BrdcstRcvrCnstrntTrckr");
-
- private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent != null) {
- onBroadcastReceive(context, intent);
- }
- }
- };
-
- public BroadcastReceiverConstraintTracker(
- @NonNull Context context,
- @NonNull TaskExecutor taskExecutor) {
- super(context, taskExecutor);
- }
-
- /**
- * Called when the {@link BroadcastReceiver} is receiving an {@link Intent} broadcast and should
- * handle the received {@link Intent}.
- *
- * @param context The {@link Context} in which the receiver is running.
- * @param intent The {@link Intent} being received.
- */
- public abstract void onBroadcastReceive(Context context, @NonNull Intent intent);
-
- /**
- * @return The {@link IntentFilter} associated with this tracker.
- */
- public abstract IntentFilter getIntentFilter();
-
- @Override
- public void startTracking() {
- Logger.get().debug(TAG, getClass().getSimpleName() + ": registering receiver");
- mAppContext.registerReceiver(mBroadcastReceiver, getIntentFilter());
- }
-
- @Override
- public void stopTracking() {
- Logger.get().debug(
- TAG,
- getClass().getSimpleName() + ": unregistering receiver");
- mAppContext.unregisterReceiver(mBroadcastReceiver);
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.kt
new file mode 100644
index 0000000..7bdd924
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 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 androidx.work.impl.constraints.trackers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.annotation.RestrictTo
+import androidx.work.Logger
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+
+/**
+ * A [ConstraintTracker] with a [BroadcastReceiver] for monitoring constraint changes.
+ *
+ * @param <T> the constraint data type observed by this tracker
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+abstract class BroadcastReceiverConstraintTracker<T>(
+ context: Context,
+ taskExecutor: TaskExecutor
+) : ConstraintTracker<T>(context, taskExecutor) {
+ private val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ onBroadcastReceive(intent)
+ }
+ }
+
+ /**
+ * Called when the [BroadcastReceiver] is receiving an [Intent] broadcast and should
+ * handle the received [Intent].
+ *
+ * @param intent The [Intent] being received.
+ */
+ abstract fun onBroadcastReceive(intent: Intent)
+
+ /**
+ * @return The [IntentFilter] associated with this tracker.
+ */
+ abstract val intentFilter: IntentFilter
+
+ override fun startTracking() {
+ Logger.get().debug(TAG, "${javaClass.simpleName}: registering receiver")
+ appContext.registerReceiver(broadcastReceiver, intentFilter)
+ }
+
+ override fun stopTracking() {
+ Logger.get().debug(TAG, "${javaClass.simpleName}: unregistering receiver")
+ appContext.unregisterReceiver(broadcastReceiver)
+ }
+}
+private val TAG = Logger.tagWithPrefix("BrdcstRcvrCnstrntTrckr")
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java
deleted file mode 100644
index 935d3ab..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2017 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 androidx.work.impl.constraints.trackers;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.work.Logger;
-import androidx.work.impl.constraints.ConstraintListener;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-
-import java.util.ArrayList;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- * A base for tracking constraints and notifying listeners of changes.
- *
- * @param <T> the constraint data type observed by this tracker
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class ConstraintTracker<T> {
-
- private static final String TAG = Logger.tagWithPrefix("ConstraintTracker");
-
- protected final TaskExecutor mTaskExecutor;
- protected final Context mAppContext;
-
- private final Object mLock = new Object();
- private final Set<ConstraintListener<T>> mListeners = new LinkedHashSet<>();
-
- // Synthetic access
- T mCurrentState;
-
- protected ConstraintTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
- mAppContext = context.getApplicationContext();
- mTaskExecutor = taskExecutor;
- }
-
- /**
- * Add the given listener for tracking.
- * This may cause {@link #getInitialState()} and {@link #startTracking()} to be invoked.
- * If a state is set, this will immediately notify the given listener.
- *
- * @param listener The target listener to start notifying
- */
- public void addListener(ConstraintListener<T> listener) {
- synchronized (mLock) {
- if (mListeners.add(listener)) {
- if (mListeners.size() == 1) {
- mCurrentState = getInitialState();
- Logger.get().debug(TAG,
- getClass().getSimpleName() + ": initial state = " + mCurrentState);
- startTracking();
- }
- listener.onConstraintChanged(mCurrentState);
- }
- }
- }
-
- /**
- * Remove the given listener from tracking.
- *
- * @param listener The listener to stop notifying.
- */
- public void removeListener(ConstraintListener<T> listener) {
- synchronized (mLock) {
- if (mListeners.remove(listener) && mListeners.isEmpty()) {
- stopTracking();
- }
- }
- }
-
- /**
- * Sets the state of the constraint.
- * If state is has not changed, nothing happens.
- *
- * @param newState new state of constraint
- */
- public void setState(T newState) {
- synchronized (mLock) {
- if (mCurrentState == newState
- || (mCurrentState != null && mCurrentState.equals(newState))) {
- return;
- }
- mCurrentState = newState;
-
- // onConstraintChanged may lead to calls to addListener or removeListener.
- // This can potentially result in a modification to the set while it is being
- // iterated over, so we handle this by creating a copy and using that for
- // iteration.
- final List<ConstraintListener<T>> listenersList = new ArrayList<>(mListeners);
- mTaskExecutor.getMainThreadExecutor().execute(new Runnable() {
- @Override
- public void run() {
- for (ConstraintListener<T> listener : listenersList) {
- listener.onConstraintChanged(mCurrentState);
- }
- }
- });
- }
- }
-
- /**
- * Determines the initial state of the constraint being tracked.
- */
- public abstract T getInitialState();
-
- /**
- * Start tracking for constraint state changes.
- */
- public abstract void startTracking();
-
- /**
- * Stop tracking for constraint state changes.
- */
- public abstract void stopTracking();
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.kt
new file mode 100644
index 0000000..403d254
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2017 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 androidx.work.impl.constraints.trackers
+
+import android.content.Context
+import androidx.annotation.RestrictTo
+import androidx.work.Logger
+import androidx.work.impl.constraints.ConstraintListener
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import java.util.LinkedHashSet
+
+/**
+ * A base for tracking constraints and notifying listeners of changes.
+ *
+ * @param <T> the constraint data type observed by this tracker
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+abstract class ConstraintTracker<T> protected constructor(
+ context: Context,
+ private val taskExecutor: TaskExecutor
+) {
+ protected val appContext: Context = context.applicationContext
+ private val lock = Any()
+ private val listeners = LinkedHashSet<ConstraintListener<T>>()
+
+ private var currentState: T? = null
+
+ /**
+ * Add the given listener for tracking.
+ * This may cause [.getInitialState] and [.startTracking] to be invoked.
+ * If a state is set, this will immediately notify the given listener.
+ *
+ * @param listener The target listener to start notifying
+ */
+ fun addListener(listener: ConstraintListener<T>) {
+ synchronized(lock) {
+ if (listeners.add(listener)) {
+ if (listeners.size == 1) {
+ currentState = initialState
+ Logger.get().debug(
+ TAG, "${javaClass.simpleName}: initial state = $currentState"
+ )
+ startTracking()
+ }
+ @Suppress("UNCHECKED_CAST")
+ listener.onConstraintChanged(currentState as T)
+ }
+ }
+ }
+
+ /**
+ * Remove the given listener from tracking.
+ *
+ * @param listener The listener to stop notifying.
+ */
+ fun removeListener(listener: ConstraintListener<T>) {
+ synchronized(lock) {
+ if (listeners.remove(listener) && listeners.isEmpty()) {
+ stopTracking()
+ }
+ }
+ }
+
+ var state: T
+ get() {
+ return currentState ?: initialState
+ }
+
+ set(newState) {
+ synchronized(lock) {
+ if (currentState != null && (currentState == newState)) {
+ return
+ }
+
+ currentState = newState
+
+ // onConstraintChanged may lead to calls to addListener or removeListener.
+ // This can potentially result in a modification to the set while it is being
+ // iterated over, so we handle this by creating a copy and using that for
+ // iteration.
+ val listenersList = listeners.toList()
+ taskExecutor.mainThreadExecutor.execute {
+ listenersList.forEach { listener ->
+ // currentState was initialized by now
+ @Suppress("UNCHECKED_CAST")
+ listener.onConstraintChanged(currentState as T)
+ }
+ }
+ }
+ }
+
+ /**
+ * Determines the initial state of the constraint being tracked.
+ */
+ abstract val initialState: T
+
+ /**
+ * Start tracking for constraint state changes.
+ */
+ abstract fun startTracking()
+
+ /**
+ * Stop tracking for constraint state changes.
+ */
+ abstract fun stopTracking()
+}
+
+private val TAG = Logger.tagWithPrefix("ConstraintTracker")
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.java
deleted file mode 100644
index ce56137..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Copyright 2017 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 androidx.work.impl.constraints.trackers;
-
-import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.ConnectivityManager;
-import android.net.ConnectivityManager.NetworkCallback;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkInfo;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
-import androidx.core.net.ConnectivityManagerCompat;
-import androidx.work.Logger;
-import androidx.work.impl.constraints.NetworkState;
-import androidx.work.impl.utils.NetworkApi21;
-import androidx.work.impl.utils.NetworkApi23;
-import androidx.work.impl.utils.NetworkApi24;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-
-/**
- * A {@link ConstraintTracker} for monitoring network state.
- * <p>
- * For API 24 and up: Network state is tracked using a registered {@link NetworkCallback} with
- * {@link ConnectivityManager#registerDefaultNetworkCallback(NetworkCallback)}, added in API 24.
- * <p>
- * For API 23 and below: Network state is tracked using a {@link android.content.BroadcastReceiver}.
- * Much less efficient than tracking with {@link NetworkCallback}s and {@link ConnectivityManager}.
- * <p>
- * Based on {@link android.app.job.JobScheduler}'s ConnectivityController on API 26.
- * {@see https://android.googlesource.com/platform/frameworks/base/+/oreo-release/services/core/java/com/android/server/job/controllers/ConnectivityController.java}
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class NetworkStateTracker extends ConstraintTracker<NetworkState> {
-
- // Synthetic Accessor
- static final String TAG = Logger.tagWithPrefix("NetworkStateTracker");
-
- private final ConnectivityManager mConnectivityManager;
-
- @RequiresApi(24)
- private NetworkStateCallback mNetworkCallback;
- private NetworkStateBroadcastReceiver mBroadcastReceiver;
-
- /**
- * Create an instance of {@link NetworkStateTracker}
- * @param context the application {@link Context}
- * @param taskExecutor The internal {@link TaskExecutor} being used by WorkManager.
- */
- public NetworkStateTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
- super(context, taskExecutor);
- mConnectivityManager =
- (ConnectivityManager) mAppContext.getSystemService(Context.CONNECTIVITY_SERVICE);
- if (isNetworkCallbackSupported()) {
- mNetworkCallback = new NetworkStateCallback();
- } else {
- mBroadcastReceiver = new NetworkStateBroadcastReceiver();
- }
- }
-
- @Override
- public NetworkState getInitialState() {
- return getActiveNetworkState();
- }
-
- @Override
- public void startTracking() {
- if (isNetworkCallbackSupported()) {
- try {
- Logger.get().debug(TAG, "Registering network callback");
- NetworkApi24.registerDefaultNetworkCallbackCompat(mConnectivityManager,
- mNetworkCallback);
- } catch (IllegalArgumentException | SecurityException e) {
- // Catching the exceptions since and moving on - this tracker is only used for
- // GreedyScheduler and there is nothing to be done about device-specific bugs.
- // IllegalStateException: Happening on NVIDIA Shield K1 Tablets. See b/136569342.
- // SecurityException: Happening on Solone W1450. See b/153246136.
- Logger.get().error(
- TAG,
- "Received exception while registering network callback",
- e);
- }
- } else {
- Logger.get().debug(TAG, "Registering broadcast receiver");
- mAppContext.registerReceiver(mBroadcastReceiver,
- new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
- }
- }
-
- @Override
- public void stopTracking() {
- if (isNetworkCallbackSupported()) {
- try {
- Logger.get().debug(TAG, "Unregistering network callback");
- NetworkApi21.unregisterNetworkCallbackCompat(mConnectivityManager,
- mNetworkCallback);
- } catch (IllegalArgumentException | SecurityException e) {
- // Catching the exceptions since and moving on - this tracker is only used for
- // GreedyScheduler and there is nothing to be done about device-specific bugs.
- // IllegalStateException: Happening on NVIDIA Shield K1 Tablets. See b/136569342.
- // SecurityException: Happening on Solone W1450. See b/153246136.
- Logger.get().error(
- TAG,
- "Received exception while unregistering network callback",
- e);
- }
- } else {
- Logger.get().debug(TAG, "Unregistering broadcast receiver");
- mAppContext.unregisterReceiver(mBroadcastReceiver);
- }
- }
-
- private static boolean isNetworkCallbackSupported() {
- // Based on requiring ConnectivityManager#registerDefaultNetworkCallback - added in API 24.
- return Build.VERSION.SDK_INT >= 24;
- }
-
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- NetworkState getActiveNetworkState() {
- // Use getActiveNetworkInfo() instead of getNetworkInfo(network) because it can detect VPNs.
- NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
- boolean isConnected = info != null && info.isConnected();
- boolean isValidated = isActiveNetworkValidated();
- boolean isMetered = ConnectivityManagerCompat.isActiveNetworkMetered(mConnectivityManager);
- boolean isNotRoaming = info != null && !info.isRoaming();
- return new NetworkState(isConnected, isValidated, isMetered, isNotRoaming);
- }
-
- @VisibleForTesting
- boolean isActiveNetworkValidated() {
- if (Build.VERSION.SDK_INT < 23) {
- return false; // NET_CAPABILITY_VALIDATED not available until API 23. Used on API 26+.
- }
- try {
- Network network = NetworkApi23.getActiveNetworkCompat(mConnectivityManager);
- NetworkCapabilities capabilities =
- NetworkApi21.getNetworkCapabilitiesCompat(mConnectivityManager, network);
- return capabilities != null
- && NetworkApi21.hasCapabilityCompat(capabilities, NET_CAPABILITY_VALIDATED);
- } catch (SecurityException exception) {
- // b/163342798
- Logger.get().error(TAG, "Unable to validate active network", exception);
- return false;
- }
- }
-
- @RequiresApi(24)
- private class NetworkStateCallback extends NetworkCallback {
- NetworkStateCallback() {
- }
-
- @Override
- public void onCapabilitiesChanged(
- @NonNull Network network, @NonNull NetworkCapabilities capabilities) {
- // The Network parameter is unreliable when a VPN app is running - use active network.
- Logger.get().debug(
- TAG,
- "Network capabilities changed: " + capabilities);
- setState(getActiveNetworkState());
- }
-
- @Override
- public void onLost(@NonNull Network network) {
- Logger.get().debug(TAG, "Network connection lost");
- setState(getActiveNetworkState());
- }
- }
-
- private class NetworkStateBroadcastReceiver extends BroadcastReceiver {
- NetworkStateBroadcastReceiver() {
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent == null || intent.getAction() == null) {
- return;
- }
- if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
- Logger.get().debug(TAG, "Network broadcast received");
- setState(getActiveNetworkState());
- }
- }
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.kt
new file mode 100644
index 0000000..07f3975
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2017 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 androidx.work.impl.constraints.trackers
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.core.net.ConnectivityManagerCompat
+import androidx.work.Logger
+import androidx.work.impl.constraints.NetworkState
+import androidx.work.impl.utils.getActiveNetworkCompat
+import androidx.work.impl.utils.getNetworkCapabilitiesCompat
+import androidx.work.impl.utils.hasCapabilityCompat
+import androidx.work.impl.utils.registerDefaultNetworkCallbackCompat
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import androidx.work.impl.utils.unregisterNetworkCallbackCompat
+
+/**
+ * A [ConstraintTracker] for monitoring network state.
+ *
+ *
+ * For API 24 and up: Network state is tracked using a registered [NetworkCallback] with
+ * [ConnectivityManager.registerDefaultNetworkCallback], added in API 24.
+ *
+ *
+ * For API 23 and below: Network state is tracked using a [android.content.BroadcastReceiver].
+ * Much less efficient than tracking with [NetworkCallback]s and [ConnectivityManager].
+ *
+ *
+ * Based on [android.app.job.JobScheduler]'s ConnectivityController on API 26.
+ * {@see https://android.googlesource.com/platform/frameworks/base/+/oreo-release/services/core/java/com/android/server/job/controllers/ConnectivityController.java}
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun NetworkStateTracker(
+ context: Context,
+ taskExecutor: TaskExecutor
+): ConstraintTracker<NetworkState> {
+ // Based on requiring ConnectivityManager#registerDefaultNetworkCallback - added in API 24.
+ return if (Build.VERSION.SDK_INT >= 24) {
+ NetworkStateTracker24(context, taskExecutor)
+ } else {
+ NetworkStateTrackerPre24(context, taskExecutor)
+ }
+}
+
+private val TAG = Logger.tagWithPrefix("NetworkStateTracker")
+
+internal val ConnectivityManager.isActiveNetworkValidated: Boolean
+ get() = if (Build.VERSION.SDK_INT < 23) {
+ false // NET_CAPABILITY_VALIDATED not available until API 23. Used on API 26+.
+ } else try {
+ val network = getActiveNetworkCompat()
+ val capabilities = getNetworkCapabilitiesCompat(network)
+ (capabilities?.hasCapabilityCompat(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) ?: false
+ } catch (exception: SecurityException) {
+ // b/163342798
+ Logger.get().error(TAG, "Unable to validate active network", exception)
+ false
+ }
+
+@Suppress("DEPRECATION")
+internal val ConnectivityManager.activeNetworkState: NetworkState
+ get() {
+ // Use getActiveNetworkInfo() instead of getNetworkInfo(network) because it can detect VPNs.
+ val info = activeNetworkInfo
+ val isConnected = info != null && info.isConnected
+ val isValidated = isActiveNetworkValidated
+ val isMetered = ConnectivityManagerCompat.isActiveNetworkMetered(this)
+ val isNotRoaming = info != null && !info.isRoaming
+ return NetworkState(isConnected, isValidated, isMetered, isNotRoaming)
+ } // b/163342798
+
+internal class NetworkStateTrackerPre24(context: Context, taskExecutor: TaskExecutor) :
+ BroadcastReceiverConstraintTracker<NetworkState>(context, taskExecutor) {
+
+ private val connectivityManager: ConnectivityManager =
+ appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+
+ override fun onBroadcastReceive(intent: Intent) {
+ @Suppress("DEPRECATION")
+ if (intent.action == ConnectivityManager.CONNECTIVITY_ACTION) {
+ Logger.get().debug(TAG, "Network broadcast received")
+ state = connectivityManager.activeNetworkState
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ override val intentFilter: IntentFilter
+ get() = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
+ override val initialState: NetworkState
+ get() = connectivityManager.activeNetworkState
+}
+
+@RequiresApi(24)
+internal class NetworkStateTracker24(context: Context, taskExecutor: TaskExecutor) :
+ ConstraintTracker<NetworkState>(context, taskExecutor) {
+
+ private val connectivityManager: ConnectivityManager =
+ appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ override val initialState: NetworkState
+ get() = connectivityManager.activeNetworkState
+
+ private val networkCallback = object : NetworkCallback() {
+ override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) {
+ // The Network parameter is unreliable when a VPN app is running - use active network.
+ Logger.get().debug(TAG, "Network capabilities changed: $capabilities")
+ state = connectivityManager.activeNetworkState
+ }
+ override fun onLost(network: Network) {
+ Logger.get().debug(TAG, "Network connection lost")
+ state = connectivityManager.activeNetworkState
+ }
+ }
+
+ override fun startTracking() {
+ try {
+ Logger.get().debug(TAG, "Registering network callback")
+ connectivityManager.registerDefaultNetworkCallbackCompat(networkCallback)
+ } catch (e: IllegalArgumentException) {
+ // Catching the exceptions since and moving on - this tracker is only used for
+ // GreedyScheduler and there is nothing to be done about device-specific bugs.
+ // IllegalStateException: Happening on NVIDIA Shield K1 Tablets. See b/136569342.
+ // SecurityException: Happening on Solone W1450. See b/153246136.
+ Logger.get().error(TAG, "Received exception while registering network callback", e)
+ } catch (e: SecurityException) {
+ Logger.get().error(TAG, "Received exception while registering network callback", e)
+ }
+ }
+
+ override fun stopTracking() {
+ try {
+ Logger.get().debug(TAG, "Unregistering network callback")
+ connectivityManager.unregisterNetworkCallbackCompat(networkCallback)
+ } catch (e: IllegalArgumentException) {
+ // Catching the exceptions since and moving on - this tracker is only used for
+ // GreedyScheduler and there is nothing to be done about device-specific bugs.
+ // IllegalStateException: Happening on NVIDIA Shield K1 Tablets. See b/136569342.
+ // SecurityException: Happening on Solone W1450. See b/153246136.
+ Logger.get().error(TAG, "Received exception while unregistering network callback", e)
+ } catch (e: SecurityException) {
+ Logger.get().error(TAG, "Received exception while unregistering network callback", e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.java
deleted file mode 100644
index 4eb98d1..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2017 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 androidx.work.impl.constraints.trackers;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.work.Logger;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-
-/**
- * Tracks whether or not the device's storage is low.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class StorageNotLowTracker extends BroadcastReceiverConstraintTracker<Boolean> {
-
- private static final String TAG = Logger.tagWithPrefix("StorageNotLowTracker");
-
- /**
- * Create an instance of {@link StorageNotLowTracker}.
- * @param context The application {@link Context}
- * @param taskExecutor The internal {@link TaskExecutor} being used by WorkManager.
- */
- public StorageNotLowTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
- super(context, taskExecutor);
- }
-
- @Override
- public Boolean getInitialState() {
- Intent intent = mAppContext.registerReceiver(null, getIntentFilter());
- if (intent == null || intent.getAction() == null) {
- // ACTION_DEVICE_STORAGE_LOW is a sticky broadcast that is removed when sufficient
- // storage is available again. ACTION_DEVICE_STORAGE_OK is not sticky. So if we
- // don't receive anything here, we can assume that the storage state is okay.
- return true;
- } else {
- switch (intent.getAction()) {
- case Intent.ACTION_DEVICE_STORAGE_OK:
- return true;
-
- case Intent.ACTION_DEVICE_STORAGE_LOW:
- return false;
-
- default:
- // This should never happen because the intent filter is configured
- // correctly.
- return false;
- }
- }
- }
-
- @Override
- public IntentFilter getIntentFilter() {
- // In API 26+, DEVICE_STORAGE_OK/LOW are deprecated and are no longer sent to
- // manifest-defined BroadcastReceivers. Since we are registering our receiver manually, this
- // is currently okay. This may change in future versions, so this will need to be monitored.
- IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
- intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
- return intentFilter;
- }
-
- @Override
- public void onBroadcastReceive(Context context, @NonNull Intent intent) {
- if (intent.getAction() == null) {
- return; // Should never happen since the IntentFilter was configured.
- }
-
- Logger.get().debug(TAG, "Received " + intent.getAction());
-
- switch (intent.getAction()) {
- case Intent.ACTION_DEVICE_STORAGE_OK:
- setState(true);
- break;
-
- case Intent.ACTION_DEVICE_STORAGE_LOW:
- setState(false);
- break;
- }
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.kt
new file mode 100644
index 0000000..815a58a
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 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 androidx.work.impl.constraints.trackers
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.annotation.RestrictTo
+import androidx.work.Logger
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+
+/**
+ * Tracks whether or not the device's storage is low.
+ * @hide
+ */
+@Suppress("DEPRECATION")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class StorageNotLowTracker(context: Context, taskExecutor: TaskExecutor) :
+ BroadcastReceiverConstraintTracker<Boolean>(context, taskExecutor) {
+
+ override val initialState: Boolean
+ get() {
+ val intent = appContext.registerReceiver(null, intentFilter)
+ return if (intent == null || intent.action == null) {
+ // ACTION_DEVICE_STORAGE_LOW is a sticky broadcast that is removed when sufficient
+ // storage is available again. ACTION_DEVICE_STORAGE_OK is not sticky. So if we
+ // don't receive anything here, we can assume that the storage state is okay.
+ true
+ } else {
+ when (intent.action) {
+ Intent.ACTION_DEVICE_STORAGE_OK -> true
+ Intent.ACTION_DEVICE_STORAGE_LOW -> false
+ else ->
+ // This should never happen because the intent filter is configured
+ // correctly.
+ false
+ }
+ }
+ }
+
+ override val intentFilter: IntentFilter
+ get() {
+ // In API 26+, DEVICE_STORAGE_OK/LOW are deprecated and are no longer sent to
+ // manifest-defined BroadcastReceivers. Since we are registering our receiver manually, this
+ // is currently okay. This may change in future versions, so this will need to be monitored.
+ val intentFilter = IntentFilter()
+ intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK)
+ intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW)
+ return intentFilter
+ }
+
+ override fun onBroadcastReceive(intent: Intent) {
+ if (intent.action == null) {
+ return // Should never happen since the IntentFilter was configured.
+ }
+ Logger.get().debug(TAG, "Received " + intent.action)
+ when (intent.action) {
+ Intent.ACTION_DEVICE_STORAGE_OK -> state = true
+ Intent.ACTION_DEVICE_STORAGE_LOW -> state = false
+ }
+ }
+}
+
+private val TAG = Logger.tagWithPrefix("StorageNotLowTracker")
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/Trackers.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/Trackers.java
deleted file mode 100644
index ea7fb22..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/Trackers.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2017 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 androidx.work.impl.constraints.trackers;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-
-/**
- * A singleton class to hold an instance of each {@link ConstraintTracker}.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class Trackers {
- private BatteryChargingTracker mBatteryChargingTracker;
- private BatteryNotLowTracker mBatteryNotLowTracker;
- private NetworkStateTracker mNetworkStateTracker;
- private StorageNotLowTracker mStorageNotLowTracker;
-
- public Trackers(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
- Context appContext = context.getApplicationContext();
- mBatteryChargingTracker = new BatteryChargingTracker(appContext, taskExecutor);
- mBatteryNotLowTracker = new BatteryNotLowTracker(appContext, taskExecutor);
- mNetworkStateTracker = new NetworkStateTracker(appContext, taskExecutor);
- mStorageNotLowTracker = new StorageNotLowTracker(appContext, taskExecutor);
- }
-
- /**
- * Gets the tracker used to track the battery charging status.
- *
- * @return The tracker used to track battery charging status
- */
- @NonNull
- public BatteryChargingTracker getBatteryChargingTracker() {
- return mBatteryChargingTracker;
- }
-
- /**
- * Gets the tracker used to track if the battery is okay or low.
- *
- * @return The tracker used to track if the battery is okay or low
- */
- @NonNull
- public BatteryNotLowTracker getBatteryNotLowTracker() {
- return mBatteryNotLowTracker;
- }
-
- /**
- * Gets the tracker used to track network state changes.
- *
- * @return The tracker used to track state of the network
- */
- @NonNull
- public NetworkStateTracker getNetworkStateTracker() {
- return mNetworkStateTracker;
- }
-
- /**
- * Gets the tracker used to track if device storage is okay or low.
- *
- * @return The tracker used to track if device storage is okay or low.
- */
- @NonNull
- public StorageNotLowTracker getStorageNotLowTracker() {
- return mStorageNotLowTracker;
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/Trackers.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/Trackers.kt
new file mode 100644
index 0000000..d6f8201
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/Trackers.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 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 androidx.work.impl.constraints.trackers
+
+import android.content.Context
+import androidx.annotation.RestrictTo
+import androidx.work.impl.constraints.NetworkState
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+
+/**
+ * A singleton class to hold an instance of each [ConstraintTracker].
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class Trackers
+@JvmOverloads
+constructor(
+ context: Context,
+ taskExecutor: TaskExecutor,
+ /**
+ * The tracker used to track the battery charging status.
+ */
+ val batteryChargingTracker: ConstraintTracker<Boolean> =
+ BatteryChargingTracker(context.applicationContext, taskExecutor),
+
+ /**
+ * The tracker used to track if the battery is okay or low.
+ */
+ val batteryNotLowTracker: BatteryNotLowTracker =
+ BatteryNotLowTracker(context.applicationContext, taskExecutor),
+
+ /**
+ * The tracker used to track network state changes.
+ */
+ val networkStateTracker: ConstraintTracker<NetworkState> =
+ NetworkStateTracker(context.applicationContext, taskExecutor),
+
+ /**
+ * The tracker used to track if device storage is okay or low.
+ */
+ val storageNotLowTracker: ConstraintTracker<Boolean> =
+ StorageNotLowTracker(context.applicationContext, taskExecutor),
+)
\ No newline at end of file