Merge "Pass repository URLs for ProjectSetupRule" into androidx-main
diff --git a/biometric/biometric-ktx/OWNERS b/biometric/biometric-ktx/OWNERS
index a1780fe..71f000e 100644
--- a/biometric/biometric-ktx/OWNERS
+++ b/biometric/biometric-ktx/OWNERS
@@ -1,3 +1,2 @@
-graciecheng@google.com
kchyn@google.com
curtislb@google.com
\ No newline at end of file
diff --git a/browser/OWNERS b/browser/OWNERS
index 2848be3..af78ac1 100644
--- a/browser/OWNERS
+++ b/browser/OWNERS
@@ -1,3 +1,2 @@
-lizeb@google.com
peconn@google.com
eirage@google.com
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index 4f01dc7..0b63bd4 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -106,7 +106,7 @@
const val REACTIVE_STREAMS = "org.reactivestreams:reactive-streams:1.0.0"
const val RX_JAVA = "io.reactivex.rxjava2:rxjava:2.2.9"
const val RX_JAVA3 = "io.reactivex.rxjava3:rxjava:3.0.0"
-val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.2.6"
+val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.2.13"
val SKIKO = "org.jetbrains.skiko:skiko-jvm:$SKIKO_VERSION"
val SKIKO_LINUX_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-linux-x64:$SKIKO_VERSION"
val SKIKO_MACOS_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-macos-x64:$SKIKO_VERSION"
diff --git a/buildSrc/src/main/kotlin/androidx/build/resources/PublicResourcesStubHelper.kt b/buildSrc/src/main/kotlin/androidx/build/resources/PublicResourcesStubHelper.kt
index f121ea7..528c6bb 100644
--- a/buildSrc/src/main/kotlin/androidx/build/resources/PublicResourcesStubHelper.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/resources/PublicResourcesStubHelper.kt
@@ -19,14 +19,23 @@
import androidx.build.getSupportRootFolder
import com.android.build.gradle.LibraryExtension
import org.gradle.api.Project
+import org.gradle.api.tasks.Copy
import java.io.File
fun Project.configurePublicResourcesStub(extension: LibraryExtension) {
+ val targetResFolder = File(project.buildDir, "generated/res/public-stub")
+
+ val generatePublicResourcesTask = tasks.register(
+ "generatePublicResourcesStub",
+ Copy::class.java
+ ) { task ->
+ task.from(File(project.getSupportRootFolder(), "buildSrc/res"))
+ task.into(targetResFolder)
+ }
+
extension.libraryVariants.all { variant ->
variant.registerGeneratedResFolders(
- project.files(
- File(project.getSupportRootFolder(), "/buildSrc/res")
- )
+ project.files(targetResFolder).builtBy(generatePublicResourcesTask)
)
}
}
diff --git a/car/app/app-activity/api/current.txt b/car/app/app-activity/api/current.txt
index 6638685..a77b152 100644
--- a/car/app/app-activity/api/current.txt
+++ b/car/app/app-activity/api/current.txt
@@ -9,11 +9,8 @@
package androidx.car.app.activity.renderer.surface {
- public final class LegacySurfacePackage implements android.os.Parcelable {
+ public final class LegacySurfacePackage {
ctor public LegacySurfacePackage(androidx.car.app.activity.renderer.surface.SurfaceControlCallback);
- method public int describeContents();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.car.app.activity.renderer.surface.LegacySurfacePackage!> CREATOR;
}
public interface SurfaceControlCallback {
@@ -22,21 +19,9 @@
method public void setSurfaceWrapper(androidx.car.app.activity.renderer.surface.SurfaceWrapper);
}
- public final class SurfacePackageWrapper implements android.os.Parcelable {
- ctor public SurfacePackageWrapper(androidx.car.app.activity.renderer.surface.LegacySurfacePackage);
- ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public SurfacePackageWrapper(android.view.SurfaceControlViewHost.SurfacePackage);
- method public int describeContents();
- method public android.os.Parcelable? getSurfacePackage();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.car.app.activity.renderer.surface.SurfacePackageWrapper!> CREATOR;
- }
-
- public final class SurfaceWrapper implements android.os.Parcelable {
+ public final class SurfaceWrapper {
ctor public SurfaceWrapper(android.os.IBinder?, int, int, int, int, android.view.Surface);
- method public int describeContents();
method public android.os.IBinder? getHostToken();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.car.app.activity.renderer.surface.SurfaceWrapper!> CREATOR;
}
}
diff --git a/car/app/app-activity/api/public_plus_experimental_current.txt b/car/app/app-activity/api/public_plus_experimental_current.txt
index 6638685..a77b152 100644
--- a/car/app/app-activity/api/public_plus_experimental_current.txt
+++ b/car/app/app-activity/api/public_plus_experimental_current.txt
@@ -9,11 +9,8 @@
package androidx.car.app.activity.renderer.surface {
- public final class LegacySurfacePackage implements android.os.Parcelable {
+ public final class LegacySurfacePackage {
ctor public LegacySurfacePackage(androidx.car.app.activity.renderer.surface.SurfaceControlCallback);
- method public int describeContents();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.car.app.activity.renderer.surface.LegacySurfacePackage!> CREATOR;
}
public interface SurfaceControlCallback {
@@ -22,21 +19,9 @@
method public void setSurfaceWrapper(androidx.car.app.activity.renderer.surface.SurfaceWrapper);
}
- public final class SurfacePackageWrapper implements android.os.Parcelable {
- ctor public SurfacePackageWrapper(androidx.car.app.activity.renderer.surface.LegacySurfacePackage);
- ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public SurfacePackageWrapper(android.view.SurfaceControlViewHost.SurfacePackage);
- method public int describeContents();
- method public android.os.Parcelable? getSurfacePackage();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.car.app.activity.renderer.surface.SurfacePackageWrapper!> CREATOR;
- }
-
- public final class SurfaceWrapper implements android.os.Parcelable {
+ public final class SurfaceWrapper {
ctor public SurfaceWrapper(android.os.IBinder?, int, int, int, int, android.view.Surface);
- method public int describeContents();
method public android.os.IBinder? getHostToken();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.car.app.activity.renderer.surface.SurfaceWrapper!> CREATOR;
}
}
diff --git a/car/app/app-activity/api/restricted_current.txt b/car/app/app-activity/api/restricted_current.txt
index 6638685..a77b152 100644
--- a/car/app/app-activity/api/restricted_current.txt
+++ b/car/app/app-activity/api/restricted_current.txt
@@ -9,11 +9,8 @@
package androidx.car.app.activity.renderer.surface {
- public final class LegacySurfacePackage implements android.os.Parcelable {
+ public final class LegacySurfacePackage {
ctor public LegacySurfacePackage(androidx.car.app.activity.renderer.surface.SurfaceControlCallback);
- method public int describeContents();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.car.app.activity.renderer.surface.LegacySurfacePackage!> CREATOR;
}
public interface SurfaceControlCallback {
@@ -22,21 +19,9 @@
method public void setSurfaceWrapper(androidx.car.app.activity.renderer.surface.SurfaceWrapper);
}
- public final class SurfacePackageWrapper implements android.os.Parcelable {
- ctor public SurfacePackageWrapper(androidx.car.app.activity.renderer.surface.LegacySurfacePackage);
- ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public SurfacePackageWrapper(android.view.SurfaceControlViewHost.SurfacePackage);
- method public int describeContents();
- method public android.os.Parcelable? getSurfacePackage();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.car.app.activity.renderer.surface.SurfacePackageWrapper!> CREATOR;
- }
-
- public final class SurfaceWrapper implements android.os.Parcelable {
+ public final class SurfaceWrapper {
ctor public SurfaceWrapper(android.os.IBinder?, int, int, int, int, android.view.Surface);
- method public int describeContents();
method public android.os.IBinder? getHostToken();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.car.app.activity.renderer.surface.SurfaceWrapper!> CREATOR;
}
}
diff --git a/car/app/app-activity/build.gradle b/car/app/app-activity/build.gradle
index d422b0c..83298c4 100644
--- a/car/app/app-activity/build.gradle
+++ b/car/app/app-activity/build.gradle
@@ -40,6 +40,10 @@
defaultConfig {
minSdkVersion 29
}
+ lintOptions {
+ // We rely on keeping a bunch of private variables in the library for serialization.
+ disable("BanKeepAnnotation")
+ }
buildFeatures {
aidl = true
}
diff --git a/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/ICarAppActivity.aidl b/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/ICarAppActivity.aidl
index 1a51024..a4bbd28 100644
--- a/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/ICarAppActivity.aidl
+++ b/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/ICarAppActivity.aidl
@@ -18,7 +18,7 @@
import androidx.car.app.activity.renderer.IRendererCallback;
import androidx.car.app.activity.renderer.surface.ISurfaceListener;
-import androidx.car.app.activity.renderer.surface.SurfacePackageWrapper;
+import androidx.car.app.serialization.Bundleable;
/**
* An interface to let renderer service communicate with the car activity.
@@ -27,7 +27,7 @@
*/
oneway interface ICarAppActivity {
/** Sets the surface package. */
- void setSurfacePackage(in SurfacePackageWrapper surfacePackageWrapper) = 1;
+ void setSurfacePackage(in Bundleable surfacePackage) = 1;
/** Registers the listener to get callbacks for surface events. */
void setSurfaceListener(ISurfaceListener listener) = 2;
diff --git a/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/ISurfaceControl.aidl b/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/ISurfaceControl.aidl
index 75c3299..4399a6d 100644
--- a/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/ISurfaceControl.aidl
+++ b/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/ISurfaceControl.aidl
@@ -1,6 +1,6 @@
package androidx.car.app.activity.renderer.surface;
-import androidx.car.app.activity.renderer.surface.SurfaceWrapper;
+import androidx.car.app.serialization.Bundleable;
/**
* Interface implemented by an off-process renderer to receive events affecting the
@@ -10,7 +10,7 @@
*/
interface ISurfaceControl {
/** Notifies that the underlying surface changed. */
- void setSurfaceWrapper(in SurfaceWrapper surfaceWrapper) = 1;
+ void setSurfaceWrapper(in Bundleable surfaceWrapper) = 1;
/** Notifies that the surface received a new touch event. */
void onTouchEvent(in MotionEvent event) = 2;
diff --git a/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/ISurfaceListener.aidl b/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/ISurfaceListener.aidl
index de0046b..f2f7486 100644
--- a/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/ISurfaceListener.aidl
+++ b/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/ISurfaceListener.aidl
@@ -1,6 +1,6 @@
package androidx.car.app.activity.renderer.surface;
-import androidx.car.app.activity.renderer.surface.SurfaceWrapper;
+import androidx.car.app.serialization.Bundleable;
/**
* A surface event listener interface.
@@ -14,7 +14,7 @@
* @param surfaceWrapper a {@link SurfaceWrapper} that contains information on the surface that
* has become available.
*/
- void onSurfaceAvailable(in SurfaceWrapper surfaceWrapper) = 1;
+ void onSurfaceAvailable(in Bundleable surfaceWrapper) = 1;
/**
* Notifies that the surface size has changed.
@@ -22,5 +22,5 @@
* @param surfaceWrapper a {@link SurfaceWrapper} that contains the updated information on the
* surface.
*/
- void onSurfaceChanged(in SurfaceWrapper surfaceWrapper) = 2;
+ void onSurfaceChanged(in Bundleable surfaceWrapper) = 2;
}
diff --git a/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/LegacySurfacePackage.aidl b/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/LegacySurfacePackage.aidl
deleted file mode 100644
index 04f4984..0000000
--- a/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/LegacySurfacePackage.aidl
+++ /dev/null
@@ -1,3 +0,0 @@
-package androidx.car.app.activity.renderer.surface;
-
-parcelable LegacySurfacePackage;
\ No newline at end of file
diff --git a/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/SurfacePackageWrapper.aidl b/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/SurfacePackageWrapper.aidl
deleted file mode 100644
index 29cb736..0000000
--- a/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/SurfacePackageWrapper.aidl
+++ /dev/null
@@ -1,3 +0,0 @@
-package androidx.car.app.activity.renderer.surface;
-
-parcelable SurfacePackageWrapper;
\ No newline at end of file
diff --git a/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/SurfaceWrapper.aidl b/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/SurfaceWrapper.aidl
deleted file mode 100644
index 3a7f762..0000000
--- a/car/app/app-activity/src/main/aidl/androidx/car/app/activity/renderer/surface/SurfaceWrapper.aidl
+++ /dev/null
@@ -1,19 +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.car.app.activity.renderer.surface;
-
-parcelable SurfaceWrapper;
diff --git a/car/app/app-activity/src/main/java/androidx/car/app/activity/CarAppActivity.java b/car/app/app-activity/src/main/java/androidx/car/app/activity/CarAppActivity.java
index df053e0..5716729 100644
--- a/car/app/app-activity/src/main/java/androidx/car/app/activity/CarAppActivity.java
+++ b/car/app/app-activity/src/main/java/androidx/car/app/activity/CarAppActivity.java
@@ -45,9 +45,9 @@
import androidx.car.app.activity.renderer.surface.OnBackPressedListener;
import androidx.car.app.activity.renderer.surface.RotaryEventCallback;
import androidx.car.app.activity.renderer.surface.SurfaceHolderListener;
-import androidx.car.app.activity.renderer.surface.SurfacePackageWrapper;
import androidx.car.app.activity.renderer.surface.SurfaceWrapperProvider;
import androidx.car.app.activity.renderer.surface.TemplateSurfaceView;
+import androidx.car.app.serialization.Bundleable;
import androidx.car.app.utils.ThreadUtils;
import java.util.List;
@@ -88,7 +88,7 @@
*/
private final ICarAppActivity.Stub mCarActivity = new ICarAppActivity.Stub() {
@Override
- public void setSurfacePackage(@NonNull SurfacePackageWrapper surfacePackage) {
+ public void setSurfacePackage(@NonNull Bundleable surfacePackage) {
requireNonNull(surfacePackage);
ThreadUtils.runOnMain(() -> mSurfaceView.setSurfacePackage(surfacePackage));
}
diff --git a/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/LegacySurfacePackage.java b/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/LegacySurfacePackage.java
index 81a226b..5844820 100644
--- a/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/LegacySurfacePackage.java
+++ b/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/LegacySurfacePackage.java
@@ -16,14 +16,18 @@
package androidx.car.app.activity.renderer.surface;
+import static androidx.car.app.activity.LogTags.TAG;
+
import static java.util.Objects.requireNonNull;
import android.annotation.SuppressLint;
-import android.os.Parcel;
-import android.os.Parcelable;
+import android.util.Log;
import android.view.MotionEvent;
+import androidx.annotation.Keep;
import androidx.annotation.NonNull;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
/**
* A serializable class containing all the data required to render and interact with a surface from
@@ -32,10 +36,9 @@
* This class exists for compatibility with Q devices. In Android R and later,
* {@link android.view.SurfaceControlViewHost.SurfacePackage} will be used instead.
*/
-//TODO(179714355): Investigate using Bundleable instead of Parcelable
-@SuppressLint({"BanParcelableUsage"})
-public final class LegacySurfacePackage implements Parcelable {
- private final ISurfaceControl mSurfaceControl;
+public final class LegacySurfacePackage {
+ @Keep
+ private ISurfaceControl mISurfaceControl;
/**
* Creates a {@link LegacySurfacePackage}.
@@ -46,14 +49,19 @@
*/
@SuppressLint("ExecutorRegistration")
public LegacySurfacePackage(@NonNull SurfaceControlCallback callback) {
- mSurfaceControl = new ISurfaceControl.Stub() {
+ mISurfaceControl = new ISurfaceControl.Stub() {
final SurfaceControlCallback mCallback = callback;
@Override
- public void setSurfaceWrapper(@NonNull SurfaceWrapper surfaceWrapper) {
+ public void setSurfaceWrapper(@NonNull Bundleable surfaceWrapper) {
requireNonNull(surfaceWrapper);
if (mCallback != null) {
- mCallback.setSurfaceWrapper(surfaceWrapper);
+ try {
+ mCallback.setSurfaceWrapper((SurfaceWrapper) surfaceWrapper.get());
+ } catch (BundlerException e) {
+ //TODO(b/179930319): Surface error on the CarAppActivity
+ Log.e(TAG, "Unable to deserialize surface wrapper", e);
+ }
}
}
@@ -74,39 +82,13 @@
};
}
- LegacySurfacePackage(@NonNull Parcel parcel) {
- mSurfaceControl = ISurfaceControl.Stub.asInterface(parcel.readStrongBinder());
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel parcel, int flags) {
- parcel.writeStrongInterface(mSurfaceControl);
- }
-
- @Override
- public int describeContents() {
- return 0;
+ /** Empty constructor needed for serializations. **/
+ private LegacySurfacePackage() {
}
@NonNull
ISurfaceControl getSurfaceControl() {
- return mSurfaceControl;
+ return mISurfaceControl;
}
-
- @NonNull
- public static final Creator<LegacySurfacePackage> CREATOR =
- new Creator<LegacySurfacePackage>() {
- @NonNull
- @Override
- public LegacySurfacePackage createFromParcel(@NonNull Parcel parcel) {
- return new LegacySurfacePackage(parcel);
- }
-
- @NonNull
- @Override
- public LegacySurfacePackage[] newArray(int size) {
- return new LegacySurfacePackage[size];
- }
- };
}
diff --git a/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceHolderListener.java b/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceHolderListener.java
index 12a55c1..50e831f 100644
--- a/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceHolderListener.java
+++ b/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceHolderListener.java
@@ -28,6 +28,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
/**
* A listener of {@link SurfaceHolder}.
@@ -80,10 +82,13 @@
private void notifySurfaceCreated() {
try {
if (mSurfaceListener != null) {
- mSurfaceListener.onSurfaceAvailable(mSurfaceWrapperProvider.createSurfaceWrapper());
+ mSurfaceListener.onSurfaceAvailable(
+ Bundleable.create(mSurfaceWrapperProvider.createSurfaceWrapper()));
}
} catch (RemoteException e) {
Log.e(TAG, "Remote connection lost", e);
+ } catch (BundlerException e) {
+ Log.e(TAG, "Unable to serialize surface wrapper", e);
}
}
@@ -91,10 +96,13 @@
private void notifySurfaceChanged() {
try {
if (mSurfaceListener != null) {
- mSurfaceListener.onSurfaceChanged(mSurfaceWrapperProvider.createSurfaceWrapper());
+ mSurfaceListener.onSurfaceChanged(
+ Bundleable.create(mSurfaceWrapperProvider.createSurfaceWrapper()));
}
} catch (RemoteException e) {
Log.e(TAG, "Remote connection lost", e);
+ } catch (BundlerException e) {
+ Log.e(TAG, "Unable to serialize surface wrapper", e);
}
}
}
diff --git a/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfacePackageWrapper.java b/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfacePackageWrapper.java
deleted file mode 100644
index 5ab44e8..0000000
--- a/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfacePackageWrapper.java
+++ /dev/null
@@ -1,116 +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.car.app.activity.renderer.surface;
-
-import android.annotation.SuppressLint;
-import android.os.Build;
-import android.os.Build.VERSION;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.view.SurfaceControlViewHost.SurfacePackage;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-
-/**
- * A parcelable that stores either a {@link LegacySurfacePackage} or a
- * {@link android.view.SurfaceControlViewHost.SurfacePackage}.
- *
- * Please note that {@link android.view.SurfaceControlViewHost.SurfacePackage} requires API level R.
- */
-//TODO(179714355): Investigate using Bundleable instead of Parcelable
-@SuppressLint({"BanParcelableUsage"})
-public final class SurfacePackageWrapper implements Parcelable {
- enum SurfacePackageType {
- LEGACY,
- SURFACE_CONTROL;
- }
-
- @Nullable
- private final Parcelable mSurfacePackage;
-
- /**
- * Creates a {@link SurfacePackageWrapper} that stores a
- * {@link android.view.SurfaceControlViewHost.SurfacePackage}.
- *
- * @param legacySurfacePackage the {@link android.view.SurfaceControlViewHost.SurfacePackage}
- * to be stored in this wrapper.
- */
- public SurfacePackageWrapper(@NonNull LegacySurfacePackage legacySurfacePackage) {
- mSurfacePackage = legacySurfacePackage;
- }
-
- /**
- * Creates a {@link SurfacePackageWrapper} that stores a {@link LegacySurfacePackage}.
- *
- * @param surfacePackage the {@link LegacySurfacePackage} to be stored in this wrapper.
- */
- @RequiresApi(Build.VERSION_CODES.R)
- public SurfacePackageWrapper(@NonNull SurfacePackage surfacePackage) {
- mSurfacePackage = surfacePackage;
- }
-
- SurfacePackageWrapper(Parcel parcel) {
- SurfacePackageType type = SurfacePackageType.values()[parcel.readInt()];
- if (type == SurfacePackageType.LEGACY) {
- mSurfacePackage = parcel.readParcelable(LegacySurfacePackage.class.getClassLoader());
- } else if (type == SurfacePackageWrapper.SurfacePackageType.SURFACE_CONTROL
- && VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- mSurfacePackage = parcel.readParcelable(SurfacePackage.class.getClassLoader());
- } else {
- mSurfacePackage = null;
- }
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel parcel, int flags) {
- SurfacePackageWrapper.SurfacePackageType type =
- mSurfacePackage instanceof LegacySurfacePackage
- ? SurfacePackageWrapper.SurfacePackageType.LEGACY
- : SurfacePackageWrapper.SurfacePackageType.SURFACE_CONTROL;
- parcel.writeInt(type.ordinal());
- parcel.writeParcelable(mSurfacePackage, flags);
- }
-
- @Nullable
- public Parcelable getSurfacePackage() {
- return mSurfacePackage;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @NonNull
- public static final Creator<SurfacePackageWrapper> CREATOR =
- new Creator<SurfacePackageWrapper>() {
- @NonNull
- @Override
- public SurfacePackageWrapper createFromParcel(@NonNull Parcel parcel) {
- return new SurfacePackageWrapper(parcel);
- }
-
- @NonNull
- @Override
- public SurfacePackageWrapper[] newArray(int size) {
- return new SurfacePackageWrapper[size];
- }
- };
-}
-
diff --git a/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceWrapper.java b/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceWrapper.java
index 4ad0e4a..43020a1 100644
--- a/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceWrapper.java
+++ b/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceWrapper.java
@@ -16,29 +16,32 @@
package androidx.car.app.activity.renderer.surface;
-import android.annotation.SuppressLint;
import android.os.IBinder;
-import android.os.Parcel;
-import android.os.Parcelable;
import android.view.Surface;
import android.view.SurfaceView;
+import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* A class holding the information needed to render the content on a surface.
*/
-//TODO(179714355): Investigate using Bundleable instead of Parcelable
-@SuppressLint("BanParcelableUsage")
-public final class SurfaceWrapper implements Parcelable {
+
+public final class SurfaceWrapper {
+ @Keep
@Nullable
- private final IBinder mHostToken;
- private final int mWidth;
- private final int mHeight;
- private final int mDisplayId;
- private final int mDensityDpi;
- private final Surface mSurface;
+ private IBinder mHostToken;
+ @Keep
+ private int mWidth;
+ @Keep
+ private int mHeight;
+ @Keep
+ private int mDisplayId;
+ @Keep
+ private int mDensityDpi;
+ @Keep
+ private Surface mSurface;
/**
* Creates a {@link SurfaceWrapper}.
@@ -61,28 +64,8 @@
mSurface = surface;
}
- SurfaceWrapper(@NonNull Parcel parcel) {
- mHostToken = parcel.readStrongBinder();
- mWidth = parcel.readInt();
- mHeight = parcel.readInt();
- mDisplayId = parcel.readInt();
- mDensityDpi = parcel.readInt();
- mSurface = parcel.readParcelable(Surface.class.getClassLoader());
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel parcel, int flags) {
- parcel.writeStrongBinder(mHostToken);
- parcel.writeInt(mWidth);
- parcel.writeInt(mHeight);
- parcel.writeInt(mDisplayId);
- parcel.writeInt(mDensityDpi);
- parcel.writeParcelable((Parcelable) mSurface, flags);
- }
-
- @Override
- public int describeContents() {
- return 0;
+ /** Empty constructor needed for serializations. **/
+ private SurfaceWrapper() {
}
@Nullable
@@ -110,19 +93,4 @@
Surface getSurface() {
return mSurface;
}
-
- @NonNull
- public static final Creator<SurfaceWrapper> CREATOR = new Creator<SurfaceWrapper>() {
- @NonNull
- @Override
- public SurfaceWrapper createFromParcel(@NonNull Parcel parcel) {
- return new SurfaceWrapper(parcel);
- }
-
- @NonNull
- @Override
- public SurfaceWrapper[] newArray(int size) {
- return new SurfaceWrapper[size];
- }
- };
}
diff --git a/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/TemplateSurfaceView.java b/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/TemplateSurfaceView.java
index 8f28ccd..6c5bfc0 100644
--- a/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/TemplateSurfaceView.java
+++ b/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/TemplateSurfaceView.java
@@ -35,7 +35,6 @@
import android.os.Build;
import android.os.Build.VERSION;
import android.os.IBinder;
-import android.os.Parcelable;
import android.os.RemoteException;
import android.util.AttributeSet;
import android.util.Log;
@@ -59,6 +58,8 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.car.app.activity.renderer.IProxyInputConnection;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
/**
* A surface view suitable for template rendering.
@@ -289,15 +290,21 @@
/**
* Updates the surface package. The surface package can be either a
- * {@link android.view.SurfaceControlViewHost.SurfacePackage} or a
- * {@link LegacySurfacePackage} wrapped in a {@link SurfacePackageWrapper}.
+ * {@link android.view.SurfaceControlViewHost.SurfacePackage} or a {@link LegacySurfacePackage}.
*/
- public void setSurfacePackage(@NonNull SurfacePackageWrapper surfacePackageWrapper) {
- Parcelable surfacePackage = surfacePackageWrapper.getSurfacePackage();
+ public void setSurfacePackage(@NonNull Bundleable bundle) {
+ Object surfacePackage;
+ try {
+ surfacePackage = bundle.get();
+ } catch (BundlerException e) {
+ Log.e(TAG, "Unable to deserialize surface package.");
+ return;
+ }
+
if (SUPPORTS_SURFACE_CONTROL && surfacePackage instanceof SurfacePackage) {
Api30Impl.setSurfacePackage(this, (SurfacePackage) surfacePackage);
} else if (surfacePackage instanceof LegacySurfacePackage) {
- setLegacySurfacePackage((LegacySurfacePackage) surfacePackage);
+ setSurfacePackage((LegacySurfacePackage) surfacePackage);
} else {
Log.e(TAG, "Unrecognized surface package");
}
@@ -309,14 +316,16 @@
* This control is used to communicate the UI events and focus with the host.
*/
@SuppressLint({"ClickableViewAccessibility"})
- private void setLegacySurfacePackage(LegacySurfacePackage surfacePackage) {
+ private void setSurfacePackage(LegacySurfacePackage surfacePackage) {
ISurfaceControl surfaceControl = surfacePackage.getSurfaceControl();
SurfaceWrapper surfaceWrapper = mSurfaceWrapperProvider.createSurfaceWrapper();
try {
- surfaceControl.setSurfaceWrapper(surfaceWrapper);
+ surfaceControl.setSurfaceWrapper(Bundleable.create(surfaceWrapper));
} catch (RemoteException e) {
Log.e(TAG, "Remote connection lost", e);
return;
+ } catch (BundlerException e) {
+ Log.e(TAG, "Unable to serialize surface wrapper", e);
}
mSurfaceControl = surfaceControl;
setOnTouchListener((view, event) -> handleTouchEvent(event));
diff --git a/car/app/app-activity/src/test/java/androidx/car/app/activity/CarAppActivityTest.java b/car/app/app-activity/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
index fa20071..254c005 100644
--- a/car/app/app-activity/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
+++ b/car/app/app-activity/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
@@ -60,7 +60,8 @@
import androidx.car.app.activity.renderer.IRendererService;
import androidx.car.app.activity.renderer.surface.LegacySurfacePackage;
import androidx.car.app.activity.renderer.surface.SurfaceControlCallback;
-import androidx.car.app.activity.renderer.surface.SurfacePackageWrapper;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
import androidx.lifecycle.Lifecycle;
import androidx.test.core.app.ActivityScenario;
import androidx.test.core.app.ApplicationProvider;
@@ -272,10 +273,9 @@
SurfaceControlCallback callback = mock(SurfaceControlCallback.class);
IRendererCallback rendererCallback = mock(IRendererCallback.class);
- SurfacePackageWrapper wrapper =
- new SurfacePackageWrapper(new LegacySurfacePackage(callback));
ICarAppActivity carAppActivity = mRenderServiceDelegate.getCarAppActivity();
- carAppActivity.setSurfacePackage(wrapper);
+ carAppActivity.setSurfacePackage(
+ Bundleable.create(new LegacySurfacePackage(callback)));
carAppActivity.registerRendererCallback(rendererCallback);
// Verify back events on surfaceView are sent to host.
@@ -320,7 +320,7 @@
y, metaState);
activity.mSurfaceView.dispatchGenericMotionEvent(event);
verify(rendererCallback, times(1)).onRotate(anyInt(), eq(false));
- } catch (RemoteException e) {
+ } catch (RemoteException | BundlerException e) {
fail(Log.getStackTraceString(e));
}
});
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationSession.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationSession.java
index ed1e8ec..cc41007 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationSession.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationSession.java
@@ -28,7 +28,6 @@
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
-import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
@@ -65,14 +64,18 @@
static final String URI_SCHEME = "samples";
static final String URI_HOST = "navigation";
- @Nullable NavigationScreen mNavigationScreen;
+ @Nullable
+ NavigationScreen mNavigationScreen;
- @Nullable SurfaceRenderer mNavigationCarSurface;
+ @Nullable
+ SurfaceRenderer mNavigationCarSurface;
// A reference to the navigation service used to get location updates and routing.
- @Nullable NavigationService mService;
+ @Nullable
+ NavigationService mService;
- @NonNull Action mSettingsAction;
+ @NonNull
+ Action mSettingsAction;
final NavigationService.Listener mServiceListener =
new NavigationService.Listener() {
@@ -109,15 +112,6 @@
public void onLocationChanged(Location location) {
mNavigationCarSurface.updateLocationString(getLocationString(location));
}
-
- @Override
- public void onStatusChanged(String provider, int status, Bundle extras) {}
-
- @Override
- public void onProviderEnabled(String provider) {}
-
- @Override
- public void onProviderDisabled(String provider) {}
};
// Monitors the state of the connection to the Navigation service.
@@ -200,8 +194,8 @@
new Action.Builder()
.setIcon(
new CarIcon.Builder(
- IconCompat.createWithResource(
- getCarContext(), R.drawable.ic_settings))
+ IconCompat.createWithResource(
+ getCarContext(), R.drawable.ic_settings))
.build())
.setOnClickListener(
() -> {
@@ -218,9 +212,9 @@
String action = intent.getAction();
if (action != null && CarContext.ACTION_NAVIGATE.equals(action)) {
CarToast.makeText(
- getCarContext(),
- "Navigation intent: " + intent.getDataString(),
- CarToast.LENGTH_LONG)
+ getCarContext(),
+ "Navigation intent: " + intent.getDataString(),
+ CarToast.LENGTH_LONG)
.show();
}
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/NavigationService.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/NavigationService.java
index fccc8be..9d07221 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/NavigationService.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/NavigationService.java
@@ -523,7 +523,8 @@
// Set the notification's background color on the car screen.
.setColor(
- getResources().getColor(R.color.nav_notification_background_color))
+ getResources().getColor(R.color.nav_notification_background_color,
+ null))
.setColorized(true)
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(
diff --git a/car/app/app-samples/showcase/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/src/main/AndroidManifest.xml
index 15e36e4..688b309 100644
--- a/car/app/app-samples/showcase/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/src/main/AndroidManifest.xml
@@ -66,6 +66,8 @@
android:excludeFromRecents="true"
android:noHistory="true"
android:launchMode="singleTask"
+ android:showWhenLocked="true"
+ android:turnScreenOn="true"
android:exported="false"/>
<receiver
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/StartScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/StartScreen.java
index 212ff6e..6b66d75 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/StartScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/StartScreen.java
@@ -74,10 +74,8 @@
.build())
.setTitle("Navigation Demos")
.setOnClickListener(
- () -> {
- getScreenManager()
- .push(new NavigationDemosScreen(getCarContext()));
- })
+ () -> getScreenManager()
+ .push(new NavigationDemosScreen(getCarContext())))
.setBrowsable(true)
.build());
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/TaskRestrictionDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/TaskRestrictionDemoScreen.java
index 6492f6c..de416c6 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/TaskRestrictionDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/TaskRestrictionDemoScreen.java
@@ -144,8 +144,4 @@
.build())
.build();
}
-
- boolean isToggleChecked() {
- return mToggleState;
- }
}
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/SamplePlaces.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/SamplePlaces.java
index 01256ff..1503215 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/SamplePlaces.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/SamplePlaces.java
@@ -55,7 +55,7 @@
return new SamplePlaces(demoScreen);
}
- /** Retusn the {@link ItemList} of the sample places. */
+ /** Return the {@link ItemList} of the sample places. */
@NonNull
public ItemList getPlaceList() {
ItemList.Builder listBuilder = new ItemList.Builder();
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/MiscDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/MiscDemoScreen.java
index 573791e..ca9bd9c 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/MiscDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/MiscDemoScreen.java
@@ -193,11 +193,7 @@
}
NotificationCompat.Builder builder;
- if (VERSION.SDK_INT >= VERSION_CODES.O) {
- builder = new NotificationCompat.Builder(getCarContext(), NOTIFICATION_CHANNEL_ID);
- } else {
- builder = new NotificationCompat.Builder(getCarContext());
- }
+ builder = new NotificationCompat.Builder(getCarContext(), NOTIFICATION_CHANNEL_ID);
Notification notification =
builder.setSmallIcon(R.drawable.ic_bug_report_24px)
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/OnPhoneActivity.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/OnPhoneActivity.java
index 440263d..493eec5 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/OnPhoneActivity.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/OnPhoneActivity.java
@@ -33,11 +33,7 @@
super.onCreate(savedInstanceState);
// Show Activity over lock screen and turn on device screen.
- getWindow()
- .addFlags(
- LayoutParams.FLAG_SHOW_WHEN_LOCKED
- | LayoutParams.FLAG_TURN_SCREEN_ON
- | LayoutParams.FLAG_KEEP_SCREEN_ON);
+ getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
setContentView(R.layout.phone_activity);
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationMapOnlyScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationMapOnlyScreen.java
index df0ec70..1057cee 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationMapOnlyScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationMapOnlyScreen.java
@@ -39,7 +39,7 @@
.addAction(
new Action.Builder()
.setTitle("BACK")
- .setOnClickListener(() -> finish())
+ .setOnClickListener(this::finish)
.build())
.build();
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationService.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationService.java
index 76eb58c..98c44a2 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationService.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationService.java
@@ -118,7 +118,7 @@
static Notification getNavigationNotification(
Context context, int notificationCount) {
NotificationCompat.Builder builder =
- getNotificationBuilder(context, NAV_NOTIFICATION_CHANNEL_ID);
+ new NotificationCompat.Builder(context, NAV_NOTIFICATION_CHANNEL_ID);
DirectionInfo directionInfo = getDirectionInfo(notificationCount);
return builder
// This title, text, and icon will be shown in both phone and car screen. These
@@ -167,13 +167,6 @@
.build();
}
- private static NotificationCompat.Builder getNotificationBuilder(
- Context context, String channelId) {
- return VERSION.SDK_INT >= VERSION_CODES.O
- ? new NotificationCompat.Builder(context, channelId)
- : new NotificationCompat.Builder(context);
- }
-
/**
* A {@link Handler.Callback} used to process the message queue for the notification service.
*/
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java
index 9a1c142..5d090be 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java
@@ -27,7 +27,7 @@
/** Creates a screen using the {@link PlaceListNavigationTemplate} */
public final class PlaceListNavigationTemplateDemoScreen extends Screen {
- private SamplePlaces mPlaces;
+ private final SamplePlaces mPlaces;
public PlaceListNavigationTemplateDemoScreen(@NonNull CarContext carContext) {
super(carContext);
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/RoutingDemoModels.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/RoutingDemoModels.java
index 072b1c7..0941b49 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/RoutingDemoModels.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/RoutingDemoModels.java
@@ -131,13 +131,11 @@
.addAction(
new Action.Builder()
.setOnClickListener(
- () -> {
- CarToast.makeText(
- carContext,
- "Bug reported!",
- CarToast.LENGTH_SHORT)
- .show();
- })
+ () -> CarToast.makeText(
+ carContext,
+ "Bug reported!",
+ CarToast.LENGTH_SHORT)
+ .show())
.setIcon(
new CarIcon.Builder(
IconCompat.createWithResource(
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/GridTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/GridTemplateDemoScreen.java
index 08111b7..239efec 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/GridTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/GridTemplateDemoScreen.java
@@ -84,17 +84,17 @@
@NonNull
@Override
public Template onGetTemplate() {
- ItemList.Builder gridItemlistBuilder = new ItemList.Builder();
+ ItemList.Builder gridItemListBuilder = new ItemList.Builder();
// Grid item with an icon and a title.
- gridItemlistBuilder.addItem(
+ gridItemListBuilder.addItem(
new GridItem.Builder()
.setImage(new CarIcon.Builder(mIcon).build())
.setTitle("Non-actionable")
.build());
// Grid item with a large icon, a title, onClickListener and no text.
- gridItemlistBuilder.addItem(
+ gridItemListBuilder.addItem(
new GridItem.Builder()
.setImage(new CarIcon.Builder(mIcon).build(), GridItem.IMAGE_TYPE_LARGE)
.setTitle("Second Item")
@@ -108,7 +108,7 @@
.build());
// Grid item with an icon marked as icon, a title, a text and a toggle in unchecked state.
- gridItemlistBuilder.addItem(
+ gridItemListBuilder.addItem(
new GridItem.Builder()
.setImage(new CarIcon.Builder(mIcon).build(), GridItem.IMAGE_TYPE_ICON)
.setTitle("Third Item")
@@ -128,27 +128,25 @@
// Grid item with an image, a title, a long text and a toggle that takes some time to
// update.
if (mIsFourthItemLoading) {
- gridItemlistBuilder.addItem(
+ gridItemListBuilder.addItem(
new GridItem.Builder()
.setTitle("Fourth")
.setText(mFourthItemToggleState ? "On" : "Off")
.setLoading(true)
.build());
} else {
- gridItemlistBuilder.addItem(
+ gridItemListBuilder.addItem(
new GridItem.Builder()
.setImage(new CarIcon.Builder(mImage).build())
.setTitle("Fourth")
.setText(mFourthItemToggleState ? "On" : "Off")
.setOnClickListener(
- () -> {
- triggerFourthItemLoading();
- })
+ this::triggerFourthItemLoading)
.build());
}
// Grid item with a large image, a long title, no text and a toggle in unchecked state.
- gridItemlistBuilder.addItem(
+ gridItemListBuilder.addItem(
new GridItem.Builder()
.setImage(new CarIcon.Builder(mImage).build(), GridItem.IMAGE_TYPE_LARGE)
.setTitle("Fifth Item has a long title set")
@@ -165,7 +163,7 @@
.build());
// Grid item with an image marked as an icon, a long title, a long text and onClickListener.
- gridItemlistBuilder.addItem(
+ gridItemListBuilder.addItem(
new GridItem.Builder()
.setImage(new CarIcon.Builder(mImage).build(), GridItem.IMAGE_TYPE_ICON)
.setTitle("Sixth Item has a long title set")
@@ -181,7 +179,7 @@
return new GridTemplate.Builder()
.setHeaderAction(Action.APP_ICON)
- .setSingleList(gridItemlistBuilder.build())
+ .setSingleList(gridItemListBuilder.build())
.setTitle("Grid Template Demo")
.setActionStrip(
new ActionStrip.Builder()
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PaneTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PaneTemplateDemoScreen.java
index ba6ff53..f525791 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PaneTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PaneTemplateDemoScreen.java
@@ -87,26 +87,22 @@
.setTitle("Search")
.setBackgroundColor(CarColor.BLUE)
.setOnClickListener(
- () -> {
- CarToast.makeText(
- getCarContext(),
- "Search button pressed",
- LENGTH_SHORT)
- .show();
- })
+ () -> CarToast.makeText(
+ getCarContext(),
+ "Search button pressed",
+ LENGTH_SHORT)
+ .show())
.build())
.addAction(
new Action.Builder()
.setTitle("Options")
.setBackgroundColor(CarColor.YELLOW)
.setOnClickListener(
- () -> {
- CarToast.makeText(
- getCarContext(),
- "Options button pressed",
- LENGTH_SHORT)
- .show();
- })
+ () -> CarToast.makeText(
+ getCarContext(),
+ "Options button pressed",
+ LENGTH_SHORT)
+ .show())
.build());
return new PaneTemplate.Builder(paneBuilder.build())
@@ -117,14 +113,12 @@
new Action.Builder()
.setTitle("Settings")
.setOnClickListener(
- () -> {
- CarToast.makeText(
- getCarContext(),
- "Settings button"
- + " pressed",
- LENGTH_SHORT)
- .show();
- })
+ () -> CarToast.makeText(
+ getCarContext(),
+ "Settings button"
+ + " pressed",
+ LENGTH_SHORT)
+ .show())
.build())
.build())
.setTitle("Pane Template Demo")
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
index d107f34..d6f5b95 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
@@ -17,7 +17,6 @@
package androidx.compose.foundation.text.selection
import androidx.compose.foundation.text.isInTouchMode
-import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
@@ -97,8 +96,7 @@
// cross-composable selection.
SimpleLayout(modifier = modifier.then(manager.modifier)) {
children()
- val isFocused = manager.interactionSource.collectIsFocusedAsState()
- if (isInTouchMode && isFocused.value) {
+ if (isInTouchMode && manager.hasFocus) {
manager.selection?.let {
for (isStartHandle in listOf(true, false)) {
SelectionHandle(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
index d630533..6448b20 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
@@ -18,7 +18,6 @@
package androidx.compose.foundation.text.selection
-import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.focusable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
@@ -95,9 +94,9 @@
var focusRequester: FocusRequester = FocusRequester()
/**
- * MutableInteractionSource for the selection container, containing focus interactions.
+ * Return true if the corresponding SelectionContainer is focused.
*/
- val interactionSource: MutableInteractionSource = MutableInteractionSource()
+ var hasFocus: Boolean by mutableStateOf(false)
/**
* Modifier for selection container.
@@ -106,11 +105,12 @@
.onGloballyPositioned { containerLayoutCoordinates = it }
.focusRequester(focusRequester)
.onFocusChanged { focusState ->
- if (!focusState.isFocused) {
+ if (!focusState.isFocused && hasFocus) {
onRelease()
}
+ hasFocus = focusState.isFocused
}
- .focusable(interactionSource = interactionSource)
+ .focusable()
.onKeyEvent {
if (isCopyKeyEvent(it)) {
copy()
@@ -126,8 +126,10 @@
var containerLayoutCoordinates: LayoutCoordinates? = null
set(value) {
field = value
- updateHandleOffsets()
- updateSelectionToolbarPosition()
+ if (hasFocus) {
+ updateHandleOffsets()
+ updateSelectionToolbarPosition()
+ }
}
/**
@@ -179,8 +181,8 @@
isStartHandle = true,
longPress = touchMode
)
- hideSelectionToolbar()
focusRequester.requestFocus()
+ hideSelectionToolbar()
}
selectionRegistrar.onSelectionUpdateCallback =
@@ -334,25 +336,27 @@
* the copy method as a callback when "copy" is clicked.
*/
internal fun showSelectionToolbar() {
- selection?.let {
- textToolbar?.showMenu(
- getContentRect(),
- onCopyRequested = {
- copy()
- onRelease()
- }
- )
+ if (hasFocus) {
+ selection?.let {
+ textToolbar?.showMenu(
+ getContentRect(),
+ onCopyRequested = {
+ copy()
+ onRelease()
+ }
+ )
+ }
}
}
internal fun hideSelectionToolbar() {
- if (textToolbar?.status == TextToolbarStatus.Shown) {
+ if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
textToolbar?.hide()
}
}
private fun updateSelectionToolbarPosition() {
- if (textToolbar?.status == TextToolbarStatus.Shown) {
+ if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
showSelectionToolbar()
}
}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
index d96c2df..61026bd 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerTest.kt
@@ -31,6 +31,7 @@
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.isNull
import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.spy
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
@@ -368,6 +369,7 @@
),
handlesCrossed = true
)
+ selectionManager.hasFocus = true
selectionManager.showSelectionToolbar()
@@ -381,6 +383,39 @@
}
@Test
+ fun showSelectionToolbar_withoutFocus_notTrigger_textToolbar_showMenu() {
+ val text = "Text Demo"
+ val annotatedString = AnnotatedString(text = text)
+ val startOffset = text.indexOf('m')
+ val endOffset = text.indexOf('x')
+ selectable.textToReturn = annotatedString
+ selectionManager.selection = Selection(
+ start = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = startOffset,
+ selectable = selectable
+ ),
+ end = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = endOffset,
+ selectable = selectable
+ ),
+ handlesCrossed = true
+ )
+ selectionManager.hasFocus = false
+
+ selectionManager.showSelectionToolbar()
+
+ verify(textToolbar, never()).showMenu(
+ eq(Rect.Zero),
+ any(),
+ isNull(),
+ isNull(),
+ isNull()
+ )
+ }
+
+ @Test
fun cancel_selection_calls_getSelection_selection_becomes_null() {
val fakeSelection =
Selection(
diff --git a/compose/integration-tests/docs-snippets/build.gradle b/compose/integration-tests/docs-snippets/build.gradle
index 289c415..e53dfad 100644
--- a/compose/integration-tests/docs-snippets/build.gradle
+++ b/compose/integration-tests/docs-snippets/build.gradle
@@ -41,6 +41,7 @@
implementation(project(":navigation:navigation-compose"))
implementation(project(":activity:activity-compose"))
implementation(project(":lifecycle:lifecycle-viewmodel-compose"))
+ implementation(project(":paging:paging-compose"))
implementation(KOTLIN_STDLIB)
implementation(KOTLIN_REFLECT)
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/animation/Animation.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/animation/Animation.kt
new file mode 100644
index 0000000..4b2b047
--- /dev/null
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/animation/Animation.kt
@@ -0,0 +1,571 @@
+/*
+ * 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.
+ */
+
+// Ignore lint warnings in documentation snippets
+@file:Suppress("CanBeVal", "UNUSED_VARIABLE", "RemoveExplicitTypeArguments", "unused")
+@file:SuppressLint("ModifierInspectorInfo", "NewApi")
+
+package androidx.compose.integration.docs.animation
+
+import android.annotation.SuppressLint
+import androidx.compose.animation.Animatable
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.Crossfade
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.AnimationVector2D
+import androidx.compose.animation.core.Easing
+import androidx.compose.animation.core.FastOutLinearInEasing
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.TargetBasedAnimation
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.TwoWayConverter
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.animateDp
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.animateRect
+import androidx.compose.animation.core.animateValueAsState
+import androidx.compose.animation.core.calculateTargetValue
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.repeatable
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.animation.splineBasedDecay
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.horizontalDrag
+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.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.integration.docs.animation.UpdateTransitionEnumState.BoxState
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChange
+import androidx.compose.ui.input.pointer.util.VelocityTracker
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import org.junit.Rule
+import org.junit.Test
+import kotlin.math.absoluteValue
+import kotlin.math.roundToInt
+
+/**
+ * This file lets DevRel track changes to snippets present in
+ * https://developer.android.com/jetpack/compose/animation
+ *
+ * No action required if it's modified.
+ */
+
+@OptIn(ExperimentalAnimationApi::class)
+@Composable
+fun AnimatedVisibilitySimple() {
+ var editable by remember { mutableStateOf(true) }
+ AnimatedVisibility(visible = editable) {
+ Text(text = "Edit")
+ }
+}
+
+@OptIn(ExperimentalAnimationApi::class)
+@Composable
+fun AnimatedVisibilityWithEnterAndExit() {
+ var visible by remember { mutableStateOf(true) }
+ AnimatedVisibility(
+ visible = visible,
+ enter = slideInVertically(
+ // Slide in from 40 px from the top.
+ initialOffsetY = { -40 }
+ ) + expandVertically(
+ // Expand from the top.
+ expandFrom = Alignment.Top
+ ) + fadeIn(
+ // Fade in with the initial alpha of 0.3f.
+ initialAlpha = 0.3f
+ ),
+ exit = slideOutVertically() + shrinkVertically() + fadeOut()
+ ) {
+ Text("Hello", Modifier.fillMaxWidth().height(200.dp))
+ }
+}
+
+@Composable
+fun AnimateContentSizeSimple() {
+ var message by remember { mutableStateOf("Hello") }
+ Box(
+ modifier = Modifier.background(Color.Blue).animateContentSize()
+ ) {
+ Text(text = message)
+ }
+}
+
+@Composable
+fun CrossfadeSimple() {
+ var currentPage by remember { mutableStateOf("A") }
+ Crossfade(targetState = currentPage) { screen ->
+ when (screen) {
+ "A" -> Text("Page A")
+ "B" -> Text("Page B")
+ }
+ }
+}
+
+@Composable
+fun AnimateAsStateSimple(enabled: Boolean) {
+ val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
+ Box(
+ Modifier.fillMaxSize()
+ .graphicsLayer(alpha = alpha)
+ .background(Color.Red)
+ )
+}
+
+@Composable
+fun AnimatableSimple(ok: Boolean) {
+ // Start out gray and animate to green/red based on `ok`
+ val color = remember { Animatable(Color.Gray) }
+ LaunchedEffect(ok) {
+ color.animateTo(if (ok) Color.Green else Color.Red)
+ }
+ Box(Modifier.fillMaxSize().background(color.value))
+}
+
+object UpdateTransitionEnumState {
+ enum class BoxState {
+ Collapsed,
+ Expanded
+ }
+}
+
+@Composable
+fun UpdateTransitionInstance() {
+ var currentState by remember { mutableStateOf(BoxState.Collapsed) }
+ val transition = updateTransition(currentState)
+}
+
+@Composable
+fun UpdateTransitionAnimationValues(transition: Transition<BoxState>) {
+ val rect by transition.animateRect { state ->
+ when (state) {
+ BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
+ BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
+ }
+ }
+ val borderWidth by transition.animateDp { state ->
+ when (state) {
+ BoxState.Collapsed -> 1.dp
+ BoxState.Expanded -> 0.dp
+ }
+ }
+}
+
+@Composable
+fun UpdateTransitionTransitionSpec(transition: Transition<BoxState>) {
+ val color by transition.animateColor(
+ transitionSpec = {
+ when {
+ BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
+ spring(stiffness = 50f)
+ else ->
+ tween(durationMillis = 500)
+ }
+ }
+ ) { state ->
+ when (state) {
+ BoxState.Collapsed -> MaterialTheme.colors.primary
+ BoxState.Expanded -> MaterialTheme.colors.background
+ }
+ }
+}
+
+@Composable
+fun UpdateTransitionMutableTransitionState() {
+ // Start in collapsed state and immediately animate to expanded
+ var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
+ currentState.targetState = BoxState.Expanded
+ val transition = updateTransition(currentState)
+ // ……
+}
+
+object UpdateTransitionEncapsulating {
+ enum class BoxState { Collapsed, Expanded }
+
+ @Composable
+ fun AnimatingBox(boxState: BoxState) {
+ val transitionData = updateTransitionData(boxState)
+ // UI tree
+ Box(
+ modifier = Modifier
+ .background(transitionData.color)
+ .size(transitionData.size)
+ )
+ }
+
+ // Holds the animation values.
+ private class TransitionData(
+ color: State<Color>,
+ size: State<Dp>
+ ) {
+ val color by color
+ val size by size
+ }
+
+ // Create a Transition and return its animation values.
+ @Composable
+ private fun updateTransitionData(boxState: BoxState): TransitionData {
+ val transition = updateTransition(boxState)
+ val color = transition.animateColor { state ->
+ when (state) {
+ BoxState.Collapsed -> Color.Gray
+ BoxState.Expanded -> Color.Red
+ }
+ }
+ val size = transition.animateDp { state ->
+ when (state) {
+ BoxState.Collapsed -> 64.dp
+ BoxState.Expanded -> 128.dp
+ }
+ }
+ return remember(transition) { TransitionData(color, size) }
+ }
+}
+
+@Composable
+fun RememberInfiniteTransitionSimple() {
+ val infiniteTransition = rememberInfiniteTransition()
+ val color by infiniteTransition.animateColor(
+ initialValue = Color.Red,
+ targetValue = Color.Green,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1000, easing = LinearEasing),
+ repeatMode = RepeatMode.Reverse
+ )
+ )
+
+ Box(Modifier.fillMaxSize().background(color))
+}
+
+@Composable
+fun TargetBasedAnimationSimple(someCustomCondition: () -> Boolean, scope: CoroutineScope) {
+ val anim = remember {
+ TargetBasedAnimation(
+ animationSpec = tween(200),
+ typeConverter = Float.VectorConverter,
+ initialValue = 200f,
+ targetValue = 1000f
+ )
+ }
+ var playTime by remember { mutableStateOf(0L) }
+
+ scope.launch {
+ val startTime = withFrameNanos { it }
+
+ do {
+ playTime = withFrameNanos { it } - startTime
+ val animationValue = anim.getValueFromNanos(playTime)
+ } while (someCustomCondition())
+ }
+}
+
+@Composable
+fun AnimationSpecTween(enabled: Boolean) {
+ val alpha: Float by animateFloatAsState(
+ targetValue = if (enabled) 1f else 0.5f,
+ // Configure the animation duration and easing.
+ animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
+ )
+}
+
+@Composable
+fun AnimationSpecSpring() {
+ val value by animateFloatAsState(
+ targetValue = 1f,
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioHighBouncy,
+ stiffness = Spring.StiffnessMedium
+ )
+ )
+}
+
+@Composable
+fun AnimationSpecTween() {
+ val value by animateFloatAsState(
+ targetValue = 1f,
+ animationSpec = tween(
+ durationMillis = 300,
+ delayMillis = 50,
+ easing = LinearOutSlowInEasing
+ )
+ )
+}
+
+@Composable
+fun AnimationSpecKeyframe() {
+ val value by animateFloatAsState(
+ targetValue = 1f,
+ animationSpec = keyframes {
+ durationMillis = 375
+ 0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
+ 0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
+ 0.4f at 75 // ms
+ 0.4f at 225 // ms
+ }
+ )
+}
+
+@Composable
+fun AnimationSpecRepeatable() {
+ val value by animateFloatAsState(
+ targetValue = 1f,
+ animationSpec = repeatable(
+ iterations = 3,
+ animation = tween(durationMillis = 300),
+ repeatMode = RepeatMode.Reverse
+ )
+ )
+}
+
+@Composable
+fun AnimationSpecInfiniteRepeatable() {
+ val value by animateFloatAsState(
+ targetValue = 1f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(durationMillis = 300),
+ repeatMode = RepeatMode.Reverse
+ )
+ )
+}
+
+@Composable
+fun AnimationSpecSnap() {
+ val value by animateFloatAsState(
+ targetValue = 1f,
+ animationSpec = snap(delayMillis = 50)
+ )
+}
+
+object Easing {
+ val CustomEasing = Easing { fraction -> fraction * fraction }
+
+ @Composable
+ fun EasingUsage() {
+ val value by animateFloatAsState(
+ targetValue = 1f,
+ animationSpec = tween(
+ durationMillis = 300,
+ easing = CustomEasing
+ )
+ )
+ // ……
+ }
+}
+
+object AnimationVectorTwoWayConverter {
+ val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
+ TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
+}
+
+object AnimationVectorCustomType {
+ data class MySize(val width: Dp, val height: Dp)
+
+ @Composable
+ fun MyAnimation(targetSize: MySize) {
+ val animSize: MySize by animateValueAsState<MySize, AnimationVector2D>(
+ targetSize,
+ TwoWayConverter(
+ convertToVector = { size: MySize ->
+ // Extract a float value from each of the `Dp` fields.
+ AnimationVector2D(size.width.value, size.height.value)
+ },
+ convertFromVector = { vector: AnimationVector2D ->
+ MySize(vector.v1.dp, vector.v2.dp)
+ }
+ )
+ )
+ }
+}
+
+object GestureAndAnimationSimple {
+ @Composable
+ fun Gesture() {
+ val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .pointerInput(Unit) {
+ coroutineScope {
+ while (true) {
+ // Detect a tap event and obtain its position.
+ val position = awaitPointerEventScope {
+ awaitFirstDown().position
+ }
+ launch {
+ // Animate to the tap position.
+ offset.animateTo(position)
+ }
+ }
+ }
+ }
+ ) {
+ Circle(modifier = Modifier.offset { offset.value.toIntOffset() })
+ }
+ }
+
+ private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
+}
+
+object GestureAndAnimationSwipeToDismiss {
+ fun Modifier.swipeToDismiss(
+ onDismissed: () -> Unit
+ ): Modifier = composed {
+ val offsetX = remember { Animatable(0f) }
+ pointerInput(Unit) {
+ val decay = splineBasedDecay<Float>(this)
+ coroutineScope {
+ while (true) {
+ // Detect a touch down event.
+ val pointerId = awaitPointerEventScope { awaitFirstDown().id }
+ val velocityTracker = VelocityTracker()
+ // Intercept an ongoing animation (if there's one).
+ offsetX.stop()
+ awaitPointerEventScope {
+ horizontalDrag(pointerId) { change ->
+ // Update the animation value with touch events.
+ launch {
+ offsetX.snapTo(
+ offsetX.value + change.positionChange().x
+ )
+ }
+ velocityTracker.addPosition(
+ change.uptimeMillis,
+ change.position
+ )
+ }
+ }
+ val velocity = velocityTracker.calculateVelocity().x
+ val targetOffsetX = decay.calculateTargetValue(
+ offsetX.value,
+ velocity
+ )
+ // The animation stops when it reaches the bounds.
+ offsetX.updateBounds(
+ lowerBound = -size.width.toFloat(),
+ upperBound = size.width.toFloat()
+ )
+ launch {
+ if (targetOffsetX.absoluteValue <= size.width) {
+ // Not enough velocity; Slide back.
+ offsetX.animateTo(
+ targetValue = 0f,
+ initialVelocity = velocity
+ )
+ } else {
+ // The element was swiped away.
+ offsetX.animateDecay(velocity, decay)
+ onDismissed()
+ }
+ }
+ }
+ }
+ }
+ .offset { IntOffset(offsetX.value.roundToInt(), 0) }
+ }
+}
+
+object Testing {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun testAnimationWithClock() {
+ // Pause animations
+ rule.mainClock.autoAdvance = false
+ var enabled by mutableStateOf(false)
+ rule.setContent {
+ val color by animateColorAsState(
+ targetValue = if (enabled) Color.Red else Color.Green,
+ animationSpec = tween(durationMillis = 250)
+ )
+ Box(Modifier.size(64.dp).background(color))
+ }
+
+ // Initiate the animation.
+ enabled = true
+
+ // Let the animation proceed.
+ rule.mainClock.advanceTimeBy(50L)
+
+ // Compare the result with the image showing the expected result.
+ rule.onRoot().captureToImage().assertAgainstGolden()
+ }
+}
+
+private fun ImageBitmap.assertAgainstGolden() {
+}
+
+@Composable
+private fun Circle(modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier
+ .size(64.dp)
+ .background(Color.Gray, CircleShape)
+ )
+}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/gestures/Gestures.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/gestures/Gestures.kt
index 6ef6d29..2b55af8 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/gestures/Gestures.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/gestures/Gestures.kt
@@ -58,7 +58,6 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.pointerInput
@@ -177,12 +176,12 @@
fun ScrollableSample() {
// actual composable state
var offset by remember { mutableStateOf(0f) }
- // state for Scrollable, describes how to consume scrolling delta and update offset
Box(
Modifier
.size(150.dp)
.scrollable(
orientation = Orientation.Vertical,
+ // Scrollable state: describes how to consume scrolling delta and update offset
state = rememberScrollableState { delta ->
offset += delta
delta
@@ -216,9 +215,7 @@
private object GesturesSnippet6 {
@Composable
fun NestedSample() {
- val gradient = Brush.verticalGradient(
- listOf(Color.Gray, Color.White), 0.0f, 1000.0f, TileMode.Repeated
- )
+ val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
Box(
modifier = Modifier
.background(Color.LightGray)
@@ -280,19 +277,19 @@
}
@Composable private fun GesturesSnippet8() {
Box(modifier = Modifier.fillMaxSize()) {
- val offsetX = remember { mutableStateOf(0f) }
- val offsetY = remember { mutableStateOf(0f) }
+ var offsetX by remember { mutableStateOf(0f) }
+ var offsetY by remember { mutableStateOf(0f) }
Box(
Modifier
- .offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
+ .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(Color.Blue)
.size(50.dp)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
- offsetX.value = (offsetX.value + dragAmount.x)
- offsetY.value = (offsetY.value + dragAmount.y)
+ offsetX += dragAmount.x
+ offsetY += dragAmount.y
}
}
)
@@ -314,7 +311,7 @@
val swipeableState = rememberSwipeableState(0)
val sizePx = with(LocalDensity.current) { squareSize.toPx() }
- val anchors = mapOf(0f to 0, sizePx to 1)
+ val anchors = mapOf(0f to 0, sizePx to 1) // Maps anchor points (in px) to states
Box(
modifier = Modifier
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/lists/Lists.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/lists/Lists.kt
new file mode 100644
index 0000000..6ddc570
--- /dev/null
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/lists/Lists.kt
@@ -0,0 +1,331 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("unused", "UNUSED_PARAMETER")
+
+package androidx.compose.integration.docs.lists
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.lazy.GridCells
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.LazyVerticalGrid
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.unit.dp
+import androidx.paging.Pager
+import androidx.paging.compose.collectAsLazyPagingItems
+import androidx.paging.compose.items
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+internal object ListsSnippetsColumn {
+ @Composable
+ fun MessageList(messages: List<Message>) {
+ Column {
+ messages.forEach { message ->
+ MessageRow(message)
+ }
+ }
+ }
+}
+
+internal object ListsSnippetsLazyListScope {
+ @Composable
+ fun Snippet() {
+ LazyColumn {
+ // Add a single item
+ item {
+ Text(text = "First item")
+ }
+
+ // Add 5 items
+ items(5) { index ->
+ Text(text = "Item: $index")
+ }
+
+ // Add another single item
+ item {
+ Text(text = "Last item")
+ }
+ }
+ }
+
+ @Composable
+ fun MessageList(messages: List<Message>) {
+ LazyColumn {
+ items(messages) { message ->
+ MessageRow(message)
+ }
+ }
+ }
+}
+
+internal object ListsSnippetsContentPadding {
+ @Composable
+ fun Snippet() {
+ LazyColumn(
+ contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
+ ) {
+ // ...
+ }
+ }
+}
+
+internal object ListsSnippetsContentSpacing {
+ @Composable
+ fun Column() {
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ // ...
+ }
+ }
+
+ @Composable
+ fun Row() {
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ // ...
+ }
+ }
+}
+
+internal object ListsSnippetsStickyHeaders {
+ @OptIn(ExperimentalFoundationApi::class)
+ @Composable
+ fun ListWithHeader(items: List<Item>) {
+ LazyColumn {
+ stickyHeader {
+ Header()
+ }
+
+ items(items) { item ->
+ ItemRow(item)
+ }
+ }
+ }
+
+ // TODO: This ideally would be done in the ViewModel
+ val grouped = contacts.groupBy { it.firstName[0] }
+
+ @OptIn(ExperimentalFoundationApi::class)
+ @Composable
+ fun ContactsList(grouped: Map<Char, List<Contact>>) {
+ LazyColumn {
+ grouped.forEach { (initial, contactsForInitial) ->
+ stickyHeader {
+ CharacterHeader(initial)
+ }
+
+ items(contactsForInitial) { contact ->
+ ContactListItem(contact)
+ }
+ }
+ }
+ }
+}
+
+internal object ListsSnippetsGrids {
+ @OptIn(ExperimentalFoundationApi::class)
+ @Composable
+ fun PhotoGrid(photos: List<Photo>) {
+ LazyVerticalGrid(
+ cells = GridCells.Adaptive(minSize = 128.dp)
+ ) {
+ items(photos) { photo ->
+ PhotoItem(photo)
+ }
+ }
+ }
+}
+
+internal object ListsSnippetsReactingScrollPosition1 {
+ @Composable
+ fun MessageList(messages: List<Message>) {
+ // Remember our own LazyListState
+ val listState = rememberLazyListState()
+
+ // Provide it to LazyColumn
+ LazyColumn(state = listState) {
+ // ...
+ }
+ }
+}
+
+internal object ListsSnippetsReactingScrollPosition2 {
+ @OptIn(ExperimentalAnimationApi::class)
+ @Composable
+ fun MessageList(messages: List<Message>) {
+ Box {
+ val listState = rememberLazyListState()
+
+ LazyColumn(state = listState) {
+ // ...
+ }
+
+ // Show the button if the first visible item is past
+ // the first item. We use a remembered derived state to
+ // minimize unnecessary compositions
+ val showButton by remember {
+ derivedStateOf {
+ listState.firstVisibleItemIndex > 0
+ }
+ }
+
+ AnimatedVisibility(visible = showButton) {
+ ScrollToTopButton()
+ }
+ }
+ }
+}
+
+@Suppress("SimplifyBooleanWithConstants")
+internal object ListsSnippetsReactingScrollPosition3 {
+ @Composable
+ fun Snippet(messages: List<Message>) {
+ val listState = rememberLazyListState()
+
+ LazyColumn(state = listState) {
+ // ...
+ }
+
+ LaunchedEffect(listState) {
+ snapshotFlow { listState.firstVisibleItemIndex }
+ .map { index -> index > 0 }
+ .distinctUntilChanged()
+ .filter { it == true }
+ .collect {
+ MyAnalyticsService.sendScrolledPastFirstItemEvent()
+ }
+ }
+ }
+}
+
+internal object ListsSnippetsControllingScrollPosition {
+ @Composable
+ fun MessageList(messages: List<Message>) {
+ val listState = rememberLazyListState()
+ // Remember a CoroutineScope to be able to launch
+ val coroutineScope = rememberCoroutineScope()
+
+ LazyColumn(state = listState) {
+ // ...
+ }
+
+ ScrollToTopButton(
+ onClick = {
+ coroutineScope.launch {
+ // Animate scroll to the first item
+ listState.animateScrollToItem(index = 0)
+ }
+ },
+ )
+ }
+}
+
+internal object ListsSnippetsPaging {
+ @Composable
+ fun MessageList(pager: Pager<Int, Message>) {
+ val lazyPagingItems = pager.flow.collectAsLazyPagingItems()
+
+ LazyColumn {
+ items(lazyPagingItems) { message ->
+ if (message != null) {
+ MessageRow(message)
+ } else {
+ MessagePlaceholder()
+ }
+ }
+ }
+ }
+}
+
+internal object ListsSnippetsItemKeys {
+ @Composable
+ fun MessageList(messages: List<Message>) {
+ LazyColumn {
+ items(
+ items = messages,
+ key = { message ->
+ // Return a stable + unique key for the item
+ message.id
+ }
+ ) { message ->
+ MessageRow(message)
+ }
+ }
+ }
+}
+
+// ========================
+// Fakes below
+// ========================
+
+class Message(val id: Long)
+class Item
+
+data class Contact(val firstName: String)
+val contacts = listOf<Contact>()
+
+class Photo
+val photos = listOf<Photo>()
+
+@Composable
+private fun MessageRow(message: Message) = Unit
+
+@Composable
+private fun MessagePlaceholder() = Unit
+
+@Composable
+private fun ItemRow(item: Item) = Unit
+
+@Composable
+private fun Header() = Unit
+
+@Composable
+private fun CharacterHeader(initial: Char) = Unit
+
+@Composable
+private fun ContactListItem(contact: Contact) = Unit
+
+@Composable
+private fun PhotoItem(photo: Photo) = Unit
+
+@Composable
+private fun ScrollToTopButton(onClick: () -> Unit = {}) = Unit
+
+object MyAnalyticsService {
+ fun sendScrolledPastFirstItemEvent() = Unit
+}
\ No newline at end of file
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt
index 7912d72..3feae6b 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt
@@ -28,7 +28,7 @@
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.SemanticsActions
-import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertAll
import androidx.compose.ui.test.assertAny
@@ -82,26 +82,18 @@
import androidx.compose.ui.test.hasAnyChild
import androidx.compose.ui.test.hasAnyDescendant
import androidx.compose.ui.test.hasAnySibling
-import androidx.compose.ui.test.hasBottomPosition
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.hasContentDescription
-import androidx.compose.ui.test.hasHeight
-import androidx.compose.ui.test.hasHeightAtLeast
import androidx.compose.ui.test.hasImeAction
-import androidx.compose.ui.test.hasLeftPosition
import androidx.compose.ui.test.hasNoClickAction
import androidx.compose.ui.test.hasNoScrollAction
import androidx.compose.ui.test.hasParent
import androidx.compose.ui.test.hasProgressBarRangeInfo
-import androidx.compose.ui.test.hasRightPosition
import androidx.compose.ui.test.hasScrollAction
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.hasStateDescription
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
-import androidx.compose.ui.test.hasTopPosition
-import androidx.compose.ui.test.hasWidth
-import androidx.compose.ui.test.hasWidthAtLeast
import androidx.compose.ui.test.height
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.isEnabled
@@ -172,6 +164,8 @@
import androidx.compose.ui.test.width
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
import android.view.KeyEvent as AndroidKeyEvent
import android.view.KeyEvent.ACTION_DOWN as ActionDown
import android.view.KeyEvent.KEYCODE_A as KeyCodeA
@@ -226,7 +220,6 @@
)
// MATCHERS
- @OptIn(ExperimentalTestApi::class)
composeTestRule.onNode(
hasClickAction() and
hasNoClickAction() and
@@ -239,14 +232,6 @@
hasStateDescription("label") and
hasTestTag("tag") and
hasText("text") and
- hasLeftPosition(0.dp) and
- hasTopPosition(0.dp) and
- hasRightPosition(0.dp) and
- hasBottomPosition(0.dp) and
- hasWidth(0.dp) and
- hasWidthAtLeast(0.dp) and
- hasHeight(0.dp) and
- hasHeightAtLeast(0.dp) and
isDialog() and
isEnabled() and
isFocusable() and
@@ -396,6 +381,7 @@
runOnIdle { }
runOnUiThread { }
waitForIdle()
+ waitUntil { true }
mainClock.apply {
autoAdvance
currentTime
@@ -403,6 +389,11 @@
advanceTimeByFrame()
advanceTimeUntil { true }
}
+ registerIdlingResource(idlingResource)
+ unregisterIdlingResource(idlingResource)
+ }
+ GlobalScope.launch {
+ nonAndroidComposeTestRule.awaitIdle()
}
// ANDROID COMPOSE TEST RULE
@@ -428,4 +419,8 @@
private val nonAndroidComposeTestRule = createComposeRule()
private val keyEvent2 = KeyEvent(AndroidKeyEvent(ActionDown, KeyCodeA))
private val offset = Offset(0f, 0f)
-private val rangeInfo = ProgressBarRangeInfo(0f, 0f..1f)
\ No newline at end of file
+private val rangeInfo = ProgressBarRangeInfo(0f, 0f..1f)
+private val idlingResource = object : IdlingResource {
+ override val isIdleNow: Boolean
+ get() = TODO("Stub!")
+}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
index f69f42d..9060916 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
@@ -224,6 +224,20 @@
composeTestRule.unregisterIdlingResource(idlingResource)
}
+private fun TestingSyncSnippets5() {
+ composeTestRule.mainClock.autoAdvance = true // default
+ composeTestRule.waitForIdle() // Advances the clock until Compose is idle
+
+ composeTestRule.mainClock.autoAdvance = false
+ composeTestRule.waitForIdle() // Only waits for Idling Resources to become idle
+}
+
+private fun TestingSyncSnippets6and7() {
+ composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }
+
+ composeTestRule.waitUntil(timeoutMs) { condition }
+}
+
private object TestingSemanticsSnippets1 {
// Creates a Semantics property of type boolean
val PickedDateKey = SemanticsPropertyKey<Long>("PickedDate")
@@ -295,7 +309,9 @@
private lateinit var key: SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>
private var keyEvent = KeyEvent(AndroidKeyEvent(ActionDown, KeyCodeA))
private const val milliseconds = 10L
-val idlingResource = object : IdlingResource {
+private const val timeoutMs = 10L
+private val idlingResource = object : IdlingResource {
override val isIdleNow: Boolean
get() = TODO("Stub!")
-}
\ No newline at end of file
+}
+private val condition = true
\ No newline at end of file
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/ParametersTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/ParametersTest.kt
index 907d2b5..8931b7e 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/ParametersTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/ParametersTest.kt
@@ -25,6 +25,8 @@
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.ComposableNode
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetComposablesResponse
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetParametersResponse
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Parameter
import org.junit.Rule
@@ -39,8 +41,9 @@
fun lambda(): Unit = runBlocking {
val composables = rule.inspectorTester.sendCommand(GetComposablesCommand(rule.rootId))
.getComposablesResponse
- // first button's id
- val buttonId = composables.rootsList[0]!!.nodesList[0]!!.childrenList[0]!!.id
+
+ val buttons = composables.filter("Button")
+ val buttonId = buttons.first().id
val params = rule.inspectorTester.sendCommand(GetParametersCommand(rule.rootId, buttonId))
.getParametersResponse
@@ -57,8 +60,8 @@
val composables = rule.inspectorTester.sendCommand(GetComposablesCommand(rule.rootId))
.getComposablesResponse
- // second's button id
- val buttonId = composables.rootsList[0]!!.nodesList[0]!!.childrenList[1]!!.id
+ val buttons = composables.filter("Button")
+ val buttonId = buttons.last().id
val params = rule.inspectorTester.sendCommand(GetParametersCommand(rule.rootId, buttonId))
.getParametersResponse
@@ -81,4 +84,14 @@
return parameterGroup.parameterList.find {
strings[it.name] == name
}
-}
\ No newline at end of file
+}
+
+private fun GetComposablesResponse.filter(name: String): List<ComposableNode> {
+ val strings = stringsList.toMap()
+ return rootsList.flatMap { it.nodesList }.flatMap { it.flatten() }.filter {
+ strings[it.name] == name
+ }
+}
+
+private fun ComposableNode.flatten(): List<ComposableNode> =
+ listOf(this).plus(this.childrenList.flatMap { it.flatten() })
\ No newline at end of file
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 e6f6c73..47cb2ae 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
@@ -16,7 +16,6 @@
package androidx.compose.ui.inspection.inspector
-import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
@@ -33,10 +32,10 @@
import androidx.compose.material.icons.filled.Call
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.tooling.CompositionData
-import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.currentComposer
+import androidx.compose.runtime.tooling.CompositionData
import androidx.compose.runtime.tooling.LocalInspectionTables
import androidx.compose.ui.Modifier
import androidx.compose.ui.R
@@ -79,7 +78,7 @@
@OptIn(UiToolingDataApi::class)
class LayoutInspectorTreeTest {
private lateinit var density: Density
- private lateinit var view: View
+ private lateinit var view: ViewGroup
@get:Rule
val activityScenario = ActivityScenarioRule(TestActivity::class.java)
@@ -88,7 +87,7 @@
fun before() {
activityScenario.scenario.onActivity {
density = Density(it)
- view = it.findViewById<ViewGroup>(android.R.id.content)
+ view = it.findViewById(android.R.id.content)
}
isDebugInspectorInfoEnabled = true
}
@@ -116,27 +115,12 @@
// TODO: Find out if we can set "settings put global debug_view_attributes 1" in tests
view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
- val viewWidth = with(density) { view.width.toDp() }
- val viewHeight = with(density) { view.height.toDp() }
val builder = LayoutInspectorTree()
val nodes = builder.convert(view)
dumpNodes(nodes, builder)
validate(nodes, builder, checkParameters = false) {
node(
- name = "Content",
- fileName = "",
- left = 0.0.dp, top = 0.0.dp, width = viewWidth, height = viewHeight,
- children = listOf("Box")
- )
- node(
- name = "Box",
- isRenderNode = true,
- fileName = "",
- left = 0.0.dp, top = 0.0.dp, width = viewWidth, height = viewHeight,
- children = listOf("Column")
- )
- node(
name = "Column",
fileName = "LayoutInspectorTreeTest.kt",
left = 0.0.dp, top = 0.0.dp, width = 72.0.dp, height = 78.9.dp,
@@ -196,31 +180,16 @@
// TODO: Find out if we can set "settings put global debug_view_attributes 1" in tests
view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
- val viewWidth = with(density) { view.width.toDp() }
- val viewHeight = with(density) { view.height.toDp() }
val builder = LayoutInspectorTree()
val nodes = builder.convert(view)
dumpNodes(nodes, builder)
validate(nodes, builder, checkParameters = false) {
node(
- name = "Content",
- fileName = "",
- left = 0.0.dp, top = 0.0.dp, width = viewWidth, height = viewHeight,
- children = listOf("Box")
- )
- node(
- name = "Box",
- isRenderNode = true,
- fileName = "",
- left = 0.0.dp, top = 0.0.dp, width = viewWidth, height = viewHeight,
- children = listOf("MaterialTheme")
- )
- node(
name = "MaterialTheme",
hasTransformations = true,
fileName = "LayoutInspectorTreeTest.kt",
- left = 65.8.dp, top = 49.7.dp, width = 86.2.dp, height = 21.7.dp,
+ left = 68.0.dp, top = 49.7.dp, width = 88.6.dp, height = 21.7.dp,
children = listOf("Text")
)
node(
@@ -228,7 +197,7 @@
isRenderNode = true,
hasTransformations = true,
fileName = "LayoutInspectorTreeTest.kt",
- left = 65.8.dp, top = 49.7.dp, width = 86.2.dp, height = 21.7.dp,
+ left = 68.0.dp, top = 49.7.dp, width = 88.6.dp, height = 21.7.dp,
)
}
}
@@ -402,7 +371,11 @@
checkParameters: Boolean,
block: TreeValidationReceiver.() -> Unit = {}
) {
- val nodes = result.flatMap { flatten(it) }.iterator()
+ val nodes = result.flatMap { flatten(it) }.listIterator()
+ // Ignore a starting CompositionLocalProvider...
+ if (nodes.next().name != "CompositionLocalProvider") {
+ nodes.previous()
+ }
val tree = TreeValidationReceiver(nodes, density, checkParameters, builder)
tree.block()
}
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 815ec9c..7138052 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
@@ -38,6 +38,7 @@
import kotlin.math.roundToInt
private val systemPackages = setOf(
+ -1,
packageNameHash("androidx.compose.animation"),
packageNameHash("androidx.compose.animation.core"),
packageNameHash("androidx.compose.desktop"),
@@ -53,27 +54,18 @@
packageNameHash("androidx.compose.ui.tooling"),
packageNameHash("androidx.compose.ui.selection"),
packageNameHash("androidx.compose.ui.semantics"),
- packageNameHash("androidx.compose.ui.viewinterop")
-)
-
-private val unwantedPackages = setOf(
- -1,
- packageNameHash("androidx.compose.ui"),
- packageNameHash("androidx.compose.runtime"),
- packageNameHash("androidx.compose.ui.tooling"),
- packageNameHash("androidx.compose.ui.selection"),
- packageNameHash("androidx.compose.ui.semantics"),
- packageNameHash("androidx.compose.ui.inspection.inspector"),
+ packageNameHash("androidx.compose.ui.viewinterop"),
+ packageNameHash("androidx.compose.ui.window"),
)
private val unwantedCalls = setOf(
"emit",
"remember",
- "Inspectable",
- "Layout",
"CompositionLocalProvider",
- "SelectionContainer",
- "SelectionLayout"
+ "Content",
+ "Inspectable",
+ "ProvideAndroidCompositionLocals",
+ "ProvideCommonCompositionLocals",
)
private fun packageNameHash(packageName: String) =
@@ -418,8 +410,7 @@
}
private fun unwantedGroup(node: MutableInspectorNode): Boolean =
- (node.packageHash in unwantedPackages && node.name in unwantedCalls) ||
- (hideSystemNodes && node.packageHash in systemPackages)
+ node.packageHash in systemPackages && (hideSystemNodes || node.name in unwantedCalls)
private fun newNode(): MutableInspectorNode =
if (cache.isNotEmpty()) cache.pop() else MutableInspectorNode()
diff --git a/compose/ui/ui-test/api/1.0.0-beta02.txt b/compose/ui/ui-test/api/1.0.0-beta02.txt
index 970280d..963ed71 100644
--- a/compose/ui/ui-test/api/1.0.0-beta02.txt
+++ b/compose/ui/ui-test/api/1.0.0-beta02.txt
@@ -76,26 +76,18 @@
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyChild(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyDescendant(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnySibling(androidx.compose.ui.test.SemanticsMatcher matcher);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasBottomPosition-ioHfwGI(float bottom, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasContentDescription(String label, optional boolean substring, optional boolean ignoreCase);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasHeight-ioHfwGI(float height, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasHeightAtLeast-ioHfwGI(float height, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasImeAction(androidx.compose.ui.text.input.ImeAction actionType);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasLeftPosition-ioHfwGI(float left, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasProgressBarRangeInfo(androidx.compose.ui.semantics.ProgressBarRangeInfo rangeInfo);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasRightPosition-ioHfwGI(float right, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasSetTextAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasStateDescription(String value);
method public static androidx.compose.ui.test.SemanticsMatcher hasTestTag(String testTag);
method public static androidx.compose.ui.test.SemanticsMatcher hasText(String text, optional boolean substring, optional boolean ignoreCase);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasTopPosition-ioHfwGI(float top, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasWidth-ioHfwGI(float width, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasWidthAtLeast-ioHfwGI(float width, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher isDialog();
method public static androidx.compose.ui.test.SemanticsMatcher isEnabled();
method public static androidx.compose.ui.test.SemanticsMatcher isFocusable();
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index 970280d..963ed71 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -76,26 +76,18 @@
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyChild(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyDescendant(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnySibling(androidx.compose.ui.test.SemanticsMatcher matcher);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasBottomPosition-ioHfwGI(float bottom, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasContentDescription(String label, optional boolean substring, optional boolean ignoreCase);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasHeight-ioHfwGI(float height, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasHeightAtLeast-ioHfwGI(float height, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasImeAction(androidx.compose.ui.text.input.ImeAction actionType);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasLeftPosition-ioHfwGI(float left, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasProgressBarRangeInfo(androidx.compose.ui.semantics.ProgressBarRangeInfo rangeInfo);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasRightPosition-ioHfwGI(float right, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasSetTextAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasStateDescription(String value);
method public static androidx.compose.ui.test.SemanticsMatcher hasTestTag(String testTag);
method public static androidx.compose.ui.test.SemanticsMatcher hasText(String text, optional boolean substring, optional boolean ignoreCase);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasTopPosition-ioHfwGI(float top, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasWidth-ioHfwGI(float width, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasWidthAtLeast-ioHfwGI(float width, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher isDialog();
method public static androidx.compose.ui.test.SemanticsMatcher isEnabled();
method public static androidx.compose.ui.test.SemanticsMatcher isFocusable();
diff --git a/compose/ui/ui-test/api/public_plus_experimental_1.0.0-beta02.txt b/compose/ui/ui-test/api/public_plus_experimental_1.0.0-beta02.txt
index 970280d..963ed71 100644
--- a/compose/ui/ui-test/api/public_plus_experimental_1.0.0-beta02.txt
+++ b/compose/ui/ui-test/api/public_plus_experimental_1.0.0-beta02.txt
@@ -76,26 +76,18 @@
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyChild(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyDescendant(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnySibling(androidx.compose.ui.test.SemanticsMatcher matcher);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasBottomPosition-ioHfwGI(float bottom, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasContentDescription(String label, optional boolean substring, optional boolean ignoreCase);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasHeight-ioHfwGI(float height, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasHeightAtLeast-ioHfwGI(float height, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasImeAction(androidx.compose.ui.text.input.ImeAction actionType);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasLeftPosition-ioHfwGI(float left, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasProgressBarRangeInfo(androidx.compose.ui.semantics.ProgressBarRangeInfo rangeInfo);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasRightPosition-ioHfwGI(float right, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasSetTextAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasStateDescription(String value);
method public static androidx.compose.ui.test.SemanticsMatcher hasTestTag(String testTag);
method public static androidx.compose.ui.test.SemanticsMatcher hasText(String text, optional boolean substring, optional boolean ignoreCase);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasTopPosition-ioHfwGI(float top, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasWidth-ioHfwGI(float width, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasWidthAtLeast-ioHfwGI(float width, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher isDialog();
method public static androidx.compose.ui.test.SemanticsMatcher isEnabled();
method public static androidx.compose.ui.test.SemanticsMatcher isFocusable();
diff --git a/compose/ui/ui-test/api/public_plus_experimental_current.txt b/compose/ui/ui-test/api/public_plus_experimental_current.txt
index 970280d..963ed71 100644
--- a/compose/ui/ui-test/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test/api/public_plus_experimental_current.txt
@@ -76,26 +76,18 @@
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyChild(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyDescendant(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnySibling(androidx.compose.ui.test.SemanticsMatcher matcher);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasBottomPosition-ioHfwGI(float bottom, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasContentDescription(String label, optional boolean substring, optional boolean ignoreCase);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasHeight-ioHfwGI(float height, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasHeightAtLeast-ioHfwGI(float height, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasImeAction(androidx.compose.ui.text.input.ImeAction actionType);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasLeftPosition-ioHfwGI(float left, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasProgressBarRangeInfo(androidx.compose.ui.semantics.ProgressBarRangeInfo rangeInfo);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasRightPosition-ioHfwGI(float right, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasSetTextAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasStateDescription(String value);
method public static androidx.compose.ui.test.SemanticsMatcher hasTestTag(String testTag);
method public static androidx.compose.ui.test.SemanticsMatcher hasText(String text, optional boolean substring, optional boolean ignoreCase);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasTopPosition-ioHfwGI(float top, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasWidth-ioHfwGI(float width, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasWidthAtLeast-ioHfwGI(float width, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher isDialog();
method public static androidx.compose.ui.test.SemanticsMatcher isEnabled();
method public static androidx.compose.ui.test.SemanticsMatcher isFocusable();
diff --git a/compose/ui/ui-test/api/restricted_1.0.0-beta02.txt b/compose/ui/ui-test/api/restricted_1.0.0-beta02.txt
index 970280d..963ed71 100644
--- a/compose/ui/ui-test/api/restricted_1.0.0-beta02.txt
+++ b/compose/ui/ui-test/api/restricted_1.0.0-beta02.txt
@@ -76,26 +76,18 @@
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyChild(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyDescendant(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnySibling(androidx.compose.ui.test.SemanticsMatcher matcher);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasBottomPosition-ioHfwGI(float bottom, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasContentDescription(String label, optional boolean substring, optional boolean ignoreCase);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasHeight-ioHfwGI(float height, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasHeightAtLeast-ioHfwGI(float height, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasImeAction(androidx.compose.ui.text.input.ImeAction actionType);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasLeftPosition-ioHfwGI(float left, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasProgressBarRangeInfo(androidx.compose.ui.semantics.ProgressBarRangeInfo rangeInfo);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasRightPosition-ioHfwGI(float right, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasSetTextAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasStateDescription(String value);
method public static androidx.compose.ui.test.SemanticsMatcher hasTestTag(String testTag);
method public static androidx.compose.ui.test.SemanticsMatcher hasText(String text, optional boolean substring, optional boolean ignoreCase);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasTopPosition-ioHfwGI(float top, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasWidth-ioHfwGI(float width, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasWidthAtLeast-ioHfwGI(float width, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher isDialog();
method public static androidx.compose.ui.test.SemanticsMatcher isEnabled();
method public static androidx.compose.ui.test.SemanticsMatcher isFocusable();
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index 970280d..963ed71 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -76,26 +76,18 @@
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyChild(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyDescendant(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnySibling(androidx.compose.ui.test.SemanticsMatcher matcher);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasBottomPosition-ioHfwGI(float bottom, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasContentDescription(String label, optional boolean substring, optional boolean ignoreCase);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasHeight-ioHfwGI(float height, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasHeightAtLeast-ioHfwGI(float height, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasImeAction(androidx.compose.ui.text.input.ImeAction actionType);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasLeftPosition-ioHfwGI(float left, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasProgressBarRangeInfo(androidx.compose.ui.semantics.ProgressBarRangeInfo rangeInfo);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasRightPosition-ioHfwGI(float right, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher hasScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasSetTextAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasStateDescription(String value);
method public static androidx.compose.ui.test.SemanticsMatcher hasTestTag(String testTag);
method public static androidx.compose.ui.test.SemanticsMatcher hasText(String text, optional boolean substring, optional boolean ignoreCase);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasTopPosition-ioHfwGI(float top, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasWidth-ioHfwGI(float width, optional float tolerance);
- method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsMatcher hasWidthAtLeast-ioHfwGI(float width, optional float tolerance);
method public static androidx.compose.ui.test.SemanticsMatcher isDialog();
method public static androidx.compose.ui.test.SemanticsMatcher isEnabled();
method public static androidx.compose.ui.test.SemanticsMatcher isFocusable();
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/BoundsAssertions.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/BoundsAssertions.kt
index 4dcbaa2..79228a4 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/BoundsAssertions.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/BoundsAssertions.kt
@@ -22,44 +22,55 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpRect
+import androidx.compose.ui.unit.toSize
+import kotlin.math.absoluteValue
+
+private const val floatTolerance = 0.5f
/**
* Asserts that the layout of this node has width equal to [expectedWidth].
*
* @throws AssertionError if comparison fails.
*/
-@OptIn(ExperimentalTestApi::class)
-fun SemanticsNodeInteraction.assertWidthIsEqualTo(expectedWidth: Dp): SemanticsNodeInteraction =
- assert(hasWidth(expectedWidth))
+fun SemanticsNodeInteraction.assertWidthIsEqualTo(expectedWidth: Dp): SemanticsNodeInteraction {
+ return withUnclippedBoundsInRoot {
+ it.width.toDp().assertIsEqualTo(expectedWidth, "width")
+ }
+}
/**
* Asserts that the layout of this node has height equal to [expectedHeight].
*
* @throws AssertionError if comparison fails.
*/
-@OptIn(ExperimentalTestApi::class)
-fun SemanticsNodeInteraction.assertHeightIsEqualTo(expectedHeight: Dp): SemanticsNodeInteraction =
- assert(hasHeight(expectedHeight))
-
+fun SemanticsNodeInteraction.assertHeightIsEqualTo(expectedHeight: Dp): SemanticsNodeInteraction {
+ return withUnclippedBoundsInRoot {
+ it.height.toDp().assertIsEqualTo(expectedHeight, "height")
+ }
+}
/**
* Asserts that the layout of this node has width that is greater ot equal to [expectedMinWidth].
*
* @throws AssertionError if comparison fails.
*/
-@OptIn(ExperimentalTestApi::class)
-fun SemanticsNodeInteraction.assertWidthIsAtLeast(expectedMinWidth: Dp): SemanticsNodeInteraction =
- assert(hasWidthAtLeast(expectedMinWidth))
+fun SemanticsNodeInteraction.assertWidthIsAtLeast(expectedMinWidth: Dp): SemanticsNodeInteraction {
+ return withUnclippedBoundsInRoot {
+ isAtLeastOrThrow("width", it.width, expectedMinWidth)
+ }
+}
/**
* Asserts that the layout of this node has height that is greater ot equal to [expectedMinHeight].
*
* @throws AssertionError if comparison fails.
*/
-@OptIn(ExperimentalTestApi::class)
fun SemanticsNodeInteraction.assertHeightIsAtLeast(
expectedMinHeight: Dp
-): SemanticsNodeInteraction =
- assert(hasHeightAtLeast(expectedMinHeight))
+): SemanticsNodeInteraction {
+ return withUnclippedBoundsInRoot {
+ isAtLeastOrThrow("height", it.height, expectedMinHeight)
+ }
+}
/**
* Asserts that the layout of this node has position in the root composable that is equal to the
@@ -70,12 +81,15 @@
*
* @throws AssertionError if comparison fails.
*/
-@OptIn(ExperimentalTestApi::class)
fun SemanticsNodeInteraction.assertPositionInRootIsEqualTo(
expectedLeft: Dp,
expectedTop: Dp
-): SemanticsNodeInteraction =
- assert(hasLeftPosition(expectedLeft).and(hasTopPosition(expectedTop)))
+): SemanticsNodeInteraction {
+ return withUnclippedBoundsInRoot {
+ it.left.toDp().assertIsEqualTo(expectedLeft, "left")
+ it.top.toDp().assertIsEqualTo(expectedTop, "top")
+ }
+}
/**
* Asserts that the layout of this node has the top position in the root composable that is equal to
@@ -85,11 +99,13 @@
*
* @throws AssertionError if comparison fails.
*/
-@OptIn(ExperimentalTestApi::class)
fun SemanticsNodeInteraction.assertTopPositionInRootIsEqualTo(
expectedTop: Dp
-): SemanticsNodeInteraction =
- assert(hasTopPosition(expectedTop))
+): SemanticsNodeInteraction {
+ return withUnclippedBoundsInRoot {
+ it.top.toDp().assertIsEqualTo(expectedTop, "top")
+ }
+}
/**
* Asserts that the layout of this node has the left position in the root composable that is
@@ -99,11 +115,13 @@
*
* @throws AssertionError if comparison fails.
*/
-@OptIn(ExperimentalTestApi::class)
fun SemanticsNodeInteraction.assertLeftPositionInRootIsEqualTo(
expectedLeft: Dp
-): SemanticsNodeInteraction =
- assert(hasLeftPosition(expectedLeft))
+): SemanticsNodeInteraction {
+ return withUnclippedBoundsInRoot {
+ it.left.toDp().assertIsEqualTo(expectedLeft, "left")
+ }
+}
/**
* Returns the bounds of the layout of this node. The bounds are relative to the root composable.
@@ -136,6 +154,30 @@
}
}
+/**
+ * Asserts that this value is equal to the given [expected] value.
+ *
+ * Performs the comparison with the given [tolerance] or the default one if none is provided. It is
+ * recommended to use tolerance when comparing positions and size coming from the framework as there
+ * can be rounding operation performed by individual layouts so the values can be slightly off from
+ * the expected ones.
+ *
+ * @param expected The expected value to which this one should be equal to.
+ * @param subject Used in the error message to identify which item this assertion failed on.
+ * @param tolerance The tolerance within which the values should be treated as equal.
+ *
+ * @throws AssertionError if comparison fails.
+ */
+private fun Dp.assertIsEqualTo(expected: Dp, subject: String = "", tolerance: Dp = Dp(.5f)) {
+ val diff = (this - expected).value.absoluteValue
+ if (diff > tolerance.value) {
+ // Comparison failed, report the error in DPs
+ throw AssertionError(
+ "Actual $subject is $this, expected $expected (tolerance: $tolerance)"
+ )
+ }
+}
+
private fun <R> SemanticsNodeInteraction.withDensity(
operation: Density.(SemanticsNode) -> R
): R {
@@ -153,3 +195,21 @@
assertion.invoke(density, node.unclippedBoundsInRoot)
return this
}
+
+private val SemanticsNode.unclippedBoundsInRoot: Rect
+ get() {
+ return Rect(positionInRoot, size.toSize())
+ }
+
+private fun Density.isAtLeastOrThrow(
+ subject: String,
+ actualPx: Float,
+ expected: Dp
+) {
+ if (actualPx + floatTolerance < expected.toPx()) {
+ // Comparison failed, report the error in DPs
+ throw AssertionError(
+ "Actual $subject is ${actualPx.toDp()}, expected at least $expected"
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt
index 94c44f0..8977883 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.test
-import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.SemanticsNode
@@ -24,11 +23,7 @@
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.toSize
import androidx.compose.ui.util.fastAny
-import kotlin.math.abs
/**
* Returns whether the node is enabled.
@@ -307,150 +302,6 @@
SemanticsMatcher.keyIsDefined(SemanticsActions.SetText)
/**
- * Returns whether the node has the given [width], within a certain [tolerance]. The default
- * tolerance is 0.5 dp.
- *
- * This is mostly useful in [assertions][assert].
- *
- * @see hasWidthAtLeast
- * @see hasHeight
- * @see hasHeightAtLeast
- */
-@ExperimentalTestApi
-fun hasWidth(width: Dp, tolerance: Dp = DefaultTolerance): SemanticsMatcher =
- SemanticsMatcher("width = $width ± $tolerance") { node ->
- node.withDensity {
- abs(node.unclippedBoundsInRoot.width - width.toPx()) <= tolerance.toPx()
- }
- }
-
-/**
- * Returns whether the node has a width of at least the given [width], with a certain [tolerance].
- * The default tolerance is 0.5 dp.
- *
- * This is mostly useful in [assertions][assert].
- *
- * @see hasWidth
- * @see hasHeight
- * @see hasHeightAtLeast
- */
-@ExperimentalTestApi
-fun hasWidthAtLeast(width: Dp, tolerance: Dp = DefaultTolerance): SemanticsMatcher =
- SemanticsMatcher("width > $width - $tolerance") { node ->
- node.withDensity {
- node.unclippedBoundsInRoot.width - width.toPx() >= -tolerance.toPx()
- }
- }
-
-/**
- * Returns whether the node has the given [height], within a certain [tolerance]. The default
- * tolerance is 0.5 dp.
- *
- * This is mostly useful in [assertions][assert].
- *
- * @see hasWidth
- * @see hasWidthAtLeast
- * @see hasHeightAtLeast
- */
-@ExperimentalTestApi
-fun hasHeight(height: Dp, tolerance: Dp = DefaultTolerance): SemanticsMatcher =
- SemanticsMatcher("height = $height ± $tolerance") { node ->
- node.withDensity {
- abs(node.unclippedBoundsInRoot.height - height.toPx()) <= tolerance.toPx()
- }
- }
-
-/**
- * Returns whether the node has a height of at least the given [height], with a certain
- * [tolerance]. The default tolerance is 0.5 dp.
- *
- * This is mostly useful in [assertions][assert].
- *
- * @see hasWidth
- * @see hasWidthAtLeast
- * @see hasHeight
- */
-@ExperimentalTestApi
-fun hasHeightAtLeast(height: Dp, tolerance: Dp = DefaultTolerance): SemanticsMatcher =
- SemanticsMatcher("height > $height - $tolerance") { node ->
- node.withDensity {
- node.unclippedBoundsInRoot.height - height.toPx() >= -tolerance.toPx()
- }
- }
-
-/**
- * Returns whether the node's left edge relative to the root matches the given [left], within a
- * certain [tolerance]. The default tolerance is 0.5 dp.
- *
- * This is mostly useful in [assertions][assert].
- *
- * @see hasTopPosition
- * @see hasRightPosition
- * @see hasBottomPosition
- */
-@ExperimentalTestApi
-fun hasLeftPosition(left: Dp, tolerance: Dp = DefaultTolerance): SemanticsMatcher =
- SemanticsMatcher("left = $left ± $tolerance") { node ->
- node.withDensity {
- abs(node.unclippedBoundsInRoot.left - left.toPx()) <= tolerance.toPx()
- }
- }
-
-/**
- * Returns whether the node's top edge relative to the root matches the given [top], within a
- * certain [tolerance]. The default tolerance is 0.5 dp.
- *
- * This is mostly useful in [assertions][assert].
- *
- * @see hasLeftPosition
- * @see hasRightPosition
- * @see hasBottomPosition
- */
-@ExperimentalTestApi
-fun hasTopPosition(top: Dp, tolerance: Dp = DefaultTolerance): SemanticsMatcher =
- SemanticsMatcher("top = $top ± $tolerance") { node ->
- node.withDensity {
- abs(node.unclippedBoundsInRoot.top - top.toPx()) <= tolerance.toPx()
- }
- }
-
-/**
- * Returns whether the node's right edge relative to the root matches the given [right], within a
- * certain [tolerance]. The default tolerance is 0.5 dp.
- *
- * This is mostly useful in [assertions][assert].
- *
- * @see hasLeftPosition
- * @see hasTopPosition
- * @see hasBottomPosition
- */
-@ExperimentalTestApi
-fun hasRightPosition(right: Dp, tolerance: Dp = DefaultTolerance): SemanticsMatcher =
- SemanticsMatcher("right = $right ± $tolerance") { node ->
- node.withDensity {
- abs(node.unclippedBoundsInRoot.right - right.toPx()) <= tolerance.toPx()
- }
- }
-
-/**
- * Returns whether the node's bottom edge relative to the root matches the given [bottom], within
- * a certain [tolerance]. The default tolerance is 0.5 dp.
- *
- * This is mostly useful in [assertions][assert].
- *
- * @see hasLeftPosition
- * @see hasTopPosition
- * @see hasRightPosition
- */
-@ExperimentalTestApi
-fun hasBottomPosition(bottom: Dp, tolerance: Dp = DefaultTolerance): SemanticsMatcher =
- SemanticsMatcher("bottom = $bottom ± $tolerance") { node ->
- node.withDensity {
- abs(node.unclippedBoundsInRoot.bottom - bottom.toPx()) <= tolerance.toPx()
- }
- }
-
-/**
* Return whether the node is the root semantics node.
*
* There is always one root in every node tree, added implicitly by Compose.
@@ -560,11 +411,3 @@
}
}
}
-
-private val DefaultTolerance = Dp(.5f)
-
-internal val SemanticsNode.unclippedBoundsInRoot: Rect
- get() = Rect(positionInRoot, size.toSize())
-
-private fun <R> SemanticsNode.withDensity(block: Density.() -> R): R =
- block.invoke(root!!.density)
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
index 244c29d..0dbe1df 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
@@ -101,27 +101,12 @@
// TODO: Find out if we can set "settings put global debug_view_attributes 1" in tests
view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
- val viewWidth = with(density) { view.width.toDp() }
- val viewHeight = with(density) { view.height.toDp() }
val builder = LayoutInspectorTree()
val nodes = builder.convert(view)
dumpNodes(nodes, builder)
validate(nodes, builder, checkParameters = false) {
node(
- name = "Content",
- fileName = "",
- left = 0.0.dp, top = 0.0.dp, width = viewWidth, height = viewHeight,
- children = listOf("Box")
- )
- node(
- name = "Box",
- isRenderNode = true,
- fileName = "",
- left = 0.0.dp, top = 0.0.dp, width = viewWidth, height = viewHeight,
- children = listOf("Column")
- )
- node(
name = "Column",
fileName = "LayoutInspectorTreeTest.kt",
left = 0.0.dp, top = 0.0.dp, width = 72.0.dp, height = 78.9.dp,
@@ -181,31 +166,16 @@
// TODO: Find out if we can set "settings put global debug_view_attributes 1" in tests
view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
- val viewWidth = with(density) { view.width.toDp() }
- val viewHeight = with(density) { view.height.toDp() }
val builder = LayoutInspectorTree()
val nodes = builder.convert(view)
dumpNodes(nodes, builder)
validate(nodes, builder, checkParameters = false) {
node(
- name = "Content",
- fileName = "",
- left = 0.0.dp, top = 0.0.dp, width = viewWidth, height = viewHeight,
- children = listOf("Box")
- )
- node(
- name = "Box",
- isRenderNode = true,
- fileName = "",
- left = 0.0.dp, top = 0.0.dp, width = viewWidth, height = viewHeight,
- children = listOf("MaterialTheme")
- )
- node(
name = "MaterialTheme",
hasTransformations = true,
fileName = "LayoutInspectorTreeTest.kt",
- left = 65.8.dp, top = 49.7.dp, width = 86.2.dp, height = 21.7.dp,
+ left = 68.0.dp, top = 49.7.dp, width = 88.6.dp, height = 21.7.dp,
children = listOf("Text")
)
node(
@@ -213,7 +183,7 @@
isRenderNode = true,
hasTransformations = true,
fileName = "LayoutInspectorTreeTest.kt",
- left = 65.8.dp, top = 49.7.dp, width = 86.2.dp, height = 21.7.dp,
+ left = 68.0.dp, top = 49.7.dp, width = 88.6.dp, height = 21.7.dp,
)
}
}
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt
index 19f8ed8..85f0933 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt
@@ -38,6 +38,7 @@
import kotlin.math.roundToInt
private val systemPackages = setOf(
+ -1,
packageNameHash("androidx.compose.animation"),
packageNameHash("androidx.compose.animation.core"),
packageNameHash("androidx.compose.desktop"),
@@ -53,26 +54,18 @@
packageNameHash("androidx.compose.ui.tooling"),
packageNameHash("androidx.compose.ui.selection"),
packageNameHash("androidx.compose.ui.semantics"),
- packageNameHash("androidx.compose.ui.viewinterop")
-)
-
-private val unwantedPackages = setOf(
- -1,
- packageNameHash("androidx.compose.ui"),
- packageNameHash("androidx.compose.runtime"),
- packageNameHash("androidx.compose.ui.tooling"),
- packageNameHash("androidx.compose.ui.selection"),
- packageNameHash("androidx.compose.ui.semantics")
+ packageNameHash("androidx.compose.ui.viewinterop"),
+ packageNameHash("androidx.compose.ui.window"),
)
private val unwantedCalls = setOf(
"emit",
"remember",
- "Inspectable",
- "Layout",
"CompositionLocalProvider",
- "SelectionContainer",
- "SelectionLayout"
+ "Content",
+ "Inspectable",
+ "ProvideAndroidCompositionLocals",
+ "ProvideCommonCompositionLocals",
)
private fun packageNameHash(packageName: String) =
@@ -416,8 +409,7 @@
}
private fun unwantedGroup(node: MutableInspectorNode): Boolean =
- (node.packageHash in unwantedPackages && node.name in unwantedCalls) ||
- (hideSystemNodes && node.packageHash in systemPackages)
+ node.packageHash in systemPackages && (hideSystemNodes || node.name in unwantedCalls)
private fun newNode(): MutableInspectorNode =
if (cache.isNotEmpty()) cache.pop() else MutableInspectorNode()
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/CustomFocusOrderDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/CustomFocusOrderDemo.kt
index aa39593..08d62d4 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/CustomFocusOrderDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/CustomFocusOrderDemo.kt
@@ -65,6 +65,7 @@
next = item2
right = item2
down = item3
+ previous = item4
}
)
FocusableText(
@@ -73,6 +74,7 @@
next = item3
left = item1
down = item4
+ previous = item1
}
)
}
@@ -83,6 +85,7 @@
next = item4
right = item4
up = item1
+ previous = item2
}
)
FocusableText(
@@ -91,6 +94,7 @@
next = item1
left = item3
up = item2
+ previous = item3
}
)
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.desktop.kt
index 550feb08..cfe712b 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.desktop.kt
@@ -17,13 +17,11 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import org.jetbrains.skiko.ClipComponent
import java.awt.Color
import java.awt.Component
-import java.awt.event.ComponentAdapter
-import java.awt.event.ComponentEvent
import javax.swing.JLayeredPane
import javax.swing.SwingUtilities.isEventDispatchThread
-import org.jetbrains.skiko.ClipComponent
/**
* ComposePanel is a panel for building UI using Compose for Desktop.
@@ -37,18 +35,17 @@
}
setBackground(Color.white)
setLayout(null)
-
- addComponentListener(object : ComponentAdapter() {
- override fun componentResized(e: ComponentEvent) {
- layer?.wrapped?.setSize(width, height)
- }
- })
}
internal var layer: ComposeLayer? = null
private val clipMap = mutableMapOf<Component, ClipComponent>()
private var content: (@Composable () -> Unit)? = null
+ override fun setBounds(x: Int, y: Int, width: Int, height: Int) {
+ layer?.wrapped?.setSize(width, height)
+ super.setBounds(x, y, width, height)
+ }
+
/**
* Sets Compose content of the ComposePanel.
*
@@ -97,6 +94,7 @@
// content.
layer = ComposeLayer()
super.add(layer!!.component, Integer.valueOf(1))
+ layer?.wrapped?.setSize(width, height)
initContent()
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.desktop.kt
index 786b392..08be312 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.desktop.kt
@@ -17,12 +17,10 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionContext
+import org.jetbrains.skiko.ClipComponent
import java.awt.Component
-import java.awt.event.ComponentAdapter
-import java.awt.event.ComponentEvent
import javax.swing.JFrame
import javax.swing.JLayeredPane
-import org.jetbrains.skiko.ClipComponent
/**
* ComposeWindow is a window for building UI using Compose for Desktop.
@@ -31,17 +29,18 @@
*/
class ComposeWindow(val parent: AppFrame) : JFrame() {
internal val layer = ComposeLayer()
- private val pane = JLayeredPane()
+ private val pane = object : JLayeredPane() {
+ override fun setBounds(x: Int, y: Int, width: Int, height: Int) {
+ layer.wrapped.setSize(width, height)
+ super.setBounds(x, y, width, height)
+ }
+ }
+
private val clipMap = mutableMapOf<Component, ClipComponent>()
init {
pane.setLayout(null)
pane.add(layer.component, Integer.valueOf(1))
- addComponentListener(object : ComponentAdapter() {
- override fun componentResized(e: ComponentEvent) {
- layer.wrapped.setSize(pane.width, pane.height)
- }
- })
contentPane.add(pane)
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt
index 73797ec..bbb543a 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt
@@ -66,6 +66,8 @@
}
val list = LinkedHashSet<DesktopOwner>()
+ private val listCopy = mutableListOf<DesktopOwner>()
+
var keyboard: Keyboard? = null
private var pointerId = 0L
@@ -113,7 +115,9 @@
dispatcher.flush()
frameClock.sendFrame(nanoTime)
- for (owner in list) {
+ listCopy.clear()
+ listCopy.addAll(list)
+ for (owner in listCopy) {
owner.render(canvas, width, height)
}
}
@@ -121,7 +125,7 @@
invalidateIfNeeded()
}
- val lastOwner: DesktopOwner?
+ private val lastOwner: DesktopOwner?
get() = list.lastOrNull()
fun onMousePressed(x: Int, y: Int, nativeEvent: MouseEvent? = null) {
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt
index 88125f3..efceb69 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt
@@ -19,14 +19,18 @@
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
@@ -196,4 +200,26 @@
Truth.assertThat(lastCompositionState).isEqualTo(1)
}
+
+ @Test(timeout = 5000)
+ fun `(Bug) use Popup inside LazyColumn`() {
+ rule.setContent {
+ var count by remember { mutableStateOf(0) }
+ LazyColumn {
+ items(count) {
+ Popup { }
+ }
+ }
+ LaunchedEffect(Unit) {
+ withFrameNanos {
+ count++
+ }
+ withFrameNanos {
+ count++
+ }
+ }
+ }
+
+ rule.waitForIdle()
+ }
}
diff --git a/core/OWNERS b/core/OWNERS
index 1de526b..458cdf0 100644
--- a/core/OWNERS
+++ b/core/OWNERS
@@ -2,7 +2,6 @@
cinek@google.com
clarabayarri@google.com
juliacr@google.com
-jmonk@google.com
# For fingerprint related files
jaggies@google.com
diff --git a/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java b/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java
index 70afe9b..c9aa1e9 100644
--- a/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java
+++ b/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java
@@ -304,10 +304,7 @@
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public final void callbackSuccessAsync(final Typeface typeface, @Nullable Handler handler) {
- if (handler == null) {
- handler = new Handler(Looper.getMainLooper());
- }
- handler.post(new Runnable() {
+ ensureHandler(handler).post(new Runnable() {
@Override
public void run() {
onFontRetrieved(typeface);
@@ -323,16 +320,17 @@
@RestrictTo(LIBRARY_GROUP_PREFIX)
public final void callbackFailAsync(
@FontRequestFailReason final int reason, @Nullable Handler handler) {
- if (handler == null) {
- handler = new Handler(Looper.getMainLooper());
- }
- handler.post(new Runnable() {
+ ensureHandler(handler).post(new Runnable() {
@Override
public void run() {
onFontRetrievalFailed(reason);
}
});
}
+
+ private static Handler ensureHandler(@Nullable Handler handler) {
+ return handler == null ? new Handler(Looper.getMainLooper()) : handler;
+ }
}
/**
@@ -411,6 +409,16 @@
* Load the given font. This method will always return null for asynchronous requests, which
* provide a fontCallback, as there is no immediate result. When the callback is not provided,
* the request is treated as synchronous and fails if async loading is required.
+ *
+ * @param context The Context to get Resources from
+ * @param id The Resource id to load
+ * @param value A TypedValue to use in the fetching
+ * @param style The font style to load
+ * @param fontCallback A callback to trigger when the font is fetched or an error occurs
+ * @param handler A handler to the thread the callback should be called on
+ * @param isRequestFromLayoutInflator Whether this request originated from XML. This is used to
+ * determine if we use or ignore the fontProviderFetchStrategy attribute in
+ * font provider XML fonts.
*/
private static Typeface loadFont(
@NonNull Context context, Resources wrapper, TypedValue value, int id, int style,
diff --git a/core/core/src/main/java/androidx/core/provider/FontRequest.java b/core/core/src/main/java/androidx/core/provider/FontRequest.java
index 4b692c3..31ffd70 100644
--- a/core/core/src/main/java/androidx/core/provider/FontRequest.java
+++ b/core/core/src/main/java/androidx/core/provider/FontRequest.java
@@ -60,8 +60,7 @@
mQuery = Preconditions.checkNotNull(query);
mCertificates = Preconditions.checkNotNull(certificates);
mCertificatesArray = 0;
- mIdentifier = new StringBuilder(mProviderAuthority).append("-").append(mProviderPackage)
- .append("-").append(mQuery).toString();
+ mIdentifier = createIdentifier(providerAuthority, providerPackage, query);
}
/**
@@ -83,8 +82,16 @@
mCertificates = null;
Preconditions.checkArgument(certificates != 0);
mCertificatesArray = certificates;
- mIdentifier = new StringBuilder(mProviderAuthority).append("-").append(mProviderPackage)
- .append("-").append(mQuery).toString();
+ mIdentifier = createIdentifier(providerAuthority, providerPackage, query);
+ }
+
+ private String createIdentifier(
+ @NonNull String providerAuthority,
+ @NonNull String providerPackage,
+ @NonNull String query
+ ) {
+ return new StringBuilder(providerAuthority).append("-").append(providerPackage)
+ .append("-").append(query).toString();
}
/**
diff --git a/core/core/src/main/java/androidx/core/provider/FontRequestThreadPool.java b/core/core/src/main/java/androidx/core/provider/FontRequestThreadPool.java
index 74e0970..fddad3c 100644
--- a/core/core/src/main/java/androidx/core/provider/FontRequestThreadPool.java
+++ b/core/core/src/main/java/androidx/core/provider/FontRequestThreadPool.java
@@ -16,7 +16,6 @@
package androidx.core.provider;
-import android.os.Handler;
import android.os.Process;
import androidx.annotation.IntRange;
@@ -75,7 +74,6 @@
@NonNull final Callable<T> callable,
@NonNull final ReplyCallback<T> callback
) {
- final Handler calleeHandler = CalleeHandler.create();
mExecutor.execute(new Runnable() {
@Override
public void run() {
@@ -86,13 +84,7 @@
t = null;
}
final T result = t;
-
- calleeHandler.post(new Runnable() {
- @Override
- public void run() {
- callback.onReply(result);
- }
- });
+ callback.onReply(result);
}
});
}
diff --git a/core/core/src/main/java/androidx/core/provider/FontRequestWorker.java b/core/core/src/main/java/androidx/core/provider/FontRequestWorker.java
index e57f6c55..9d9ecbd 100644
--- a/core/core/src/main/java/androidx/core/provider/FontRequestWorker.java
+++ b/core/core/src/main/java/androidx/core/provider/FontRequestWorker.java
@@ -16,12 +16,14 @@
package androidx.core.provider;
+import static androidx.core.provider.FontsContractCompat.FontFamilyResult.STATUS_OK;
import static androidx.core.provider.FontsContractCompat.FontFamilyResult.STATUS_UNEXPECTED_DATA_PROVIDED;
import static androidx.core.provider.FontsContractCompat.FontFamilyResult.STATUS_WRONG_CERTIFICATES;
import static androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR;
import static androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND;
import static androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND;
import static androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES;
+import static androidx.core.provider.FontsContractCompat.FontRequestCallback.RESULT_SUCCESS;
import android.annotation.SuppressLint;
import android.content.Context;
@@ -78,10 +80,17 @@
final @NonNull Handler handler
) {
final Handler callerHandler = CalleeHandler.create();
+ final int defaultStyle = Typeface.NORMAL;
+
+ final String id = createRequestId(request, defaultStyle);
+ Typeface cached = sTypefaceCache.get(id);
+ if (cached != null) {
+ notifyRetrieved(callerHandler, callback, cached);
+ }
+
handler.post(new Runnable() {
@Override
public void run() {
- // TODO: Cache the result.
FontsContractCompat.FontFamilyResult result;
try {
result = FontProvider.getFontFamilyResult(appContext, request, null);
@@ -90,7 +99,7 @@
return;
}
- if (result.getStatusCode() != FontsContractCompat.FontFamilyResult.STATUS_OK) {
+ if (result.getStatusCode() != STATUS_OK) {
switch (result.getStatusCode()) {
case STATUS_WRONG_CERTIFICATES:
notifyFailed(callerHandler, callback, FAIL_REASON_WRONG_CERTIFICATES);
@@ -129,7 +138,7 @@
final Typeface typeface = TypefaceCompat.createFromFontInfo(appContext,
null /* cancellationSignal */,
fonts,
- Typeface.NORMAL
+ defaultStyle
);
if (typeface == null) {
@@ -139,6 +148,9 @@
return;
}
+ if (typeface != null) {
+ sTypefaceCache.put(id, typeface);
+ }
notifyRetrieved(callerHandler, callback, typeface);
}
});
@@ -170,13 +182,23 @@
});
}
+ /**
+ * Used by TypefaceCompat and tests.
+ *
+ * @param context Context
+ * @param request FontRequest that defines the font to be loaded.
+ * @param fontCallback the callback to be called for async loading
+ * @param handler the Handler that the callback will be called on.
+ * @param isBlockingFetch when boolean the call will be synchronous
+ * @return Typeface the resulting Typeface if it is not an asynch request.
+ */
static Typeface getTypeface(
@NonNull final Context context,
@NonNull final FontRequest request,
@Nullable final ResourcesCompat.FontCallback fontCallback,
@Nullable final Handler handler, boolean isBlockingFetch, int timeout,
final int style) {
- final String id = request.getId() + "-" + style;
+ final String id = createRequestId(request, style);
Typeface cached = sTypefaceCache.get(id);
if (cached != null) {
if (fontCallback != null) {
@@ -185,26 +207,25 @@
return cached;
}
+ // when timeout is infinite, do not post to bg thread, since it will block other requests
if (isBlockingFetch && timeout == FontResourcesParserCompat.INFINITE_TIMEOUT_VALUE) {
// Wait forever. No need to post to the thread.
- TypefaceResult typefaceResult = getFontInternal(context, request, style);
+ TypefaceResult typefaceResult = getFontInternal(id, context, request, style);
+ Typeface typeface = typefaceResult.mTypeface;
if (fontCallback != null) {
- if (typefaceResult.mResult == FontsContractCompat.FontFamilyResult.STATUS_OK) {
- fontCallback.callbackSuccessAsync(typefaceResult.mTypeface, handler);
+ if (typefaceResult.mResult == STATUS_OK) {
+ fontCallback.callbackSuccessAsync(typeface, handler);
} else {
fontCallback.callbackFailAsync(typefaceResult.mResult, handler);
}
}
- return typefaceResult.mTypeface;
+ return typeface;
}
final Callable<TypefaceResult> fetcher = new Callable<TypefaceResult>() {
@Override
public TypefaceResult call() {
- TypefaceResult typeface = getFontInternal(context, request, style);
- if (typeface.mTypeface != null) {
- sTypefaceCache.put(id, typeface.mTypeface);
- }
+ TypefaceResult typeface = getFontInternal(id, context, request, style);
return typeface;
}
};
@@ -223,8 +244,7 @@
if (typeface == null) {
fontCallback.callbackFailAsync(
FAIL_REASON_FONT_NOT_FOUND, handler);
- } else if (typeface.mResult
- == FontsContractCompat.FontFamilyResult.STATUS_OK) {
+ } else if (typeface.mResult == STATUS_OK) {
fontCallback.callbackSuccessAsync(typeface.mTypeface, handler);
} else {
fontCallback.callbackFailAsync(typeface.mResult, handler);
@@ -268,29 +288,40 @@
}
}
+ private static String createRequestId(@NonNull FontRequest request, int style) {
+ return request.getId() + "-" + style;
+ }
+
/** Package protected to prevent synthetic accessor */
@SuppressLint("WrongConstant")
@NonNull
static TypefaceResult getFontInternal(
+ @NonNull final String cacheId,
@NonNull final Context context,
@NonNull final FontRequest request,
- int style) {
+ int style
+ ) {
FontsContractCompat.FontFamilyResult result;
try {
result = FontProvider.getFontFamilyResult(context, request, null);
} catch (PackageManager.NameNotFoundException e) {
return new TypefaceResult(null, FAIL_REASON_PROVIDER_NOT_FOUND);
}
- if (result.getStatusCode() == FontsContractCompat.FontFamilyResult.STATUS_OK) {
+ if (result.getStatusCode() == STATUS_OK) {
final Typeface typeface = TypefaceCompat.createFromFontInfo(
context, null /* CancellationSignal */, result.getFonts(), style);
+ if (typeface != null) {
+ sTypefaceCache.put(cacheId, typeface);
+ }
+
return new TypefaceResult(typeface, typeface != null
- ? FontsContractCompat.FontRequestCallback.RESULT_SUCCESS
+ ? RESULT_SUCCESS
: FAIL_REASON_FONT_LOAD_ERROR);
}
int resultCode = result.getStatusCode() == STATUS_WRONG_CERTIFICATES
? FAIL_REASON_WRONG_CERTIFICATES
: FAIL_REASON_FONT_LOAD_ERROR;
+
return new TypefaceResult(null, resultCode);
}
diff --git a/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java b/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java
index a878fd3..1d30889 100644
--- a/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java
+++ b/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java
@@ -53,7 +53,6 @@
public class FontsContractCompat {
private FontsContractCompat() { }
- // TODO deprecated from here, move to TypefaceCompat
/**
* Build a Typeface from an array of {@link FontInfo}
*
@@ -124,6 +123,16 @@
/**
* Used by TypefaceCompat and tests.
+ * @param context Context
+ * @param request FontRequest that defines the font to be loaded.
+ * @param fontCallback the callback to be called for async loading
+ * @param handler the Handler that the callback will be called on.
+ * @param isBlockingFetch when boolean the call will be synchronous
+ * @param timeout timeout the timeout for blocking requests
+ * @param style Typeface Style such as NORMAL, BOLD, ITALIC, BOLD_ITALIC
+ *
+ * @return Typeface the resulting Typeface if it is not an asynch request.
+ *
* @hide
*/
@RestrictTo(LIBRARY)
diff --git a/datastore/datastore-core/api/current.txt b/datastore/datastore-core/api/current.txt
index d0bb6a8..0b1e88c 100644
--- a/datastore/datastore-core/api/current.txt
+++ b/datastore/datastore-core/api/current.txt
@@ -27,8 +27,8 @@
public interface Serializer<T> {
method public T! getDefaultValue();
- method public T! readFrom(java.io.InputStream input);
- method public void writeTo(T? t, java.io.OutputStream output);
+ method public suspend Object? readFrom(java.io.InputStream input, kotlin.coroutines.Continuation<? super T> p);
+ method public suspend Object? writeTo(T? t, java.io.OutputStream output, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
property public abstract T! defaultValue;
}
diff --git a/datastore/datastore-core/api/public_plus_experimental_current.txt b/datastore/datastore-core/api/public_plus_experimental_current.txt
index d0bb6a8..0b1e88c 100644
--- a/datastore/datastore-core/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-core/api/public_plus_experimental_current.txt
@@ -27,8 +27,8 @@
public interface Serializer<T> {
method public T! getDefaultValue();
- method public T! readFrom(java.io.InputStream input);
- method public void writeTo(T? t, java.io.OutputStream output);
+ method public suspend Object? readFrom(java.io.InputStream input, kotlin.coroutines.Continuation<? super T> p);
+ method public suspend Object? writeTo(T? t, java.io.OutputStream output, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
property public abstract T! defaultValue;
}
diff --git a/datastore/datastore-core/api/restricted_current.txt b/datastore/datastore-core/api/restricted_current.txt
index d0bb6a8..0b1e88c 100644
--- a/datastore/datastore-core/api/restricted_current.txt
+++ b/datastore/datastore-core/api/restricted_current.txt
@@ -27,8 +27,8 @@
public interface Serializer<T> {
method public T! getDefaultValue();
- method public T! readFrom(java.io.InputStream input);
- method public void writeTo(T? t, java.io.OutputStream output);
+ method public suspend Object? readFrom(java.io.InputStream input, kotlin.coroutines.Continuation<? super T> p);
+ method public suspend Object? writeTo(T? t, java.io.OutputStream output, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
property public abstract T! defaultValue;
}
diff --git a/datastore/datastore-core/src/main/java/androidx/datastore/core/Serializer.kt b/datastore/datastore-core/src/main/java/androidx/datastore/core/Serializer.kt
index fdcda14..74ae35c 100644
--- a/datastore/datastore-core/src/main/java/androidx/datastore/core/Serializer.kt
+++ b/datastore/datastore-core/src/main/java/androidx/datastore/core/Serializer.kt
@@ -39,7 +39,7 @@
*
* @param input the InputStream with the data to deserialize
*/
- public fun readFrom(input: InputStream): T
+ public suspend fun readFrom(input: InputStream): T
/**
* Marshal object to a stream. Closing the provided OutputStream is a no-op.
@@ -47,7 +47,7 @@
* @param t the data to write to output
* @output the OutputStream to serialize data to
*/
- public fun writeTo(t: T, output: OutputStream)
+ public suspend fun writeTo(t: T, output: OutputStream)
}
/**
diff --git a/datastore/datastore-core/src/main/java/androidx/datastore/core/SingleProcessDataStore.kt b/datastore/datastore-core/src/main/java/androidx/datastore/core/SingleProcessDataStore.kt
index 65f5551..bded0c8 100644
--- a/datastore/datastore-core/src/main/java/androidx/datastore/core/SingleProcessDataStore.kt
+++ b/datastore/datastore-core/src/main/java/androidx/datastore/core/SingleProcessDataStore.kt
@@ -417,7 +417,7 @@
* Internal only to prevent creation of synthetic accessor function. Do not call this from
* outside this class.
*/
- internal fun writeData(newData: T) {
+ internal suspend fun writeData(newData: T) {
file.createParentDirectories()
val scratchFile = File(file.absolutePath + SCRATCH_SUFFIX)
diff --git a/datastore/datastore-core/src/test/java/androidx/datastore/core/SingleProcessDataStoreStressTest.kt b/datastore/datastore-core/src/test/java/androidx/datastore/core/SingleProcessDataStoreStressTest.kt
index 2e6a069..182b8c1 100644
--- a/datastore/datastore-core/src/test/java/androidx/datastore/core/SingleProcessDataStoreStressTest.kt
+++ b/datastore/datastore-core/src/test/java/androidx/datastore/core/SingleProcessDataStoreStressTest.kt
@@ -234,14 +234,14 @@
Serializer<Long> {
override val defaultValue = 0L
- override fun readFrom(input: InputStream): Long {
+ override suspend fun readFrom(input: InputStream): Long {
if (failReads) {
throw IOException("failing read")
}
return DataInputStream(input).readLong()
}
- override fun writeTo(t: Long, output: OutputStream) {
+ override suspend fun writeTo(t: Long, output: OutputStream) {
if (failWrites) {
throw IOException("failing write")
}
diff --git a/datastore/datastore-core/src/test/java/androidx/datastore/core/SingleProcessDataStoreTest.kt b/datastore/datastore-core/src/test/java/androidx/datastore/core/SingleProcessDataStoreTest.kt
index a0f58ba4..33f6f74 100644
--- a/datastore/datastore-core/src/test/java/androidx/datastore/core/SingleProcessDataStoreTest.kt
+++ b/datastore/datastore-core/src/test/java/androidx/datastore/core/SingleProcessDataStoreTest.kt
@@ -757,7 +757,7 @@
fun testClosingOutputStreamDoesntCloseUnderlyingStream() = runBlockingTest {
val delegate = TestingSerializer()
val serializer = object : Serializer<Byte> by delegate {
- override fun writeTo(t: Byte, output: OutputStream) {
+ override suspend fun writeTo(t: Byte, output: OutputStream) {
delegate.writeTo(t, output)
output.close() // This will be a no-op so the fd.sync() call will succeed.
}
@@ -898,11 +898,11 @@
override val defaultValue = ByteWrapper(delegate.defaultValue)
- override fun readFrom(input: InputStream): ByteWrapper {
+ override suspend fun readFrom(input: InputStream): ByteWrapper {
return ByteWrapper(delegate.readFrom(input))
}
- override fun writeTo(t: ByteWrapper, output: OutputStream) {
+ override suspend fun writeTo(t: ByteWrapper, output: OutputStream) {
delegate.writeTo(t.byte, output)
}
}
diff --git a/datastore/datastore-core/src/test/java/androidx/datastore/core/TestingSerializer.kt b/datastore/datastore-core/src/test/java/androidx/datastore/core/TestingSerializer.kt
index 9ccfe72..3aea9d7 100644
--- a/datastore/datastore-core/src/test/java/androidx/datastore/core/TestingSerializer.kt
+++ b/datastore/datastore-core/src/test/java/androidx/datastore/core/TestingSerializer.kt
@@ -26,7 +26,7 @@
@Volatile var failingWrite: Boolean = false,
override val defaultValue: Byte = 0
) : Serializer<Byte> {
- override fun readFrom(input: InputStream): Byte {
+ override suspend fun readFrom(input: InputStream): Byte {
if (failReadWithCorruptionException) {
throw CorruptionException(
"CorruptionException",
@@ -45,7 +45,7 @@
return read.toByte()
}
- override fun writeTo(t: Byte, output: OutputStream) {
+ override suspend fun writeTo(t: Byte, output: OutputStream) {
if (failingWrite) {
throw IOException("I was asked to fail on writes")
}
diff --git a/datastore/datastore-preferences-core/src/main/java/androidx/datastore/preferences/core/PreferencesSerializer.kt b/datastore/datastore-preferences-core/src/main/java/androidx/datastore/preferences/core/PreferencesSerializer.kt
index 00429d6..bd0a531 100644
--- a/datastore/datastore-preferences-core/src/main/java/androidx/datastore/preferences/core/PreferencesSerializer.kt
+++ b/datastore/datastore-preferences-core/src/main/java/androidx/datastore/preferences/core/PreferencesSerializer.kt
@@ -42,7 +42,7 @@
}
@Throws(IOException::class, CorruptionException::class)
- override fun readFrom(input: InputStream): Preferences {
+ override suspend fun readFrom(input: InputStream): Preferences {
val preferencesProto = PreferencesMapCompat.readFrom(input)
val mutablePreferences = mutablePreferencesOf()
@@ -55,7 +55,7 @@
}
@Throws(IOException::class, CorruptionException::class)
- override fun writeTo(t: Preferences, output: OutputStream) {
+ override suspend fun writeTo(t: Preferences, output: OutputStream) {
val preferences = t.asMap()
val protoBuilder = PreferenceMap.newBuilder()
diff --git a/datastore/datastore-preferences-core/src/test/java/androidx/datastore/preferences/core/PreferencesSerializerTest.kt b/datastore/datastore-preferences-core/src/test/java/androidx/datastore/preferences/core/PreferencesSerializerTest.kt
index a4b2dbf..b181c0f 100644
--- a/datastore/datastore-preferences-core/src/test/java/androidx/datastore/preferences/core/PreferencesSerializerTest.kt
+++ b/datastore/datastore-preferences-core/src/test/java/androidx/datastore/preferences/core/PreferencesSerializerTest.kt
@@ -17,6 +17,7 @@
package androidx.datastore.preferences.core
import androidx.datastore.core.CorruptionException
+import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -42,7 +43,7 @@
}
@Test
- fun testWriteAndReadString() {
+ fun testWriteAndReadString() = runBlockingTest {
val stringKey = stringPreferencesKey("string_key")
val prefs = preferencesOf(
@@ -61,7 +62,7 @@
}
@Test
- fun testWriteAndReadStringSet() {
+ fun testWriteAndReadStringSet() = runBlockingTest {
val stringSetKey =
stringSetPreferencesKey("string_set_key")
@@ -81,7 +82,7 @@
}
@Test
- fun testWriteAndReadLong() {
+ fun testWriteAndReadLong() = runBlockingTest {
val longKey = longPreferencesKey("long_key")
val prefs = preferencesOf(
@@ -100,7 +101,7 @@
}
@Test
- fun testWriteAndReadInt() {
+ fun testWriteAndReadInt() = runBlockingTest {
val intKey = intPreferencesKey("int_key")
val prefs = preferencesOf(
@@ -119,7 +120,7 @@
}
@Test
- fun testWriteAndReadBoolean() {
+ fun testWriteAndReadBoolean() = runBlockingTest {
val booleanKey = booleanPreferencesKey("boolean_key")
val prefs = preferencesOf(
@@ -138,7 +139,7 @@
}
@Test
- fun testWriteAndReadFloat() {
+ fun testWriteAndReadFloat() = runBlockingTest {
val floatKey = floatPreferencesKey("float_key")
val prefs = preferencesOf(
@@ -157,7 +158,7 @@
}
@Test
- fun testWriteAndReadDouble() {
+ fun testWriteAndReadDouble() = runBlockingTest {
val maxDouble = doublePreferencesKey("max_double_key")
val minDouble = doublePreferencesKey("min_double_key")
@@ -178,7 +179,7 @@
}
@Test
- fun testThrowsCorruptionException() {
+ fun testThrowsCorruptionException() = runBlockingTest {
// Not a valid proto - protos cannot start with a 0 byte.
testFile.writeBytes(byteArrayOf(0, 1, 2, 3, 4))
diff --git a/datastore/datastore-proto/build.gradle b/datastore/datastore-proto/build.gradle
index 83313e5..fd54e14 100644
--- a/datastore/datastore-proto/build.gradle
+++ b/datastore/datastore-proto/build.gradle
@@ -32,10 +32,12 @@
dependencies {
api(project(":datastore:datastore-core"))
api(PROTOBUF_LITE)
+ implementation(KOTLIN_COROUTINES_CORE)
testImplementation(TRUTH)
testImplementation(JUNIT)
testImplementation(project(":internal-testutils-truth"))
+ testImplementation(KOTLIN_COROUTINES_TEST)
}
protobuf {
diff --git a/datastore/datastore-proto/src/main/java/androidx/datastore/protos/ProtoSerializer.kt b/datastore/datastore-proto/src/main/java/androidx/datastore/protos/ProtoSerializer.kt
index d164822..eb25438 100644
--- a/datastore/datastore-proto/src/main/java/androidx/datastore/protos/ProtoSerializer.kt
+++ b/datastore/datastore-proto/src/main/java/androidx/datastore/protos/ProtoSerializer.kt
@@ -36,7 +36,7 @@
) : Serializer<T> {
@Suppress("UNCHECKED_CAST")
- override fun readFrom(input: InputStream): T {
+ override suspend fun readFrom(input: InputStream): T {
try {
return defaultValue.parserForType.parseFrom(input, extensionRegistryLite) as T
} catch (invalidProtocolBufferException: InvalidProtocolBufferException) {
@@ -46,7 +46,7 @@
}
}
- override fun writeTo(t: T, output: OutputStream) {
+ override suspend fun writeTo(t: T, output: OutputStream) {
t.writeTo(output)
}
}
\ No newline at end of file
diff --git a/datastore/datastore-proto/src/test/java/androidx/datastore/protos/ProtoSerializerTest.kt b/datastore/datastore-proto/src/test/java/androidx/datastore/protos/ProtoSerializerTest.kt
index aec23de6..0e05782 100644
--- a/datastore/datastore-proto/src/test/java/androidx/datastore/protos/ProtoSerializerTest.kt
+++ b/datastore/datastore-proto/src/test/java/androidx/datastore/protos/ProtoSerializerTest.kt
@@ -24,18 +24,21 @@
import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
import com.google.protobuf.ExtensionRegistryLite
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runBlockingTest
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
+@ExperimentalCoroutinesApi
class ProtoSerializerTest {
@get:Rule
val temporaryFolder = TemporaryFolder()
@Test
- fun testReadWriteProto() {
+ fun testReadWriteProto() = runBlockingTest {
val file = temporaryFolder.newFile("test_file.pb")
val fooProtoWithText = FooProto.newBuilder().setText("abc").build()
@@ -55,7 +58,7 @@
}
@Test
- fun testReadWriteProtoWithExtension() {
+ fun testReadWriteProtoWithExtension() = runBlockingTest {
val file = temporaryFolder.newFile("test_file.pb")
val registry = ExtensionRegistryLite.newInstance()
@@ -80,7 +83,7 @@
}
@Test
- fun testThrowsCorruptionException() {
+ fun testThrowsCorruptionException() = runBlockingTest {
val file = temporaryFolder.newFile("test_file.pb")
file.writeBytes(byteArrayOf(0x00, 0x02)) // Protos cannot start with 0x00.
diff --git a/datastore/datastore-rxjava2/src/test-common/java/androidx/datastore/rxjava2/TestingSerializer.kt b/datastore/datastore-rxjava2/src/test-common/java/androidx/datastore/rxjava2/TestingSerializer.kt
index 77a9ab6..2dedd9a 100644
--- a/datastore/datastore-rxjava2/src/test-common/java/androidx/datastore/rxjava2/TestingSerializer.kt
+++ b/datastore/datastore-rxjava2/src/test-common/java/androidx/datastore/rxjava2/TestingSerializer.kt
@@ -27,7 +27,7 @@
@Volatile var failingRead: Boolean = false,
@Volatile var failingWrite: Boolean = false
) : Serializer<Byte> {
- override fun readFrom(input: InputStream): Byte {
+ override suspend fun readFrom(input: InputStream): Byte {
if (failReadWithCorruptionException) {
throw CorruptionException(
"CorruptionException",
@@ -46,7 +46,7 @@
return read.toByte()
}
- override fun writeTo(t: Byte, output: OutputStream) {
+ override suspend fun writeTo(t: Byte, output: OutputStream) {
if (failingWrite) {
throw IOException("I was asked to fail on writes")
}
diff --git a/datastore/datastore-rxjava3/src/test-common/java/androidx/datastore/rxjava3/TestingSerializer.kt b/datastore/datastore-rxjava3/src/test-common/java/androidx/datastore/rxjava3/TestingSerializer.kt
index f1e02bb..5a51410 100644
--- a/datastore/datastore-rxjava3/src/test-common/java/androidx/datastore/rxjava3/TestingSerializer.kt
+++ b/datastore/datastore-rxjava3/src/test-common/java/androidx/datastore/rxjava3/TestingSerializer.kt
@@ -27,7 +27,7 @@
@Volatile var failingRead: Boolean = false,
@Volatile var failingWrite: Boolean = false
) : Serializer<Byte> {
- override fun readFrom(input: InputStream): Byte {
+ override suspend fun readFrom(input: InputStream): Byte {
if (failReadWithCorruptionException) {
throw CorruptionException(
"CorruptionException",
@@ -46,7 +46,7 @@
return read.toByte()
}
- override fun writeTo(t: Byte, output: OutputStream) {
+ override suspend fun writeTo(t: Byte, output: OutputStream) {
if (failingWrite) {
throw IOException("I was asked to fail on writes")
}
diff --git a/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/KotlinSerializationActivity.kt b/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/KotlinSerializationActivity.kt
index 0228738..1569be7 100644
--- a/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/KotlinSerializationActivity.kt
+++ b/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/KotlinSerializationActivity.kt
@@ -119,7 +119,7 @@
override val defaultValue: MySettings
get() = MySettings()
- override fun readFrom(input: InputStream): MySettings {
+ override suspend fun readFrom(input: InputStream): MySettings {
try {
return Json.decodeFromString<MySettings>(input.readBytes().decodeToString())
} catch (serialization: SerializationException) {
@@ -127,7 +127,7 @@
}
}
- override fun writeTo(t: MySettings, output: OutputStream) {
+ override suspend fun writeTo(t: MySettings, output: OutputStream) {
output.write(Json.encodeToString(t).encodeToByteArray())
}
}
\ No newline at end of file
diff --git a/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/ProtoDataStoreActivity.kt b/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/ProtoDataStoreActivity.kt
index 3d30e1a..1956c6b 100644
--- a/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/ProtoDataStoreActivity.kt
+++ b/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/ProtoDataStoreActivity.kt
@@ -109,7 +109,7 @@
override val defaultValue: Settings = Settings.getDefaultInstance()
- override fun readFrom(input: InputStream): Settings {
+ override suspend fun readFrom(input: InputStream): Settings {
try {
return Settings.parseFrom(input)
} catch (ipbe: InvalidProtocolBufferException) {
@@ -117,6 +117,6 @@
}
}
- override fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
+ override suspend fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
}
}
\ No newline at end of file
diff --git a/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/SettingsFragment.kt b/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/SettingsFragment.kt
index 91c8edd..4e31b91 100644
--- a/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/SettingsFragment.kt
+++ b/datastore/datastore-sampleapp/src/main/java/com/example/datastoresampleapp/SettingsFragment.kt
@@ -149,7 +149,7 @@
private object SettingsSerializer : Serializer<Settings> {
override val defaultValue: Settings = Settings.getDefaultInstance()
- override fun readFrom(input: InputStream): Settings {
+ override suspend fun readFrom(input: InputStream): Settings {
try {
return Settings.parseFrom(input)
} catch (ipbe: InvalidProtocolBufferException) {
@@ -157,5 +157,5 @@
}
}
- override fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
+ override suspend fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
}
\ No newline at end of file
diff --git a/datastore/datastore/src/androidTest/java/androidx/datastore/TestingSerializer.kt b/datastore/datastore/src/androidTest/java/androidx/datastore/TestingSerializer.kt
index 2448889..ffc73f3 100644
--- a/datastore/datastore/src/androidTest/java/androidx/datastore/TestingSerializer.kt
+++ b/datastore/datastore/src/androidTest/java/androidx/datastore/TestingSerializer.kt
@@ -29,7 +29,7 @@
) : Serializer<Byte> {
override val defaultValue: Byte = 0
- override fun readFrom(input: InputStream): Byte {
+ override suspend fun readFrom(input: InputStream): Byte {
if (failReadWithCorruptionException) {
throw CorruptionException(
"CorruptionException",
@@ -48,7 +48,7 @@
return read.toByte()
}
- override fun writeTo(t: Byte, output: OutputStream) {
+ override suspend fun writeTo(t: Byte, output: OutputStream) {
if (failingWrite) {
throw IOException("I was asked to fail on writes")
}
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
index 338ceac..f674bbd 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
@@ -30,8 +30,10 @@
import androidx.fragment.test.R
import androidx.lifecycle.ViewModelStore
import androidx.test.annotation.UiThreadTest
+import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
+import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Rule
import org.junit.Test
@@ -109,8 +111,8 @@
}
@Test
- fun testInflatedDialogFragmentShowsNow() {
- val fragment = InflatedDialogFragment()
+ fun testInflatedFragmentTagDialogFragmentShowsNow() {
+ val fragment = InflatedDialogFragment(false)
activityTestRule.runOnUiThread {
fragment.showNow(activityTestRule.activity.supportFragmentManager, null)
@@ -121,6 +123,32 @@
.isTrue()
}
+ @Test
+ fun testInflatedFragmentContainerViewDialogFragmentShowsNow() {
+ with(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
+ val fragment = InflatedDialogFragment()
+
+ withActivity {
+ fragment.showNow(supportFragmentManager, "fragment1")
+ }
+
+ assertWithMessage("Dialog was not being shown")
+ .that(fragment.dialog?.isShowing)
+ .isTrue()
+
+ recreate()
+
+ val restoredFragment = withActivity {
+ val fm = supportFragmentManager
+ fm.findFragmentByTag("fragment1") as InflatedDialogFragment
+ }
+
+ assertWithMessage("Dialog was not being shown")
+ .that(restoredFragment.dialog?.isShowing)
+ .isTrue()
+ }
+ }
+
@UiThreadTest
@Test
fun testDialogFragmentWithChild() {
@@ -423,9 +451,17 @@
}
}
- class InflatedDialogFragment : DialogFragment() {
+ class InflatedDialogFragment(val useFragmentContainerView: Boolean = true) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val view = layoutInflater.inflate(R.layout.inflated_fragment_tag, null, false)
+ val view = if (useFragmentContainerView) {
+ layoutInflater.inflate(
+ R.layout.inflated_fragment_container_view_no_parent,
+ null,
+ false
+ )
+ } else {
+ layoutInflater.inflate(R.layout.inflated_fragment_tag, null, false)
+ }
return AlertDialog.Builder(context)
.setTitle("Test")
.setMessage("Message")
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFinishEarlyTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFinishEarlyTest.kt
index a3f9af5..68ae0531 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFinishEarlyTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFinishEarlyTest.kt
@@ -16,7 +16,6 @@
package androidx.fragment.app
-import android.os.Build
import android.os.Bundle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -43,7 +42,7 @@
/**
* FragmentActivity should not raise the state of a Fragment while it is being destroyed.
*/
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
+ @SdkSuppress(minSdkVersion = 24) // this is failing remotely for API 23 devices b/178692379
@Test
fun fragmentActivityFinishEarly() {
val activity = activityRule.launchActivity(null)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
index 42f6704..3ffa1bb 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
@@ -1371,7 +1371,7 @@
}
@Test
- fun inflatedFragmentAfterResume() {
+ fun inflatedFragmentTagAfterResume() {
with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
val fragment = withActivity {
setContentView(R.layout.activity_inflated_fragment)
@@ -1385,6 +1385,77 @@
}
@Test
+ fun inflatedFragmentContainerViewAfterResume() {
+ with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+ var fragment = withActivity {
+ setContentView(R.layout.inflated_fragment_container_view)
+ val fm = supportFragmentManager
+ fm.findFragmentById(R.id.fragment_container_view) as InflatedFragment
+ }
+
+ assertThat(fragment).isNotNull()
+ assertThat(fragment.isResumed).isTrue()
+
+ recreate()
+
+ fragment = withActivity {
+ setContentView(R.layout.inflated_fragment_container_view)
+ val fm = supportFragmentManager
+ fm.findFragmentById(R.id.fragment_container_view) as InflatedFragment
+ }
+
+ assertThat(fragment).isNotNull()
+ assertThat(fragment.requireView().parent).isNotNull()
+ assertThat(fragment.isResumed).isTrue()
+ }
+ }
+
+ @Test
+ fun inflatedFragmentContainerViewWithMultipleFragmentsAfterResume() {
+ with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+ val addedFragment1 = StrictViewFragment()
+ val addedFragment2 = StrictViewFragment()
+ var fragment = withActivity {
+ setContentView(R.layout.inflated_fragment_container_view)
+ val fm = supportFragmentManager
+ fm.beginTransaction()
+ .add(R.id.fragment_container_view, addedFragment1, "addedFragment1")
+ .add(R.id.fragment_container_view, addedFragment2, "addedFragment2")
+ .commitNow()
+ fm.findFragmentByTag("fragment1") as InflatedFragment
+ }
+
+ assertThat(fragment).isNotNull()
+ assertThat(fragment.isResumed).isTrue()
+ assertThat(addedFragment1.isResumed).isTrue()
+ assertThat(addedFragment2.isResumed).isTrue()
+
+ recreate()
+
+ val fm = withActivity {
+ setContentView(R.layout.inflated_fragment_container_view)
+ supportFragmentManager
+ }
+
+ fragment = fm.findFragmentByTag("fragment1") as InflatedFragment
+ val restoredAddedFragment1 =
+ fm.findFragmentByTag("addedFragment1") as StrictViewFragment
+ val restoredAddedFragment2 =
+ fm.findFragmentByTag("addedFragment2") as StrictViewFragment
+
+ assertThat(fragment).isNotNull()
+
+ assertThat(fragment.requireView().parent).isNotNull()
+ assertThat(restoredAddedFragment1.requireView().parent).isNotNull()
+ assertThat(restoredAddedFragment2.requireView().parent).isNotNull()
+
+ assertThat(fragment.isResumed).isTrue()
+ assertThat(restoredAddedFragment1.isResumed).isTrue()
+ assertThat(restoredAddedFragment2.isResumed).isTrue()
+ }
+ }
+
+ @Test
@UiThreadTest
fun testReplaceChildFragmentInViewCreated() {
val viewModelStore = ViewModelStore()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.kt
index 256a07b..21201eb 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.kt
@@ -60,9 +60,12 @@
checkGetActivity()
checkActivityNotDestroyed()
checkState("onViewStateRestored", State.ACTIVITY_CREATED)
- assertWithMessage("Fragment should have a view parent")
- .that(requireView().parent)
- .isNotNull()
+ // Restored fragments can get to this point without being attached b/149024125
+ if (!mRestored) {
+ assertWithMessage("Fragment should have a view parent")
+ .that(requireView().parent)
+ .isNotNull()
+ }
}
override fun onDestroyView() {
diff --git a/fragment/fragment/src/androidTest/res/layout/inflated_fragment_container_view_no_parent.xml b/fragment/fragment/src/androidTest/res/layout/inflated_fragment_container_view_no_parent.xml
new file mode 100644
index 0000000..f5096d6
--- /dev/null
+++ b/fragment/fragment/src/androidTest/res/layout/inflated_fragment_container_view_no_parent.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 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.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/fragment_container_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:name="androidx.fragment.app.InflatedFragment"
+ android:tag="fragment1"/>
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.java
index 3e30902..27a1dfd 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.java
@@ -175,6 +175,7 @@
.add(this, containerFragment, tag)
.commitNowAllowingStateLoss();
}
+ fm.onContainerAvailable(this);
}
/**
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 6b62941..c0f196d 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -1037,6 +1037,19 @@
return null;
}
+ void onContainerAvailable(@NonNull FragmentContainerView container) {
+ for (FragmentStateManager fragmentStateManager:
+ mFragmentStore.getActiveFragmentStateManagers()) {
+ Fragment fragment = fragmentStateManager.getFragment();
+ if (fragment.mContainerId == container.getId() && fragment.mView != null
+ && fragment.mView.getParent() == null
+ ) {
+ fragment.mContainer = container;
+ fragmentStateManager.addViewToContainer();
+ }
+ }
+ }
+
/**
* Recurse up the view hierarchy, looking for a FragmentManager
*
diff --git a/paging/paging-compose/OWNERS b/paging/paging-compose/OWNERS
index 870a17c..64e60ba 100644
--- a/paging/paging-compose/OWNERS
+++ b/paging/paging-compose/OWNERS
@@ -1,2 +1 @@
andreykulikov@google.com
-mburlaciuc@google.com
\ No newline at end of file
diff --git a/room/common/src/main/java/androidx/room/AutoMigration.java b/room/common/src/main/java/androidx/room/AutoMigration.java
new file mode 100644
index 0000000..4c9bc18
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/AutoMigration.java
@@ -0,0 +1,49 @@
+/*
+ * 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.room;
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/**
+ * Automatic migration strategy for Room databases.
+ *
+ * @hide
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public @interface AutoMigration {
+ /**
+ * Version of the original database schema to migrate from.
+ *
+ * @return Version number of the original database schema.
+ */
+ int from();
+
+ /**
+ * Version of the new database schema to migrate to.
+ *
+ * @return Version number of the new database schema.
+ */
+ int to();
+}
diff --git a/room/common/src/main/java/androidx/room/Database.java b/room/common/src/main/java/androidx/room/Database.java
index f7a0f6f..2659bc8 100644
--- a/room/common/src/main/java/androidx/room/Database.java
+++ b/room/common/src/main/java/androidx/room/Database.java
@@ -16,6 +16,8 @@
package androidx.room;
+import androidx.annotation.RestrictTo;
+
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -101,4 +103,14 @@
* {@code room.schemaLocation} argument is set. Defaults to {@code true}.
*/
boolean exportSchema() default true;
+
+
+ /**
+ * List of AutoMigrations that can be performed on this Database.
+ *
+ * @return List of AutoMigrations.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ Class<?>[] autoMigrations() default {};
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt b/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
index ba1a146..8934d45 100644
--- a/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
@@ -92,6 +92,7 @@
ClassName.get("$ROOM_PACKAGE.util", "DBUtil")
val CURSOR_UTIL: ClassName =
ClassName.get("$ROOM_PACKAGE.util", "CursorUtil")
+ val MIGRATION: ClassName = ClassName.get("$ROOM_PACKAGE.migration", "Migration")
}
object PagingTypeNames {
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt
new file mode 100644
index 0000000..cbdb379
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.room.processor
+
+import androidx.room.AutoMigration
+import androidx.room.compiler.processing.XTypeElement
+import androidx.room.migration.bundle.DatabaseBundle
+import androidx.room.migration.bundle.SchemaBundle.deserialize
+import androidx.room.util.DiffException
+import androidx.room.util.SchemaDiffer
+import androidx.room.vo.AutoMigrationResult
+import java.io.File
+
+class AutoMigrationProcessor(
+ val context: Context,
+ val element: XTypeElement,
+ val latestDbSchema: DatabaseBundle
+) {
+ // TODO: (b/180116756) Validate annotated class extends auto migration interface
+
+ /**
+ * Retrieves two schemas of the same database provided in the @AutoMigration annotation,
+ * detects the schema changes that occurred between the two versions.
+ *
+ * @return the AutoMigrationResult containing the schema changes detected
+ */
+ fun process(): AutoMigrationResult? {
+ if (!element.isAbstract()) {
+ context.logger.e(ProcessorErrors.AUTOMIGRATION_ANNOTATED_TYPE_ELEMENT_MUST_BE_ABSTRACT)
+ return null
+ }
+
+ val annotationBox = element.toAnnotationBox(AutoMigration::class)
+ if (annotationBox == null) {
+ context.logger.e(
+ element,
+ ProcessorErrors.AUTOMIGRATION_ANNOTATION_MISSING
+ )
+ return null
+ }
+
+ val from = annotationBox.value.from
+ val to = annotationBox.value.to
+
+ if (to <= from) {
+ context.logger.e(ProcessorErrors.autoMigrationToVersionMustBeGreaterThanFrom(to, from))
+ return null
+ }
+
+ val validatedFromSchemaFile = getValidatedSchemaFile(from) ?: return null
+ val fromSchemaBundle = validatedFromSchemaFile.inputStream().use {
+ deserialize(it).database
+ }
+
+ val validatedToSchemaFile = getValidatedSchemaFile(to) ?: return null
+ val toSchemaBundle = if (to == latestDbSchema.version) {
+ latestDbSchema
+ } else {
+ validatedToSchemaFile.inputStream().use {
+ deserialize(it).database
+ }
+ }
+
+ val schemaDiff = try {
+ SchemaDiffer(
+ fromSchemaBundle = fromSchemaBundle,
+ toSchemaBundle = toSchemaBundle
+ ).diffSchemas()
+ } catch (ex: DiffException) {
+ context.logger.e(ex.errorMessage)
+ return null
+ }
+
+ return AutoMigrationResult(
+ element = element,
+ from = fromSchemaBundle.version,
+ to = toSchemaBundle.version,
+ addedColumns = schemaDiff.added
+ )
+ }
+
+ // TODO: File bug for not supporting downgrades.
+ // TODO: (b/180389433) If the files don't exist the getSchemaFile() method should return
+ // null and before calling process
+ private fun getValidatedSchemaFile(version: Int): File? {
+ val schemaFile = File(
+ context.schemaOutFolder,
+ "${element.qualifiedName}/$version.json"
+ )
+ if (!schemaFile.exists()) {
+ context.logger.e(
+ ProcessorErrors.autoMigrationSchemasNotFound(
+ context.schemaOutFolder.toString(),
+ "${element.qualifiedName}/$version.json"
+ )
+ )
+ return null
+ }
+ return schemaFile
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
index f97846d..745bd1f 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
@@ -22,8 +22,10 @@
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeElement
import androidx.room.ext.RoomTypeNames
+import androidx.room.migration.bundle.DatabaseBundle
import androidx.room.verifier.DatabaseVerificationErrors
import androidx.room.verifier.DatabaseVerifier
+import androidx.room.vo.AutoMigrationResult
import androidx.room.vo.Dao
import androidx.room.vo.DaoMethod
import androidx.room.vo.Database
@@ -115,9 +117,42 @@
exportSchema = dbAnnotation.value.exportSchema,
enableForeignKeys = hasForeignKeys
)
+ database.autoMigrations = processAutoMigrations(element, database.bundle)
return database
}
+ private fun processAutoMigrations(
+ element: XTypeElement,
+ latestDbSchema: DatabaseBundle
+ ): List<AutoMigrationResult> {
+ val dbAnnotation = element.toAnnotationBox(androidx.room.Database::class)!!
+ val autoMigrationList = dbAnnotation.getAsTypeList("autoMigrations")
+ context.checker.check(
+ autoMigrationList.isEmpty() || dbAnnotation.value.exportSchema,
+ element,
+ ProcessorErrors.AUTO_MIGRATION_FOUND_BUT_EXPORT_SCHEMA_OFF
+ )
+
+ return autoMigrationList.mapNotNull {
+ val typeElement = it.typeElement
+ if (typeElement == null) {
+ context.logger.e(
+ element,
+ ProcessorErrors.invalidAutoMigrationTypeInDatabaseAnnotation(
+ it.typeName
+ )
+ )
+ null
+ } else {
+ AutoMigrationProcessor(
+ context = context,
+ element = typeElement,
+ latestDbSchema = latestDbSchema
+ ).process()
+ }
+ }
+ }
+
private fun validateForeignKeys(element: XTypeElement, entities: List<Entity>) {
val byTableName = entities.associateBy { it.tableName }
entities.forEach { entity ->
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index 498357d..9d14b74 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -788,6 +788,11 @@
"interface."
}
+ fun invalidAutoMigrationTypeInDatabaseAnnotation(typeName: TypeName): String {
+ return "Invalid AutoMigration type: $typeName. An automigration in the database must be a" +
+ " class."
+ }
+
val EMBEDDED_TYPES_MUST_BE_A_CLASS_OR_INTERFACE = "The type of an Embedded field must be a " +
"class or an interface."
val RELATION_TYPE_MUST_BE_A_CLASS_OR_INTERFACE = "Entity type in a Relation must be a class " +
@@ -798,4 +803,51 @@
): String {
return "Invalid query argument: $typeName. It must be a class or an interface."
}
+
+ val AUTOMIGRATION_ANNOTATED_TYPE_ELEMENT_MUST_BE_ABSTRACT = "The @AutoMigration annotated " +
+ "type must be an abstract class."
+ val AUTOMIGRATION_ANNOTATION_MISSING = "The @AutoMigration annotation has not been found. " +
+ "Cannot generate auto migrations."
+
+ // TODO: (b/180389433) If the files don't exist the getSchemaFile() method should return
+ // null and before calling process
+ fun autoMigrationToVersionMustBeGreaterThanFrom(to: Int, from: Int) =
+ if (from > to) {
+ "Downgrades are not supported in AutoMigration."
+ } else {
+ "The versions provided (to: $to, from: $from) are invalid. The To version must" +
+ " be greater than the From version."
+ }
+
+ fun autoMigrationSchemasNotFound(schemaOutFolderPath: String, versionFile: String): String {
+ return "Schemas required for migration are not found at path: " +
+ "$schemaOutFolderPath$versionFile. Cannot generate auto migrations."
+ }
+
+ fun newNotNullColumnMustHaveDefaultValue(columnName: String): String {
+ return "New NOT NULL " +
+ "column'$columnName' " +
+ "added with no default value specified. Please specify the default value using " +
+ "@ColumnInfo."
+ }
+
+ fun nullabilityOfColumnChangedNotNullColumnMustHaveDefaultValue(columnName: String): String {
+ return "The nullability of the " +
+ "column '$columnName' " +
+ "has been changed from NULL to NOT NULL with no default value specified. Please " +
+ "specify the default value using @ColumnInfo."
+ }
+
+ fun columnWithChangedSchemaFound(columnName: String): String {
+ return "Encountered column '$columnName' with a changed FieldBundle schema. This change " +
+ "is not currently supported by AutoMigration."
+ }
+
+ fun removedOrRenamedColumnFound(columnName: String): String {
+ return "Column '$columnName' has been either removed or " +
+ "renamed. This change is not currently supported by AutoMigration."
+ }
+
+ val AUTO_MIGRATION_FOUND_BUT_EXPORT_SCHEMA_OFF = "Cannot create auto-migrations when export " +
+ "schema is OFF."
}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt b/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
new file mode 100644
index 0000000..80435f6
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.room.util
+
+import androidx.room.migration.bundle.DatabaseBundle
+import androidx.room.migration.bundle.EntityBundle
+import androidx.room.processor.ProcessorErrors
+import androidx.room.vo.AutoMigrationResult
+
+/**
+ * This exception should be thrown to abandon processing an @AutoMigration.
+ */
+class DiffException(val errorMessage: String) : RuntimeException(errorMessage)
+
+/**
+ * Contains the added, changed and removed columns detected.
+ */
+data class SchemaDiffResult(
+ val added: List<AutoMigrationResult.AddedColumn>,
+ val changed: List<AutoMigrationResult.ChangedColumn>,
+ val removed: List<AutoMigrationResult.RemovedColumn>
+)
+
+/**
+ * Receives the two bundles, diffs and returns a @SchemaDiffResult.
+ *
+ * Throws an @AutoMigrationException with a detailed error message when an AutoMigration cannot
+ * be generated.
+ */
+class SchemaDiffer(
+ val fromSchemaBundle: DatabaseBundle,
+ val toSchemaBundle: DatabaseBundle
+) {
+
+ /**
+ * Compares the two versions of the database based on the schemas provided, and detects
+ * schema changes.
+ *
+ * @return the AutoMigrationResult containing the schema changes detected
+ */
+ fun diffSchemas(): SchemaDiffResult {
+ val addedTables = mutableListOf<EntityBundle>()
+ val removedTables = mutableListOf<EntityBundle>()
+
+ val addedColumns = mutableListOf<AutoMigrationResult.AddedColumn>()
+ val changedColumns = mutableListOf<AutoMigrationResult.ChangedColumn>()
+ val removedColumns = mutableListOf<AutoMigrationResult.RemovedColumn>()
+
+ // Check going from the original version of the schema to the new version for changed and
+ // removed columns/tables
+ fromSchemaBundle.entitiesByTableName.forEach { v1Table ->
+ val v2Table = toSchemaBundle.entitiesByTableName[v1Table.key]
+ if (v2Table == null) {
+ removedTables.add(v1Table.value)
+ } else {
+ val v1Columns = v1Table.value.fieldsByColumnName
+ val v2Columns = v2Table.fieldsByColumnName
+ v1Columns.entries.forEach { v1Column ->
+ val match = v2Columns[v1Column.key]
+ if (match != null && !match.isSchemaEqual(v1Column.value)) {
+ changedColumns.add(
+ AutoMigrationResult.ChangedColumn(
+ v1Table.key,
+ v1Column.value,
+ match
+ )
+ )
+ } else if (match == null) {
+ removedColumns.add(
+ AutoMigrationResult.RemovedColumn(
+ v1Table.key,
+ v1Column.value
+ )
+ )
+ }
+ }
+ }
+ }
+ // Check going from the new version of the schema to the original version for added
+ // tables/columns. Skip the columns with the same name as the previous loop would have
+ // processed them already.
+ toSchemaBundle.entitiesByTableName.forEach { v2Table ->
+ val v1Table = fromSchemaBundle.entitiesByTableName[v2Table.key]
+ if (v1Table == null) {
+ addedTables.add(v2Table.value)
+ } else {
+ val v2Columns = v2Table.value.fieldsByColumnName
+ val v1Columns = v1Table.fieldsByColumnName
+ v2Columns.entries.forEach { v2Column ->
+ val match = v1Columns[v2Column.key]
+ if (match == null) {
+ if (v2Column.value.isNonNull && v2Column.value.defaultValue == null) {
+ throw DiffException(
+ ProcessorErrors.newNotNullColumnMustHaveDefaultValue(v2Column.key)
+ )
+ }
+ addedColumns.add(
+ AutoMigrationResult.AddedColumn(
+ v2Table.key,
+ v2Column.value
+ )
+ )
+ }
+ }
+ }
+ }
+
+ if (changedColumns.isNotEmpty()) {
+ changedColumns.forEach { changedColumn ->
+ throw DiffException(
+ ProcessorErrors.columnWithChangedSchemaFound(
+ changedColumn.originalFieldBundle.columnName
+ )
+ )
+ }
+ } else if (removedColumns.isNotEmpty()) {
+ removedColumns.forEach { removedColumn ->
+ throw DiffException(
+ ProcessorErrors.removedOrRenamedColumnFound(
+ removedColumn.fieldBundle.columnName
+ )
+ )
+ }
+ }
+
+ return SchemaDiffResult(
+ added = addedColumns,
+ changed = changedColumns,
+ removed = removedColumns
+ )
+ }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt b/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt
new file mode 100644
index 0000000..26d5230
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.room.vo
+
+import androidx.room.compiler.processing.XTypeElement
+import androidx.room.migration.bundle.FieldBundle
+
+data class AutoMigrationResult(
+ val element: XTypeElement,
+ val from: Int?,
+ val to: Int?,
+ val addedColumns: List<AddedColumn>
+) {
+ /**
+ * Stores the table name and the relevant field bundle of a column that was added to a
+ * database in a newer version.
+ */
+ data class AddedColumn(val tableName: String, val fieldBundle: FieldBundle)
+
+ /**
+ * Stores the table name and the relevant field bundle of a column that was present in both
+ * the old and new version of the same database, but had a change in the field schema (e.g.
+ * change in affinity).
+ */
+ data class ChangedColumn(
+ val tableName: String,
+ val originalFieldBundle: FieldBundle,
+ val newFieldBundle: FieldBundle
+ )
+
+ /**
+ * Stores the table name and the relevant field bundle of a column that was present in the
+ * old version of a database but is not present in a new version of the same database, either
+ * because it was removed or renamed.
+ *
+ * In the current implementation, we cannot differ between whether the column was removed or
+ * renamed.
+ */
+ data class RemovedColumn(val tableName: String, val fieldBundle: FieldBundle)
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Database.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Database.kt
index a22fb9d..f9d3f27 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/Database.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Database.kt
@@ -38,6 +38,9 @@
val exportSchema: Boolean,
val enableForeignKeys: Boolean
) {
+ // This variable will be set once auto-migrations are processed given the DatabaseBundle from
+ // this object. This is necessary for tracking the versions involved in the auto-migration.
+ lateinit var autoMigrations: List<AutoMigrationResult>
val typeName: ClassName by lazy { element.className }
private val implClassName by lazy {
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/AutoMigrationProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/AutoMigrationProcessorTest.kt
new file mode 100644
index 0000000..64d6733
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/AutoMigrationProcessorTest.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.room.processor
+
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.runProcessorTest
+import androidx.room.migration.bundle.DatabaseBundle
+import androidx.room.migration.bundle.EntityBundle
+import androidx.room.migration.bundle.FieldBundle
+import androidx.room.migration.bundle.PrimaryKeyBundle
+import androidx.room.migration.bundle.SchemaBundle
+import androidx.room.testing.context
+import org.junit.Test
+
+class AutoMigrationProcessorTest {
+ @Test
+ fun testElementIsAbstract() {
+ val source = Source.java(
+ "foo.bar.MyAutoMigration",
+ """
+ package foo.bar;
+ import androidx.room.AutoMigration;
+ @AutoMigration(from=1, to=2)
+ class MyAutoMigration implements AutoMigration {}
+ """.trimIndent()
+ )
+
+ runProcessorTest(listOf(source)) { invocation ->
+ AutoMigrationProcessor(
+ invocation.context,
+ invocation.processingEnv.requireTypeElement("foo.bar.MyAutoMigration"),
+ from.database
+ ).process()
+ invocation.assertCompilationResult {
+ hasError(ProcessorErrors.AUTOMIGRATION_ANNOTATED_TYPE_ELEMENT_MUST_BE_ABSTRACT)
+ }
+ }
+ }
+
+ /**
+ * Schemas for processor testing.
+ */
+ val from = SchemaBundle(
+ 1,
+ DatabaseBundle(
+ 1,
+ "",
+ mutableListOf(
+ EntityBundle(
+ "Song",
+ "CREATE TABLE IF NOT EXISTS `Song` (`id` INTEGER NOT NULL, " +
+ "`title` TEXT NOT NULL, `length` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ listOf(
+ FieldBundle(
+ "id",
+ "id",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "title",
+ "title",
+ "TEXT",
+ true,
+ ""
+ ),
+ FieldBundle(
+ "length",
+ "length",
+ "INTEGER",
+ true,
+ "1"
+ )
+ ),
+ PrimaryKeyBundle(
+ false,
+ mutableListOf("id")
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ )
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
index 9cdfce9..8094bb1d 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
@@ -172,6 +172,16 @@
}
"""
)
+
+ val AUTOMIGRATION: JavaFileObject = JavaFileObjects.forSourceString(
+ "foo.bar.MyAutoMigration",
+ """
+ package foo.bar;
+ import androidx.room.AutoMigration;
+ @AutoMigration(from=41, to=42)
+ abstract class MyAutoMigration implements AutoMigration {}
+ """
+ )
}
@Test
@@ -1127,6 +1137,20 @@
)
}
+ @Test
+ fun autoMigrationDefinedButDatabaseSchemaExportOff() {
+ singleDb(
+ """
+ @Database(entities = {User.class}, version = 42, exportSchema = false,
+ autoMigrations = {MyAutoMigration.class})
+ public abstract class MyDb extends RoomDatabase {}
+ """,
+ USER, AUTOMIGRATION
+ ) { _, _ -> }
+ .failsToCompile()
+ .withErrorContaining(ProcessorErrors.AUTO_MIGRATION_FOUND_BUT_EXPORT_SCHEMA_OFF)
+ }
+
private fun resolveDatabaseViews(
views: Map<String, Set<String>>,
body: (List<DatabaseView>) -> Unit
diff --git a/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt b/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt
new file mode 100644
index 0000000..6be4eea
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt
@@ -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.room.util
+
+import androidx.room.migration.bundle.DatabaseBundle
+import androidx.room.migration.bundle.EntityBundle
+import androidx.room.migration.bundle.FieldBundle
+import androidx.room.migration.bundle.PrimaryKeyBundle
+import androidx.room.migration.bundle.SchemaBundle
+import androidx.room.processor.ProcessorErrors
+import com.google.common.truth.Truth.assertThat
+import com.ibm.icu.impl.Assert.fail
+import org.junit.Test
+
+class SchemaDifferTest {
+
+ @Test
+ fun testColumnAddedWithColumnInfoDefaultValue() {
+ val schemaDiffResult = SchemaDiffer(
+ fromSchemaBundle = from.database,
+ toSchemaBundle = toColumnAddedWithColumnInfoDefaultValue.database
+ ).diffSchemas()
+ assertThat(schemaDiffResult.added[0].fieldBundle.columnName).isEqualTo("artistId")
+ }
+
+ @Test
+ fun testColumnAddedWithNoDefaultValue() {
+ try {
+ SchemaDiffer(
+ fromSchemaBundle = from.database,
+ toSchemaBundle = toColumnAddedWithNoDefaultValue.database
+ ).diffSchemas()
+ fail("DiffException should have been thrown.")
+ } catch (ex: DiffException) {
+ assertThat(ex.errorMessage).isEqualTo(
+ ProcessorErrors.newNotNullColumnMustHaveDefaultValue("artistId")
+ )
+ }
+ }
+
+ @Test
+ fun testColumnRenamed() {
+ try {
+ SchemaDiffer(
+ fromSchemaBundle = from.database,
+ toSchemaBundle = toColumnRenamed.database
+ ).diffSchemas()
+ fail("DiffException should have been thrown.")
+ } catch (ex: DiffException) {
+ assertThat(ex.errorMessage).isEqualTo(
+ ProcessorErrors.removedOrRenamedColumnFound("length")
+ )
+ }
+ }
+
+ @Test
+ fun testColumnAffinityChanged() {
+ try {
+ SchemaDiffer(
+ fromSchemaBundle = from.database,
+ toSchemaBundle = toColumnAffinityChanged.database
+ ).diffSchemas()
+ fail("DiffException should have been thrown.")
+ } catch (ex: DiffException) {
+ assertThat(ex.errorMessage).isEqualTo(
+ ProcessorErrors.columnWithChangedSchemaFound("length")
+ )
+ }
+ }
+
+ @Test
+ fun testColumnRemoved() {
+ try {
+ SchemaDiffer(
+ fromSchemaBundle = from.database,
+ toSchemaBundle = toColumnRemoved.database
+ ).diffSchemas()
+ fail("DiffException should have been thrown.")
+ } catch (ex: DiffException) {
+ assertThat(ex.errorMessage).isEqualTo(
+ ProcessorErrors.removedOrRenamedColumnFound("length")
+ )
+ }
+ }
+
+ val from = SchemaBundle(
+ 1,
+ DatabaseBundle(
+ 1,
+ "",
+ mutableListOf(
+ EntityBundle(
+ "Song",
+ "CREATE TABLE IF NOT EXISTS `Song` (`id` INTEGER NOT NULL, " +
+ "`title` TEXT NOT NULL, `length` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ listOf(
+ FieldBundle(
+ "id",
+ "id",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "title",
+ "title",
+ "TEXT",
+ true,
+ ""
+ ),
+ FieldBundle(
+ "length",
+ "length",
+ "INTEGER",
+ true,
+ "1"
+ )
+ ),
+ PrimaryKeyBundle(
+ false,
+ mutableListOf("id")
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ )
+
+ /** Valid "to" Schemas */
+ val toColumnAddedWithColumnInfoDefaultValue = SchemaBundle(
+ 2,
+ DatabaseBundle(
+ 2,
+ "",
+ listOf(
+ EntityBundle(
+ "Song",
+ "CREATE TABLE IF NOT EXISTS `Song` (`id` INTEGER NOT NULL, " +
+ "`title` TEXT NOT NULL, `length` INTEGER NOT NULL, `artistId` " +
+ "INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))",
+ listOf(
+ FieldBundle(
+ "id",
+ "id",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "title",
+ "title",
+ "TEXT",
+ true,
+ ""
+ ),
+ FieldBundle(
+ "length",
+ "length",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "artistId",
+ "artistId",
+ "INTEGER",
+ true,
+ "0"
+ )
+ ),
+ PrimaryKeyBundle(
+ false,
+ mutableListOf("id")
+ ),
+ emptyList(),
+ emptyList()
+ )
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ )
+
+ /** Invalid "to" Schemas (These are expected to throw an error.) */
+
+ /**
+ * The length column is removed from the first version. No other changes made.
+ *
+ */
+ val toColumnRemoved = SchemaBundle(
+ 2,
+ DatabaseBundle(
+ 2,
+ "",
+ listOf(
+ EntityBundle(
+ "Song",
+ "CREATE TABLE IF NOT EXISTS `Song` (`id` INTEGER NOT NULL, " +
+ "`title` TEXT NOT NULL, PRIMARY KEY(`id`))",
+ listOf(
+ FieldBundle(
+ "id",
+ "id",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "title",
+ "title",
+ "TEXT",
+ true,
+ ""
+ ),
+ ),
+ PrimaryKeyBundle(
+ false,
+ mutableListOf("id")
+ ),
+ emptyList(),
+ emptyList()
+ )
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ )
+
+ /**
+ * If the user declared the default value in the SQL statement and not used a @ColumnInfo,
+ * Room will put null for that default value in the exported schema. In this case we
+ * can't migrate.
+ */
+ val toColumnAddedWithNoDefaultValue = SchemaBundle(
+ 2,
+ DatabaseBundle(
+ 2,
+ "",
+ listOf(
+ EntityBundle(
+ "Song",
+ "CREATE TABLE IF NOT EXISTS `Song` (`id` INTEGER NOT NULL, " +
+ "`title` TEXT NOT NULL, `length` INTEGER NOT NULL, `artistId` " +
+ "INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ listOf(
+ FieldBundle(
+ "id",
+ "id",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "title",
+ "title",
+ "TEXT",
+ true,
+ ""
+ ),
+ FieldBundle(
+ "length",
+ "length",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "artistId",
+ "artistId",
+ "INTEGER",
+ true,
+ null
+ )
+ ),
+ PrimaryKeyBundle(
+ false,
+ mutableListOf("id")
+ ),
+ emptyList(),
+ emptyList()
+ )
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ )
+
+ /**
+ * Renaming the length column to duration.
+ */
+ // TODO: We currently do not support column renames as we can't detect rename or deletion
+ // yet.
+ val toColumnRenamed = SchemaBundle(
+ 2,
+ DatabaseBundle(
+ 2,
+ "",
+ mutableListOf(
+ EntityBundle(
+ "Song",
+ "CREATE TABLE IF NOT EXISTS `Song` (`id` INTEGER NOT " +
+ "NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL DEFAULT 0, " +
+ "PRIMARY KEY(`id`))",
+ listOf(
+ FieldBundle(
+ "id",
+ "id",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "title",
+ "title",
+ "TEXT",
+ true,
+ ""
+ ),
+ FieldBundle(
+ "duration",
+ "duration",
+ "INTEGER",
+ true,
+ "0"
+ )
+ ),
+ PrimaryKeyBundle(
+ false,
+ mutableListOf("id")
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ )
+
+ /**
+ * The affinity of a length column is changed from Integer to Text. No columns are
+ * added/removed.
+ */
+ // TODO: We currently do not support column affinity changes.
+ val toColumnAffinityChanged = SchemaBundle(
+ 2,
+ DatabaseBundle(
+ 2,
+ "",
+ mutableListOf(
+ EntityBundle(
+ "Song",
+ "CREATE TABLE IF NOT EXISTS `Song` (`id` INTEGER NOT NULL, " +
+ "`title` TEXT NOT NULL, `length` TEXT NOT NULL DEFAULT length, " +
+ "PRIMARY KEY(`id`))",
+ listOf(
+ FieldBundle(
+ "id",
+ "id",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "title",
+ "title",
+ "TEXT",
+ true,
+ ""
+ ),
+ FieldBundle(
+ "length",
+ "length",
+ "TEXT",
+ true,
+ "length"
+ )
+ ),
+ PrimaryKeyBundle(
+ false,
+ mutableListOf("id")
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ )
+}
\ No newline at end of file
diff --git a/samples/SupportSliceDemos/OWNERS b/samples/SupportSliceDemos/OWNERS
index c60f11f..8a9709c 100644
--- a/samples/SupportSliceDemos/OWNERS
+++ b/samples/SupportSliceDemos/OWNERS
@@ -1,4 +1,3 @@
-jmonk@google.com
madym@google.com
pinyaoting@google.com
sunnygoyal@google.com
diff --git a/slidingpanelayout/slidingpanelayout/build.gradle b/slidingpanelayout/slidingpanelayout/build.gradle
index 5401758..7a23a69 100644
--- a/slidingpanelayout/slidingpanelayout/build.gradle
+++ b/slidingpanelayout/slidingpanelayout/build.gradle
@@ -12,7 +12,7 @@
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.core:core:1.1.0")
api(project(":customview:customview"))
- implementation(project(":window:window"))
+ implementation("androidx.window:window:1.0.0-alpha03")
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
androidTestImplementation(ANDROIDX_TEST_RUNNER)
diff --git a/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/DatabaseLockingTest.kt b/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/DatabaseLockingTest.kt
index ca38758..67e4214 100644
--- a/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/DatabaseLockingTest.kt
+++ b/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/DatabaseLockingTest.kt
@@ -44,6 +44,7 @@
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors.newSingleThreadExecutor
import java.util.concurrent.TimeUnit.SECONDS
+import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.regex.Pattern.CASE_INSENSITIVE
@@ -288,6 +289,63 @@
Unit
}
+ @Test
+ fun test_endToEnd_inspector_lock_query_unlock_walOn() =
+ test_endToEnd_inspector_lock_query_unlock(writeAheadLoggingEnabled = true)
+
+ @Test
+ fun test_endToEnd_inspector_lock_query_unlock_walOff() =
+ test_endToEnd_inspector_lock_query_unlock(writeAheadLoggingEnabled = false)
+
+ private fun test_endToEnd_inspector_lock_query_unlock(
+ writeAheadLoggingEnabled: Boolean
+ ) = runBlocking {
+ // create database
+ val db = database.createInstance(temporaryFolder, writeAheadLoggingEnabled)
+ insertValue(db, value1)
+ assertThat(getValueSum(db)).isEqualTo(value1)
+
+ // inspect database
+ val dbId = testEnvironment.inspectDatabase(db)
+
+ // establish a lock
+ val lockId =
+ testEnvironment.sendCommand(acquireLockCommand(dbId)).acquireDatabaseLock.lockId
+ assertThat(lockId).isGreaterThan(0) // check that locking succeeded
+
+ // try to insert a value on app thread
+ val appInsertDone = AtomicBoolean(false)
+ val appInsertTask = applicationThread.submit {
+ insertValue(db, value2)
+ appInsertDone.set(true)
+ }
+ assertThat(appInsertDone.get()).isFalse()
+
+ // query schema
+ testEnvironment.sendCommand(MessageFactory.createGetSchemaCommand(dbId)).let {
+ assertThat(it.getSchema.tablesList.map { t -> t.name }).isEqualTo(listOf(table.name))
+ }
+
+ // repeat lock (testing simultaneous locks)
+ testEnvironment.sendCommand(acquireLockCommand(dbId)).let {
+ assertThat(it.acquireDatabaseLock.lockId).isEqualTo(lockId) // the same lock id expected
+ }
+
+ // query table
+ testEnvironment.issueQuery(dbId, "select sum(${column.name}) from ${table.name}").let {
+ assertThat(appInsertDone.get()).isFalse()
+ assertThat(it.rowsList.single().valuesList.single().longValue.toInt()).isEqualTo(value1)
+ }
+
+ // release all locks and verify the app thread insert operation succeeds
+ testEnvironment.sendCommand(releaseLockCommand(lockId))
+ assertThat(appInsertDone.get()).isFalse()
+ testEnvironment.sendCommand(releaseLockCommand(lockId))
+ appInsertTask.get(2, SECONDS)
+ assertThat(appInsertDone.get()).isTrue()
+ assertThat(getValueSum(db)).isEqualTo(value1 + value2)
+ }
+
@Suppress("SameParameterValue")
private fun withLockingTimeoutOverride(overrideMs: Int, block: () -> Any) {
val current = DatabaseLockRegistry.sTimeoutMs
diff --git a/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/DatabaseLockRegistry.java b/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/DatabaseLockRegistry.java
index 9c30e2d..3f4b9af 100644
--- a/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/DatabaseLockRegistry.java
+++ b/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/DatabaseLockRegistry.java
@@ -21,7 +21,9 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import androidx.sqlite.inspection.SqliteInspector.DatabaseConnection;
import java.util.HashMap;
import java.util.Map;
@@ -99,6 +101,19 @@
}
/**
+ * @return `null` if the database is not locked; the database and the executor that locked the
+ * database otherwise
+ */
+ @Nullable DatabaseConnection getConnection(int databaseId) {
+ synchronized (mLock) {
+ Lock lock = mDatabaseIdToLockMap.get(databaseId);
+ return (lock == null)
+ ? null
+ : new DatabaseConnection(lock.mDatabase, mExecutor);
+ }
+ }
+
+ /**
* Starts a database transaction and acquires an extra database reference to keep the database
* open while the lock is in place.
*/
diff --git a/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/DatabaseRegistry.java b/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/DatabaseRegistry.java
index fb0bd81..2ff137d 100644
--- a/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/DatabaseRegistry.java
+++ b/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/DatabaseRegistry.java
@@ -250,17 +250,15 @@
* Consumer of this method must release the reference when done using it.
* Thread-safe
*/
- // TODO: rename as can be confused with {@link SQLiteDatabase#acquireReference}.
@Nullable
- SQLiteDatabase acquireReference(int databaseId) {
+ SQLiteDatabase getConnection(int databaseId) {
synchronized (mLock) {
- return acquireReferenceImpl(databaseId);
+ return getConnectionImpl(databaseId);
}
}
- // TODO: rename as can be confused with {@link SQLiteDatabase#acquireReference}.
@GuardedBy("mLock")
- private SQLiteDatabase acquireReferenceImpl(int databaseId) {
+ private SQLiteDatabase getConnectionImpl(int databaseId) {
KeepOpenReference keepOpenReference = mKeepOpenReferences.get(databaseId);
if (keepOpenReference != null) {
return keepOpenReference.mDatabase;
@@ -313,7 +311,7 @@
}
// Try secure a keep-open reference
- SQLiteDatabase reference = acquireReferenceImpl(id);
+ SQLiteDatabase reference = getConnectionImpl(id);
if (reference != null) {
mKeepOpenReferences.put(id, new KeepOpenReference(reference));
}
diff --git a/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspector.java b/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspector.java
index ef524ed..0472a2d 100644
--- a/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspector.java
+++ b/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspector.java
@@ -295,21 +295,27 @@
}
}
+ /**
+ * Secures a lock (transaction) on the database. Note that while the lock is in place, no
+ * changes to the database are possible:
+ * - the lock prevents other threads from modifying the database,
+ * - lock thread, on releasing the lock, rolls-back all changes (transaction is rolled-back).
+ */
@SuppressWarnings("FutureReturnValueIgnored") // code inside the future is exception-proofed
private void handleAcquireDatabaseLock(
AcquireDatabaseLockCommand command,
final CommandCallback callback) {
final int databaseId = command.getDatabaseId();
- final SQLiteDatabase reference = acquireReference(databaseId, callback);
- if (reference == null) return;
+ final DatabaseConnection connection = acquireConnection(databaseId, callback);
+ if (connection == null) return;
- // TODO(161081452): consider cancellations
+ // Timeout is covered by mDatabaseLockRegistry
SqliteInspectionExecutors.submit(mIOExecutor, new Runnable() {
@Override
public void run() {
int lockId;
try {
- lockId = mDatabaseLockRegistry.acquireLock(databaseId, reference);
+ lockId = mDatabaseLockRegistry.acquireLock(databaseId, connection.mDatabase);
} catch (Exception e) {
processLockingException(callback, e, true);
return;
@@ -326,7 +332,7 @@
private void handleReleaseDatabaseLock(
final ReleaseDatabaseLockCommand command,
final CommandCallback callback) {
- // TODO(161081452): add cancellations when adding export job cancellations
+ // Timeout is covered by mDatabaseLockRegistry
SqliteInspectionExecutors.submit(mIOExecutor, new Runnable() {
@Override
public void run() {
@@ -590,25 +596,34 @@
).build().toByteArray());
}
- private void handleGetSchema(GetSchemaCommand command, CommandCallback callback) {
- SQLiteDatabase reference = acquireReference(command.getDatabaseId(), callback);
- if (reference == null) return;
+ @SuppressWarnings("FutureReturnValueIgnored") // code inside the future is exception-proofed
+ private void handleGetSchema(GetSchemaCommand command, final CommandCallback callback) {
+ final DatabaseConnection connection = acquireConnection(command.getDatabaseId(), callback);
+ if (connection == null) return;
- callback.reply(querySchema(reference).toByteArray());
+ // TODO: consider a timeout
+ SqliteInspectionExecutors.submit(connection.mExecutor, new Runnable() {
+ @Override
+ public void run() {
+ callback.reply(querySchema(connection.mDatabase).toByteArray());
+ }
+ });
}
private void handleQuery(final QueryCommand command, final CommandCallback callback) {
- final SQLiteDatabase reference = acquireReference(command.getDatabaseId(), callback);
- if (reference == null) return;
+ final DatabaseConnection connection = acquireConnection(command.getDatabaseId(), callback);
+ if (connection == null) return;
final CancellationSignal cancellationSignal = new CancellationSignal();
- final Future<?> future = SqliteInspectionExecutors.submit(mIOExecutor, new Runnable() {
+ final Executor executor = connection.mExecutor;
+ // TODO: consider a timeout
+ final Future<?> future = SqliteInspectionExecutors.submit(executor, new Runnable() {
@Override
public void run() {
String[] params = parseQueryParameterValues(command);
Cursor cursor = null;
try {
- cursor = rawQuery(reference, command.getQuery(), params,
+ cursor = rawQuery(connection.mDatabase, command.getQuery(), params,
cancellationSignal);
List<String> columnNames = Arrays.asList(cursor.getColumnNames());
callback.reply(Response.newBuilder()
@@ -715,18 +730,36 @@
* Tries to find a database for an id. If no such database is found, it replies with an
* {@link ErrorOccurredResponse} via the {@code callback} provided.
*
- * Consumer of this method must release the reference when done using it.
+ * TODO: remove race condition (affects WAL=off)
+ * - lock request is received and in the process of being secured
+ * - query request is received and since no lock in place, receives an IO Executor
+ * - lock request completes and holds a lock on the database
+ * - query cannot run because there is a lock in place
+ *
+ * The race condition can be mitigated by clients by securing a lock synchronously with no
+ * other queries in place.
*
* @return null if no database found for the provided id. A database reference otherwise.
*/
@Nullable
- private SQLiteDatabase acquireReference(int databaseId, CommandCallback callback) {
- SQLiteDatabase database = mDatabaseRegistry.acquireReference(databaseId);
+ private DatabaseConnection acquireConnection(int databaseId, CommandCallback callback) {
+ DatabaseConnection connection = mDatabaseLockRegistry.getConnection(databaseId);
+ if (connection != null) {
+ // With WAL enabled, we prefer to use the IO executor. With WAL off we don't have a
+ // choice and must use the executor that has a lock (transaction) on the database.
+ return connection.mDatabase.isWriteAheadLoggingEnabled()
+ ? new DatabaseConnection(connection.mDatabase, mIOExecutor)
+ : connection;
+ }
+
+ SQLiteDatabase database = mDatabaseRegistry.getConnection(databaseId);
if (database == null) {
replyNoDatabaseWithId(callback, databaseId);
return null;
}
- return database;
+
+ // Given no lock, IO executor is appropriate.
+ return new DatabaseConnection(database, mIOExecutor);
}
private static List<Row> convert(Cursor cursor) {
@@ -916,4 +949,20 @@
String path = file.getPath();
return path.endsWith("-journal") || path.endsWith("-shm") || path.endsWith("-wal");
}
+
+ /**
+ * Provides a reference to the database and an executor to access the database.
+ *
+ * Executor is relevant in the context of locking, where a locked database with WAL disabled
+ * needs to run queries on the thread that locked it.
+ */
+ static final class DatabaseConnection {
+ @NonNull final SQLiteDatabase mDatabase;
+ @NonNull final Executor mExecutor;
+
+ DatabaseConnection(@NonNull SQLiteDatabase database, @NonNull Executor executor) {
+ mDatabase = database;
+ mExecutor = executor;
+ }
+ }
}
diff --git a/transition/transition-ktx/OWNERS b/transition/transition-ktx/OWNERS
index e450f4c..e69de29 100644
--- a/transition/transition-ktx/OWNERS
+++ b/transition/transition-ktx/OWNERS
@@ -1 +0,0 @@
-jakew@google.com
diff --git a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index c7d3a21..92f2a52 100644
--- a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -339,9 +339,7 @@
internal suspend fun getPreviewData(
providerInfoRetriever: ProviderInfoRetriever,
providerInfo: ComplicationProviderInfo?
- ): ComplicationData? = TraceEvent(
- "BaseEditorSession.getPreviewData for ${providerInfo?.providerComponentName}"
- ).use {
+ ): ComplicationData? = TraceEvent("BaseEditorSession.getPreviewData").use {
if (providerInfo == null) {
return null
}
diff --git a/window/window-extensions/build.gradle b/window/window-extensions/build.gradle
index 6e84dbc..e4db938 100644
--- a/window/window-extensions/build.gradle
+++ b/window/window-extensions/build.gradle
@@ -33,7 +33,7 @@
android {
defaultConfig {
- minSdkVersion 16
+ minSdkVersion 14
}
}
diff --git a/window/window-sidecar/build.gradle b/window/window-sidecar/build.gradle
index 5c704f4..6c81f6e 100644
--- a/window/window-sidecar/build.gradle
+++ b/window/window-sidecar/build.gradle
@@ -26,7 +26,7 @@
android {
defaultConfig {
- minSdkVersion 16
+ minSdkVersion 14
}
}
diff --git a/window/window/build.gradle b/window/window/build.gradle
index db51a1c..706f6f6 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -36,7 +36,7 @@
android {
defaultConfig {
- minSdkVersion 16
+ minSdkVersion 14
}
buildTypes.all {
consumerProguardFiles "proguard-rules.pro"
diff --git a/window/window/src/androidTest/java/androidx/window/TestActivity.java b/window/window/src/androidTest/java/androidx/window/TestActivity.java
index 65d1a81..b5632cd 100644
--- a/window/window/src/androidTest/java/androidx/window/TestActivity.java
+++ b/window/window/src/androidTest/java/androidx/window/TestActivity.java
@@ -27,7 +27,6 @@
public class TestActivity extends Activity implements View.OnLayoutChangeListener {
- private int mRootViewId;
private CountDownLatch mLayoutLatch = new CountDownLatch(1);
private static CountDownLatch sResumeLatch = new CountDownLatch(1);
@@ -35,8 +34,6 @@
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final View contentView = new View(this);
- mRootViewId = View.generateViewId();
- contentView.setId(mRootViewId);
setContentView(contentView);
getWindow().getDecorView().addOnLayoutChangeListener(this);
diff --git a/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java b/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java
index 5db5039..c320d4d 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java
@@ -17,6 +17,7 @@
package androidx.window;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
@@ -286,6 +287,8 @@
Point realDisplaySize = WindowBoundsHelper.getRealSizeForDisplay(display);
Rect bounds = WindowBoundsHelper.getInstance().computeCurrentWindowBounds(activity);
+ assertNotEquals("Device can not have zero width", 0, realDisplaySize.x);
+ assertNotEquals("Device can not have zero height", 0, realDisplaySize.y);
assertEquals("Window bounds width does not match real display width",
realDisplaySize.x, bounds.width());
assertEquals("Window bounds height does not match real display height",
diff --git a/window/window/src/main/java/androidx/window/WindowBoundsHelper.java b/window/window/src/main/java/androidx/window/WindowBoundsHelper.java
index d2db428..c71b772 100644
--- a/window/window/src/main/java/androidx/window/WindowBoundsHelper.java
+++ b/window/window/src/main/java/androidx/window/WindowBoundsHelper.java
@@ -16,7 +16,7 @@
package androidx.window;
-import static android.os.Build.VERSION_CODES.JELLY_BEAN;
+import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.P;
@@ -118,7 +118,7 @@
} else if (Build.VERSION.SDK_INT >= N) {
return computeWindowBoundsN(activity);
} else {
- return computeWindowBoundsJellyBean(activity);
+ return computeWindowBoundsIceCreamSandwich(activity);
}
}
@@ -296,14 +296,18 @@
* size which should match the window size of a full-screen app.
*/
@NonNull
- @RequiresApi(JELLY_BEAN)
- private static Rect computeWindowBoundsJellyBean(Activity activity) {
+ @RequiresApi(ICE_CREAM_SANDWICH)
+ private static Rect computeWindowBoundsIceCreamSandwich(Activity activity) {
Display defaultDisplay = activity.getWindowManager().getDefaultDisplay();
Point realDisplaySize = getRealSizeForDisplay(defaultDisplay);
Rect bounds = new Rect();
- bounds.right = realDisplaySize.x;
- bounds.bottom = realDisplaySize.y;
+ if (realDisplaySize.x == 0 || realDisplaySize.y == 0) {
+ defaultDisplay.getRectSize(bounds);
+ } else {
+ bounds.right = realDisplaySize.x;
+ bounds.bottom = realDisplaySize.y;
+ }
return bounds;
}
@@ -319,7 +323,7 @@
*/
@NonNull
@VisibleForTesting
- @RequiresApi(JELLY_BEAN)
+ @RequiresApi(ICE_CREAM_SANDWICH)
static Point getRealSizeForDisplay(Display display) {
Point size = new Point();
if (Build.VERSION.SDK_INT >= JELLY_BEAN_MR1) {