Merge "Update KSP to alpha07" into androidx-main
diff --git a/activity/activity-ktx/OWNERS b/activity/activity-ktx/OWNERS
index e450f4c..6fd8227 100644
--- a/activity/activity-ktx/OWNERS
+++ b/activity/activity-ktx/OWNERS
@@ -1 +1 @@
-jakew@google.com
+yboyar@google.com
diff --git a/activity/settings.gradle b/activity/settings.gradle
index 387f67e..94d2b98 100644
--- a/activity/settings.gradle
+++ b/activity/settings.gradle
@@ -24,6 +24,8 @@
if (name == ":internal-testutils-runtime") return true
if (name == ":compose:lint:common") return true
if (name == ":compose:lint:internal-lint-checks") return true
+ if (name == ":compose:test-utils") return true
+ if (name == ":test-screenshot") return true
return false
})
diff --git a/appcompat/appcompat/api/current.txt b/appcompat/appcompat/api/current.txt
index 6b91606..3d0e9f6 100644
--- a/appcompat/appcompat/api/current.txt
+++ b/appcompat/appcompat/api/current.txt
@@ -506,11 +506,16 @@
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportButtonTintMode(android.graphics.PorterDuff.Mode?);
}
- public class AppCompatCheckedTextView extends android.widget.CheckedTextView {
+ public class AppCompatCheckedTextView extends android.widget.CheckedTextView implements androidx.core.view.TintableBackgroundView {
ctor public AppCompatCheckedTextView(android.content.Context);
ctor public AppCompatCheckedTextView(android.content.Context, android.util.AttributeSet?);
ctor public AppCompatCheckedTextView(android.content.Context, android.util.AttributeSet?, int);
- method public void setTextAppearance(android.content.Context!, int);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.content.res.ColorStateList? getSupportBackgroundTintList();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.graphics.PorterDuff.Mode? getSupportBackgroundTintMode();
+ method public void setBackgroundDrawable(android.graphics.drawable.Drawable?);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintList(android.content.res.ColorStateList?);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
+ method public void setTextAppearance(android.content.Context, int);
}
public class AppCompatEditText extends android.widget.EditText implements androidx.core.view.OnReceiveContentViewBehavior androidx.core.view.TintableBackgroundView {
diff --git a/appcompat/appcompat/api/public_plus_experimental_current.txt b/appcompat/appcompat/api/public_plus_experimental_current.txt
index 8886644..cef99ba 100644
--- a/appcompat/appcompat/api/public_plus_experimental_current.txt
+++ b/appcompat/appcompat/api/public_plus_experimental_current.txt
@@ -506,11 +506,20 @@
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportButtonTintMode(android.graphics.PorterDuff.Mode?);
}
- public class AppCompatCheckedTextView extends android.widget.CheckedTextView {
+ public class AppCompatCheckedTextView extends android.widget.CheckedTextView implements androidx.core.view.TintableBackgroundView androidx.core.widget.TintableCheckedTextView {
ctor public AppCompatCheckedTextView(android.content.Context);
ctor public AppCompatCheckedTextView(android.content.Context, android.util.AttributeSet?);
ctor public AppCompatCheckedTextView(android.content.Context, android.util.AttributeSet?, int);
- method public void setTextAppearance(android.content.Context!, int);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.content.res.ColorStateList? getSupportBackgroundTintList();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.graphics.PorterDuff.Mode? getSupportBackgroundTintMode();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.content.res.ColorStateList? getSupportCheckMarkTintList();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.graphics.PorterDuff.Mode? getSupportCheckMarkTintMode();
+ method public void setBackgroundDrawable(android.graphics.drawable.Drawable?);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintList(android.content.res.ColorStateList?);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportCheckMarkTintList(android.content.res.ColorStateList?);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportCheckMarkTintMode(android.graphics.PorterDuff.Mode?);
+ method public void setTextAppearance(android.content.Context, int);
}
public class AppCompatEditText extends android.widget.EditText implements androidx.core.view.OnReceiveContentViewBehavior androidx.core.view.TintableBackgroundView {
diff --git a/appcompat/appcompat/api/restricted_current.txt b/appcompat/appcompat/api/restricted_current.txt
index d86ea87..e0b7fe6 100644
--- a/appcompat/appcompat/api/restricted_current.txt
+++ b/appcompat/appcompat/api/restricted_current.txt
@@ -1374,11 +1374,20 @@
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportButtonTintMode(android.graphics.PorterDuff.Mode?);
}
- public class AppCompatCheckedTextView extends android.widget.CheckedTextView {
+ public class AppCompatCheckedTextView extends android.widget.CheckedTextView implements androidx.core.view.TintableBackgroundView androidx.core.widget.TintableCheckedTextView {
ctor public AppCompatCheckedTextView(android.content.Context);
ctor public AppCompatCheckedTextView(android.content.Context, android.util.AttributeSet?);
ctor public AppCompatCheckedTextView(android.content.Context, android.util.AttributeSet?, int);
- method public void setTextAppearance(android.content.Context!, int);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.content.res.ColorStateList? getSupportBackgroundTintList();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.graphics.PorterDuff.Mode? getSupportBackgroundTintMode();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.content.res.ColorStateList? getSupportCheckMarkTintList();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.graphics.PorterDuff.Mode? getSupportCheckMarkTintMode();
+ method public void setBackgroundDrawable(android.graphics.drawable.Drawable?);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintList(android.content.res.ColorStateList?);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportCheckMarkTintList(android.content.res.ColorStateList?);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportCheckMarkTintMode(android.graphics.PorterDuff.Mode?);
+ method public void setTextAppearance(android.content.Context, int);
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class AppCompatDrawableManager {
diff --git a/appcompat/appcompat/src/androidTest/AndroidManifest.xml b/appcompat/appcompat/src/androidTest/AndroidManifest.xml
index da2fca5..41d32b4 100644
--- a/appcompat/appcompat/src/androidTest/AndroidManifest.xml
+++ b/appcompat/appcompat/src/androidTest/AndroidManifest.xml
@@ -148,6 +148,11 @@
android:theme="@style/Theme.AppCompat.Light"/>
<activity
+ android:name="androidx.appcompat.widget.AppCompatCheckedTextViewActivity"
+ android:label="@string/app_compat_checkedtextview_activity"
+ android:theme="@style/Theme.AppCompat.Light"/>
+
+ <activity
android:name="androidx.appcompat.widget.AppCompatToggleButtonActivity"
android:label="@string/app_compat_toggle_button_activity"
android:theme="@style/Theme.AppCompat.Light"/>
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatCheckedTextViewActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatCheckedTextViewActivity.java
new file mode 100644
index 0000000..a0756d2
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatCheckedTextViewActivity.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 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.appcompat.widget;
+
+import androidx.appcompat.test.R;
+import androidx.appcompat.testutils.BaseTestActivity;
+
+public final class AppCompatCheckedTextViewActivity extends BaseTestActivity {
+ @Override
+ protected int getContentViewLayoutResId() {
+ return R.layout.appcompat_checkedtextview_activity;
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatCheckedTextViewTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatCheckedTextViewTest.java
new file mode 100644
index 0000000..92e3a95
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatCheckedTextViewTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 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.appcompat.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Typeface;
+import android.graphics.drawable.AnimatedStateListDrawable;
+import android.graphics.drawable.Drawable;
+
+import androidx.appcompat.graphics.drawable.AnimatedStateListDrawableCompat;
+import androidx.appcompat.test.R;
+import androidx.core.content.res.ResourcesCompat;
+import androidx.core.widget.CheckedTextViewCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Provides tests specific to {@link AppCompatCheckedTextView} class.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class AppCompatCheckedTextViewTest extends AppCompatBaseViewTest<
+ AppCompatCheckedTextViewActivity, AppCompatCheckedTextView> {
+
+ public AppCompatCheckedTextViewTest() {
+ super(AppCompatCheckedTextViewActivity.class);
+ }
+
+ @Override
+ protected boolean hasBackgroundByDefault() {
+ return true;
+ }
+
+ @Test
+ public void testFontResources() {
+ AppCompatCheckedTextView textView =
+ mContainer.findViewById(R.id.checkedtextview_fontresource);
+ Typeface expected = ResourcesCompat.getFont(mActivity, R.font.samplefont);
+
+ assertEquals(expected, textView.getTypeface());
+ }
+
+ @Test
+ public void testCheckMarkDefault_isNull() {
+ // Given an ACCTV with the theme's check mark drawable
+ final AppCompatCheckedTextView textView =
+ mContainer.findViewById(R.id.checkedtextview_check_mark_default);
+ final Drawable checkMark = CheckedTextViewCompat.getCheckMarkDrawable(textView);
+
+ // Then this drawable should be null
+ assertNull(checkMark);
+ }
+
+ @Test
+ public void testCheckMarkBoth_isAnimated() {
+ // Given an ACCTV which specifies both non-null android:checkMark and app:checkMarkCompat
+ final AppCompatCheckedTextView checkedTextView =
+ mContainer.findViewById(R.id.checkedtextview_check_mark_both);
+ final Drawable checkMark = CheckedTextViewCompat.getCheckMarkDrawable(checkedTextView);
+
+ // Then this drawable should be an animated-selector
+ // i.e. compat version has precedence
+ assertTrue(checkMark instanceof AnimatedStateListDrawableCompat
+ || checkMark instanceof AnimatedStateListDrawable);
+ }
+
+ /* Max SDK as we use this test to verify the fallback behavior in situations where the ASLD
+ backport should not be used (e.g. building with AAPT1). */
+ @SdkSuppress(maxSdkVersion = 20)
+ @Test
+ public void testCheckMarkPlatformOnly_isNotNull() {
+ // Given an ACCTV which specifies a null app:checkMarkCompat and non-null android:checkMark
+ final AppCompatCheckedTextView checkedTextView =
+ mContainer.findViewById(R.id.checkedtextview_check_mark_platform);
+ final Drawable checkMark = CheckedTextViewCompat.getCheckMarkDrawable(checkedTextView);
+
+ // Then the drawable should be present
+ assertNotNull(checkMark);
+ }
+
+ @Test
+ public void testCheckMarkCompatOnly_isNotNull() {
+ // Given an ACCTV which specifies a null android:checkMark and non-null app:checkMarkCompat
+ final AppCompatCheckedTextView checkedTextView =
+ mContainer.findViewById(R.id.checkedtextview_check_mark_compat);
+ final Drawable checkMark = CheckedTextViewCompat.getCheckMarkDrawable(checkedTextView);
+
+ // Then the drawable should be present
+ assertNotNull(checkMark);
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/res/layout/appcompat_checkedtextview_activity.xml b/appcompat/appcompat/src/androidTest/res/layout/appcompat_checkedtextview_activity.xml
new file mode 100644
index 0000000..02ff5e5
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/res/layout/appcompat_checkedtextview_activity.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <androidx.appcompat.widget.AppCompatCheckedTextView
+ android:id="@+id/checkedtextview_fontresource"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text1"
+ app:fontFamily="@font/samplefont" />
+
+ <androidx.appcompat.widget.AppCompatCheckedTextView
+ android:id="@+id/checkedtextview_check_mark_default"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <androidx.appcompat.widget.AppCompatCheckedTextView
+ android:id="@+id/checkedtextview_check_mark_platform"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:checkMark="?android:attr/listChoiceIndicatorMultiple" />
+
+ <androidx.appcompat.widget.AppCompatCheckedTextView
+ android:id="@+id/checkedtextview_check_mark_compat"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:checkMarkCompat="?android:attr/listChoiceIndicatorMultiple" />
+
+ <androidx.appcompat.widget.AppCompatCheckedTextView
+ android:id="@+id/checkedtextview_check_mark_both"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:checkMark="?android:attr/listChoiceIndicatorMultiple"
+ app:checkMarkCompat="?attr/listChoiceIndicatorMultipleAnimated" />
+
+ <androidx.appcompat.widget.AppCompatCheckedTextView
+ android:id="@+id/view_tinted_no_background"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text1"
+ app:backgroundTint="@color/color_state_list_lilac"
+ app:backgroundTintMode="src_in" />
+
+ <androidx.appcompat.widget.AppCompatCheckedTextView
+ android:id="@+id/view_tinted_background"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text2"
+ android:background="@drawable/test_drawable"
+ app:backgroundTint="@color/color_state_list_lilac"
+ app:backgroundTintMode="src_in" />
+
+ <androidx.appcompat.widget.AppCompatCheckedTextView
+ android:id="@+id/view_untinted_no_background"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text2" />
+
+ <androidx.appcompat.widget.AppCompatCheckedTextView
+ android:id="@+id/view_untinted_background"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text2"
+ android:background="@drawable/test_background_green" />
+
+</LinearLayout>
diff --git a/appcompat/appcompat/src/androidTest/res/values/strings.xml b/appcompat/appcompat/src/androidTest/res/values/strings.xml
index 563bc0e..34698e7 100644
--- a/appcompat/appcompat/src/androidTest/res/values/strings.xml
+++ b/appcompat/appcompat/src/androidTest/res/values/strings.xml
@@ -67,6 +67,7 @@
<string name="app_compat_image_view_activity">AppCompat image view</string>
<string name="app_compat_button_activity">AppCompat button</string>
<string name="app_compat_checkbox_activity">AppCompat checkbox</string>
+ <string name="app_compat_checkedtextview_activity">AppCompat checked text view</string>
<string name="app_compat_radio_button_activity">AppCompat radio button</string>
<string name="app_compat_toggle_button_activity">AppCompat toggle button</string>
<string name="app_compat_icons_activity">AppCompat icons</string>
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCheckedTextView.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCheckedTextView.java
index 60d71e1..39d75d1 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCheckedTextView.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCheckedTextView.java
@@ -16,33 +16,56 @@
package androidx.appcompat.widget;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.ActionMode;
+import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.CheckedTextView;
+import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appcompat.R;
import androidx.appcompat.content.res.AppCompatResources;
+import androidx.core.view.TintableBackgroundView;
+import androidx.core.view.ViewCompat;
import androidx.core.widget.TextViewCompat;
+import androidx.core.widget.TintableCheckedTextView;
/**
- * A {@link CheckedTextView} which supports compatible features on older versions of the platform.
+ * A {@link CheckedTextView} which supports compatible features on older versions of the platform,
+ * including:
+ * <ul>
+ * <li>Allows dynamic tint of its background via the background tint methods in
+ * {@link androidx.core.view.ViewCompat}.</li>
+ * <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
+ * {@link R.attr#backgroundTintMode}.</li>
+ * <li>Allows dynamic tint of its check mark via the check mark tint methods in
+ * {@link androidx.core.widget.CheckedTextViewCompat}.</li>
+ * <li>Allows setting of the check mark tint using {@link R.attr#checkMarkTint} and
+ * {@link R.attr#checkMarkTintMode}.</li>
+ * <li>Allows setting of the font family using {@link android.R.attr#fontFamily}</li>
+ * </ul>
*
* <p>This will automatically be used when you use {@link CheckedTextView} in your layouts
* and the top-level activity / dialog is provided by
* <a href="{@docRoot}topic/libraries/support-library/packages.html#v7-appcompat">appcompat</a>.
* You should only need to manually use this class when writing custom views.</p>
*/
-public class AppCompatCheckedTextView extends CheckedTextView {
+public class AppCompatCheckedTextView extends CheckedTextView implements TintableCheckedTextView,
+ TintableBackgroundView {
- private static final int[] TINT_ATTRS = {
- android.R.attr.checkMark
- };
-
+ private final AppCompatCheckedTextViewHelper mCheckedHelper;
+ private final AppCompatBackgroundHelper mBackgroundTintHelper;
private final AppCompatTextHelper mTextHelper;
public AppCompatCheckedTextView(@NonNull Context context) {
@@ -50,7 +73,7 @@
}
public AppCompatCheckedTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, android.R.attr.checkedTextViewStyle);
+ this(context, attrs, R.attr.checkedTextViewStyle);
}
public AppCompatCheckedTextView(
@@ -63,10 +86,19 @@
mTextHelper.loadFromAttributes(attrs, defStyleAttr);
mTextHelper.applyCompoundDrawablesTints();
- TintTypedArray a = TintTypedArray.obtainStyledAttributes(getContext(), attrs,
- TINT_ATTRS, defStyleAttr, 0);
- setCheckMarkDrawable(a.getDrawable(0));
- a.recycle();
+ mBackgroundTintHelper = new AppCompatBackgroundHelper(this);
+ mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
+
+ mCheckedHelper = new AppCompatCheckedTextViewHelper(this);
+ mCheckedHelper.loadFromAttributes(attrs, defStyleAttr);
+ }
+
+ @Override
+ public void setCheckMarkDrawable(@Nullable Drawable d) {
+ super.setCheckMarkDrawable(d);
+ if (mCheckedHelper != null) {
+ mCheckedHelper.onSetCheckMarkDrawable();
+ }
}
@Override
@@ -74,8 +106,133 @@
setCheckMarkDrawable(AppCompatResources.getDrawable(getContext(), resId));
}
+ /**
+ * This should be accessed from {@link androidx.core.widget.CheckedTextViewCompat}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
- public void setTextAppearance(Context context, int resId) {
+ public void setSupportCheckMarkTintList(@Nullable ColorStateList tint) {
+ if (mCheckedHelper != null) {
+ mCheckedHelper.setSupportCheckMarkTintList(tint);
+ }
+ }
+
+ /**
+ * This should be accessed from {@link androidx.core.widget.CheckedTextViewCompat}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Nullable
+ @Override
+ public ColorStateList getSupportCheckMarkTintList() {
+ return mCheckedHelper != null
+ ? mCheckedHelper.getSupportCheckMarkTintList()
+ : null;
+ }
+
+ /**
+ * This should be accessed from {@link androidx.core.widget.CheckedTextViewCompat}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public void setSupportCheckMarkTintMode(@Nullable PorterDuff.Mode tintMode) {
+ if (mCheckedHelper != null) {
+ mCheckedHelper.setSupportCheckMarkTintMode(tintMode);
+ }
+ }
+
+ /**
+ * This should be accessed from {@link androidx.core.widget.CheckedTextViewCompat}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Nullable
+ @Override
+ public PorterDuff.Mode getSupportCheckMarkTintMode() {
+ return mCheckedHelper != null
+ ? mCheckedHelper.getSupportCheckMarkTintMode()
+ : null;
+ }
+
+ /**
+ * This should be accessed via
+ * {@link ViewCompat#setBackgroundTintList(View, ColorStateList)}
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public void setSupportBackgroundTintList(@Nullable ColorStateList tint) {
+ if (mBackgroundTintHelper != null) {
+ mBackgroundTintHelper.setSupportBackgroundTintList(tint);
+ }
+ }
+
+ /**
+ * This should be accessed via
+ * {@link androidx.core.view.ViewCompat#getBackgroundTintList(android.view.View)}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ @Nullable
+ public ColorStateList getSupportBackgroundTintList() {
+ return mBackgroundTintHelper != null
+ ? mBackgroundTintHelper.getSupportBackgroundTintList() : null;
+ }
+
+ /**
+ * This should be accessed via
+ * {@link ViewCompat#setBackgroundTintMode(View, PorterDuff.Mode)}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ public void setSupportBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
+ if (mBackgroundTintHelper != null) {
+ mBackgroundTintHelper.setSupportBackgroundTintMode(tintMode);
+ }
+ }
+
+ /**
+ * This should be accessed via
+ * {@link androidx.core.view.ViewCompat#getBackgroundTintMode(android.view.View)}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Override
+ @Nullable
+ public PorterDuff.Mode getSupportBackgroundTintMode() {
+ return mBackgroundTintHelper != null
+ ? mBackgroundTintHelper.getSupportBackgroundTintMode() : null;
+ }
+
+ @Override
+ public void setBackgroundDrawable(@Nullable Drawable background) {
+ super.setBackgroundDrawable(background);
+ if (mBackgroundTintHelper != null) {
+ mBackgroundTintHelper.onSetBackgroundDrawable(background);
+ }
+ }
+
+ @Override
+ public void setBackgroundResource(@DrawableRes int resId) {
+ super.setBackgroundResource(resId);
+ if (mBackgroundTintHelper != null) {
+ mBackgroundTintHelper.onSetBackgroundResource(resId);
+ }
+ }
+
+ @Override
+ public void setTextAppearance(@NonNull Context context, int resId) {
super.setTextAppearance(context, resId);
if (mTextHelper != null) {
mTextHelper.onSetTextAppearance(context, resId);
@@ -88,10 +245,17 @@
if (mTextHelper != null) {
mTextHelper.applyCompoundDrawablesTints();
}
+ if (mBackgroundTintHelper != null) {
+ mBackgroundTintHelper.applySupportBackgroundTint();
+ }
+ if (mCheckedHelper != null) {
+ mCheckedHelper.applyCheckMarkTint();
+ }
}
@Override
- public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ @Nullable
+ public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
return AppCompatHintHelper.onCreateInputConnection(super.onCreateInputConnection(outAttrs),
outAttrs, this);
}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCheckedTextViewHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCheckedTextViewHelper.java
new file mode 100644
index 0000000..7fe18e6
--- /dev/null
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCheckedTextViewHelper.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 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.appcompat.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.CheckedTextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appcompat.R;
+import androidx.appcompat.content.res.AppCompatResources;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.core.view.ViewCompat;
+import androidx.core.widget.CheckedTextViewCompat;
+
+/** @hide */
+@RestrictTo(LIBRARY)
+class AppCompatCheckedTextViewHelper {
+ @NonNull
+ private final CheckedTextView mView;
+
+ private ColorStateList mCheckMarkTintList = null;
+ private PorterDuff.Mode mCheckMarkTintMode = null;
+ private boolean mHasCheckMarkTint = false;
+ private boolean mHasCheckMarkTintMode = false;
+
+ private boolean mSkipNextApply;
+
+ AppCompatCheckedTextViewHelper(@NonNull CheckedTextView view) {
+ mView = view;
+ }
+
+ void loadFromAttributes(@Nullable AttributeSet attrs, int defStyleAttr) {
+ TintTypedArray a =
+ TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,
+ R.styleable.CheckedTextView, defStyleAttr, 0);
+ ViewCompat.saveAttributeDataForStyleable(mView, mView.getContext(),
+ R.styleable.CheckedTextView, attrs, a.getWrappedTypeArray(), defStyleAttr, 0);
+ try {
+ boolean checkMarkDrawableLoaded = false;
+ if (a.hasValue(R.styleable.CheckedTextView_checkMarkCompat)) {
+ final int resourceId = a.getResourceId(R.styleable.CheckedTextView_checkMarkCompat,
+ 0);
+ if (resourceId != 0) {
+ try {
+ mView.setCheckMarkDrawable(
+ AppCompatResources.getDrawable(mView.getContext(), resourceId));
+ checkMarkDrawableLoaded = true;
+ } catch (Resources.NotFoundException ignore) {
+ // Animated checkMarkCompat relies on AAPT2 features. If not found then
+ // swallow this error and fall back to the regular drawable.
+ }
+ }
+ }
+ if (!checkMarkDrawableLoaded && a.hasValue(
+ R.styleable.CheckedTextView_android_checkMark)) {
+ final int resourceId = a.getResourceId(
+ R.styleable.CheckedTextView_android_checkMark, 0);
+ if (resourceId != 0) {
+ mView.setCheckMarkDrawable(
+ AppCompatResources.getDrawable(mView.getContext(), resourceId));
+ }
+ }
+ if (a.hasValue(R.styleable.CheckedTextView_checkMarkTint)) {
+ CheckedTextViewCompat.setCheckMarkTintList(mView,
+ a.getColorStateList(R.styleable.CheckedTextView_checkMarkTint));
+ }
+ if (a.hasValue(R.styleable.CheckedTextView_checkMarkTintMode)) {
+ CheckedTextViewCompat.setCheckMarkTintMode(mView,
+ DrawableUtils.parseTintMode(
+ a.getInt(R.styleable.CheckedTextView_checkMarkTintMode, -1),
+ null));
+ }
+ } finally {
+ a.recycle();
+ }
+ }
+
+ void setSupportCheckMarkTintList(ColorStateList tint) {
+ mCheckMarkTintList = tint;
+ mHasCheckMarkTint = true;
+
+ applyCheckMarkTint();
+ }
+
+ ColorStateList getSupportCheckMarkTintList() {
+ return mCheckMarkTintList;
+ }
+
+ void setSupportCheckMarkTintMode(@Nullable PorterDuff.Mode tintMode) {
+ mCheckMarkTintMode = tintMode;
+ mHasCheckMarkTintMode = true;
+
+ applyCheckMarkTint();
+ }
+
+ PorterDuff.Mode getSupportCheckMarkTintMode() {
+ return mCheckMarkTintMode;
+ }
+
+ void onSetCheckMarkDrawable() {
+ if (mSkipNextApply) {
+ mSkipNextApply = false;
+ return;
+ }
+
+ mSkipNextApply = true;
+ applyCheckMarkTint();
+ }
+
+ void applyCheckMarkTint() {
+ Drawable checkMarkDrawable = CheckedTextViewCompat.getCheckMarkDrawable(mView);
+
+ if (checkMarkDrawable != null && (mHasCheckMarkTint || mHasCheckMarkTintMode)) {
+ checkMarkDrawable = DrawableCompat.wrap(checkMarkDrawable);
+ checkMarkDrawable = checkMarkDrawable.mutate();
+ if (mHasCheckMarkTint) {
+ DrawableCompat.setTintList(checkMarkDrawable, mCheckMarkTintList);
+ }
+ if (mHasCheckMarkTintMode) {
+ DrawableCompat.setTintMode(checkMarkDrawable, mCheckMarkTintMode);
+ }
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (checkMarkDrawable.isStateful()) {
+ checkMarkDrawable.setState(mView.getDrawableState());
+ }
+ mView.setCheckMarkDrawable(checkMarkDrawable);
+ }
+ }
+}
diff --git a/appcompat/appcompat/src/main/res/values/attrs.xml b/appcompat/appcompat/src/main/res/values/attrs.xml
index c758fe3..1f7cb89 100644
--- a/appcompat/appcompat/src/main/res/values/attrs.xml
+++ b/appcompat/appcompat/src/main/res/values/attrs.xml
@@ -1125,6 +1125,35 @@
</attr>
</declare-styleable>
+ <declare-styleable name="CheckedTextView">
+ <attr name="android:checkMark"/>
+ <!-- Compat attr to load backported drawable types -->
+ <attr name="checkMarkCompat" format="reference"/>
+ <!-- Tint to apply to the check mark drawable. -->
+ <attr name="checkMarkTint" format="color" />
+
+ <!-- Blending mode used to apply the check mark tint. -->
+ <attr name="checkMarkTintMode">
+ <!-- The tint is drawn on top of the drawable.
+ [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] -->
+ <enum name="src_over" value="3" />
+ <!-- The tint is masked by the alpha channel of the drawable. The drawable’s
+ color channels are thrown out. [Sa * Da, Sc * Da] -->
+ <enum name="src_in" value="5" />
+ <!-- The tint is drawn above the drawable, but with the drawable’s alpha
+ channel masking the result. [Da, Sc * Da + (1 - Sa) * Dc] -->
+ <enum name="src_atop" value="9" />
+ <!-- Multiplies the color and alpha channels of the drawable with those of
+ the tint. [Sa * Da, Sc * Dc] -->
+ <enum name="multiply" value="14" />
+ <!-- [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] -->
+ <enum name="screen" value="15" />
+ <!-- Combines the tint and icon color and alpha channels, clamping the
+ result to valid color values. Saturate(S + D) -->
+ <enum name="add" value="16" />
+ </attr>
+ </declare-styleable>
+
<declare-styleable name="SwitchCompat">
<!-- Drawable to use as the "thumb" that switches back and forth. -->
<attr name="android:thumb" />
diff --git a/appcompat/appcompat/src/main/res/values/themes_base.xml b/appcompat/appcompat/src/main/res/values/themes_base.xml
index bed6f98..5d505c9 100644
--- a/appcompat/appcompat/src/main/res/values/themes_base.xml
+++ b/appcompat/appcompat/src/main/res/values/themes_base.xml
@@ -298,6 +298,7 @@
<item name="drawerArrowStyle">@style/Widget.AppCompat.DrawerArrowToggle</item>
+ <item name="checkedTextViewStyle">?android:attr/checkedTextViewStyle</item>
<item name="checkboxStyle">@style/Widget.AppCompat.CompoundButton.CheckBox</item>
<item name="radioButtonStyle">@style/Widget.AppCompat.CompoundButton.RadioButton</item>
<item name="switchStyle">@style/Widget.AppCompat.CompoundButton.Switch</item>
diff --git a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
index f82adad..0e23046 100644
--- a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -137,6 +137,8 @@
configBuilder.isBenchmark(true)
if (configBuilder.isPostsubmit) {
configBuilder.tag("microbenchmarks")
+ } else {
+ configBuilder.tag("microbenchmarks_presubmit")
}
} else if (testProjectPath.get().endsWith("macrobenchmark")) {
configBuilder.tag("macrobenchmarks")
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/SignInTemplateDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/SignInTemplateDemoScreen.java
index a1b4f05..000eb03 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/SignInTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/SignInTemplateDemoScreen.java
@@ -69,7 +69,7 @@
() -> getScreenManager().push(new LongMessageTemplateDemoScreen(getCarContext())));
private final Action mProviderSignInAction = new Action.Builder()
- .setTitle("Sign In with Google")
+ .setTitle("Google Sign-In")
.setOnClickListener(() -> {
mState = State.PROVIDER;
invalidate();
@@ -77,7 +77,7 @@
.build();
private final Action mPinSignInAction = new Action.Builder()
- .setTitle("Sign In with PIN")
+ .setTitle("Use PIN")
.setOnClickListener(() -> {
mState = State.PIN;
invalidate();
diff --git a/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
index 8b58ce6..46ef1092 100644
--- a/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
@@ -54,8 +54,6 @@
<action android:name="androidx.car.app.CarAppService"/>
<category android:name="androidx.car.app.category.NAVIGATION"/>
</intent-filter>
- <meta-data android:name="androidx.car.app.CAR_APP_ACTIVITY"
- android:value="androidx.car.app.samples.showcase.Showcase" />
</service>
<service
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index feae637..419ee77 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -29,7 +29,7 @@
AndroidXUiPlugin.applyAndConfigureKotlinPlugin(project)
dependencies {
- kotlinPlugin(project(":compose:compiler:compiler"))
+ kotlinPlugin(projectOrArtifact(":compose:compiler:compiler"))
if(!AndroidXUiPlugin.isMultiplatformEnabled(project)) {
/*
@@ -38,23 +38,23 @@
*/
api("androidx.activity:activity:1.2.0")
- api(project(":compose:ui:ui-test-junit4"))
+ api(projectOrArtifact(":compose:ui:ui-test-junit4"))
api(project(":test-screenshot"))
implementation(KOTLIN_STDLIB_COMMON)
- implementation(project(":compose:runtime:runtime"))
- implementation(project(":compose:ui:ui-unit"))
- implementation(project(":compose:ui:ui-graphics"))
+ implementation(projectOrArtifact(":compose:runtime:runtime"))
+ implementation(projectOrArtifact(":compose:ui:ui-unit"))
+ implementation(projectOrArtifact(":compose:ui:ui-graphics"))
implementation(ANDROIDX_TEST_RULES)
// This has stub APIs for access to legacy Android APIs, so we don't want
// any dependency on this module.
- compileOnly(project(":compose:ui:ui-android-stubs"))
+ compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
testImplementation(TRUTH)
androidTestImplementation(TRUTH)
- androidTestImplementation(project(":compose:material:material"))
+ androidTestImplementation(projectOrArtifact(":compose:material:material"))
}
}
@@ -68,19 +68,19 @@
sourceSets {
commonMain.dependencies {
implementation(KOTLIN_STDLIB_COMMON)
- implementation(project(":compose:runtime:runtime"))
- implementation(project(":compose:ui:ui-unit"))
- implementation(project(":compose:ui:ui-graphics"))
- implementation(project(":compose:ui:ui-test-junit4"))
+ implementation(projectOrArtifact(":compose:runtime:runtime"))
+ implementation(projectOrArtifact(":compose:ui:ui-unit"))
+ implementation(projectOrArtifact(":compose:ui:ui-graphics"))
+ implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
}
androidMain.dependencies {
api("androidx.activity:activity:1.2.0")
- api(project(":compose:ui:ui-test-junit4"))
+ api(projectOrArtifact(":compose:ui:ui-test-junit4"))
api(project(":test-screenshot"))
// This has stub APIs for access to legacy Android APIs, so we don't want
// any dependency on this module.
- compileOnly(project(":compose:ui:ui-android-stubs"))
+ compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
implementation(ANDROIDX_TEST_RULES)
}
@@ -90,7 +90,7 @@
androidAndroidTest.dependencies {
implementation(TRUTH)
- implementation(project(":compose:material:material"))
+ implementation(projectOrArtifact(":compose:material:material"))
}
}
}
diff --git a/core/core-ktx/OWNERS b/core/core-ktx/OWNERS
index e450f4c..6fd8227 100644
--- a/core/core-ktx/OWNERS
+++ b/core/core-ktx/OWNERS
@@ -1 +1 @@
-jakew@google.com
+yboyar@google.com
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index c8e7acb..024bf6e 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -3341,6 +3341,14 @@
field public static final float RELATIVE_UNSPECIFIED = 0.0f;
}
+ public final class CheckedTextViewCompat {
+ method public static android.graphics.drawable.Drawable? getCheckMarkDrawable(android.widget.CheckedTextView);
+ method public static android.content.res.ColorStateList? getCheckMarkTintList(android.widget.CheckedTextView);
+ method public static android.graphics.PorterDuff.Mode? getCheckMarkTintMode(android.widget.CheckedTextView);
+ method public static void setCheckMarkTintList(android.widget.CheckedTextView, android.content.res.ColorStateList?);
+ method public static void setCheckMarkTintMode(android.widget.CheckedTextView, android.graphics.PorterDuff.Mode?);
+ }
+
public final class CompoundButtonCompat {
method public static android.graphics.drawable.Drawable? getButtonDrawable(android.widget.CompoundButton);
method public static android.content.res.ColorStateList? getButtonTintList(android.widget.CompoundButton);
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 10dd3c9..7854121 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -3339,6 +3339,14 @@
field public static final float RELATIVE_UNSPECIFIED = 0.0f;
}
+ public final class CheckedTextViewCompat {
+ method public static android.graphics.drawable.Drawable? getCheckMarkDrawable(android.widget.CheckedTextView);
+ method public static android.content.res.ColorStateList? getCheckMarkTintList(android.widget.CheckedTextView);
+ method public static android.graphics.PorterDuff.Mode? getCheckMarkTintMode(android.widget.CheckedTextView);
+ method public static void setCheckMarkTintList(android.widget.CheckedTextView, android.content.res.ColorStateList?);
+ method public static void setCheckMarkTintMode(android.widget.CheckedTextView, android.graphics.PorterDuff.Mode?);
+ }
+
public final class CompoundButtonCompat {
method public static android.graphics.drawable.Drawable? getButtonDrawable(android.widget.CompoundButton);
method public static android.content.res.ColorStateList? getButtonTintList(android.widget.CompoundButton);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index a9f1cae..66203e4 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -3800,6 +3800,14 @@
field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final boolean PLATFORM_SUPPORTS_AUTOSIZE;
}
+ public final class CheckedTextViewCompat {
+ method public static android.graphics.drawable.Drawable? getCheckMarkDrawable(android.widget.CheckedTextView);
+ method public static android.content.res.ColorStateList? getCheckMarkTintList(android.widget.CheckedTextView);
+ method public static android.graphics.PorterDuff.Mode? getCheckMarkTintMode(android.widget.CheckedTextView);
+ method public static void setCheckMarkTintList(android.widget.CheckedTextView, android.content.res.ColorStateList?);
+ method public static void setCheckMarkTintMode(android.widget.CheckedTextView, android.graphics.PorterDuff.Mode?);
+ }
+
public final class CompoundButtonCompat {
method public static android.graphics.drawable.Drawable? getButtonDrawable(android.widget.CompoundButton);
method public static android.content.res.ColorStateList? getButtonTintList(android.widget.CompoundButton);
@@ -3974,6 +3982,13 @@
method public androidx.core.view.ContentInfoCompat? onReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
}
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface TintableCheckedTextView {
+ method public android.content.res.ColorStateList? getSupportCheckMarkTintList();
+ method public android.graphics.PorterDuff.Mode? getSupportCheckMarkTintMode();
+ method public void setSupportCheckMarkTintList(android.content.res.ColorStateList?);
+ method public void setSupportCheckMarkTintMode(android.graphics.PorterDuff.Mode?);
+ }
+
public interface TintableCompoundButton {
method public android.content.res.ColorStateList? getSupportButtonTintList();
method public android.graphics.PorterDuff.Mode? getSupportButtonTintMode();
diff --git a/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java b/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java
new file mode 100644
index 0000000..95d06da
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.widget;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.widget.CheckedTextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.graphics.drawable.DrawableCompat;
+
+import java.lang.reflect.Field;
+
+/**
+ * Helper for accessing {@link CheckedTextView}.
+ */
+public final class CheckedTextViewCompat {
+ private static final String TAG = "CheckedTextViewCompat";
+
+ private CheckedTextViewCompat() {
+ }
+
+ /**
+ * Applies a tint to the check mark drawable. Does not modify the current tint
+ * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to {@link CheckedTextView#setCheckMarkDrawable(Drawable)} should
+ * automatically mutate the drawable and apply the specified tint and tint
+ * mode using {@link DrawableCompat#setTintList(Drawable, ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ * @see #setCheckMarkTintList(CheckedTextView, ColorStateList)
+ */
+ public static void setCheckMarkTintList(@NonNull CheckedTextView textView,
+ @Nullable ColorStateList tint) {
+ if (SDK_INT >= 21) {
+ Api21Impl.setCheckMarkTintList(textView, tint);
+ } else if (textView instanceof TintableCheckedTextView) {
+ ((TintableCheckedTextView) textView).setSupportCheckMarkTintList(tint);
+ }
+ }
+
+ /**
+ * Returns the tint applied to the check mark drawable
+ *
+ * @see #setCheckMarkTintList(CheckedTextView, ColorStateList)
+ */
+ @Nullable
+ public static ColorStateList getCheckMarkTintList(@NonNull CheckedTextView textView) {
+ if (SDK_INT >= 21) {
+ return Api21Impl.getCheckMarkTintList(textView);
+ }
+ if (textView instanceof TintableCheckedTextView) {
+ return ((TintableCheckedTextView) textView).getSupportCheckMarkTintList();
+ }
+ return null;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setCheckMarkTintList(CheckedTextView, ColorStateList)}} to the check mark drawable.
+ * The default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @see #getCheckMarkTintMode(CheckedTextView)
+ * @see DrawableCompat#setTintMode(Drawable, PorterDuff.Mode)
+ */
+ public static void setCheckMarkTintMode(@NonNull CheckedTextView textView,
+ @Nullable PorterDuff.Mode tintMode) {
+ if (SDK_INT >= 21) {
+ Api21Impl.setCheckMarkTintMode(textView, tintMode);
+ } else if (textView instanceof TintableCheckedTextView) {
+ ((TintableCheckedTextView) textView).setSupportCheckMarkTintMode(tintMode);
+ }
+ }
+
+ /**
+ * @return the blending mode used to apply the tint to the check mark drawable
+ * @attr name android:checkMarkTintMode
+ * @see #setCheckMarkTintMode(CheckedTextView, PorterDuff.Mode)
+ */
+ @Nullable
+ public static PorterDuff.Mode getCheckMarkTintMode(@NonNull CheckedTextView textView) {
+ if (SDK_INT >= 21) {
+ return Api21Impl.getCheckMarkTintMode(textView);
+ }
+ if (textView instanceof TintableCheckedTextView) {
+ return ((TintableCheckedTextView) textView).getSupportCheckMarkTintMode();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the drawable used as the check mark image
+ *
+ * @see CheckedTextView#setCheckMarkDrawable(Drawable)
+ */
+ @Nullable
+ public static Drawable getCheckMarkDrawable(@NonNull CheckedTextView textView) {
+ if (SDK_INT >= 16) {
+ return Api16Impl.getCheckMarkDrawable(textView);
+ } else {
+ return Api14Impl.getCheckMarkDrawable(textView);
+ }
+ }
+
+ @RequiresApi(21)
+ private static class Api21Impl {
+
+ private Api21Impl() {
+ }
+
+ static void setCheckMarkTintList(@NonNull CheckedTextView textView,
+ @Nullable ColorStateList tint) {
+ textView.setCheckMarkTintList(tint);
+ }
+
+ @Nullable
+ static ColorStateList getCheckMarkTintList(@NonNull CheckedTextView textView) {
+ return textView.getCheckMarkTintList();
+ }
+
+ static void setCheckMarkTintMode(@NonNull CheckedTextView textView,
+ @Nullable PorterDuff.Mode tintMode) {
+ textView.setCheckMarkTintMode(tintMode);
+ }
+
+ @Nullable
+ static PorterDuff.Mode getCheckMarkTintMode(@NonNull CheckedTextView textView) {
+ return textView.getCheckMarkTintMode();
+ }
+ }
+
+ @RequiresApi(16)
+ private static class Api16Impl {
+
+ private Api16Impl() {
+ }
+
+ @Nullable
+ static Drawable getCheckMarkDrawable(@NonNull CheckedTextView textView) {
+ return textView.getCheckMarkDrawable();
+ }
+ }
+
+ private static class Api14Impl {
+
+ private static Field sCheckMarkDrawableField;
+ private static boolean sResolved;
+
+ private Api14Impl() {
+ }
+
+ @Nullable
+ static Drawable getCheckMarkDrawable(@NonNull CheckedTextView textView) {
+ if (!sResolved) {
+ try {
+ sCheckMarkDrawableField =
+ CheckedTextView.class.getDeclaredField("mCheckMarkDrawable");
+ sCheckMarkDrawableField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ Log.i(TAG, "Failed to retrieve mCheckMarkDrawable field", e);
+ }
+ sResolved = true;
+ }
+
+ if (sCheckMarkDrawableField != null) {
+ try {
+ return (Drawable) sCheckMarkDrawableField.get(textView);
+ } catch (IllegalAccessException e) {
+ Log.i(TAG, "Failed to get check mark drawable via reflection", e);
+ sCheckMarkDrawableField = null;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/widget/TintableCheckedTextView.java b/core/core/src/main/java/androidx/core/widget/TintableCheckedTextView.java
new file mode 100644
index 0000000..748987d
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/widget/TintableCheckedTextView.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Interface which allows a {@link android.widget.CheckedTextView} to receive tinting
+ * calls from {@code CheckedTextViewCompat} when running on API v20 devices or lower.
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+public interface TintableCheckedTextView {
+
+ /**
+ * Applies a tint to the check mark drawable. Does not modify the current tint
+ * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to
+ * {@link android.widget.CheckedTextView#setCheckMarkDrawable(Drawable)
+ * setCheckMarkDrawable(Drawable)}
+ * should automatically mutate the drawable and apply the specified tint and tint mode.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ */
+ void setSupportCheckMarkTintList(@Nullable ColorStateList tint);
+
+ /**
+ * Returns the tint applied to the check mark drawable
+ *
+ * @see #setSupportCheckMarkTintList(ColorStateList)
+ */
+ @Nullable
+ ColorStateList getSupportCheckMarkTintList();
+
+ /**
+ * Specifies the blending mode which should be used to apply the tint specified by
+ * {@link #setSupportCheckMarkTintList(ColorStateList)} to the check mark drawable. The
+ * default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ *
+ * @see #getSupportCheckMarkTintMode()
+ * @see androidx.core.graphics.drawable.DrawableCompat#setTintMode(Drawable,
+ * PorterDuff.Mode)
+ */
+ void setSupportCheckMarkTintMode(@Nullable PorterDuff.Mode tintMode);
+
+ /**
+ * Returns the blending mode used to apply the tint to the check mark drawable
+ *
+ * @see #setSupportCheckMarkTintMode(PorterDuff.Mode)
+ */
+ @Nullable
+ PorterDuff.Mode getSupportCheckMarkTintMode();
+}
diff --git a/datastore/datastore/src/main/java/androidx/datastore/DataStoreDelegate.kt b/datastore/datastore/src/main/java/androidx/datastore/DataStoreDelegate.kt
index 9ac7640..3e50a16 100644
--- a/datastore/datastore/src/main/java/androidx/datastore/DataStoreDelegate.kt
+++ b/datastore/datastore/src/main/java/androidx/datastore/DataStoreDelegate.kt
@@ -101,7 +101,7 @@
val applicationContext = thisRef.applicationContext
INSTANCE = DataStoreFactory.create(
serializer = serializer,
- produceFile = { thisRef.dataStoreFile(fileName) },
+ produceFile = { applicationContext.dataStoreFile(fileName) },
corruptionHandler = corruptionHandler,
migrations = produceMigrations(applicationContext),
scope = scope
diff --git a/fragment/fragment-ktx/OWNERS b/fragment/fragment-ktx/OWNERS
index e450f4c..6fd8227 100644
--- a/fragment/fragment-ktx/OWNERS
+++ b/fragment/fragment-ktx/OWNERS
@@ -1 +1 @@
-jakew@google.com
+yboyar@google.com
diff --git a/fragment/settings.gradle b/fragment/settings.gradle
index 46fed17..424ef67 100644
--- a/fragment/settings.gradle
+++ b/fragment/settings.gradle
@@ -20,7 +20,7 @@
setupPlayground(this, "..")
selectProjectsFromAndroidX({ name ->
if (name.startsWith(":fragment")) return true
- if (name.startsWith(":internal-testutils-runtime")) return true
- if (name.startsWith(":internal-testutils-truth")) return true
+ if (name == ":internal-testutils-runtime") return true
+ if (name == ":internal-testutils-truth") return true
return false
})
diff --git a/lifecycle/lifecycle-reactivestreams-ktx/OWNERS b/lifecycle/lifecycle-reactivestreams-ktx/OWNERS
index e450f4c..6fd8227 100644
--- a/lifecycle/lifecycle-reactivestreams-ktx/OWNERS
+++ b/lifecycle/lifecycle-reactivestreams-ktx/OWNERS
@@ -1 +1 @@
-jakew@google.com
+yboyar@google.com
diff --git a/lifecycle/settings.gradle b/lifecycle/settings.gradle
index 84747cf..4ae5e5a 100644
--- a/lifecycle/settings.gradle
+++ b/lifecycle/settings.gradle
@@ -26,6 +26,8 @@
if (name == ":internal-testutils-truth") return true
if (name == ":compose:lint:common") return true
if (name == ":compose:lint:internal-lint-checks") return true
+ if (name == ":compose:test-utils") return true
+ if (name == ":test-screenshot") return true
return false
})
diff --git a/loader/loader-ktx/OWNERS b/loader/loader-ktx/OWNERS
index e450f4c..6fd8227 100644
--- a/loader/loader-ktx/OWNERS
+++ b/loader/loader-ktx/OWNERS
@@ -1 +1 @@
-jakew@google.com
+yboyar@google.com
diff --git a/navigation/settings.gradle b/navigation/settings.gradle
index 4b2759e..6352fd2 100644
--- a/navigation/settings.gradle
+++ b/navigation/settings.gradle
@@ -25,11 +25,13 @@
if (name == ":annotation:annotation-sampled") return true
if (name == ":compose:integration-tests:demos:common") return true
if (name == ":lifecycle:lifecycle-viewmodel-savedstate") return true
- if (name.startsWith(":internal-testutils-navigation")) return true
- if (name.startsWith(":internal-testutils-runtime")) return true
- if (name.startsWith(":internal-testutils-truth")) return true
+ if (name == ":internal-testutils-navigation") return true
+ if (name == ":internal-testutils-runtime") return true
+ if (name == ":internal-testutils-truth") return true
if (name == ":compose:lint:common") return true
if (name == ":compose:lint:internal-lint-checks") return true
+ if (name == ":compose:test-utils") return true
+ if (name == ":test-screenshot") return true
return false
})
diff --git a/palette/palette-ktx/OWNERS b/palette/palette-ktx/OWNERS
index e450f4c..6fd8227 100644
--- a/palette/palette-ktx/OWNERS
+++ b/palette/palette-ktx/OWNERS
@@ -1 +1 @@
-jakew@google.com
+yboyar@google.com
diff --git a/preference/preference-ktx/OWNERS b/preference/preference-ktx/OWNERS
index e450f4c..6fd8227 100644
--- a/preference/preference-ktx/OWNERS
+++ b/preference/preference-ktx/OWNERS
@@ -1 +1 @@
-jakew@google.com
+yboyar@google.com
diff --git a/room/common/src/main/java/androidx/room/AutoMigration.java b/room/common/src/main/java/androidx/room/AutoMigration.java
index 4c9bc18..54361b0 100644
--- a/room/common/src/main/java/androidx/room/AutoMigration.java
+++ b/room/common/src/main/java/androidx/room/AutoMigration.java
@@ -46,4 +46,11 @@
* @return Version number of the new database schema.
*/
int to();
+
+ /**
+ * User implemented custom AutoMigration callback interface.
+ *
+ * @return Null if the user has not implemented a callback
+ */
+ Class<?> callback() default Object.class;
}
diff --git a/room/common/src/main/java/androidx/room/Database.java b/room/common/src/main/java/androidx/room/Database.java
index 2659bc8..e4f9366 100644
--- a/room/common/src/main/java/androidx/room/Database.java
+++ b/room/common/src/main/java/androidx/room/Database.java
@@ -63,7 +63,7 @@
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
-public @interface Database {
+public @interface Database {
/**
* The list of entities included in the database. Each entity turns into a table in the
* database.
@@ -108,9 +108,9 @@
/**
* List of AutoMigrations that can be performed on this Database.
*
- * @return List of AutoMigrations.
+ * @return List of AutoMigration annotations.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- Class<?>[] autoMigrations() default {};
+ AutoMigration[] autoMigrations() default {};
}
diff --git a/room/common/src/main/java/androidx/room/DeleteColumn.java b/room/common/src/main/java/androidx/room/DeleteColumn.java
new file mode 100644
index 0000000..854b969
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/DeleteColumn.java
@@ -0,0 +1,63 @@
+/*
+ * 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.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Repeatable annotation to be used by the user in specifying deleted columns in the new versions
+ * of one database.
+ *
+ * @hide
+ */
+@Repeatable(DeleteColumn.Entries.class)
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public @interface DeleteColumn {
+ /**
+ * Name of the table in the previous version of the database the column was deleted from.
+ *
+ * @return Name of the table
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ String tableName();
+
+ /**
+ * Name of the column deleted in the new version of the database.
+ *
+ * @return Name of the column.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ String deletedColumnName();
+
+ @Target(ElementType.TYPE)
+ @Retention(RetentionPolicy.CLASS)
+ @interface Entries {
+ DeleteColumn[] value();
+ }
+}
diff --git a/room/common/src/main/java/androidx/room/DeleteTable.java b/room/common/src/main/java/androidx/room/DeleteTable.java
new file mode 100644
index 0000000..a8dd32f
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/DeleteTable.java
@@ -0,0 +1,53 @@
+/*
+ * 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.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Repeatable annotation to be used by the user in specifying deleted tables between the old and
+ * new versions of one database.
+ *
+ * @hide
+ */
+@Repeatable(DeleteTable.Entries.class)
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public @interface DeleteTable {
+ /**
+ * Name of the table in the previous version of the database to be deleted.
+ *
+ * @return Name of the table.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ String deletedTableName();
+
+ @Target(ElementType.TYPE)
+ @Retention(RetentionPolicy.CLASS)
+ @interface Entries {
+ DeleteTable[] value();
+ }
+}
diff --git a/room/common/src/main/java/androidx/room/RenameColumn.java b/room/common/src/main/java/androidx/room/RenameColumn.java
new file mode 100644
index 0000000..8663c6f
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/RenameColumn.java
@@ -0,0 +1,74 @@
+/*
+ * 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.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Repeatable annotation to be used by the user in specifying renamed columns between the old and
+ * new versions of one database.
+ *
+ * @hide
+ */
+@Repeatable(RenameColumn.Entries.class)
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public @interface RenameColumn {
+ /**
+ * Name of the table the renamed column is found in.
+ *
+ * @return Name of the table
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ String tableName();
+
+ /**
+ * Original name of the column to be renamed from.
+ *
+ * @return Name of the column in the previous version of the database.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ String originalColumnName();
+
+ /**
+ * New name of the column to be renamed to.
+ *
+ * @return Name of the column in the new version of the database.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ String newColumnName();
+
+ @Target(ElementType.TYPE)
+ @Retention(RetentionPolicy.CLASS)
+ @interface Entries {
+ RenameColumn[] value();
+ }
+}
+
diff --git a/room/common/src/main/java/androidx/room/RenameTable.java b/room/common/src/main/java/androidx/room/RenameTable.java
new file mode 100644
index 0000000..4599033
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/RenameTable.java
@@ -0,0 +1,64 @@
+/*
+ * 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.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/**
+ * Repeatable annotation to be used by the user in specifying renamed tables between the old and
+ * new versions of one database.
+ *
+ * @hide
+ */
+@Repeatable(RenameTable.Entries.class)
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public @interface RenameTable {
+ /**
+ * Name of the table in the previous version of the database.
+ *
+ * @return Original name of the table.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ String originalTableName();
+
+ /**
+ * Name of the table in the new version of the database.
+ *
+ * @return New name of the table.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ String newTableName();
+
+ @Target(ElementType.TYPE)
+ @Retention(RetentionPolicy.CLASS)
+ @interface Entries {
+ RenameTable[] value();
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt
index 1a8aa08..a6ddb62 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt
@@ -16,22 +16,30 @@
package androidx.room.processor
-import androidx.room.AutoMigration
+import androidx.room.DeleteColumn
+import androidx.room.DeleteTable
+import androidx.room.RenameColumn
+import androidx.room.RenameTable
+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.migration.bundle.SchemaBundle.deserialize
+import androidx.room.processor.ProcessorErrors.AUTOMIGRATION_CALLBACK_MUST_BE_INTERFACE
+import androidx.room.processor.ProcessorErrors.autoMigrationElementMustExtendCallback
+import androidx.room.processor.ProcessorErrors.autoMigrationToVersionMustBeGreaterThanFrom
import androidx.room.util.DiffException
import androidx.room.util.SchemaDiffer
import androidx.room.vo.AutoMigrationResult
import java.io.File
// TODO: (b/183435544) Support downgrades in AutoMigrations.
-// TODO: (b/183007590) Use the callback in the AutoMigration annotation while end-to-end
-// testing, when column/table rename/deletes are supported
class AutoMigrationProcessor(
- val context: Context,
val element: XTypeElement,
+ val context: Context,
+ val from: Int,
+ val to: Int,
+ val callback: XType,
val latestDbSchema: DatabaseBundle
) {
/**
@@ -41,41 +49,36 @@
* @return the AutoMigrationResult containing the schema changes detected
*/
fun process(): AutoMigrationResult? {
- if (!element.isInterface()) {
- context.logger.e(
- ProcessorErrors.AUTOMIGRATION_ANNOTATED_TYPE_ELEMENT_MUST_BE_INTERFACE,
- element
- )
- return null
- }
+ val callbackElement = callback.typeElement
+ if (!callback.isTypeOf(Any::class)) {
+ if (callbackElement == null) {
+ context.logger.e(element, AUTOMIGRATION_CALLBACK_MUST_BE_INTERFACE)
+ return null
+ }
- if (!context.processingEnv
- .requireType(RoomTypeNames.AUTO_MIGRATION_CALLBACK)
- .isAssignableFrom(element.type)
- ) {
- context.logger.e(
- ProcessorErrors.AUTOMIGRATION_ELEMENT_MUST_IMPLEMENT_AUTOMIGRATION_CALLBACK,
- element
- )
- return null
- }
+ if (!callbackElement.isInterface()) {
+ context.logger.e(
+ callbackElement,
+ AUTOMIGRATION_CALLBACK_MUST_BE_INTERFACE
+ )
+ return null
+ }
- val annotationBox = element.getAnnotation(AutoMigration::class)
- if (annotationBox == null) {
- context.logger.e(
- element,
- ProcessorErrors.AUTOMIGRATION_ANNOTATION_MISSING
- )
- return null
+ val extendsMigrationCallback =
+ context.processingEnv.requireType(RoomTypeNames.AUTO_MIGRATION_CALLBACK)
+ .isAssignableFrom(callback)
+ if (!extendsMigrationCallback) {
+ context.logger.e(
+ callbackElement,
+ autoMigrationElementMustExtendCallback(callbackElement.className.simpleName())
+ )
+ return null
+ }
}
- val from = annotationBox.value.from
- val to = annotationBox.value.to
-
if (to <= from) {
context.logger.e(
- ProcessorErrors.autoMigrationToVersionMustBeGreaterThanFrom(to, from),
- element
+ autoMigrationToVersionMustBeGreaterThanFrom(to, from)
)
return null
}
@@ -94,10 +97,52 @@
}
}
+ val callbackClassName = callbackElement?.className?.simpleName()
+ val deleteColumnEntries = callbackElement?.let { element ->
+ element.getAnnotations(DeleteColumn::class).map {
+ AutoMigrationResult.DeletedColumn(
+ tableName = it.value.tableName,
+ columnName = it.value.deletedColumnName
+ )
+ }
+ } ?: emptyList()
+
+ val deleteTableEntries = callbackElement?.let { element ->
+ element.getAnnotations(DeleteTable::class).map {
+ AutoMigrationResult.DeletedTable(
+ deletedTableName = it.value.deletedTableName
+ )
+ }
+ } ?: emptyList()
+
+ val renameTableEntries = callbackElement?.let { element ->
+ element.getAnnotations(RenameTable::class).map {
+ AutoMigrationResult.RenamedTable(
+ originalTableName = it.value.originalTableName,
+ newTableName = it.value.newTableName
+ )
+ }
+ } ?: emptyList()
+
+ val renameColumnEntries = callbackElement?.let { element ->
+ element.getAnnotations(RenameColumn::class).map {
+ AutoMigrationResult.RenamedColumn(
+ tableName = it.value.tableName,
+ originalColumnName = it.value.originalColumnName,
+ newColumnName = it.value.newColumnName
+ )
+ }
+ } ?: emptyList()
+
val schemaDiff = try {
SchemaDiffer(
fromSchemaBundle = fromSchemaBundle,
- toSchemaBundle = toSchemaBundle
+ toSchemaBundle = toSchemaBundle,
+ className = callbackClassName,
+ deleteColumnEntries = deleteColumnEntries,
+ deleteTableEntries = deleteTableEntries,
+ renameTableEntries = renameTableEntries,
+ renameColumnEntries = renameColumnEntries
).diffSchemas()
} catch (ex: DiffException) {
context.logger.e(ex.errorMessage)
@@ -116,13 +161,13 @@
private fun getValidatedSchemaFile(version: Int): File? {
val schemaFile = File(
context.schemaOutFolder,
- "${element.className.enclosingClassName()}/$version.json"
+ "${element.className.canonicalName()}/$version.json"
)
if (!schemaFile.exists()) {
context.logger.e(
ProcessorErrors.autoMigrationSchemasNotFound(
context.schemaOutFolder.toString(),
- "${element.className.enclosingClassName()}/$version.json"
+ "${element.className.canonicalName()}/$version.json"
),
element
)
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 35ca51e..76dcc58 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
@@ -16,6 +16,7 @@
package androidx.room.processor
+import androidx.room.AutoMigration
import androidx.room.SkipQueryVerification
import androidx.room.compiler.processing.XAnnotationBox
import androidx.room.compiler.processing.XElement
@@ -126,7 +127,8 @@
latestDbSchema: DatabaseBundle
): List<AutoMigrationResult> {
val dbAnnotation = element.getAnnotation(androidx.room.Database::class)!!
- val autoMigrationList = dbAnnotation.getAsTypeList("autoMigrations")
+ val autoMigrationList = dbAnnotation
+ .getAsAnnotationBoxArray<AutoMigration>("autoMigrations")
context.checker.check(
autoMigrationList.isEmpty() || dbAnnotation.value.exportSchema,
element,
@@ -134,22 +136,15 @@
)
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()
- }
+ val autoMigration = it.value
+ AutoMigrationProcessor(
+ element = element,
+ context = context,
+ from = autoMigration.from,
+ to = autoMigration.to,
+ callback = it.getAsType("callback")!!,
+ latestDbSchema = latestDbSchema
+ ).process()
}
}
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 a426677..f6a8799 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -793,9 +793,9 @@
"interface."
}
- fun invalidAutoMigrationTypeInDatabaseAnnotation(typeName: TypeName): String {
- return "Invalid AutoMigration type: $typeName. An automigration in the database must be a" +
- " class."
+ fun invalidAutoMigrationTypeInDatabaseAnnotation(): String {
+ return "Invalid AutoMigration type: An automigration in the database must be " +
+ "an @AutoMigration annotation."
}
val EMBEDDED_TYPES_MUST_BE_A_CLASS_OR_INTERFACE = "The type of an Embedded field must be a " +
@@ -809,12 +809,13 @@
return "Invalid query argument: $typeName. It must be a class or an interface."
}
- val AUTOMIGRATION_ANNOTATED_TYPE_ELEMENT_MUST_BE_INTERFACE = "The @AutoMigration annotated " +
+ val AUTOMIGRATION_CALLBACK_MUST_BE_INTERFACE = "The @AutoMigration callback " +
"type must be an interface."
- val AUTOMIGRATION_ANNOTATION_MISSING = "The @AutoMigration annotation has not been found. " +
- "Cannot generate auto migrations."
- val AUTOMIGRATION_ELEMENT_MUST_IMPLEMENT_AUTOMIGRATION_CALLBACK = "AutoMigration element must" +
- " implement the AutoMigrationCallback interface."
+
+ fun autoMigrationElementMustExtendCallback(callback: String): String {
+ return "The AutoMigration callback " +
+ "$callback must extend the AutoMigrationCallback interface."
+ }
// TODO: (b/180389433) If the files don't exist the getSchemaFile() method should return
// null and before calling process
@@ -838,40 +839,109 @@
"@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 an unsupported schema change at the column " +
"level (e.g. affinity change). These changes are not yet " +
"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."
+ fun deletedOrRenamedColumnFound(
+ className: String?,
+ columnName: String,
+ tableName: String
+ ): String {
+ return if (className != null) {
+ """
+ AutoMigration Failure in ‘$className’: Column ‘$columnName’ in table ‘$tableName’ has
+ been either removed or renamed. Please annotate ‘$className’ with the @RenameColumn
+ or @RemoveColumn annotation to specify the change to be performed:
+ 1) RENAME: @RenameColumn.Entries(
+ @RenameColumn(
+ tableName = "$tableName",
+ originalColumnName = "$columnName",
+ newColumnName = <NEW_COLUMN_NAME>
+ )
+ )
+ 2) DELETE: @DeleteColumn.Entries(
+ @DeleteColumn=(
+ tableName = "$tableName",
+ deletedColumnName = "$columnName"
+ )
+ )
+ """
+ } else {
+ """
+ AutoMigration Failure: Please declare an interface extending 'AutoMigrationCallback',
+ and annotate with the @RenameColumn or @RemoveColumn annotation to specify the
+ change to be performed:
+ 1) RENAME: @RenameColumn.Entries(
+ @RenameColumn(
+ tableName = "$tableName",
+ originalColumnName = "$columnName",
+ newColumnName = <NEW_COLUMN_NAME>
+ )
+ )
+ 2) DELETE: @DeleteColumn.Entries(
+ @DeleteColumn=(
+ tableName = "$tableName",
+ deletedColumnName = "$columnName"
+ )
+ )
+ """
+ }
}
- fun tableWithComplexChangedSchemaFound(tableName: String): String {
- return "Encountered table '$tableName' with an unsupported schema change at the table " +
- "level (e.g. primary key, foreign key or index change). These changes are not yet " +
- "supported by AutoMigration."
+ fun deletedOrRenamedTableFound(
+ className: String?,
+ tableName: String
+ ): String {
+ return if (className != null) {
+ """
+ AutoMigration Failure in '$className': Table '$tableName' has been either removed or
+ renamed. Please annotate '$className' with the @RenameTable or @RemoveTable
+ annotation to specify the change to be performed:
+ 1) RENAME: @RenameTable.Entries(
+ @RenameTable(originalTableName = "$tableName", newTableName = <NEW_TABLE_NAME>))
+ 2) DELETE: @DeleteTable.Entries(@DeleteTable(deletedTableName = "$tableName"))
+ """
+ } else {
+ """
+ AutoMigration Failure: Please declare an interface extending 'AutoMigrationCallback',
+ and annotate with the @RenameTable or @RemoveTable
+ annotation to specify the change to be performed:
+ 1) RENAME: @RenameTable.Entries(
+ @RenameTable(originalTableName = "$tableName", newTableName = <NEW_TABLE_NAME>))
+ 2) DELETE: @DeleteTable.Entries(@DeleteTable(deletedTableName = "$tableName"))
+ """
+ }
}
- fun removedOrRenamedTableFound(tableName: String): String {
- return "Table '$tableName' has been either removed or " +
- "renamed. This change is not currently supported by AutoMigration."
+ fun tableRenameError(
+ className: String,
+ originalTableName: String,
+ newTableName: String
+ ): String {
+ return "AutoMigration Failure in '$className': The table renamed from " +
+ "'$originalTableName' to '$newTableName' is " +
+ "not found in the new version of the database."
+ }
+
+ fun conflictingRenameTableAnnotationsFound(annotations: String): String {
+ return "Conflicting @RenameTable annotations found: [$annotations]"
+ }
+ fun conflictingRenameColumnAnnotationsFound(annotations: String): String {
+ return "Conflicting @RenameColumn annotations found: [$annotations]"
}
val AUTO_MIGRATION_FOUND_BUT_EXPORT_SCHEMA_OFF = "Cannot create auto-migrations when export " +
"schema is OFF."
- fun tableWithNewTablePrefixFound(tableName: String): String {
- return "The new version of the schema contains `$tableName` a table name" +
- " with the prefix '_new_', which is causing a conflict during autoMigration."
+ fun tableWithConflictingPrefixFound(tableName: String): String {
+ return "The new version of the schema contains '$tableName' a table name" +
+ " with the prefix '_new_', which will cause conflicts for auto migrations. Please use" +
+ " a different name."
}
+
+ val FTS_TABLE_NOT_CURRENTLY_SUPPORTED = "Schemas involving FTS tables are not currently " +
+ "supported."
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt b/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
index 2208483..7a2a4d8 100644
--- a/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
@@ -18,40 +18,81 @@
import androidx.room.migration.bundle.DatabaseBundle
import androidx.room.migration.bundle.EntityBundle
+import androidx.room.migration.bundle.FieldBundle
import androidx.room.migration.bundle.ForeignKeyBundle
+import androidx.room.migration.bundle.FtsEntityBundle
import androidx.room.migration.bundle.IndexBundle
+import androidx.room.processor.ProcessorErrors.FTS_TABLE_NOT_CURRENTLY_SUPPORTED
+import androidx.room.processor.ProcessorErrors.deletedOrRenamedTableFound
+import androidx.room.processor.ProcessorErrors.tableRenameError
+import androidx.room.processor.ProcessorErrors.conflictingRenameColumnAnnotationsFound
+import androidx.room.processor.ProcessorErrors.conflictingRenameTableAnnotationsFound
import androidx.room.processor.ProcessorErrors.newNotNullColumnMustHaveDefaultValue
-import androidx.room.processor.ProcessorErrors.removedOrRenamedColumnFound
-import androidx.room.processor.ProcessorErrors.removedOrRenamedTableFound
-import androidx.room.processor.ProcessorErrors.tableWithNewTablePrefixFound
+import androidx.room.processor.ProcessorErrors.deletedOrRenamedColumnFound
+import androidx.room.processor.ProcessorErrors.tableWithConflictingPrefixFound
import androidx.room.vo.AutoMigrationResult
/**
* This exception should be thrown to abandon processing an @AutoMigration.
+ *
+ * @param errorMessage Error message to be thrown with the exception
+ * @return RuntimeException with the provided error message
*/
class DiffException(val errorMessage: String) : RuntimeException(errorMessage)
/**
- * Contains the added, changed and removed columns detected.
+ * Contains the changes detected between the two schema versions provided.
*/
data class SchemaDiffResult(
val addedColumns: Map<String, AutoMigrationResult.AddedColumn>,
- val removedOrRenamedColumns: List<AutoMigrationResult.RemovedOrRenamedColumn>,
- val addedTables: List<AutoMigrationResult.AddedTable>,
+ val deletedColumns: List<AutoMigrationResult.DeletedColumn>,
+ val addedTables: Set<AutoMigrationResult.AddedTable>,
+ val renamedTables: Map<String, String>,
val complexChangedTables: Map<String, AutoMigrationResult.ComplexChangedTable>,
- val removedOrRenamedTables: List<AutoMigrationResult.RemovedOrRenamedTable>
+ val deletedTables: List<String>
)
/**
- * Receives the two bundles, diffs and returns a @SchemaDiffResult.
+ * Receives the two bundles, detects all changes between the two versions and returns a
+ * @SchemaDiffResult.
*
- * Throws an @AutoMigrationException with a detailed error message when an AutoMigration cannot
+ * Throws an @DiffException with a detailed error message when an AutoMigration cannot
* be generated.
+ *
+ * @param fromSchemaBundle Original database schema to migrate from
+ * @param toSchemaBundle New database schema to migrate to
+ * @param className Name of the user implemented AutoMigrationCallback interface, if available
+ * @param renameColumnEntries List of repeatable annotations specifying column renames
+ * @param deleteColumnEntries List of repeatable annotations specifying column deletes
+ * @param renameTableEntries List of repeatable annotations specifying table renames
+ * @param deleteTableEntries List of repeatable annotations specifying table deletes
*/
+// TODO: (b/181777611) Handle FTS tables
class SchemaDiffer(
- val fromSchemaBundle: DatabaseBundle,
- val toSchemaBundle: DatabaseBundle
+ private val fromSchemaBundle: DatabaseBundle,
+ private val toSchemaBundle: DatabaseBundle,
+ private val className: String?,
+ private val renameColumnEntries: List<AutoMigrationResult.RenamedColumn>,
+ private val deleteColumnEntries: List<AutoMigrationResult.DeletedColumn>,
+ private val renameTableEntries: List<AutoMigrationResult.RenamedTable>,
+ private val deleteTableEntries: List<AutoMigrationResult.DeletedTable>
) {
+ private val potentiallyDeletedTables = mutableSetOf<String>()
+
+ private val addedTables = mutableSetOf<AutoMigrationResult.AddedTable>()
+ // Any table that has been renamed, but also does not contain any complex changes.
+ private val renamedTables = mutableMapOf<String, String>()
+
+ // Map of tables with complex changes, keyed by the table name, note that if the table is
+ // renamed, the original table name is used as key.
+ private val complexChangedTables =
+ mutableMapOf<String, AutoMigrationResult.ComplexChangedTable>()
+ private val deletedTables = deleteTableEntries.map { it.deletedTableName }.toSet()
+
+ // Map of columns that have been added in the database, keyed by the column name, note that
+ // the table these columns have been added to will not contain any complex schema changes.
+ private val addedColumns = mutableMapOf<String, AutoMigrationResult.AddedColumn>()
+ private val deletedColumns = deleteColumnEntries
/**
* Compares the two versions of the database based on the schemas provided, and detects
@@ -60,163 +101,258 @@
* @return the AutoMigrationResult containing the schema changes detected
*/
fun diffSchemas(): SchemaDiffResult {
- val addedTables = mutableListOf<AutoMigrationResult.AddedTable>()
- val complexChangedTables = mutableMapOf<String, AutoMigrationResult.ComplexChangedTable>()
- val removedOrRenamedTables = mutableListOf<AutoMigrationResult.RemovedOrRenamedTable>()
-
- val addedColumns = mutableMapOf<String, AutoMigrationResult.AddedColumn>()
- val removedOrRenamedColumns = mutableListOf<AutoMigrationResult.RemovedOrRenamedColumn>()
+ val processedTablesAndColumnsInNewVersion = mutableMapOf<String, List<String>>()
// Check going from the original version of the schema to the new version for changed and
- // removed columns/tables
- fromSchemaBundle.entitiesByTableName.forEach { fromTable ->
- val toTable = toSchemaBundle.entitiesByTableName[fromTable.key]
- if (toTable == null) {
- // TODO: (b/183007590) When handling renames, check if a complex changed table
- // exists for the renamed table. If so, edit the entry to
- // reflect the rename. If not, create a new
- // RenamedTable object to be handled by addSimpleChangeStatements().
- removedOrRenamedTables.add(
- AutoMigrationResult.RemovedOrRenamedTable(fromTable.value)
- )
- } else {
- val complexChangedTable = tableContainsComplexChanges(fromTable.value, toTable)
- if (complexChangedTable != null) {
- complexChangedTables[complexChangedTable.tableName] = complexChangedTable
- }
- val fromColumns = fromTable.value.fieldsByColumnName
- val toColumns = toTable.fieldsByColumnName
- fromColumns.entries.forEach { fromColumn ->
- val match = toColumns[fromColumn.key]
- if (match != null && !match.isSchemaEqual(fromColumn.value) &&
- !complexChangedTables.containsKey(fromTable.key)
- ) {
- if (toSchemaBundle.entitiesByTableName.containsKey(toTable.newTableName)) {
- // TODO: (b/183975119) Use another prefix automatically in these cases
- diffError(tableWithNewTablePrefixFound(toTable.newTableName))
- }
- complexChangedTables[fromTable.key] =
- AutoMigrationResult.ComplexChangedTable(
- tableName = fromTable.key,
- newTableName = toTable.newTableName,
- oldVersionEntityBundle = fromTable.value,
- newVersionEntityBundle = toTable,
- foreignKeyChanged = false,
- indexChanged = false
- )
- } else if (match == null) {
- // TODO: (b/183007590) When handling renames, check if a complex changed
- // table exists for the table of the renamed column. If so, edit the
- // entry to reflect the column rename. If not, create a new
- // RenamedColumn object to be handled by addSimpleChangeStatements().
- removedOrRenamedColumns.add(
- AutoMigrationResult.RemovedOrRenamedColumn(
- fromTable.key,
- fromColumn.value
- )
- )
- }
- }
+ // deleted columns/tables
+ fromSchemaBundle.entitiesByTableName.values.forEach { fromTable ->
+ val toTable = detectTableLevelChanges(fromTable)
+ if (fromTable is FtsEntityBundle) {
+ diffError(FTS_TABLE_NOT_CURRENTLY_SUPPORTED)
}
- }
- // 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 { toTable ->
- val fromTable = fromSchemaBundle.entitiesByTableName[toTable.key]
- if (fromTable == null) {
- addedTables.add(AutoMigrationResult.AddedTable(toTable.value))
- } else {
+
+ // Check for column related changes. Since we require toTable to not be null, any
+ // deleted tables will be skipped here.
+ if (toTable != null) {
val fromColumns = fromTable.fieldsByColumnName
- val toColumns = toTable.value.fieldsByColumnName
- toColumns.entries.forEach { toColumn ->
- val match = fromColumns[toColumn.key]
- if (match == null) {
- if (toColumn.value.isNonNull && toColumn.value.defaultValue == null) {
- diffError(
- newNotNullColumnMustHaveDefaultValue(toColumn.key)
- )
- }
- // Check if the new column is on a table with complex changes. If so, no
- // need to account for it as the table will be recreated with the new
- // table already.
- if (!complexChangedTables.containsKey(toTable.key)) {
- addedColumns[toColumn.value.columnName] =
- AutoMigrationResult.AddedColumn(
- toTable.key,
- toColumn.value
- )
- }
- }
+ val processedColumnsInNewVersion = fromColumns.values.mapNotNull { fromColumn ->
+ detectColumnLevelChanges(
+ fromTable,
+ toTable,
+ fromColumn
+ )
}
+ processedTablesAndColumnsInNewVersion[toTable.tableName] =
+ processedColumnsInNewVersion
}
}
- if (removedOrRenamedColumns.isNotEmpty()) {
- removedOrRenamedColumns.forEach { removedColumn ->
- diffError(
- removedOrRenamedColumnFound(
- removedColumn.fieldBundle.columnName
- )
+ // Check going from the new version of the schema to the original version for added
+ // tables/columns. Skip the columns that have been processed already.
+ toSchemaBundle.entitiesByTableName.forEach { toTable ->
+ processAddedTableAndColumns(toTable.value, processedTablesAndColumnsInNewVersion)
+ }
+
+ potentiallyDeletedTables.forEach { tableName ->
+ diffError(
+ deletedOrRenamedTableFound(
+ className = className,
+ tableName = tableName
)
- }
+ )
}
- if (removedOrRenamedTables.isNotEmpty()) {
- removedOrRenamedTables.forEach { removedTable ->
- diffError(
- removedOrRenamedTableFound(
- removedTable.entityBundle.tableName
- )
- )
- }
- }
-
+ processDeletedColumns()
return SchemaDiffResult(
addedColumns = addedColumns,
- removedOrRenamedColumns = removedOrRenamedColumns,
+ deletedColumns = deletedColumns,
addedTables = addedTables,
+ renamedTables = renamedTables,
complexChangedTables = complexChangedTables,
- removedOrRenamedTables = removedOrRenamedTables
+ deletedTables = deletedTables.toList()
)
}
/**
- * Check for complex schema changes at a Table level and returns a ComplexTableChange
- * including information on which table changes were found on, and whether foreign key or
- * index related changes have occurred.
+ * Detects any changes at the table-level, independent of any changes that may be present at
+ * the column-level (e.g. column add/rename/delete).
*
- * @return null if complex schema change has not been found
+ * @param fromTable The original version of the table
+ * @return The EntityBundle of the table in the new version of the database. If the
+ * table was renamed, this will be reflected in the return value. If the table was removed, a
+ * null object will be returned.
*/
- // TODO: (b/181777611) Handle FTS tables
- private fun tableContainsComplexChanges(
- fromTable: EntityBundle,
- toTable: EntityBundle
- ): AutoMigrationResult.ComplexChangedTable? {
- val foreignKeyChanged = !isForeignKeyBundlesListEqual(
- fromTable.foreignKeys,
- toTable.foreignKeys
- )
- val indexChanged = !isIndexBundlesListEqual(fromTable.indices, toTable.indices)
- val primaryKeyChanged = !fromTable.primaryKey.isSchemaEqual(toTable.primaryKey)
-
- if (primaryKeyChanged || foreignKeyChanged || indexChanged) {
- if (toSchemaBundle.entitiesByTableName.containsKey(toTable.newTableName)) {
- diffError(tableWithNewTablePrefixFound(toTable.newTableName))
+ private fun detectTableLevelChanges(
+ fromTable: EntityBundle
+ ): EntityBundle? {
+ // Check if the table was renamed. If so, check for other complex changes that could
+ // be found on the table level. Save the end result to the complex changed tables map.
+ val renamedTable = isTableRenamed(fromTable.tableName)
+ if (renamedTable != null) {
+ val toTable = toSchemaBundle.entitiesByTableName[renamedTable.newTableName]
+ if (toTable != null) {
+ val isComplexChangedTable = tableContainsComplexChanges(
+ fromTable,
+ toTable
+ )
+ if (isComplexChangedTable) {
+ if (toSchemaBundle.entitiesByTableName.containsKey(toTable.newTableName)) {
+ diffError(tableWithConflictingPrefixFound(toTable.newTableName))
+ }
+ renamedTables.remove(renamedTable.originalTableName)
+ complexChangedTables[renamedTable.originalTableName] =
+ AutoMigrationResult.ComplexChangedTable(
+ tableName = toTable.tableName,
+ tableNameWithNewPrefix = toTable.newTableName,
+ oldVersionEntityBundle = fromTable,
+ newVersionEntityBundle = toTable,
+ renamedColumnsMap = mutableMapOf()
+ )
+ } else {
+ renamedTables[fromTable.tableName] = toTable.tableName
+ }
+ } else {
+ // The table we renamed TO does not exist in the new version
+ diffError(
+ tableRenameError(
+ className!!,
+ renamedTable.originalTableName,
+ renamedTable.newTableName
+ )
+ )
}
- return AutoMigrationResult.ComplexChangedTable(
- tableName = toTable.tableName,
- newTableName = toTable.newTableName,
- oldVersionEntityBundle = fromTable,
- newVersionEntityBundle = toTable,
- foreignKeyChanged = foreignKeyChanged,
- indexChanged = indexChanged
+ return toTable
+ }
+ val toTable = toSchemaBundle.entitiesByTableName[fromTable.tableName]
+ val isDeletedTable = deletedTables.contains(fromTable.tableName)
+ if (toTable != null) {
+ if (isDeletedTable) {
+ diffError(
+ deletedOrRenamedTableFound(className, toTable.tableName)
+ )
+ }
+
+ // Check if this table exists in both versions of the schema, hence is not renamed or
+ // deleted, but contains other complex changes (index/primary key/foreign key change).
+ val isComplexChangedTable = tableContainsComplexChanges(
+ fromTable = fromTable,
+ toTable = toTable
+ )
+ if (isComplexChangedTable) {
+ complexChangedTables[fromTable.tableName] =
+ AutoMigrationResult.ComplexChangedTable(
+ tableName = toTable.tableName,
+ tableNameWithNewPrefix = toTable.newTableName,
+ oldVersionEntityBundle = fromTable,
+ newVersionEntityBundle = toTable,
+ renamedColumnsMap = mutableMapOf()
+ )
+ }
+ return toTable
+ }
+ if (!isDeletedTable) {
+ potentiallyDeletedTables.add(fromTable.tableName)
+ }
+ return null
+ }
+
+ /**
+ * Detects any changes at the column-level.
+ *
+ * @param fromTable The original version of the table
+ * @param toTable The new version of the table
+ * @param fromColumn The original version of the column
+ * @return The name of the column in the new version of the database. Will return a null
+ * value if the column was deleted.
+ */
+ private fun detectColumnLevelChanges(
+ fromTable: EntityBundle,
+ toTable: EntityBundle,
+ fromColumn: FieldBundle,
+ ): String? {
+ // Check if this column was renamed. If so, no need to check further, we can mark this
+ // table as a complex change and include the renamed column.
+ val renamedToColumn = isColumnRenamed(fromColumn.columnName, fromTable.tableName)
+ if (renamedToColumn != null) {
+ val renamedColumnsMap = mutableMapOf(
+ renamedToColumn.newColumnName to fromColumn.columnName
+ )
+ // Make sure there are no conflicts in the new version of the table with the
+ // temporary new table name
+ // TODO: (b/183975119) Use another prefix automatically in these cases
+ if (toSchemaBundle.entitiesByTableName.containsKey(toTable.newTableName)) {
+ diffError(tableWithConflictingPrefixFound(toTable.newTableName))
+ }
+ renamedTables.remove(fromTable.tableName)
+ complexChangedTables[fromTable.tableName] =
+ AutoMigrationResult.ComplexChangedTable(
+ tableName = fromTable.tableName,
+ tableNameWithNewPrefix = toTable.newTableName,
+ oldVersionEntityBundle = fromTable,
+ newVersionEntityBundle = toTable,
+ renamedColumnsMap = renamedColumnsMap
+ )
+ return renamedToColumn.newColumnName
+ }
+ // The column was not renamed. So we check if the column was deleted, and
+ // if not, we check for column level complex changes.
+ val match = toTable.fieldsByColumnName[fromColumn.columnName]
+ if (match != null) {
+ val columnChanged = !match.isSchemaEqual(fromColumn)
+ if (columnChanged && !complexChangedTables.containsKey(fromTable.tableName)) {
+ // Make sure there are no conflicts in the new version of the table with the
+ // temporary new table name
+ // TODO: (b/183975119) Use another prefix automatically in these cases
+ if (toSchemaBundle.entitiesByTableName.containsKey(toTable.newTableName)) {
+ diffError(tableWithConflictingPrefixFound(toTable.newTableName))
+ }
+ renamedTables.remove(fromTable.tableName)
+ complexChangedTables[fromTable.tableName] =
+ AutoMigrationResult.ComplexChangedTable(
+ tableName = fromTable.tableName,
+ tableNameWithNewPrefix = toTable.newTableName,
+ oldVersionEntityBundle = fromTable,
+ newVersionEntityBundle = toTable,
+ renamedColumnsMap = mutableMapOf()
+ )
+ }
+ return match.columnName
+ }
+
+ val isColumnDeleted = deletedColumns.any {
+ it.tableName == fromTable.tableName && it.columnName == fromColumn.columnName
+ }
+
+ if (!isColumnDeleted) {
+ // We have encountered an ambiguous scenario, need more input from the user.
+ diffError(
+ deletedOrRenamedColumnFound(
+ className = className,
+ tableName = fromTable.tableName,
+ columnName = fromColumn.columnName
+ )
)
}
return null
}
- private fun diffError(errorMsg: String) {
+ /**
+ * Checks for complex schema changes at a Table level and returns a ComplexTableChange
+ * including information on which table changes were found on, and whether foreign key or
+ * index related changes have occurred.
+ *
+ * @param fromTable The original version of the table
+ * @param toTable The new version of the table
+ * @return A ComplexChangedTable object, null if complex schema change has not been found
+ */
+ private fun tableContainsComplexChanges(
+ fromTable: EntityBundle,
+ toTable: EntityBundle
+ ): Boolean {
+ if (!isForeignKeyBundlesListEqual(fromTable.foreignKeys, toTable.foreignKeys)) {
+ return true
+ }
+ if (!isIndexBundlesListEqual(fromTable.indices, toTable.indices)) {
+ return true
+ }
+
+ if (!fromTable.primaryKey.isSchemaEqual(toTable.primaryKey)) {
+ return true
+ }
+ // Check if any foreign keys are referencing a renamed table.
+ return fromTable.foreignKeys.any { foreignKey ->
+ renameTableEntries.any {
+ it.originalTableName == foreignKey.table
+ }
+ }
+ }
+
+ /**
+ * Throws a DiffException with the provided error message.
+ *
+ * @param errorMsg Error message to be thrown with the exception
+ */
+ private fun diffError(errorMsg: String): Nothing {
throw DiffException(errorMsg)
}
@@ -224,6 +360,8 @@
* Takes in two ForeignKeyBundle lists, attempts to find potential matches based on the columns
* of the Foreign Keys. Processes these potential matches by checking for schema equality.
*
+ * @param fromBundle List of foreign keys in the old schema version
+ * @param toBundle List of foreign keys in the new schema version
* @return true if the two lists of foreign keys are equal
*/
private fun isForeignKeyBundlesListEqual(
@@ -252,6 +390,8 @@
* Takes in two IndexBundle lists, attempts to find potential matches based on the names
* of the indexes. Processes these potential matches by checking for schema equality.
*
+ * @param fromBundle List of indexes in the old schema version
+ * @param toBundle List of indexes in the new schema version
* @return true if the two lists of indexes are equal
*/
private fun isIndexBundlesListEqual(
@@ -272,4 +412,129 @@
}
return true
}
-}
\ No newline at end of file
+
+ /**
+ * Checks if the table provided has been renamed in the new version of the database.
+ *
+ * @param tableName Name of the table in the original database version
+ * @return A RenameTable object if the table has been renamed, otherwise null
+ */
+ private fun isTableRenamed(tableName: String): AutoMigrationResult.RenamedTable? {
+ val annotations = renameTableEntries
+ val renamedTableAnnotations = annotations.filter {
+ it.originalTableName == tableName
+ }
+
+ // Make sure there aren't multiple renames on the same table
+ if (renamedTableAnnotations.size > 1) {
+ diffError(
+ conflictingRenameTableAnnotationsFound(
+ renamedTableAnnotations.joinToString(",")
+ )
+ )
+ }
+ return renamedTableAnnotations.firstOrNull()
+ }
+
+ /**
+ * Checks if the column provided has been renamed in the new version of the database.
+ *
+ * @param columnName Name of the column in the original database version
+ * @param tableName Name of the table the column belongs to in the original database version
+ * @return A RenameColumn object if the column has been renamed, otherwise null
+ */
+ private fun isColumnRenamed(
+ columnName: String,
+ tableName: String
+ ): AutoMigrationResult.RenamedColumn? {
+ val annotations = renameColumnEntries
+ val renamedColumnAnnotations = annotations.filter {
+ it.originalColumnName == columnName && it.tableName == tableName
+ }
+
+ // Make sure there aren't multiple renames on the same column
+ if (renamedColumnAnnotations.size > 1) {
+ diffError(
+ conflictingRenameColumnAnnotationsFound(renamedColumnAnnotations.joinToString(","))
+ )
+ }
+ return renamedColumnAnnotations.firstOrNull()
+ }
+
+ /**
+ * Looks for any new tables and columns that have been added between versions.
+ *
+ * @param toTable The new version of the table
+ * @param processedTablesAndColumnsInNewVersion List of all columns in the new version of the
+ * database that have been already processed
+ */
+ private fun processAddedTableAndColumns(
+ toTable: EntityBundle,
+ processedTablesAndColumnsInNewVersion: MutableMap<String, List<String>>
+ ) {
+ // Old table bundle will be found even if table is renamed.
+ val isRenamed = renameTableEntries.firstOrNull {
+ it.newTableName == toTable.tableName
+ }
+ val fromTable = if (isRenamed != null) {
+ fromSchemaBundle.entitiesByTableName[isRenamed.originalTableName]
+ } else {
+ fromSchemaBundle.entitiesByTableName[toTable.tableName]
+ }
+
+ if (fromTable == null) {
+ // It's a new table
+ addedTables.add(AutoMigrationResult.AddedTable(toTable))
+ return
+ }
+ val fromColumns = fromTable.fieldsByColumnName
+ val toColumns =
+ processedTablesAndColumnsInNewVersion[toTable.tableName]?.let { processedColumns ->
+ toTable.fieldsByColumnName.filterKeys { !processedColumns.contains(it) }
+ } ?: toTable.fieldsByColumnName
+
+ toColumns.values.forEach { toColumn ->
+ val match = fromColumns[toColumn.columnName]
+ if (match == null) {
+ if (toColumn.isNonNull && toColumn.defaultValue == null) {
+ diffError(
+ newNotNullColumnMustHaveDefaultValue(toColumn.columnName)
+ )
+ }
+ // Check if the new column is on a table with complex changes. If so, no
+ // need to account for it as the table will be recreated already with the new
+ // table.
+ if (!complexChangedTables.containsKey(toTable.tableName)) {
+ addedColumns[toColumn.columnName] =
+ AutoMigrationResult.AddedColumn(
+ toTable.tableName,
+ toColumn
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * Goes through the deleted columns list and marks the table of each as a complex changed
+ * table if it was not already.
+ */
+ private fun processDeletedColumns() {
+ deletedColumns.filterNot {
+ complexChangedTables.contains(it.tableName)
+ }.forEach { deletedColumn ->
+ val fromTableBundle =
+ fromSchemaBundle.entitiesByTableName.getValue(deletedColumn.tableName)
+ val toTableBundle =
+ toSchemaBundle.entitiesByTableName.getValue(deletedColumn.tableName)
+ complexChangedTables[deletedColumn.tableName] =
+ AutoMigrationResult.ComplexChangedTable(
+ tableName = deletedColumn.tableName,
+ tableNameWithNewPrefix = fromTableBundle.newTableName,
+ oldVersionEntityBundle = fromTableBundle,
+ newVersionEntityBundle = toTableBundle,
+ renamedColumnsMap = mutableMapOf()
+ )
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt b/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt
index ba352db..6f5c0a8 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt
@@ -31,11 +31,10 @@
val to: Int?,
val schemaDiff: SchemaDiffResult
) {
-
val implTypeName: ClassName by lazy {
ClassName.get(
element.className.packageName(),
- "${element.className.simpleNames().joinToString("_")}_Impl"
+ "AutoMigration_${from}_${to}_Impl"
)
}
@@ -46,14 +45,19 @@
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 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.
+ * Stores the table name, original name, and the new name of a column that was renamed in the
+ * new version of the database.
*/
- data class RemovedOrRenamedColumn(val tableName: String, val fieldBundle: FieldBundle)
+ data class RenamedColumn(
+ val tableName: String,
+ val originalColumnName: String,
+ val newColumnName: String
+ )
+
+ /**
+ * Stores the table name and the column name of a column that was deleted from the database.
+ */
+ data class DeletedColumn(val tableName: String, val columnName: String)
/**
* Stores the table that was added to a database in a newer version.
@@ -61,33 +65,36 @@
data class AddedTable(val entityBundle: EntityBundle)
/**
- * Stores the table name that contains a change in the primary key, foreign key(s) or index(es)
- * in a newer version. Explicitly provides information on whether a foreign key change and/or
- * an index change has occurred.
+ * Stores the table that contains a change in the primary key, foreign key(s) or index(es)
+ * in a newer version, as well as any complex changes and renames on the column-level.
*
* As it is possible to have a table with only simple (non-complex) changes, which will be
- * categorized as "AddedColumn" or "RemovedColumn" changes, all other
+ * categorized as "AddedColumn" or "DeletedColumn" changes, all other
* changes at the table level are categorized as "complex" changes, using the category
* "ComplexChangedTable".
*
- * At the column level, any change that is not a column add or a
- * removal will be categorized as "ChangedColumn".
+ * The renamed columns map contains a mapping from the NEW name of the column to the OLD name
+ * of the column.
*/
data class ComplexChangedTable(
val tableName: String,
- val newTableName: String,
+ val tableNameWithNewPrefix: String,
val oldVersionEntityBundle: EntityBundle,
val newVersionEntityBundle: EntityBundle,
- val foreignKeyChanged: Boolean,
- val indexChanged: Boolean
+ val renamedColumnsMap: MutableMap<String, String>
)
/**
- * Stores the table 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.
+ * Stores the original name and the new name of a table that was renamed in the
+ * new version of the database.
*
- * In the current implementation, we cannot differ between whether the table was removed or
- * renamed.
+ * This container will only be used for tables that got renamed, but do not have any complex
+ * changes on it, both on the table and column level.
*/
- data class RemovedOrRenamedTable(val entityBundle: EntityBundle)
+ data class RenamedTable(val originalTableName: String, val newTableName: String)
+
+ /**
+ * Stores the name of the table that was deleted from the database.
+ */
+ data class DeletedTable(val deletedTableName: String)
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
index 9748802..254e282 100644
--- a/room/compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
@@ -23,6 +23,7 @@
import androidx.room.ext.RoomTypeNames
import androidx.room.ext.S
import androidx.room.ext.SupportDbTypeNames
+import androidx.room.migration.bundle.EntityBundle
import androidx.room.vo.AutoMigrationResult
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterSpec
@@ -38,11 +39,11 @@
private val dbElement: XElement,
val autoMigrationResult: AutoMigrationResult
) : ClassWriter(autoMigrationResult.implTypeName) {
- val addedColumns = autoMigrationResult.schemaDiff.addedColumns
- val removedOrRenamedColumns = autoMigrationResult.schemaDiff.removedOrRenamedColumns
- val addedTables = autoMigrationResult.schemaDiff.addedTables
- val complexChangedTables = autoMigrationResult.schemaDiff.complexChangedTables
- val removedOrRenamedTables = autoMigrationResult.schemaDiff.removedOrRenamedTables
+ private val addedColumns = autoMigrationResult.schemaDiff.addedColumns
+ private val addedTables = autoMigrationResult.schemaDiff.addedTables
+ private val renamedTables = autoMigrationResult.schemaDiff.renamedTables
+ private val complexChangedTables = autoMigrationResult.schemaDiff.complexChangedTables
+ private val deletedTables = autoMigrationResult.schemaDiff.deletedTables
override fun createTypeSpecBuilder(): TypeSpec.Builder {
val builder = TypeSpec.classBuilder(autoMigrationResult.implTypeName)
@@ -94,9 +95,7 @@
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addAutoMigrationResultToMigrate(migrateBuilder: MethodSpec.Builder) {
- if (complexChangedTables.isNotEmpty()) {
- addComplexChangeStatements(migrateBuilder)
- }
+ addComplexChangeStatements(migrateBuilder)
addSimpleChangeStatements(migrateBuilder)
}
@@ -107,15 +106,34 @@
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addComplexChangeStatements(migrateBuilder: MethodSpec.Builder) {
- val tablesToProcess = complexChangedTables
- tablesToProcess.forEach { table ->
- // TODO: (b/183007590) Check for column / table renames here before processing
- // complex changes
- addStatementsToCreateNewTable(table.value, migrateBuilder)
- addStatementsToContentTransfer(table.value, migrateBuilder)
- addStatementsToDropTableAndRenameTempTable(table.value, migrateBuilder)
- addStatementsToRecreateIndexes(table.value, migrateBuilder)
- addStatementsToCheckForeignKeyConstraint(table.value, migrateBuilder)
+ complexChangedTables.values.forEach {
+ (
+ _,
+ tableNameWithNewPrefix,
+ oldEntityBundle,
+ newEntityBundle,
+ renamedColumnsMap
+ ) ->
+
+ addStatementsToCreateNewTable(newEntityBundle, migrateBuilder)
+ addStatementsToContentTransfer(
+ oldEntityBundle.tableName,
+ tableNameWithNewPrefix,
+ oldEntityBundle,
+ newEntityBundle,
+ renamedColumnsMap,
+ migrateBuilder
+ )
+ addStatementsToDropTableAndRenameTempTable(
+ oldEntityBundle.tableName,
+ newEntityBundle.tableName,
+ tableNameWithNewPrefix,
+ migrateBuilder
+ )
+ addStatementsToRecreateIndexes(newEntityBundle, migrateBuilder)
+ if (newEntityBundle.foreignKeys.isNotEmpty()) {
+ addStatementsToCheckForeignKeyConstraint(newEntityBundle.tableName, migrateBuilder)
+ }
}
}
@@ -126,55 +144,62 @@
*
* @param migrateBuilder Builder for the migrate() function to be generated
*/
- // TODO: (b/183007590) Handle column/table renames here
private fun addSimpleChangeStatements(migrateBuilder: MethodSpec.Builder) {
-
- if (addedColumns.isNotEmpty()) {
- addNewColumnStatements(migrateBuilder)
- }
-
- if (addedTables.isNotEmpty()) {
- addNewTableStatements(migrateBuilder)
- }
+ addDeleteTableStatements(migrateBuilder)
+ addRenameTableStatements(migrateBuilder)
+ addNewColumnStatements(migrateBuilder)
+ addNewTableStatements(migrateBuilder)
}
/**
* Adds the SQL statements for creating a new table in the desired revised format of table.
*
+ * @param newTable Schema of the new table to be created
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addStatementsToCreateNewTable(
- table: AutoMigrationResult.ComplexChangedTable,
+ newTable: EntityBundle,
migrateBuilder: MethodSpec.Builder
) {
addDatabaseExecuteSqlStatement(
migrateBuilder,
- table.newVersionEntityBundle.createNewTable()
+ newTable.createNewTable()
)
}
/**
* Adds the SQL statements for transferring the contents of the old table to the new version.
*
+ * @param oldTableName Name of the table in the old version of the database
+ * @param tableNameWithNewPrefix Name of the table with the '_new_' prefix added
+ * @param oldEntityBundle Entity bundle of the table in the old version of the database
+ * @param newEntityBundle Entity bundle of the table in the new version of the database
+ * @param renamedColumnsMap Map of the renamed columns of the table (new name -> old name)
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addStatementsToContentTransfer(
- table: AutoMigrationResult.ComplexChangedTable,
+ oldTableName: String,
+ tableNameWithNewPrefix: String,
+ oldEntityBundle: EntityBundle,
+ newEntityBundle: EntityBundle,
+ renamedColumnsMap: MutableMap<String, String>,
migrateBuilder: MethodSpec.Builder
) {
- // TODO: (b/183007590) Account for renames and deletes here as ordering is important.
- val oldColumnSequence = table.oldVersionEntityBundle.fieldsByColumnName.keys
- .joinToString(",") { "`$it`" }
- val newColumnSequence = (
- table.newVersionEntityBundle.fieldsByColumnName.keys - addedColumns.keys
- ).joinToString(",") { "`$it`" }
+ val newColumnSequence = newEntityBundle.fieldsByColumnName.keys.filter {
+ oldEntityBundle.fieldsByColumnName.keys.contains(it) ||
+ renamedColumnsMap.containsKey(it)
+ }
+ val oldColumnSequence = mutableListOf<String>()
+ newColumnSequence.forEach { column ->
+ oldColumnSequence.add(renamedColumnsMap[column] ?: column)
+ }
addDatabaseExecuteSqlStatement(
migrateBuilder,
buildString {
append(
- "INSERT INTO `${table.newTableName}` ($newColumnSequence) " +
- "SELECT $oldColumnSequence FROM `${table.tableName}`"
+ "INSERT INTO `$tableNameWithNewPrefix` (${newColumnSequence.joinToString(",")
+ }) SELECT ${oldColumnSequence.joinToString(",")} FROM `$oldTableName`",
)
}
)
@@ -184,32 +209,38 @@
* Adds the SQL statements for dropping the table at the old version and renaming the
* temporary table to the name of the original table.
*
+ * @param oldTableName Name of the table in the old version of the database
+ * @param newTableName Name of the table in the new version of the database
+ * @param tableNameWithNewPrefix Name of the table with the '_new_' prefix added
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addStatementsToDropTableAndRenameTempTable(
- table: AutoMigrationResult.ComplexChangedTable,
+ oldTableName: String,
+ newTableName: String,
+ tableNameWithNewPrefix: String,
migrateBuilder: MethodSpec.Builder
) {
addDatabaseExecuteSqlStatement(
migrateBuilder,
- "DROP TABLE `${table.tableName}`"
+ "DROP TABLE `$oldTableName`"
)
addDatabaseExecuteSqlStatement(
migrateBuilder,
- "ALTER TABLE `${table.newTableName}` RENAME TO `${table.tableName}`"
+ "ALTER TABLE `$tableNameWithNewPrefix` RENAME TO `$newTableName`"
)
}
/**
* Adds the SQL statements for recreating indexes.
*
+ * @param table The table the indexes of which will be recreated
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addStatementsToRecreateIndexes(
- table: AutoMigrationResult.ComplexChangedTable,
+ table: EntityBundle,
migrateBuilder: MethodSpec.Builder
) {
- table.newVersionEntityBundle.indices.forEach { index ->
+ table.indices.forEach { index ->
addDatabaseExecuteSqlStatement(
migrateBuilder,
index.getCreateSql(table.tableName)
@@ -220,19 +251,58 @@
/**
* Adds the SQL statement for checking the foreign key constraints.
*
+ * @param tableName Name of the table
* @param migrateBuilder Builder for the migrate() function to be generated
*/
private fun addStatementsToCheckForeignKeyConstraint(
- table: AutoMigrationResult.ComplexChangedTable,
+ tableName: String,
migrateBuilder: MethodSpec.Builder
) {
addDatabaseExecuteSqlStatement(
migrateBuilder,
- "PRAGMA foreign_key_check(`${table.tableName}`)"
+ "PRAGMA foreign_key_check(`$tableName`)"
)
}
/**
+ * Adds the SQL statements for removing a table.
+ *
+ * @param migrateBuilder Builder for the migrate() function to be generated
+ */
+ private fun addDeleteTableStatements(migrateBuilder: MethodSpec.Builder) {
+ deletedTables.forEach { tableName ->
+ val deleteTableSql = buildString {
+ append(
+ "DROP TABLE `$tableName`"
+ )
+ }
+ addDatabaseExecuteSqlStatement(
+ migrateBuilder,
+ deleteTableSql
+ )
+ }
+ }
+
+ /**
+ * Adds the SQL statements for renaming a table.
+ *
+ * @param migrateBuilder Builder for the migrate() function to be generated
+ */
+ private fun addRenameTableStatements(migrateBuilder: MethodSpec.Builder) {
+ renamedTables.forEach { (oldName, newName) ->
+ val renameTableSql = buildString {
+ append(
+ "ALTER TABLE `$oldName` RENAME TO `$newName`"
+ )
+ }
+ addDatabaseExecuteSqlStatement(
+ migrateBuilder,
+ renameTableSql
+ )
+ }
+ }
+
+ /**
* Adds the SQL statements for adding new columns to a table.
*
* @param migrateBuilder Builder for the migrate() function to be generated
@@ -276,13 +346,15 @@
* database.
*
* @param migrateBuilder Builder for the migrate() function to be generated
+ * @param sql The SQL statement to be executed by the database
*/
private fun addDatabaseExecuteSqlStatement(
migrateBuilder: MethodSpec.Builder,
sql: String
) {
migrateBuilder.addStatement(
- "database.execSQL($S)", sql
+ "database.execSQL($S)",
+ sql
)
}
}
diff --git a/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithDefault.java b/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithDefault.java
index c75033d..e799cad 100644
--- a/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithDefault.java
+++ b/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithDefault.java
@@ -10,8 +10,8 @@
@Generated("androidx.room.RoomProcessor")
@SuppressWarnings({"unchecked", "deprecation"})
-class ValidAutoMigrationWithDefault_Impl extends Migration implements AutoMigrationCallback {
- public ValidAutoMigrationWithDefault_Impl() {
+class AutoMigration_1_2_Impl extends Migration implements AutoMigrationCallback {
+ public AutoMigration_1_2_Impl() {
super(1, 2);
}
diff --git a/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithoutDefault.java b/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithoutDefault.java
index 93eb47d..ed2a2a9 100644
--- a/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithoutDefault.java
+++ b/room/compiler/src/test/data/autoMigrationWriter/output/ValidAutoMigrationWithoutDefault.java
@@ -10,8 +10,8 @@
@Generated("androidx.room.RoomProcessor")
@SuppressWarnings({"unchecked", "deprecation"})
-class ValidAutoMigrationWithoutDefault_Impl extends Migration implements AutoMigrationCallback {
- public ValidAutoMigrationWithoutDefault_Impl() {
+class AutoMigration_1_2_Impl extends Migration implements AutoMigrationCallback {
+ public AutoMigration_1_2_Impl() {
super(1, 2);
}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/AutoMigrationProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/AutoMigrationProcessorTest.kt
index be99a31..886bfbd 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/AutoMigrationProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/AutoMigrationProcessorTest.kt
@@ -23,6 +23,7 @@
import androidx.room.migration.bundle.FieldBundle
import androidx.room.migration.bundle.PrimaryKeyBundle
import androidx.room.migration.bundle.SchemaBundle
+import androidx.room.processor.ProcessorErrors.autoMigrationElementMustExtendCallback
import androidx.room.testing.context
import org.junit.Test
@@ -34,19 +35,21 @@
"""
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"),
+ invocation.context,
+ 1,
+ 2,
+ invocation.processingEnv.requireType("foo.bar.MyAutoMigration"),
from.database
).process()
invocation.assertCompilationResult {
- hasError(ProcessorErrors.AUTOMIGRATION_ANNOTATED_TYPE_ELEMENT_MUST_BE_INTERFACE)
+ hasError(ProcessorErrors.AUTOMIGRATION_CALLBACK_MUST_BE_INTERFACE)
}
}
}
@@ -66,13 +69,16 @@
runProcessorTest(listOf(source)) { invocation ->
AutoMigrationProcessor(
- invocation.context,
invocation.processingEnv.requireTypeElement("foo.bar.MyAutoMigration"),
+ invocation.context,
+ 1,
+ 2,
+ invocation.processingEnv.requireType("foo.bar.MyAutoMigration"),
from.database
).process()
invocation.assertCompilationResult {
hasError(
- ProcessorErrors.AUTOMIGRATION_ELEMENT_MUST_IMPLEMENT_AUTOMIGRATION_CALLBACK
+ autoMigrationElementMustExtendCallback("MyAutoMigration")
)
}
}
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 700eb3c..baf2464 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
@@ -1214,7 +1214,8 @@
singleDb(
"""
@Database(entities = {User.class}, version = 42, exportSchema = false,
- autoMigrations = {MyAutoMigration.class})
+ autoMigrations = {@AutoMigration(from = 1, to = 2, callback = MyAutoMigration
+ .class), @AutoMigration(from = 2, to = 3)})
public abstract class MyDb extends RoomDatabase {}
""",
USER, AUTOMIGRATION
diff --git a/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt b/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt
index 402e271..c416c40 100644
--- a/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt
@@ -34,7 +34,12 @@
fun testPrimaryKeyChanged() {
val diffResult = SchemaDiffer(
fromSchemaBundle = from.database,
- toSchemaBundle = toChangeInPrimaryKey.database
+ toSchemaBundle = toChangeInPrimaryKey.database,
+ className = "MyAutoMigration",
+ renameColumnEntries = listOf(),
+ deleteColumnEntries = listOf(),
+ renameTableEntries = listOf(),
+ deleteTableEntries = listOf()
).diffSchemas()
assertThat(diffResult.complexChangedTables.keys).contains("Song")
@@ -44,31 +49,42 @@
fun testForeignKeyFieldChanged() {
val diffResult = SchemaDiffer(
fromSchemaBundle = from.database,
- toSchemaBundle = toForeignKeyAdded.database
+ toSchemaBundle = toForeignKeyAdded.database,
+ className = "MyAutoMigration",
+ renameColumnEntries = listOf(),
+ deleteColumnEntries = listOf(),
+ renameTableEntries = listOf(),
+ deleteTableEntries = listOf()
).diffSchemas()
- assertThat(diffResult.complexChangedTables.isNotEmpty())
- assertThat(diffResult.complexChangedTables["Song"]?.foreignKeyChanged).isTrue()
- assertThat(diffResult.complexChangedTables["Song"]?.indexChanged).isFalse()
+ assertThat(diffResult.complexChangedTables["Song"] != null)
}
@Test
fun testComplexChangeInvolvingIndex() {
val diffResult = SchemaDiffer(
fromSchemaBundle = from.database,
- toSchemaBundle = toIndexAdded.database
+ toSchemaBundle = toIndexAdded.database,
+ className = "MyAutoMigration",
+ renameColumnEntries = listOf(),
+ deleteColumnEntries = listOf(),
+ renameTableEntries = listOf(),
+ deleteTableEntries = listOf()
).diffSchemas()
- assertThat(diffResult.complexChangedTables.isNotEmpty())
- assertThat(diffResult.complexChangedTables["Song"]?.foreignKeyChanged).isFalse()
- assertThat(diffResult.complexChangedTables["Song"]?.indexChanged).isTrue()
+ assertThat(diffResult.complexChangedTables["Song"] != null)
}
@Test
fun testColumnAddedWithColumnInfoDefaultValue() {
val schemaDiffResult = SchemaDiffer(
fromSchemaBundle = from.database,
- toSchemaBundle = toColumnAddedWithColumnInfoDefaultValue.database
+ toSchemaBundle = toColumnAddedWithColumnInfoDefaultValue.database,
+ className = "MyAutoMigration",
+ renameColumnEntries = listOf(),
+ deleteColumnEntries = listOf(),
+ renameTableEntries = listOf(),
+ deleteTableEntries = listOf()
).diffSchemas()
assertThat(schemaDiffResult.addedColumns["artistId"]?.fieldBundle?.columnName)
.isEqualTo("artistId")
@@ -79,7 +95,12 @@
try {
SchemaDiffer(
fromSchemaBundle = from.database,
- toSchemaBundle = toColumnAddedWithNoDefaultValue.database
+ toSchemaBundle = toColumnAddedWithNoDefaultValue.database,
+ className = "MyAutoMigration",
+ renameColumnEntries = listOf(),
+ deleteColumnEntries = listOf(),
+ renameTableEntries = listOf(),
+ deleteTableEntries = listOf()
).diffSchemas()
fail("DiffException should have been thrown.")
} catch (ex: DiffException) {
@@ -93,10 +114,15 @@
fun testTableAddedWithColumnInfoDefaultValue() {
val schemaDiffResult = SchemaDiffer(
fromSchemaBundle = from.database,
- toSchemaBundle = toTableAddedWithColumnInfoDefaultValue.database
+ toSchemaBundle = toTableAddedWithColumnInfoDefaultValue.database,
+ className = "MyAutoMigration",
+ renameColumnEntries = listOf(),
+ deleteColumnEntries = listOf(),
+ renameTableEntries = listOf(),
+ deleteTableEntries = listOf()
).diffSchemas()
- assertThat(schemaDiffResult.addedTables[0].entityBundle.tableName).isEqualTo("Artist")
- assertThat(schemaDiffResult.addedTables[1].entityBundle.tableName).isEqualTo("Album")
+ assertThat(schemaDiffResult.addedTables.toList()[0].entityBundle.tableName)
+ .isEqualTo("Album")
}
@Test
@@ -104,12 +130,17 @@
try {
SchemaDiffer(
fromSchemaBundle = from.database,
- toSchemaBundle = toColumnRenamed.database
+ toSchemaBundle = toColumnRenamed.database,
+ className = "MyAutoMigration",
+ renameColumnEntries = listOf(),
+ deleteColumnEntries = listOf(),
+ renameTableEntries = listOf(),
+ deleteTableEntries = listOf()
).diffSchemas()
fail("DiffException should have been thrown.")
} catch (ex: DiffException) {
assertThat(ex.errorMessage).isEqualTo(
- ProcessorErrors.removedOrRenamedColumnFound("length")
+ ProcessorErrors.deletedOrRenamedColumnFound("MyAutoMigration", "length", "Song")
)
}
}
@@ -119,12 +150,57 @@
try {
SchemaDiffer(
fromSchemaBundle = from.database,
- toSchemaBundle = toColumnRemoved.database
+ toSchemaBundle = toColumnRemoved.database,
+ className = "MyAutoMigration",
+ renameColumnEntries = listOf(),
+ deleteColumnEntries = listOf(),
+ renameTableEntries = listOf(),
+ deleteTableEntries = listOf()
).diffSchemas()
fail("DiffException should have been thrown.")
} catch (ex: DiffException) {
assertThat(ex.errorMessage).isEqualTo(
- ProcessorErrors.removedOrRenamedColumnFound("length")
+ ProcessorErrors.deletedOrRenamedColumnFound("MyAutoMigration", "length", "Song")
+ )
+ }
+ }
+
+ @Test
+ fun testTableRenamedWithoutAnnotation() {
+ try {
+ SchemaDiffer(
+ fromSchemaBundle = from.database,
+ toSchemaBundle = toTableRenamed.database,
+ className = "MyAutoMigration",
+ renameColumnEntries = listOf(),
+ deleteColumnEntries = listOf(),
+ renameTableEntries = listOf(),
+ deleteTableEntries = listOf()
+ ).diffSchemas()
+ fail("DiffException should have been thrown.")
+ } catch (ex: DiffException) {
+ assertThat(ex.errorMessage).isEqualTo(
+ ProcessorErrors.deletedOrRenamedTableFound("MyAutoMigration", "Artist")
+ )
+ }
+ }
+
+ @Test
+ fun testTableRemovedWithoutAnnotation() {
+ try {
+ SchemaDiffer(
+ fromSchemaBundle = from.database,
+ toSchemaBundle = toTableDeleted.database,
+ className = "MyAutoMigration",
+ renameColumnEntries = listOf(),
+ deleteColumnEntries = listOf(),
+ renameTableEntries = listOf(),
+ deleteTableEntries = listOf()
+ ).diffSchemas()
+ fail("DiffException should have been thrown.")
+ } catch (ex: DiffException) {
+ assertThat(ex.errorMessage).isEqualTo(
+ ProcessorErrors.deletedOrRenamedTableFound("MyAutoMigration", "Artist")
)
}
}
@@ -168,6 +244,40 @@
),
mutableListOf(),
mutableListOf()
+ ),
+ EntityBundle(
+ "Artist",
+ "CREATE TABLE IF NOT EXISTS `Artist` (`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(),
@@ -176,6 +286,132 @@
)
/** Valid "to" Schemas */
+ val toTableRenamed = SchemaBundle(
+ 2,
+ DatabaseBundle(
+ 2,
+ "",
+ 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()
+ ),
+ EntityBundle(
+ "Album",
+ "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()
+ )
+ )
+
+ val toTableDeleted = SchemaBundle(
+ 2,
+ DatabaseBundle(
+ 2,
+ "",
+ 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()
+ )
+ )
+
val toColumnAddedWithColumnInfoDefaultValue = SchemaBundle(
2,
DatabaseBundle(
@@ -223,6 +459,40 @@
),
emptyList(),
emptyList()
+ ),
+ EntityBundle(
+ "Artist",
+ "CREATE TABLE IF NOT EXISTS `Artist` (`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(),
@@ -268,6 +538,40 @@
),
emptyList(),
emptyList()
+ ),
+ EntityBundle(
+ "Artist",
+ "CREATE TABLE IF NOT EXISTS `Artist` (`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(),
@@ -327,6 +631,40 @@
),
emptyList(),
emptyList()
+ ),
+ EntityBundle(
+ "Artist",
+ "CREATE TABLE IF NOT EXISTS `Artist` (`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(),
@@ -379,6 +717,40 @@
),
mutableListOf(),
mutableListOf()
+ ),
+ EntityBundle(
+ "Artist",
+ "CREATE TABLE IF NOT EXISTS `Artist` (`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(),
@@ -390,7 +762,6 @@
* 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(
@@ -431,6 +802,40 @@
),
mutableListOf(),
mutableListOf()
+ ),
+ EntityBundle(
+ "Artist",
+ "CREATE TABLE IF NOT EXISTS `Artist` (`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(),
@@ -480,20 +885,37 @@
),
EntityBundle(
"Artist",
- "CREATE TABLE IF NOT EXISTS `Artist` (`artistId` INTEGER NOT NULL, `name` " +
- "TEXT NOT NULL, PRIMARY KEY(`artistId`))",
+ "CREATE TABLE IF NOT EXISTS `Artist` (`id` INTEGER NOT NULL, " +
+ "`title` TEXT NOT NULL, `length` INTEGER NOT NULL, PRIMARY KEY(`id`))",
listOf(
FieldBundle(
- "artistId",
- "artistId",
+ "id",
+ "id",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "title",
+ "title",
+ "TEXT",
+ true,
+ ""
+ ),
+ FieldBundle(
+ "length",
+ "length",
"INTEGER",
true,
"1"
)
),
- PrimaryKeyBundle(true, listOf("artistId")),
- listOf(),
- listOf()
+ PrimaryKeyBundle(
+ false,
+ mutableListOf("id")
+ ),
+ mutableListOf(),
+ mutableListOf()
),
EntityBundle(
"Album",
@@ -575,6 +997,40 @@
listOf("artistId")
)
)
+ ),
+ EntityBundle(
+ "Artist",
+ "CREATE TABLE IF NOT EXISTS `Artist` (`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(),
@@ -629,6 +1085,40 @@
)
),
mutableListOf()
+ ),
+ EntityBundle(
+ "Artist",
+ "CREATE TABLE IF NOT EXISTS `Artist` (`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(),
@@ -675,6 +1165,40 @@
),
mutableListOf(),
mutableListOf()
+ ),
+ EntityBundle(
+ "Artist",
+ "CREATE TABLE IF NOT EXISTS `Artist` (`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(),
diff --git a/room/compiler/src/test/kotlin/androidx/room/writer/AutoMigrationWriterTest.kt b/room/compiler/src/test/kotlin/androidx/room/writer/AutoMigrationWriterTest.kt
index 4da432f..9497ce2 100644
--- a/room/compiler/src/test/kotlin/androidx/room/writer/AutoMigrationWriterTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/writer/AutoMigrationWriterTest.kt
@@ -36,7 +36,6 @@
"""
package foo.bar;
import androidx.room.migration.AutoMigrationCallback;
- import androidx.room.AutoMigration;
import androidx.sqlite.db.SupportSQLiteDatabase;
interface ValidAutoMigrationWithDefault extends AutoMigrationCallback {}
""".trimIndent()
@@ -65,10 +64,11 @@
)
)
),
- removedOrRenamedColumns = listOf(),
- addedTables = listOf(),
+ deletedColumns = listOf(),
+ addedTables = setOf(),
complexChangedTables = mapOf(),
- removedOrRenamedTables = listOf()
+ renamedTables = mapOf(),
+ deletedTables = listOf()
),
)
AutoMigrationWriter(
@@ -82,7 +82,7 @@
loadTestSource(
"autoMigrationWriter/output/ValidAutoMigrationWithDefault" +
".java",
- "foo.bar.ValidAutoMigrationWithDefault_Impl"
+ "foo.bar.AutoMigration_1_2_Impl"
)
)
}
@@ -96,9 +96,7 @@
"""
package foo.bar;
import androidx.room.migration.AutoMigrationCallback;
- import androidx.room.AutoMigration;
import androidx.sqlite.db.SupportSQLiteDatabase;
- @AutoMigration(from=1, to=2)
interface ValidAutoMigrationWithoutDefault extends AutoMigrationCallback {}
""".trimIndent()
)
@@ -126,10 +124,11 @@
)
)
),
- removedOrRenamedColumns = listOf(),
- addedTables = listOf(),
+ deletedColumns = listOf(),
+ addedTables = setOf(),
complexChangedTables = mapOf(),
- removedOrRenamedTables = listOf()
+ renamedTables = mapOf(),
+ deletedTables = listOf()
),
)
AutoMigrationWriter(
@@ -142,7 +141,7 @@
generatedSource(
loadTestSource(
"autoMigrationWriter/output/ValidAutoMigrationWithoutDefault.java",
- "foo.bar.ValidAutoMigrationWithoutDefault_Impl"
+ "foo.bar.AutoMigration_1_2_Impl"
)
)
}
diff --git a/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/1.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/1.json
index c02e4d1..8b7d1c0 100644
--- a/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/1.json
+++ b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/1.json
@@ -467,6 +467,204 @@
}
],
"foreignKeys": []
+ },
+ {
+ "tableName": "Entity15",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity16",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity17",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity18",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity19",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity20",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
}
],
"views": [],
diff --git a/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/2.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/2.json
index 6ebbddb..a925d683 100644
--- a/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/2.json
+++ b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/2.json
@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 2,
- "identityHash": "cb0906b793f21e53ed5ff0db35729db5",
+ "identityHash": "083a4e91debef71d5d900ca2cf0baccf",
"entities": [
{
"tableName": "Entity1",
@@ -314,7 +314,7 @@
},
{
"tableName": "Entity10",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`), FOREIGN KEY(`addedInV1`) REFERENCES `Entity13`(`addedInV1`) ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`), FOREIGN KEY(`addedInV1`) REFERENCES `Entity13_V2`(`addedInV1`) ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "id",
@@ -354,7 +354,7 @@
],
"foreignKeys": [
{
- "table": "Entity13",
+ "table": "Entity13_V2",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
@@ -442,7 +442,7 @@
"foreignKeys": []
},
{
- "tableName": "Entity13",
+ "tableName": "Entity13_V2",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
"fields": [
{
@@ -473,12 +473,12 @@
},
"indices": [
{
- "name": "index_Entity13_addedInV1",
+ "name": "index_Entity13_V2_addedInV1",
"unique": true,
"columnNames": [
"addedInV1"
],
- "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Entity13_addedInV1` ON `${TABLE_NAME}` (`addedInV1`)"
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Entity13_V2_addedInV1` ON `${TABLE_NAME}` (`addedInV1`)"
}
],
"foreignKeys": []
@@ -508,12 +508,177 @@
},
"indices": [],
"foreignKeys": []
+ },
+ {
+ "tableName": "Entity15",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity16",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `renamedInV2` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "renamedInV2",
+ "columnName": "renamedInV2",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity17",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `renamedInV2` TEXT DEFAULT '1', PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "renamedInV2",
+ "columnName": "renamedInV2",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'1'"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity19_V2",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity20_V2",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `renamedInV2` TEXT DEFAULT '1', `addedInV2` INTEGER NOT NULL DEFAULT 2, PRIMARY KEY(`name`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "renamedInV2",
+ "columnName": "renamedInV2",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'1'"
+ },
+ {
+ "fieldPath": "addedInV2",
+ "columnName": "addedInV2",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "2"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cb0906b793f21e53ed5ff0db35729db5')"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '083a4e91debef71d5d900ca2cf0baccf')"
]
}
}
\ No newline at end of file
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationDb.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationDb.java
index ce96535..db63fc3 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationDb.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationDb.java
@@ -22,11 +22,15 @@
import androidx.room.ColumnInfo;
import androidx.room.Dao;
import androidx.room.Database;
+import androidx.room.DeleteColumn;
+import androidx.room.DeleteTable;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Index;
import androidx.room.PrimaryKey;
import androidx.room.Query;
+import androidx.room.RenameColumn;
+import androidx.room.RenameTable;
import androidx.room.RoomDatabase;
import androidx.room.migration.AutoMigrationCallback;
@@ -47,15 +51,28 @@
AutoMigrationDb.Entity10.class,
AutoMigrationDb.Entity11.class,
AutoMigrationDb.Entity12.class,
- AutoMigrationDb.Entity13.class,
- AutoMigrationDb.Entity14.class
+ AutoMigrationDb.Entity13_V2.class,
+ AutoMigrationDb.Entity14.class,
+ AutoMigrationDb.Entity15.class,
+ AutoMigrationDb.Entity16.class,
+ AutoMigrationDb.Entity17.class,
+ AutoMigrationDb.Entity19_V2.class,
+ AutoMigrationDb.Entity20_V2.class
},
- autoMigrations = AutoMigrationDb.SimpleAutoMigration.class,
+ autoMigrations = {
+ @AutoMigration(
+ from = 1, to = 2, callback = AutoMigrationDb.SimpleAutoMigration1.class
+ )
+ },
exportSchema = true
)
public abstract class AutoMigrationDb extends RoomDatabase {
static final int LATEST_VERSION = 2;
abstract AutoMigrationDb.AutoMigrationDao dao();
+
+ /**
+ * No change between versions.
+ */
@Entity
static class Entity1 {
public static final String TABLE_NAME = "Entity1";
@@ -66,6 +83,9 @@
public int addedInV1;
}
+ /**
+ * A new simple column added to Entity 2 with a default value.
+ */
@Entity
static class Entity2 {
public static final String TABLE_NAME = "Entity2";
@@ -78,6 +98,9 @@
public int addedInV2;
}
+ /**
+ * Added Entity 3 to the schema. No foreign keys, views, indices added.
+ */
@Entity
static class Entity3 {
public static final String TABLE_NAME = "Entity3";
@@ -86,6 +109,9 @@
public String name;
}
+ /**
+ * Changing the default value of ‘addedInV1’ in Entity 4.
+ */
@Entity
static class Entity4 {
public static final String TABLE_NAME = "Entity4";
@@ -96,6 +122,9 @@
public int addedInV1;
}
+ /**
+ * Changing the affinity of ‘addedInV1’ in Entity 5.
+ */
@Entity
static class Entity5 {
public static final String TABLE_NAME = "Entity5";
@@ -106,6 +135,9 @@
public String addedInV1;
}
+ /**
+ * Changing the nullability of ‘addedInV1’ in Entity 6.
+ */
@Entity
static class Entity6 {
public static final String TABLE_NAME = "Entity6";
@@ -116,6 +148,9 @@
public int addedInV1;
}
+ /**
+ * No change between versions.
+ */
@Entity
static class Entity7 {
public static final String TABLE_NAME = "Entity7";
@@ -126,6 +161,9 @@
public int addedInV1;
}
+ /**
+ * Change the primary key of Entity 8.
+ */
@Entity
static class Entity8 {
public static final String TABLE_NAME = "Entity8";
@@ -137,6 +175,9 @@
public int addedInV1;
}
+ /**
+ * Add a foreign key to Entity 9.
+ */
@Entity(foreignKeys = {
@ForeignKey(entity = Entity12.class,
parentColumns = "id",
@@ -151,8 +192,13 @@
public int addedInV1;
}
+ /**
+ * Change the foreign key added in Entity 10 to ‘addedInV1’. Add index for addedInV1 on
+ * Entity 10. The reference table of the foreign key has been renamed from Entity13 to
+ * Entity13_V2.
+ */
@Entity(foreignKeys = {
- @ForeignKey(entity = Entity13.class,
+ @ForeignKey(entity = Entity13_V2.class,
parentColumns = "addedInV1",
childColumns = "addedInV1",
deferred = true)},
@@ -166,6 +212,9 @@
public int addedInV1;
}
+ /**
+ * Remove the foreign key in Entity 11.
+ */
@Entity
static class Entity11 {
public static final String TABLE_NAME = "Entity11";
@@ -176,6 +225,9 @@
public int addedInV1;
}
+ /**
+ * Add an index ‘name’ to Entity 12.
+ */
@Entity(indices = {@Index(value = "name", unique = true)})
static class Entity12 {
public static final String TABLE_NAME = "Entity12";
@@ -186,8 +238,12 @@
public int addedInV1;
}
+ /**
+ * Rename to Entity13_V2, it is a table referenced by the foreign key in Entity10. Change the
+ * index added in Entity 13 to ‘addedInV1’.
+ */
@Entity(indices = {@Index(value = "addedInV1", unique = true)})
- static class Entity13 {
+ static class Entity13_V2 {
public static final String TABLE_NAME = "Entity13";
@PrimaryKey
public int id;
@@ -196,6 +252,9 @@
public int addedInV1;
}
+ /**
+ * Remove the index ‘name’ added in Entity 14.
+ */
@Entity
static class Entity14 {
public static final String TABLE_NAME = "Entity14";
@@ -204,14 +263,94 @@
public String name;
}
+ /**
+ * Deleting the column ‘addedInV1’ from Entity 15.
+ */
+ @Entity
+ static class Entity15 {
+ public static final String TABLE_NAME = "Entity15";
+ @PrimaryKey
+ public int id;
+ public String name;
+ }
+
+ /**
+ * Renaming the column ‘addedInV1’ from Entity 16 to ‘renamedInV2’.
+ */
+ @Entity
+ static class Entity16 {
+ public static final String TABLE_NAME = "Entity16";
+ @PrimaryKey
+ public int id;
+ public String name;
+ @ColumnInfo(defaultValue = "1")
+ public int renamedInV2;
+ }
+
+ /**
+ * Renaming the column ‘addedInV1’ from Entity 17 to ‘renamedInV2’. Changing the affinity of
+ * this column.
+ */
+ @Entity
+ static class Entity17 {
+ public static final String TABLE_NAME = "Entity17";
+ @PrimaryKey
+ public int id;
+ public String name;
+ @ColumnInfo(defaultValue = "1")
+ public String renamedInV2;
+ }
+
+ /**
+ * Deleted Entity 18.
+ *
+ * Rename Entity19 to ‘Entity19_V2’.
+ */
+ @Entity
+ static class Entity19_V2 {
+ public static final String TABLE_NAME = "Entity19_V2";
+ @PrimaryKey
+ public int id;
+ public String name;
+ @ColumnInfo(defaultValue = "1")
+ public int addedInV1;
+ }
+
+ /**
+ * Rename Entity20 to ‘Entity20_V2’. Rename the column ‘addedInV1’ to ‘renamedInV2’. Change
+ * the primary key of this table to ‘name’. Change the affinity of the column ‘renamedInV2’.
+ * Add new column ‘addedInV2’.
+ */
+ @Entity
+ static class Entity20_V2 {
+ public static final String TABLE_NAME = "Entity20_V2";
+ public int id;
+ @PrimaryKey
+ @NonNull
+ public String name;
+ @ColumnInfo(defaultValue = "1")
+ public String renamedInV2;
+ @ColumnInfo(defaultValue = "2")
+ public int addedInV2;
+ }
+
@Dao
interface AutoMigrationDao {
@Query("SELECT * from Entity1 ORDER BY id ASC")
List<AutoMigrationDb.Entity1> getAllEntity1s();
}
- @AutoMigration(from=1, to=2)
- interface SimpleAutoMigration extends AutoMigrationCallback {
-
+ @DeleteTable(deletedTableName = "Entity18")
+ @RenameTable(originalTableName = "Entity19", newTableName = "Entity19_V2")
+ @RenameTable(originalTableName = "Entity20", newTableName = "Entity20_V2")
+ @RenameTable(originalTableName = "Entity13", newTableName = "Entity13_V2")
+ @RenameColumn(tableName = "Entity16", originalColumnName = "addedInV1",
+ newColumnName = "renamedInV2")
+ @RenameColumn(tableName = "Entity17", originalColumnName = "addedInV1",
+ newColumnName = "renamedInV2")
+ @RenameColumn(tableName = "Entity20", originalColumnName = "addedInV1",
+ newColumnName = "renamedInV2")
+ @DeleteColumn(tableName = "Entity15", deletedColumnName = "addedInV1")
+ interface SimpleAutoMigration1 extends AutoMigrationCallback {
}
}
\ No newline at end of file
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
index 5000a72..7a45a06 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
@@ -48,6 +48,12 @@
AutoMigrationDb.class.getCanonicalName());
}
+ // Run this to create the very 1st version of the db.
+ public void createFirstVersion() throws IOException {
+ SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 2);
+ db.close();
+ }
+
@Test
public void testBadAutoMigrationInput() throws IOException {
try (SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1)) {
@@ -57,15 +63,15 @@
TEST_DB,
2,
true,
- autoMigrationDbV2.getAutoGeneratedMigration(2, 3)
+ autoMigrationDbV2.getAutoGeneratedMigration(3, 4)
);
fail();
} catch (IllegalArgumentException ex) {
assertThat(
ex.getMessage(),
- is("No AutoMigrations between versions 'from = 2', 'to = "
- + "3' have been provided. Annotate Database class with @AutoMigration(from = "
- + "2, to = 3) to generate this AutoMigration.")
+ is("No AutoMigrations between versions 'from = 3', 'to = "
+ + "4' have been provided. Annotate Database class with @AutoMigration(from = "
+ + "3, to = 4) to generate this AutoMigration.")
);
}
}
@@ -85,18 +91,6 @@
assertThat(autoMigrationDbV2.dao().getAllEntity1s().size(), is(1));
}
- // Run this to create the very 1st version of the db.
- public void createFirstVersion() throws IOException {
- SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
- db.close();
- }
-
- public void createSecondVersion() {
- AutoMigrationDb autoMigrationDbV2 = getLatestDb();
-
- assertThat(autoMigrationDbV2.dao().getAllEntity1s().size(), is(1));
- }
-
private AutoMigrationDb getLatestDb() {
AutoMigrationDb db = Room.databaseBuilder(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
diff --git a/room/runtime/api/current.txt b/room/runtime/api/current.txt
index d708dbf..003bb27 100644
--- a/room/runtime/api/current.txt
+++ b/room/runtime/api/current.txt
@@ -111,6 +111,7 @@
method public void addMigrations(androidx.room.migration.Migration!...);
method public void addMigrations(java.util.List<androidx.room.migration.Migration!>);
method public java.util.List<androidx.room.migration.Migration!>? findMigrationPath(int, int);
+ method public java.util.Map<java.lang.Integer!,java.util.Map<java.lang.Integer!,androidx.room.migration.Migration!>!> getMigrations();
}
public abstract static class RoomDatabase.PrepackagedDatabaseCallback {
diff --git a/room/runtime/api/public_plus_experimental_current.txt b/room/runtime/api/public_plus_experimental_current.txt
index 608cd54..fc91b21 100644
--- a/room/runtime/api/public_plus_experimental_current.txt
+++ b/room/runtime/api/public_plus_experimental_current.txt
@@ -116,6 +116,7 @@
method public void addMigrations(androidx.room.migration.Migration!...);
method public void addMigrations(java.util.List<androidx.room.migration.Migration!>);
method public java.util.List<androidx.room.migration.Migration!>? findMigrationPath(int, int);
+ method public java.util.Map<java.lang.Integer!,java.util.Map<java.lang.Integer!,androidx.room.migration.Migration!>!> getMigrations();
}
public abstract static class RoomDatabase.PrepackagedDatabaseCallback {
diff --git a/room/runtime/api/restricted_current.txt b/room/runtime/api/restricted_current.txt
index 89994c2..7f8f611 100644
--- a/room/runtime/api/restricted_current.txt
+++ b/room/runtime/api/restricted_current.txt
@@ -154,6 +154,7 @@
method public void addMigrations(androidx.room.migration.Migration!...);
method public void addMigrations(java.util.List<androidx.room.migration.Migration!>);
method public java.util.List<androidx.room.migration.Migration!>? findMigrationPath(int, int);
+ method public java.util.Map<java.lang.Integer!,java.util.Map<java.lang.Integer!,androidx.room.migration.Migration!>!> getMigrations();
}
public abstract static class RoomDatabase.PrepackagedDatabaseCallback {
diff --git a/room/runtime/src/main/java/androidx/room/RoomDatabase.java b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
index f0d88eb..cd187e2 100644
--- a/room/runtime/src/main/java/androidx/room/RoomDatabase.java
+++ b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
@@ -186,8 +186,12 @@
public void init(@NonNull DatabaseConfiguration configuration) {
mOpenHelper = createOpenHelper(configuration);
List<Migration> autoMigrations = getAutoMigrations();
- if (autoMigrations.size() > 0) {
- configuration.migrationContainer.addMigrations(autoMigrations);
+ for (Migration autoMigration : autoMigrations) {
+ boolean migrationExists = configuration.migrationContainer.getMigrations()
+ .containsKey(autoMigration.startVersion);
+ if (!migrationExists) {
+ configuration.migrationContainer.addMigrations(autoMigrations);
+ }
}
// Configure SqliteCopyOpenHelper if it is available:
@@ -261,13 +265,13 @@
}
}
- @NonNull
+
/**
- * Returns a list of {@link Migration} of a database that have been generated using
- * {@link AutoMigration}.
+ * Returns a list of {@link Migration} of a database that have been automatically generated.
*
* @return A list of migration instances each of which is a generated autoMigration
*/
+ @NonNull
protected List<Migration> getAutoMigrations() {
return Arrays.asList();
}
@@ -1445,13 +1449,23 @@
}
Migration existing = targetMap.get(end);
if (existing != null) {
- // TODO: (b/182251019) Favor user specified migration over generated automigrations
Log.w(Room.LOG_TAG, "Overriding migration " + existing + " with " + migration);
}
targetMap.put(end, migration);
}
/**
+ * Returns the map of available migrations where the key is the start version of the
+ * migration, and the value is a map of (end version -> Migration).
+ *
+ * @return Map of migrations keyed by the start version
+ */
+ @NonNull
+ public Map<Integer, Map<Integer, Migration>> getMigrations() {
+ return Collections.unmodifiableMap(mMigrations);
+ }
+
+ /**
* Finds the list of migrations that should be run to move from {@code start} version to
* {@code end} version.
*
diff --git a/slices/builders/ktx/OWNERS b/slices/builders/ktx/OWNERS
index 4e131e2..1706cb06 100644
--- a/slices/builders/ktx/OWNERS
+++ b/slices/builders/ktx/OWNERS
@@ -1,3 +1,2 @@
-sigelbaum@google.com
-jakew@google.com
+yboyar@google.com
madym@google.com
diff --git a/sqlite/sqlite-ktx/OWNERS b/sqlite/sqlite-ktx/OWNERS
index e450f4c..6fd8227 100644
--- a/sqlite/sqlite-ktx/OWNERS
+++ b/sqlite/sqlite-ktx/OWNERS
@@ -1 +1 @@
-jakew@google.com
+yboyar@google.com
diff --git a/testutils/testutils-ktx/OWNERS b/testutils/testutils-ktx/OWNERS
index e450f4c..6fd8227 100644
--- a/testutils/testutils-ktx/OWNERS
+++ b/testutils/testutils-ktx/OWNERS
@@ -1 +1 @@
-jakew@google.com
+yboyar@google.com
diff --git a/tv-provider/OWNERS b/tv-provider/OWNERS
new file mode 100644
index 0000000..a386cda
--- /dev/null
+++ b/tv-provider/OWNERS
@@ -0,0 +1,2 @@
+shubang@google.com
+quxiangfang@google.com
diff --git a/wear/wear-complications-provider/api/current.txt b/wear/wear-complications-provider/api/current.txt
index 5a2cfe4..2273217 100644
--- a/wear/wear-complications-provider/api/current.txt
+++ b/wear/wear-complications-provider/api/current.txt
@@ -4,7 +4,7 @@
public abstract class ComplicationProviderService extends android.app.Service {
ctor public ComplicationProviderService();
method public abstract androidx.wear.complications.data.ComplicationData? getPreviewData(androidx.wear.complications.data.ComplicationType);
- method public android.os.IBinder? onBind(android.content.Intent);
+ method public final android.os.IBinder? onBind(android.content.Intent);
method @UiThread public void onComplicationActivated(int, int);
method @UiThread public void onComplicationDeactivated(int);
method @UiThread public abstract void onComplicationUpdate(int, androidx.wear.complications.data.ComplicationType, androidx.wear.complications.ComplicationProviderService.ComplicationUpdateListener);
diff --git a/wear/wear-complications-provider/api/public_plus_experimental_current.txt b/wear/wear-complications-provider/api/public_plus_experimental_current.txt
index 5a2cfe4..2273217 100644
--- a/wear/wear-complications-provider/api/public_plus_experimental_current.txt
+++ b/wear/wear-complications-provider/api/public_plus_experimental_current.txt
@@ -4,7 +4,7 @@
public abstract class ComplicationProviderService extends android.app.Service {
ctor public ComplicationProviderService();
method public abstract androidx.wear.complications.data.ComplicationData? getPreviewData(androidx.wear.complications.data.ComplicationType);
- method public android.os.IBinder? onBind(android.content.Intent);
+ method public final android.os.IBinder? onBind(android.content.Intent);
method @UiThread public void onComplicationActivated(int, int);
method @UiThread public void onComplicationDeactivated(int);
method @UiThread public abstract void onComplicationUpdate(int, androidx.wear.complications.data.ComplicationType, androidx.wear.complications.ComplicationProviderService.ComplicationUpdateListener);
diff --git a/wear/wear-complications-provider/api/restricted_current.txt b/wear/wear-complications-provider/api/restricted_current.txt
index 83d6e4b..0d4e30b 100644
--- a/wear/wear-complications-provider/api/restricted_current.txt
+++ b/wear/wear-complications-provider/api/restricted_current.txt
@@ -4,7 +4,7 @@
public abstract class ComplicationProviderService extends android.app.Service {
ctor public ComplicationProviderService();
method public abstract androidx.wear.complications.data.ComplicationData? getPreviewData(androidx.wear.complications.data.ComplicationType);
- method public android.os.IBinder? onBind(android.content.Intent);
+ method public final android.os.IBinder? onBind(android.content.Intent);
method @UiThread public void onComplicationActivated(int, int);
method @UiThread public void onComplicationDeactivated(int);
method @UiThread public abstract void onComplicationUpdate(int, androidx.wear.complications.data.ComplicationType, androidx.wear.complications.ComplicationProviderService.ComplicationUpdateListener);
diff --git a/wear/wear-complications-provider/src/main/java/androidx/wear/complications/ComplicationProviderService.java b/wear/wear-complications-provider/src/main/java/androidx/wear/complications/ComplicationProviderService.java
index 32e1fe3..a7e5463 100644
--- a/wear/wear-complications-provider/src/main/java/androidx/wear/complications/ComplicationProviderService.java
+++ b/wear/wear-complications-provider/src/main/java/androidx/wear/complications/ComplicationProviderService.java
@@ -269,7 +269,7 @@
@SuppressLint("SyntheticAccessor")
@Override
@Nullable
- public IBinder onBind(@NonNull Intent intent) {
+ public final IBinder onBind(@NonNull Intent intent) {
if (ACTION_COMPLICATION_UPDATE_REQUEST.equals(intent.getAction())) {
if (mWrapper == null) {
mWrapper = new IComplicationProviderWrapper();