Merge "Revert "Wire up animation scale in Dev Options with Compose animation"" into androidx-main
diff --git a/activity/activity-ktx/api/api_lint.ignore b/activity/activity-ktx/api/api_lint.ignore
index 85e5f3a..43509e8 100644
--- a/activity/activity-ktx/api/api_lint.ignore
+++ b/activity/activity-ktx/api/api_lint.ignore
@@ -1,6 +1,8 @@
// Baseline format: 1.0
MissingNullability: androidx.activity.ActivityViewModelLazyKt#viewModels(androidx.activity.ComponentActivity, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>):
Missing nullability on method `viewModels` return
+MissingNullability: androidx.activity.ActivityViewModelLazyKt#viewModels(androidx.activity.ComponentActivity, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>):
+ Missing nullability on method `viewModels` return
RegistrationName: androidx.activity.OnBackPressedDispatcherKt#addCallback(androidx.activity.OnBackPressedDispatcher, androidx.lifecycle.LifecycleOwner, boolean, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit>):
diff --git a/activity/activity-ktx/api/current.txt b/activity/activity-ktx/api/current.txt
index f844b35..0a94311 100644
--- a/activity/activity-ktx/api/current.txt
+++ b/activity/activity-ktx/api/current.txt
@@ -2,7 +2,8 @@
package androidx.activity {
public final class ActivityViewModelLazyKt {
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
}
public final class OnBackPressedDispatcherKt {
diff --git a/activity/activity-ktx/api/public_plus_experimental_current.txt b/activity/activity-ktx/api/public_plus_experimental_current.txt
index 5dff406..0ca1317 100644
--- a/activity/activity-ktx/api/public_plus_experimental_current.txt
+++ b/activity/activity-ktx/api/public_plus_experimental_current.txt
@@ -2,7 +2,8 @@
package androidx.activity {
public final class ActivityViewModelLazyKt {
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
}
public final class OnBackPressedDispatcherKt {
diff --git a/activity/activity-ktx/api/restricted_current.txt b/activity/activity-ktx/api/restricted_current.txt
index f844b35..0a94311 100644
--- a/activity/activity-ktx/api/restricted_current.txt
+++ b/activity/activity-ktx/api/restricted_current.txt
@@ -2,7 +2,8 @@
package androidx.activity {
public final class ActivityViewModelLazyKt {
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
}
public final class OnBackPressedDispatcherKt {
diff --git a/activity/activity-ktx/src/androidTest/java/androidx/activity/ActivityViewModelLazyTest.kt b/activity/activity-ktx/src/androidTest/java/androidx/activity/ActivityViewModelLazyTest.kt
index ac106f6..bfcd4e1 100644
--- a/activity/activity-ktx/src/androidTest/java/androidx/activity/ActivityViewModelLazyTest.kt
+++ b/activity/activity-ktx/src/androidTest/java/androidx/activity/ActivityViewModelLazyTest.kt
@@ -17,8 +17,14 @@
package androidx.activity
import android.os.Bundle
+import androidx.core.os.bundleOf
+import androidx.lifecycle.DEFAULT_ARGS_KEY
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.SavedStateViewModelFactory
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewmodel.CreationExtras
+import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.test.annotation.UiThreadTest
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
@@ -39,6 +45,7 @@
assertThat(activity.vm).isNotNull()
assertThat(activity.factoryVM.prop).isEqualTo("activity")
assertThat(activity.daggerPoorCopyVM.prop).isEqualTo("dagger")
+ assertThat(activity.savedStateViewModel.defaultValue).isEqualTo("value")
}
class TestActivity : ComponentActivity() {
@@ -46,16 +53,32 @@
val factoryVM: TestFactorizedViewModel by viewModels { VMFactory("activity") }
lateinit var injectedFactory: ViewModelProvider.Factory
val daggerPoorCopyVM: TestDaggerViewModel by viewModels { injectedFactory }
+ val savedStateViewModel: TestSavedStateViewModel by viewModels(
+ extrasProducer = { defaultViewModelCreationExtras }
+ )
override fun onCreate(savedInstanceState: Bundle?) {
injectedFactory = VMFactory("dagger")
super.onCreate(savedInstanceState)
}
+
+ override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
+ return SavedStateViewModelFactory()
+ }
+
+ override fun getDefaultViewModelCreationExtras(): CreationExtras {
+ val extras = MutableCreationExtras(super.getDefaultViewModelCreationExtras())
+ extras[DEFAULT_ARGS_KEY] = bundleOf("test" to "value")
+ return extras
+ }
}
class TestViewModel : ViewModel()
class TestFactorizedViewModel(val prop: String) : ViewModel()
class TestDaggerViewModel(val prop: String) : ViewModel()
+ class TestSavedStateViewModel(val savedStateHandle: SavedStateHandle) : ViewModel() {
+ val defaultValue = savedStateHandle.get<String>("test")
+ }
private class VMFactory(val prop: String) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
diff --git a/activity/activity-ktx/src/main/java/androidx/activity/ActivityViewModelLazy.kt b/activity/activity-ktx/src/main/java/androidx/activity/ActivityViewModelLazy.kt
index bf64ade..b7985fc 100644
--- a/activity/activity-ktx/src/main/java/androidx/activity/ActivityViewModelLazy.kt
+++ b/activity/activity-ktx/src/main/java/androidx/activity/ActivityViewModelLazy.kt
@@ -21,6 +21,41 @@
import androidx.lifecycle.ViewModelLazy
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.Factory
+import androidx.lifecycle.viewmodel.CreationExtras
+
+/**
+ * Returns a [Lazy] delegate to access the ComponentActivity's ViewModel, if [factoryProducer]
+ * is specified then [ViewModelProvider.Factory] returned by it will be used
+ * to create [ViewModel] first time.
+ *
+ * ```
+ * class MyComponentActivity : ComponentActivity() {
+ * val viewmodel: MyViewModel by viewModels()
+ * }
+ * ```
+ *
+ * This property can be accessed only after the Activity is attached to the Application,
+ * and access prior to that will result in IllegalArgumentException.
+ */
+@Deprecated(
+ "Superseded by viewModels that takes a CreationExtras",
+ level = DeprecationLevel.HIDDEN
+)
+@MainThread
+public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
+ noinline factoryProducer: (() -> Factory)? = null
+): Lazy<VM> {
+ val factoryPromise = factoryProducer ?: {
+ defaultViewModelProviderFactory
+ }
+
+ return ViewModelLazy(
+ VM::class,
+ { viewModelStore },
+ factoryPromise,
+ { this.defaultViewModelCreationExtras }
+ )
+}
/**
* Returns a [Lazy] delegate to access the ComponentActivity's ViewModel, if [factoryProducer]
@@ -38,6 +73,7 @@
*/
@MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
+ noinline extrasProducer: (() -> CreationExtras)? = null,
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
@@ -48,6 +84,6 @@
VM::class,
{ viewModelStore },
factoryPromise,
- { this.defaultViewModelCreationExtras }
+ { extrasProducer?.invoke() ?: this.defaultViewModelCreationExtras }
)
}
diff --git a/ads/ads-identifier-common/src/main/java/androidx/ads/identifier/AdvertisingIdUtils.java b/ads/ads-identifier-common/src/main/java/androidx/ads/identifier/AdvertisingIdUtils.java
index ae660f2..93d1e63 100644
--- a/ads/ads-identifier-common/src/main/java/androidx/ads/identifier/AdvertisingIdUtils.java
+++ b/ads/ads-identifier-common/src/main/java/androidx/ads/identifier/AdvertisingIdUtils.java
@@ -66,7 +66,7 @@
* <p>Only system-level providers will be returned.
*/
@NonNull
- @SuppressWarnings("MixedMutabilityReturnType")
+ @SuppressWarnings({"MixedMutabilityReturnType", "deprecation"})
public static List<ServiceInfo> getAdvertisingIdProviderServices(
@NonNull PackageManager packageManager) {
Intent intent = new Intent(GET_AD_ID_ACTION);
@@ -89,6 +89,7 @@
return systemLevelServiceInfos;
}
+ @SuppressWarnings("deprecation")
private static boolean isSystemByApplicationInfo(
@NonNull String packageName, @NonNull PackageManager packageManager) {
try {
@@ -123,6 +124,7 @@
* package is found.
*/
@Nullable
+ @SuppressWarnings("deprecation")
public static ServiceInfo selectServiceByPriority(
@NonNull List<ServiceInfo> serviceInfos, @NonNull PackageManager packageManager) {
if (serviceInfos.isEmpty()) {
diff --git a/ads/ads-identifier-common/src/test/java/androidx/ads/identifier/AdvertisingIdUtilsTest.java b/ads/ads-identifier-common/src/test/java/androidx/ads/identifier/AdvertisingIdUtilsTest.java
index ace51c6..50f7933 100644
--- a/ads/ads-identifier-common/src/test/java/androidx/ads/identifier/AdvertisingIdUtilsTest.java
+++ b/ads/ads-identifier-common/src/test/java/androidx/ads/identifier/AdvertisingIdUtilsTest.java
@@ -128,6 +128,7 @@
assertThat(serviceInfo).isNull();
}
+ @SuppressWarnings("deprecation")
private ServiceInfo createServiceInfo(String packageName, boolean requestHighPriority,
long firstInstallTime) throws Exception {
PackageInfo packageInfo = new PackageInfo();
diff --git a/ads/ads-identifier-provider/src/main/java/androidx/ads/identifier/provider/AdvertisingIdProviderManager.java b/ads/ads-identifier-provider/src/main/java/androidx/ads/identifier/provider/AdvertisingIdProviderManager.java
index b503dd0..990e786 100644
--- a/ads/ads-identifier-provider/src/main/java/androidx/ads/identifier/provider/AdvertisingIdProviderManager.java
+++ b/ads/ads-identifier-provider/src/main/java/androidx/ads/identifier/provider/AdvertisingIdProviderManager.java
@@ -148,7 +148,7 @@
* <p>This is achieved by looking up which activities can handle {@link #OPEN_SETTINGS_ACTION}
* intent action.
*/
- @SuppressWarnings("MixedMutabilityReturnType")
+ @SuppressWarnings({"MixedMutabilityReturnType", "deprecation"})
private static Map<String, String> getOpenSettingsActivities(PackageManager packageManager) {
Intent settingsIntent = new Intent(OPEN_SETTINGS_ACTION);
List<ResolveInfo> settingsResolveInfos = packageManager.queryIntentActivities(
diff --git a/ads/ads-identifier-testing/src/main/java/androidx/ads/identifier/testing/MockPackageManagerHelper.java b/ads/ads-identifier-testing/src/main/java/androidx/ads/identifier/testing/MockPackageManagerHelper.java
index 88b5abc..4fb5e1f 100644
--- a/ads/ads-identifier-testing/src/main/java/androidx/ads/identifier/testing/MockPackageManagerHelper.java
+++ b/ads/ads-identifier-testing/src/main/java/androidx/ads/identifier/testing/MockPackageManagerHelper.java
@@ -64,6 +64,7 @@
}
/** Mocks the {@link PackageManager#queryIntentServices(Intent, int)}. */
+ @SuppressWarnings("deprecation")
public void mockQueryGetAdIdServices(@NonNull List<ResolveInfo> resolveInfos) throws Exception {
boolean supportMatchSystemOnly = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
when(mMockPackageManager.queryIntentServices(hasAction(GET_AD_ID_ACTION),
@@ -85,6 +86,7 @@
}
/** Mocks the {@link PackageManager#queryIntentActivities(Intent, int)}. */
+ @SuppressWarnings("deprecation")
public void mockQueryOpenSettingsActivities(@NonNull List<ResolveInfo> resolveInfos) {
when(mMockPackageManager.queryIntentActivities(hasAction(OPEN_SETTINGS_ACTION), eq(0)))
.thenReturn(resolveInfos);
diff --git a/ads/ads-identifier/integration-tests/testapp/src/main/java/androidx/ads/identifier/testapp/AdsIdentifierActivity.java b/ads/ads-identifier/integration-tests/testapp/src/main/java/androidx/ads/identifier/testapp/AdsIdentifierActivity.java
index cb12f2c..2f575fd 100644
--- a/ads/ads-identifier/integration-tests/testapp/src/main/java/androidx/ads/identifier/testapp/AdsIdentifierActivity.java
+++ b/ads/ads-identifier/integration-tests/testapp/src/main/java/androidx/ads/identifier/testapp/AdsIdentifierActivity.java
@@ -102,6 +102,7 @@
}
/** Lists all the providers. */
+ @SuppressWarnings("deprecation")
public void listProvider(View view) {
TextView textView = findViewById(R.id.text);
textView.setText("Services:\n");
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerRtlTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerRtlTest.java
index 3ff4329..ba59ede 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerRtlTest.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerRtlTest.java
@@ -18,6 +18,7 @@
import android.app.Instrumentation;
import android.os.Build;
+import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -49,6 +50,7 @@
mInstrumentation = InstrumentationRegistry.getInstrumentation();
}
+ @FlakyTest
@Test
public void testHorizontalOffsetRtl() {
AppCompatSpinnerTest.checkOffsetIsCorrect(mInstrumentation, mContainer, 200, false, true);
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
index b659955..eb24e477 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
@@ -31,7 +31,11 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(21)
public class PerfettoCapture(
- unbundled: Boolean = Build.VERSION.SDK_INT in 21..28
+ /**
+ * Bundled is available above API 28, but we default to using unbundled as well on API 29, as
+ * ProcessStatsConfig.scan_all_processes_on_start isn't supported on the bundled version.
+ */
+ unbundled: Boolean = Build.VERSION.SDK_INT in 21..29
) {
private val helper: PerfettoHelper = PerfettoHelper(unbundled)
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
index 64fd2ee..b1be5ed 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
@@ -20,7 +20,6 @@
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
-import androidx.benchmark.Arguments
import androidx.benchmark.Outputs
import androidx.benchmark.Outputs.dateToFileName
import androidx.benchmark.PropOverride
@@ -72,8 +71,8 @@
iteration: Int? = null,
block: () -> Unit
): String? {
- // skip if can't capture Perfetto, or Cuttlefish + dryRun (traces aren't needed anyway)
- if (Build.VERSION.SDK_INT < 21 || (Arguments.dryRunMode && !isAbiSupported())) {
+ // skip if Perfetto not supported, or on Cuttlefish (where tracing doesn't work)
+ if (Build.VERSION.SDK_INT < 21 || !isAbiSupported()) {
block()
return null // tracing not supported
}
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
index 3b96d05..3c92e79 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
@@ -94,6 +94,9 @@
target_buffer = 1,
process_stats_config = ProcessStatsConfig(
proc_stats_poll_ms = 10000,
+ // This flag appears to be unreliable on API 29 unbundled perfetto, so to avoid very
+ // frequent proc stats polling to name processes correctly, we currently use unbundled
+ // perfetto on API 29, even though the bundled version exists. (b/218668335)
scan_all_processes_on_start = true
)
)
@@ -192,4 +195,4 @@
}
}
return encode()
-}
\ No newline at end of file
+}
diff --git a/benchmark/benchmark-macro/lint-baseline.xml b/benchmark/benchmark-macro/lint-baseline.xml
deleted file mode 100644
index cde602f..0000000
--- a/benchmark/benchmark-macro/lint-baseline.xml
+++ /dev/null
@@ -1,70 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.1.0-beta02" type="baseline" client="gradle" dependencies="false" name="AGP (7.1.0-beta02)" variant="all" version="7.1.0-beta02">
-
- <issue
- id="MissingTestSizeAnnotation"
- message="Missing test size annotation"
- errorLine1=" fun fixedApi24Cold() = validateFixedTrace("
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt"
- line="54"
- column="9"/>
- </issue>
-
- <issue
- id="MissingTestSizeAnnotation"
- message="Missing test size annotation"
- errorLine1=" fun fixedApi24Warm() = validateFixedTrace("
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt"
- line="65"
- column="9"/>
- </issue>
-
- <issue
- id="MissingTestSizeAnnotation"
- message="Missing test size annotation"
- errorLine1=" fun fixedApi24Hot() = validateFixedTrace("
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt"
- line="76"
- column="9"/>
- </issue>
-
- <issue
- id="MissingTestSizeAnnotation"
- message="Missing test size annotation"
- errorLine1=" fun fixedApi31Cold() = validateFixedTrace("
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt"
- line="87"
- column="9"/>
- </issue>
-
- <issue
- id="MissingTestSizeAnnotation"
- message="Missing test size annotation"
- errorLine1=" fun fixedApi31Warm() = validateFixedTrace("
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt"
- line="98"
- column="9"/>
- </issue>
-
- <issue
- id="MissingTestSizeAnnotation"
- message="Missing test size annotation"
- errorLine1=" fun fixedApi31Hot() = validateFixedTrace("
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt"
- line="109"
- column="9"/>
- </issue>
-
-</issues>
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
index 4ad481e..9308615 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
@@ -41,6 +41,7 @@
private val instrumentation = InstrumentationRegistry.getInstrumentation()
@Before
+ @Suppress("DEPRECATION")
fun setup() {
// validate target is installed with clear error message,
// since error messages from e.g. startActivityAndWait may be less clear
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
index 51be679..b913cfa 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
@@ -20,24 +20,24 @@
import androidx.benchmark.macro.createTempFileFromAsset
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
import java.util.Locale
import kotlin.test.assertEquals
+@MediumTest
@RunWith(AndroidJUnit4::class)
class StartupTimingQueryTest {
private fun validateFixedTrace(
@Suppress("SameParameterValue") api: Int,
startupMode: StartupMode,
- expectedMetrics: StartupTimingQuery.SubMetrics
+ expectedMetrics: StartupTimingQuery.SubMetrics?,
+ tracePrefix: String = "api${api}_startup_${startupMode.name.lowercase(Locale.getDefault())}"
) {
assumeTrue(isAbiSupported())
- val traceFile = createTempFileFromAsset(
- prefix = "api${api}_startup_${startupMode.name.lowercase(Locale.getDefault())}",
- suffix = ".perfetto-trace"
- )
+ val traceFile = createTempFileFromAsset(prefix = tracePrefix, suffix = ".perfetto-trace")
val startupSubMetrics = StartupTimingQuery.getFrameSubMetrics(
absoluteTracePath = traceFile.absolutePath,
@@ -115,4 +115,15 @@
timelineRangeNs = 186969441973689..186969984196243
)
)
-}
\ No newline at end of file
+
+ /**
+ * Validate that StartupTimingQuery returns null and doesn't crash when process name truncated
+ */
+ @Test
+ fun fixedApi29ColdProcessNameTruncated() = validateFixedTrace(
+ api = 29,
+ startupMode = StartupMode.COLD,
+ tracePrefix = "api29_cold_startup_processname_truncated",
+ expectedMetrics = null // process name is truncated, and we currently don't handle this
+ )
+}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 0dd14cf..28a5952 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -23,6 +23,9 @@
import androidx.annotation.RestrictTo
import androidx.benchmark.DeviceInfo
import androidx.benchmark.Shell
+import androidx.benchmark.macro.CompilationMode.Full
+import androidx.benchmark.macro.CompilationMode.None
+import androidx.benchmark.macro.CompilationMode.Partial
import androidx.profileinstaller.ProfileInstallReceiver
import androidx.profileinstaller.ProfileInstaller
import org.junit.AssumptionViolatedException
@@ -65,10 +68,78 @@
if (Build.VERSION.SDK_INT >= 24) {
Log.d(TAG, "Clearing profiles for $packageName")
Shell.executeCommand("cmd package compile --reset $packageName")
+ writeProfileInstallerSkipFile(packageName)
compileImpl(packageName, warmupBlock)
}
}
+ /**
+ * Writes a skip file via a [ProfileInstallReceiver] broadcast, so profile installation
+ * does not interfere with benchmarks.
+ */
+ private fun writeProfileInstallerSkipFile(packageName: String) {
+ val result = profileInstallerSkipFileOperation(packageName, "WRITE_SKIP_FILE")
+ if (result != null) {
+ Log.w(
+ TAG,
+ """
+ $packageName should use the latest version of `androidx.profileinstaller`
+ for stable benchmarks. ($result)"
+ """.trimIndent()
+ )
+ }
+ }
+
+ /**
+ * Uses skip files for avoiding interference from ProfileInstaller when using
+ * [CompilationMode.None].
+ *
+ * Operation name is one of `WRITE_SKIP_FILE` or `DELETE_SKIP_FILE`.
+ *
+ * Returned error strings aren't thrown, to let the calling function decide strictness.
+ */
+ private fun profileInstallerSkipFileOperation(
+ packageName: String,
+ operation: String
+ ): String? {
+ // Redefining constants here, because these are only defined in the latest alpha for
+ // ProfileInstaller.
+
+ // Use an explicit broadcast given the app was force-stopped.
+ val name = ProfileInstallReceiver::class.java.name
+ val action = "androidx.profileinstaller.action.SKIP_FILE"
+ val operationKey = "EXTRA_SKIP_FILE_OPERATION"
+ val extras = "$operationKey $operation"
+ Log.d(TAG, "Profile Installation Skip File Operation: $operation")
+ val result = Shell.executeCommand("am broadcast -a $action -e $extras $packageName/$name")
+ .substringAfter("Broadcast completed: result=")
+ .trim()
+ .toIntOrNull()
+ return when {
+ result == null || result == 0 -> {
+ // 0 is returned by the platform by default, and also if no broadcast receiver
+ // receives the broadcast.
+
+ "The baseline profile skip file broadcast was not received. " +
+ "This most likely means that the `androidx.profileinstaller` library " +
+ "used by the target apk is old. Please use `1.2.0-alpha03` or newer. " +
+ "For more information refer to the release notes at " +
+ "https://developer.android.com/jetpack/androidx/releases/profileinstaller."
+ }
+ operation == "WRITE_SKIP_FILE" && result == 10 -> { // RESULT_INSTALL_SKIP_FILE_SUCCESS
+ null // success!
+ }
+ operation == "DELETE_SKIP_FILE" && result == 11 -> { // RESULT_DELETE_SKIP_FILE_SUCCESS
+ null // success!
+ }
+ else -> {
+ throw RuntimeException(
+ "unrecognized ProfileInstaller result code: $result"
+ )
+ }
+ }
+ }
+
@RequiresApi(24)
internal fun cmdPackageCompile(packageName: String, compileArgument: String) {
Shell.executeCommand("cmd package compile -f -m $compileArgument $packageName")
@@ -207,6 +278,7 @@
override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
if (baselineProfileMode != BaselineProfileMode.Disable) {
+ // Ignores the presence of a skip file.
val installErrorString = broadcastBaselineProfileInstall(packageName)
if (installErrorString == null) {
// baseline profile install success, kill process before compiling
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 9265979..aca7f19 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -38,6 +38,7 @@
import androidx.tracing.trace
import java.io.File
+@Suppress("DEPRECATION")
internal fun checkErrors(packageName: String): ConfigurationError.SuppressionState? {
val pm = InstrumentationRegistry.getInstrumentation().context.packageManager
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
index 5103fa2..bf07e48 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
@@ -16,8 +16,10 @@
package androidx.benchmark.macro.perfetto
+import androidx.benchmark.macro.perfetto.PerfettoTraceProcessor.processNameLikePkg
+
internal object FrameTimingQuery {
- private fun getFullQuery(processName: String) = """
+ private fun getFullQuery(packageName: String) = """
------ Select all frame-relevant slices from slice table
SELECT
slice.name as name,
@@ -30,7 +32,7 @@
WHERE (
( slice.name LIKE "Choreographer#doFrame%" AND process.pid LIKE thread.tid ) OR
( slice.name LIKE "DrawFrame%" AND thread.name like "RenderThread" )
- ) AND (process.name LIKE "$processName")
+ ) AND ${processNameLikePkg(packageName)}
------ Add in actual frame slices (prepended with "actual " to differentiate)
UNION
SELECT
@@ -40,7 +42,7 @@
FROM actual_frame_timeline_slice
INNER JOIN process USING(upid)
WHERE
- process.name LIKE "$processName"
+ ${processNameLikePkg(packageName)}
------ Add in expected time slices (prepended with "expected " to differentiate)
UNION
SELECT
@@ -50,7 +52,7 @@
FROM expected_frame_timeline_slice
INNER JOIN process USING(upid)
WHERE
- process.name LIKE "$processName"
+ ${processNameLikePkg(packageName)}
ORDER BY ts ASC
""".trimIndent()
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt
index b94b9a1..66a570b 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt
@@ -114,4 +114,11 @@
queryFile.delete()
}
}
+
+ /**
+ * Helper for fuzzy matching process name to package
+ */
+ internal fun processNameLikePkg(pkg: String): String {
+ return """(process.name LIKE "$pkg" OR process.name LIKE "$pkg:%")"""
+ }
}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupTimingQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupTimingQuery.kt
index 368b6d9..a31965c 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupTimingQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupTimingQuery.kt
@@ -16,11 +16,13 @@
package androidx.benchmark.macro.perfetto
+import android.util.Log
import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.perfetto.PerfettoTraceProcessor.processNameLikePkg
internal object StartupTimingQuery {
- private fun getFullQuery(testProcessName: String, targetProcessName: String) = """
+ private fun getFullQuery(testPackageName: String, targetPackageName: String) = """
------ Select all startup-relevant slices from slice table
SELECT
slice.name as name,
@@ -31,9 +33,9 @@
INNER JOIN thread USING(utid)
INNER JOIN process USING(upid)
WHERE (
- (process.name LIKE "$testProcessName" AND slice.name LIKE "startActivityAndWait") OR
+ (${processNameLikePkg(testPackageName)} AND slice.name LIKE "startActivityAndWait") OR
(
- process.name LIKE "$targetProcessName" AND (
+ ${processNameLikePkg(targetPackageName)} AND (
(slice.name LIKE "activityResume" AND process.pid LIKE thread.tid) OR
(slice.name LIKE "Choreographer#doFrame%" AND process.pid LIKE thread.tid) OR
(slice.name LIKE "reportFullyDrawn() for %" AND process.pid LIKE thread.tid) OR
@@ -114,8 +116,8 @@
val queryResult = PerfettoTraceProcessor.rawQuery(
absoluteTracePath = absoluteTracePath,
query = getFullQuery(
- testProcessName = testPackageName,
- targetProcessName = targetPackageName
+ testPackageName = testPackageName,
+ targetPackageName = targetPackageName
)
)
val slices = Slice.parseListFromQueryResult(queryResult)
@@ -144,6 +146,11 @@
val uiSlices = groupedData.getOrElse(StartupSliceType.FrameUiThread) { listOf() }
val rtSlices = groupedData.getOrElse(StartupSliceType.FrameRenderThread) { listOf() }
+ if (uiSlices.isEmpty() || rtSlices.isEmpty()) {
+ Log.d("Benchmark", "No UI / RT slices seen, not reporting startup.")
+ return null
+ }
+
val startTs: Long
val initialDisplayTs: Long
if (captureApiLevel >= 29 || startupMode != StartupMode.HOT) {
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index 69c5a3b..b742f04 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -71,16 +71,23 @@
</intent-filter>
</activity>
- <!--
- Activities need to be exported so the macrobenchmark can discover them
- -->
<activity
android:name=".NotExportedActivity"
- android:exported="false">
+ android:exported="false"> <!-- intentionally not exported -->
<intent-filter>
<action android:name="androidx.benchmark.integration.macrobenchmark.target.NOT_EXPORTED_ACTIVITY" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
+
+ <activity
+ android:name=".SeparateProcessActivity"
+ android:process=":ui"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="androidx.benchmark.integration.macrobenchmark.target.SEPARATE_PROCESS_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
</application>
</manifest>
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/SeparateProcessActivity.kt b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/SeparateProcessActivity.kt
new file mode 100644
index 0000000..830679e
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/SeparateProcessActivity.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.integration.macrobenchmark.target
+
+import android.os.Bundle
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+
+/**
+ * Trivial activity which lives in a separate ":ui" process
+ */
+class SeparateProcessActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ val notice = findViewById<TextView>(R.id.txtNotice)
+ notice.setText(R.string.app_notice)
+ }
+}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SeparateProcessBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SeparateProcessBenchmark.kt
new file mode 100644
index 0000000..2b4d47c
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SeparateProcessBenchmark.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.integration.macrobenchmark
+
+import android.content.Intent
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.FrameTimingMetric
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.StartupTimingMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Validates metrics coming from an app's sub-process.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class SeparateProcessBenchmark {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ @Test
+ fun startup() = benchmarkRule.measureRepeated(
+ compilationMode = CompilationMode.DEFAULT,
+ startupMode = StartupMode.COLD,
+ packageName = "androidx.benchmark.integration.macrobenchmark.target",
+ metrics = listOf(StartupTimingMetric(), FrameTimingMetric()),
+ iterations = 2
+ ) {
+ startActivityAndWait(Intent("$packageName.SEPARATE_PROCESS_ACTIVITY"))
+ }
+}
diff --git a/benchmark/integration-tests/test-module-sample/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialTestModuleTest.kt b/benchmark/integration-tests/test-module-sample/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialTestModuleTest.kt
index 5f84e75..aac7497 100644
--- a/benchmark/integration-tests/test-module-sample/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialTestModuleTest.kt
+++ b/benchmark/integration-tests/test-module-sample/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialTestModuleTest.kt
@@ -49,6 +49,7 @@
@Ignore // b/202321897
@Test
+ @Suppress("DEPRECATION")
fun targetPackageInstalled() {
val pm = InstrumentationRegistry.getInstrumentation().context.packageManager
try {
diff --git a/browser/browser/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java b/browser/browser/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java
index 3e80ac84..7ea9202 100644
--- a/browser/browser/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java
+++ b/browser/browser/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java
@@ -348,6 +348,7 @@
/** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@VisibleForTesting
+ @SuppressWarnings("deprecation")
static void launchIntent(Context context, Intent intent, List<ResolveInfo> handlers) {
if (handlers == null || handlers.size() == 0) {
openFallbackBrowserActionsMenu(context, intent);
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
index edf8144..a597247 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
@@ -138,6 +138,7 @@
* @param ignoreDefault If set, the default VIEW handler won't get priority over other browsers.
* @return The preferred package name for handling Custom Tabs, or <code>null</code>.
*/
+ @SuppressWarnings("deprecation")
public static @Nullable String getPackageName(
@NonNull Context context, @Nullable List<String> packages, boolean ignoreDefault) {
PackageManager pm = context.getPackageManager();
diff --git a/browser/browser/src/main/java/androidx/browser/trusted/PackageIdentityUtils.java b/browser/browser/src/main/java/androidx/browser/trusted/PackageIdentityUtils.java
index b554dbd..7900b85 100644
--- a/browser/browser/src/main/java/androidx/browser/trusted/PackageIdentityUtils.java
+++ b/browser/browser/src/main/java/androidx/browser/trusted/PackageIdentityUtils.java
@@ -77,6 +77,7 @@
}
@RequiresApi(28)
+ @SuppressWarnings("deprecation")
static class Api28Implementation implements SignaturesCompat {
@Override
@Nullable
diff --git a/browser/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityService.java b/browser/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityService.java
index b62d039..e0630fc 100644
--- a/browser/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityService.java
+++ b/browser/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityService.java
@@ -339,6 +339,7 @@
* @return A resource id for the small icon, or {@link #SMALL_ICON_NOT_SET} if not found.
*/
@BinderThread
+ @SuppressWarnings("deprecation")
public int onGetSmallIconId() {
try {
ServiceInfo info = getPackageManager().getServiceInfo(
diff --git a/browser/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPool.java b/browser/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPool.java
index a7438ef..31d2cd4 100644
--- a/browser/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPool.java
+++ b/browser/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPool.java
@@ -205,6 +205,7 @@
* {@code possiblePackages}.
* Will return {@code null} if there is no applicable Service.
*/
+ @SuppressWarnings("deprecation")
private @Nullable Intent createServiceIntent(Context appContext, Uri scope,
Set<Token> possiblePackages, boolean shouldLog) {
if (possiblePackages == null || possiblePackages.size() == 0) {
diff --git a/camera/camera-camera2/src/main/AndroidManifest.xml b/camera/camera-camera2/src/main/AndroidManifest.xml
index a5795e3..ff5826d 100644
--- a/camera/camera-camera2/src/main/AndroidManifest.xml
+++ b/camera/camera-camera2/src/main/AndroidManifest.xml
@@ -22,7 +22,7 @@
android:name="androidx.camera.core.impl.MetadataHolderService"
android:exported="false"
android:enabled="false"
- tools:ignore="Instantiatable"
+ tools:ignore="Instantiatable,MissingServiceExportedEqualsTrue"
tools:node="merge">
<meta-data
android:name=
diff --git a/camera/camera-core/src/main/AndroidManifest.xml b/camera/camera-core/src/main/AndroidManifest.xml
index d41ce9b..1656645 100644
--- a/camera/camera-core/src/main/AndroidManifest.xml
+++ b/camera/camera-core/src/main/AndroidManifest.xml
@@ -24,6 +24,6 @@
android:name="androidx.camera.core.impl.MetadataHolderService"
android:enabled="false"
android:exported="false"
- tools:ignore="Instantiatable" />
+ tools:ignore="Instantiatable,MissingServiceExportedEqualsTrue" />
</application>
</manifest>
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
index 00a7812..3c3ac31 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
@@ -152,6 +152,7 @@
}
@Nullable
+ @SuppressWarnings("deprecation")
private static CameraXConfig.Provider getConfigProvider(@NonNull Context context) {
CameraXConfig.Provider configProvider = null;
Application application = ContextUtil.getApplicationFromContext(context);
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
index c9d826a..509c110 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
@@ -183,7 +183,7 @@
@Test
fun correctAvailability_whenExtensionIsNotAvailable() {
// Skips the test if extensions availability is disabled by quirk.
- assumeFalse(ExtensionsTestUtil.extensionsDisabledByQuirk())
+ assumeFalse(ExtensionsTestUtil.extensionsDisabledByQuirk(lensFacing, extensionMode))
extensionsManager = ExtensionsManager.getInstanceAsync(
context,
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java
index e93b482..d171589 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java
@@ -209,13 +209,17 @@
/**
* Returns whether extensions is disabled by quirk.
*/
- public static boolean extensionsDisabledByQuirk() {
+ public static boolean extensionsDisabledByQuirk(@CameraSelector.LensFacing int lensFacing,
+ @ExtensionMode.Mode int extensionMode) {
+
boolean isAdvancedExtenderSupported = false;
if (ExtensionVersion.getRuntimeVersion().compareTo(Version.VERSION_1_2) >= 0) {
isAdvancedExtenderSupported = ExtensionVersion.isAdvancedExtenderSupported();
}
- return new ExtensionDisabledValidator().shouldDisableExtension(isAdvancedExtenderSupported);
+ return new ExtensionDisabledValidator().shouldDisableExtension(
+ CameraUtil.getCameraIdWithLensFacing(lensFacing), extensionMode,
+ isAdvancedExtenderSupported);
}
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
index 918b6d5..4204cc3 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
@@ -44,7 +44,6 @@
import androidx.camera.extensions.internal.ExtensionsUseCaseConfigFactory;
import androidx.camera.extensions.internal.VendorExtender;
import androidx.camera.extensions.internal.Version;
-import androidx.camera.extensions.internal.compat.workaround.ExtensionDisabledValidator;
import java.util.Collections;
import java.util.List;
@@ -64,8 +63,6 @@
final class ExtensionsInfo {
private static final String EXTENDED_CAMERA_CONFIG_PROVIDER_ID_PREFIX = ":camera:camera"
+ "-extensions-";
- private static final ExtensionDisabledValidator sExtensionDisabledValidator =
- new ExtensionDisabledValidator();
private final CameraProvider mCameraProvider;
ExtensionsInfo(@NonNull CameraProvider cameraProvider) {
@@ -240,11 +237,6 @@
return new DisabledVendorExtender();
}
- // Force disable extension for some devices by quirk.
- if (sExtensionDisabledValidator.shouldDisableExtension(isAdvancedExtenderSupported)) {
- return new DisabledVendorExtender();
- }
-
VendorExtender vendorExtender;
if (isAdvancedExtenderSupported) {
vendorExtender = new AdvancedVendorExtender(mode);
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java
index 08a60e7..831af2d 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java
@@ -38,6 +38,7 @@
import androidx.camera.extensions.impl.advanced.BokehAdvancedExtenderImpl;
import androidx.camera.extensions.impl.advanced.HdrAdvancedExtenderImpl;
import androidx.camera.extensions.impl.advanced.NightAdvancedExtenderImpl;
+import androidx.camera.extensions.internal.compat.workaround.ExtensionDisabledValidator;
import androidx.camera.extensions.internal.sessionprocessor.AdvancedSessionProcessor;
import androidx.core.util.Preconditions;
@@ -51,10 +52,14 @@
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class AdvancedVendorExtender implements VendorExtender {
+ private final ExtensionDisabledValidator mExtensionDisabledValidator =
+ new ExtensionDisabledValidator();
private final AdvancedExtenderImpl mAdvancedExtenderImpl;
private String mCameraId;
+ private final @ExtensionMode.Mode int mMode;
public AdvancedVendorExtender(@ExtensionMode.Mode int mode) {
+ mMode = mode;
try {
switch (mode) {
case ExtensionMode.BOKEH:
@@ -95,6 +100,11 @@
@Override
public boolean isExtensionAvailable(@NonNull String cameraId,
@NonNull Map<String, CameraCharacteristics> characteristicsMap) {
+
+ if (mExtensionDisabledValidator.shouldDisableExtension(cameraId, mMode, true)) {
+ return false;
+ }
+
return mAdvancedExtenderImpl.isExtensionAvailable(cameraId, characteristicsMap);
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
index d42b2c5..ca27b15 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
@@ -49,6 +49,7 @@
import androidx.camera.extensions.impl.NightPreviewExtenderImpl;
import androidx.camera.extensions.impl.PreviewExtenderImpl;
import androidx.camera.extensions.impl.ProcessorImpl;
+import androidx.camera.extensions.internal.compat.workaround.ExtensionDisabledValidator;
import androidx.core.util.Preconditions;
import java.util.Arrays;
@@ -61,6 +62,8 @@
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class BasicVendorExtender implements VendorExtender {
private static final String TAG = "BasicVendorExtender";
+ private final ExtensionDisabledValidator mExtensionDisabledValidator =
+ new ExtensionDisabledValidator();
private final @ExtensionMode.Mode int mMode;
private PreviewExtenderImpl mPreviewExtenderImpl;
private ImageCaptureExtenderImpl mImageCaptureExtenderImpl;
@@ -122,6 +125,11 @@
@Override
public boolean isExtensionAvailable(@NonNull String cameraId,
@NonNull Map<String, CameraCharacteristics> characteristicsMap) {
+
+ if (mExtensionDisabledValidator.shouldDisableExtension(cameraId, mMode, false)) {
+ return false;
+ }
+
CameraCharacteristics cameraCharacteristics = characteristicsMap.get(cameraId);
return mPreviewExtenderImpl.isExtensionAvailable(cameraId, cameraCharacteristics)
&& mImageCaptureExtenderImpl.isExtensionAvailable(cameraId, cameraCharacteristics);
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java
index 7447218..19a8e0f 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java
@@ -18,30 +18,51 @@
import android.os.Build;
+import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.core.impl.Quirk;
+import androidx.camera.extensions.ExtensionMode;
+
/**
* Quirk required to disable extension for some devices.
*
- * <p>An example is Pixel 5 which the availability check result of the basic extension interface
- * face should be false, but it actually return true. Therefore, a default VendorExtender will
- * be used to return false availability check result. See b/199408131.
+ * <p>An example is that Pixel 5's availability check result of the basic extension
+ * interface should be false, but it actually returns true. Therefore, force disable Basic
+ * Extender capability on the device. See b/199408131.
+ *
+ * <p>Another example is that Motorola razr 5G's availability check results of both back
+ * and front camera are true, but it will cause the black preview screen issue. Therefore, force
+ * disable the bokeh mode on the device. See b/214130117.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class ExtensionDisabledQuirk implements Quirk {
static boolean load() {
- return isPixel5();
+ return isPixel5() || isMotoRazr5G();
}
/**
* Checks whether extension should be disabled.
*/
- public boolean shouldDisableExtension(boolean isAdvancedExtenderSupported) {
- return !isAdvancedExtenderSupported && isPixel5();
+ public boolean shouldDisableExtension(@NonNull String cameraId,
+ @ExtensionMode.Mode int extensionMode, boolean isAdvancedInterface) {
+ if (isPixel5() && !isAdvancedInterface) {
+ // 1. Disables Pixel 5's Basic Extender capability.
+ return true;
+ } else if (isMotoRazr5G() && ("0".equals(cameraId) || "1".equals(cameraId)) && (
+ ExtensionMode.BOKEH == extensionMode)) {
+ // 2. Disables Motorola Razr 5G's bokeh capability.
+ return true;
+ }
+
+ return false;
}
private static boolean isPixel5() {
return "google".equalsIgnoreCase(Build.BRAND) && "redfin".equalsIgnoreCase(Build.DEVICE);
}
+
+ private static boolean isMotoRazr5G() {
+ return "motorola".equalsIgnoreCase(Build.BRAND) && "smith".equalsIgnoreCase(Build.DEVICE);
+ }
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidator.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidator.java
index d920317..ff7403e 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidator.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidator.java
@@ -16,16 +16,19 @@
package androidx.camera.extensions.internal.compat.workaround;
+import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.camera.extensions.ExtensionMode;
import androidx.camera.extensions.internal.compat.quirk.DeviceQuirks;
import androidx.camera.extensions.internal.compat.quirk.ExtensionDisabledQuirk;
/**
- * Validates whether extension should be disabled.
+ * Validates whether the specified extension mode should be disabled for the specified camera on
+ * the device.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class ExtensionDisabledValidator {
- private ExtensionDisabledQuirk mQuirk;
+ private final ExtensionDisabledQuirk mQuirk;
/**
* Constructs an instance of {@link ExtensionDisabledValidator}.
@@ -35,9 +38,11 @@
}
/**
- * Checks whether extension should be disabled.
+ * Checks whether extension should be disabled.
*/
- public boolean shouldDisableExtension(boolean isAdvancedExtenderSupported) {
- return mQuirk == null ? false : mQuirk.shouldDisableExtension(isAdvancedExtenderSupported);
+ public boolean shouldDisableExtension(@NonNull String cameraId,
+ @ExtensionMode.Mode int extensionMode, boolean isAdvancedInterface) {
+ return mQuirk != null && mQuirk.shouldDisableExtension(cameraId, extensionMode,
+ isAdvancedInterface);
}
}
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirks.java b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirks.java
new file mode 100644
index 0000000..8d61c48
--- /dev/null
+++ b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/quirk/DeviceQuirks.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.compat.quirk;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.List;
+
+/**
+ * Tests version of main/.../DeviceQuirks.java, which provides device specific quirks, used for
+ * device specific workarounds.
+ * <p>
+ * In main/.../DeviceQuirks, Device quirks are loaded the first time a device workaround is
+ * encountered, and remain in memory until the process is killed. When running tests, this means
+ * that the same device quirks are used for all the tests. This causes an issue when tests modify
+ * device properties (using Robolectric for instance). Instead of force-reloading the device
+ * quirks in every test that uses a device workaround, this class internally reloads the quirks
+ * every time a device workaround is needed.
+ */
+public class DeviceQuirks {
+
+ private DeviceQuirks() {
+ }
+
+ /**
+ * Retrieves a specific device {@link Quirk} instance given its type.
+ *
+ * @param quirkClass The type of device quirk to retrieve.
+ * @return A device {@link Quirk} instance of the provided type, or {@code null} if it isn't
+ * found.
+ */
+ @SuppressWarnings("unchecked")
+ @Nullable
+ public static <T extends Quirk> T get(@NonNull final Class<T> quirkClass) {
+ final List<Quirk> quirks = DeviceQuirksLoader.loadQuirks();
+ for (final Quirk quirk : quirks) {
+ if (quirk.getClass() == quirkClass) {
+ return (T) quirk;
+ }
+ }
+ return null;
+ }
+}
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidatorTest.kt b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidatorTest.kt
index 4203a6b..2f4ddb0 100644
--- a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidatorTest.kt
+++ b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidatorTest.kt
@@ -17,6 +17,7 @@
package androidx.camera.extensions.internal.compat.workaround
import android.os.Build
+import androidx.camera.extensions.ExtensionMode
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -29,24 +30,29 @@
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class ExtensionDisabledValidatorTest(private val config: TestConfig) {
- @Test
- fun shouldUseDefaultVendorExtender() {
- // Set up device properties
- if (config.brand != null) {
- ReflectionHelpers.setStaticField(Build::class.java, "BRAND", config.brand)
- ReflectionHelpers.setStaticField(Build::class.java, "DEVICE", config.device)
- }
- val validator =
- ExtensionDisabledValidator()
- assertThat(validator.shouldDisableExtension(config.isAdvancedExtenderSupported))
- .isEqualTo(config.shouldDisableExtension)
+ @Test
+ fun shouldDisableExtensionMode() {
+ // Set up device properties
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", config.brand)
+ ReflectionHelpers.setStaticField(Build::class.java, "DEVICE", config.device)
+
+ val validator = ExtensionDisabledValidator()
+ assertThat(
+ validator.shouldDisableExtension(
+ config.cameraId,
+ config.extensionMode,
+ config.isAdvancedInterface
+ )
+ ).isEqualTo(config.shouldDisableExtension)
}
class TestConfig(
- val brand: String?,
- val device: String?,
- val isAdvancedExtenderSupported: Boolean,
+ val brand: String,
+ val device: String,
+ val cameraId: String,
+ val extensionMode: Int,
+ val isAdvancedInterface: Boolean,
val shouldDisableExtension: Boolean
)
@@ -55,10 +61,33 @@
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
fun createTestSet(): List<TestConfig> {
return listOf(
- TestConfig("Google", "Redfin", false, true),
- TestConfig("Google", "Redfin", true, false),
- TestConfig("", "", false, false),
- TestConfig("", "", true, false)
+ // Pixel 5 extension capability is disabled on basic extender
+ TestConfig("Google", "Redfin", "0", ExtensionMode.BOKEH, false, true),
+ TestConfig("Google", "Redfin", "0", ExtensionMode.HDR, false, true),
+ TestConfig("Google", "Redfin", "0", ExtensionMode.NIGHT, false, true),
+ TestConfig("Google", "Redfin", "0", ExtensionMode.FACE_RETOUCH, false, true),
+ TestConfig("Google", "Redfin", "0", ExtensionMode.AUTO, false, true),
+ TestConfig("Google", "Redfin", "1", ExtensionMode.BOKEH, false, true),
+ TestConfig("Google", "Redfin", "1", ExtensionMode.HDR, false, true),
+ TestConfig("Google", "Redfin", "1", ExtensionMode.NIGHT, false, true),
+ TestConfig("Google", "Redfin", "1", ExtensionMode.FACE_RETOUCH, false, true),
+ TestConfig("Google", "Redfin", "1", ExtensionMode.AUTO, false, true),
+
+ // Pixel 5 extension capability is not disabled on advanced extender
+ TestConfig("Google", "Redfin", "0", ExtensionMode.NIGHT, true, false),
+ TestConfig("Google", "Redfin", "1", ExtensionMode.NIGHT, true, false),
+
+ // Motorola Razr 5G bokeh mode is disabled. Other extension modes should still work.
+ TestConfig("Motorola", "Smith", "0", ExtensionMode.BOKEH, false, true),
+ TestConfig("Motorola", "Smith", "0", ExtensionMode.HDR, false, false),
+ TestConfig("Motorola", "Smith", "1", ExtensionMode.BOKEH, false, true),
+ TestConfig("Motorola", "Smith", "1", ExtensionMode.HDR, false, false),
+ TestConfig("Motorola", "Smith", "2", ExtensionMode.BOKEH, false, false),
+ TestConfig("Motorola", "Smith", "2", ExtensionMode.HDR, false, false),
+
+ // Other cases should be kept normal.
+ TestConfig("", "", "0", ExtensionMode.BOKEH, false, false),
+ TestConfig("", "", "1", ExtensionMode.BOKEH, false, false)
)
}
}
diff --git a/camera/camera-previewview/src/main/java/androidx/camera/previewview/internal/quirk/QuirkSummary.java b/camera/camera-previewview/src/main/java/androidx/camera/previewview/internal/quirk/QuirkSummary.java
new file mode 100644
index 0000000..3779367
--- /dev/null
+++ b/camera/camera-previewview/src/main/java/androidx/camera/previewview/internal/quirk/QuirkSummary.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.previewview.internal.quirk;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+@Documented
+@Retention(CLASS)
+public @interface QuirkSummary {
+}
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
index de8cebe..d49133e 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
@@ -466,6 +466,7 @@
}
/** Tries to acquire all the necessary permissions through a dialog. */
+ @SuppressWarnings("deprecation")
private String[] getRequiredPermissions() {
PackageInfo info;
try {
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestUtils.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestUtils.kt
index 72e3e10..bc16441 100644
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestUtils.kt
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestUtils.kt
@@ -610,7 +610,8 @@
/**
* Return the version name of the Activity
*/
+@Suppress("DEPRECATION")
fun getVersionName(activity: MainActivity): String {
val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0)
return packageInfo.versionName
-}
\ No newline at end of file
+}
diff --git a/car/app/app-automotive/src/main/AndroidManifest.xml b/car/app/app-automotive/src/main/AndroidManifest.xml
index 43b025b..6f5812b 100644
--- a/car/app/app-automotive/src/main/AndroidManifest.xml
+++ b/car/app/app-automotive/src/main/AndroidManifest.xml
@@ -32,7 +32,7 @@
android:exported="false"
android:enabled="false"
android:process=""
- tools:ignore="Instantiatable"
+ tools:ignore="Instantiatable,MissingServiceExportedEqualsTrue"
tools:node="merge">
<meta-data
android:name=
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
index e53a8b8..913ae84 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
@@ -419,11 +419,14 @@
@Override
protected void onDestroy() {
requireNonNull(mHostUpdateReceiver).unregister(this);
+ requireNonNull(mSurfaceHolderListener).setSurfaceListener(null);
+ requireNonNull(mViewModel).unbind();
requireNonNull(mViewModel).setActivity(null);
super.onDestroy();
}
@Nullable
+ @SuppressWarnings("deprecation")
private ComponentName retrieveServiceComponentName() {
Intent intent = new Intent(SERVICE_INTERFACE);
intent.setPackage(getPackageName());
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModel.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModel.java
index 5b3c684..28cce46 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModel.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModel.java
@@ -132,6 +132,7 @@
void unbind() {
mServiceConnectionManager.unbind();
mIInsetsListener = null;
+ mIRendererCallback = null;
}
@Override
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceConnectionManager.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceConnectionManager.java
index 4eaa1d2..8d91a334 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceConnectionManager.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceConnectionManager.java
@@ -185,6 +185,7 @@
* Initializes the renderer service with given properties if already bound to the renderer
* service.
*/
+ @SuppressWarnings("deprecation")
void bind(@NonNull Intent intent, @NonNull ICarAppActivity iCarAppActivity, int displayId) {
mIntent = requireNonNull(intent);
mICarAppActivity = requireNonNull(iCarAppActivity);
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/ui/ErrorMessageView.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/ui/ErrorMessageView.java
index 7c6e9e7..331537b 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/ui/ErrorMessageView.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/ui/ErrorMessageView.java
@@ -135,6 +135,7 @@
throw new IllegalArgumentException("Unknown action type: " + mErrorType.getActionType());
}
+ @SuppressWarnings("deprecation")
private boolean isVendingPackageInstalled() {
try {
requireActivity().getPackageManager().getPackageInfo(VENDING_PACKAGE, 0);
@@ -145,6 +146,7 @@
return true;
}
+ @SuppressWarnings("deprecation")
private Intent getVendingIntent() {
Intent rendererIntent = new Intent(ACTION_RENDER);
List<ResolveInfo> resolveInfoList =
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/ui/LoadingView.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/ui/LoadingView.java
index a41b574..9467648 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/ui/LoadingView.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/ui/LoadingView.java
@@ -66,6 +66,7 @@
mAppIcon.setImageDrawable(getActivityIcon());
}
+ @SuppressWarnings("deprecation")
private Drawable getActivityIcon() {
PackageManager packageManager = getContext().getPackageManager();
diff --git a/car/app/app-projected/src/main/AndroidManifest.xml b/car/app/app-projected/src/main/AndroidManifest.xml
index 186a4fd..82385a2 100644
--- a/car/app/app-projected/src/main/AndroidManifest.xml
+++ b/car/app/app-projected/src/main/AndroidManifest.xml
@@ -24,7 +24,7 @@
android:exported="false"
android:enabled="false"
android:process=""
- tools:ignore="Instantiatable"
+ tools:ignore="Instantiatable,MissingServiceExportedEqualsTrue"
tools:node="merge">
<meta-data
android:name=
diff --git a/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarHardwareHostDispatcherTest.java b/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarHardwareHostDispatcherTest.java
index 041b16b..006edc6 100644
--- a/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarHardwareHostDispatcherTest.java
+++ b/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarHardwareHostDispatcherTest.java
@@ -121,7 +121,7 @@
@Test
public void dispatchUnsubscribeCarHardwareResult() throws RemoteException, BundlerException {
int desiredResultType = ICarHardwareResultTypes.TYPE_SENSOR_ACCELEROMETER;
- Bundleable bundle = Bundleable.create(new Integer(10));
+ Bundleable bundle = Bundleable.create(10);
mCarHardwareHostDispatcher.dispatchUnsubscribeCarHardwareResult(desiredResultType, bundle);
verify(mMockCarHardwareHost).unsubscribeCarHardwareResult(eq(desiredResultType),
eq(bundle));
diff --git a/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubMapTest.java b/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubMapTest.java
index 4d0faa7..04e9656 100644
--- a/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubMapTest.java
+++ b/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubMapTest.java
@@ -92,7 +92,7 @@
Integer desiredResult = 5;
Bundleable desiredBundleable = Bundleable.create(desiredResult);
int desiredResultType = ICarHardwareResultTypes.TYPE_SENSOR_ACCELEROMETER;
- Integer unsupportedResult = new Integer(-1);
+ Integer unsupportedResult = -1;
String param = "param";
Bundleable paramBundle = Bundleable.create(param);
diff --git a/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubTest.java b/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubTest.java
index e70fba4..7215449 100644
--- a/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubTest.java
+++ b/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubTest.java
@@ -77,7 +77,7 @@
Integer desiredResult = 5;
Bundleable desiredBundleable = Bundleable.create(desiredResult);
int desiredResultType = ICarHardwareResultTypes.TYPE_INFO_MODEL;
- Integer unsupportedResult = new Integer(-1);
+ Integer unsupportedResult = -1;
String param = "param";
Bundleable paramBundle = Bundleable.create(param);
@@ -98,7 +98,7 @@
public void addListener_callHost_unsupported_singleShot() throws BundlerException,
RemoteException {
int desiredResultType = ICarHardwareResultTypes.TYPE_INFO_MODEL;
- Integer unsupportedResult = new Integer(-1);
+ Integer unsupportedResult = -1;
String param = "param";
Bundleable paramBundle = Bundleable.create(param);
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/RequestPermissionScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/RequestPermissionScreen.java
index ad6e272..7f9e70d 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/RequestPermissionScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/RequestPermissionScreen.java
@@ -79,6 +79,7 @@
@NonNull
@Override
+ @SuppressWarnings("deprecation")
public Template onGetTemplate() {
final Action headerAction = mPreSeedMode ? Action.APP_ICON : Action.BACK;
List<String> permissions = new ArrayList<>();
diff --git a/car/app/app-testing/src/test/java/androidx/car/app/testing/TestScreenManagerTest.java b/car/app/app-testing/src/test/java/androidx/car/app/testing/TestScreenManagerTest.java
index 97c6352..30c9974 100644
--- a/car/app/app-testing/src/test/java/androidx/car/app/testing/TestScreenManagerTest.java
+++ b/car/app/app-testing/src/test/java/androidx/car/app/testing/TestScreenManagerTest.java
@@ -22,10 +22,10 @@
import androidx.car.app.Screen;
import androidx.car.app.ScreenManager;
import androidx.car.app.model.Template;
+import androidx.lifecycle.Lifecycle;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@@ -40,6 +40,10 @@
@Before
public void setup() {
mCarContext = TestCarContext.createCarContext(ApplicationProvider.getApplicationContext());
+ // Set the app's lifecycle to STARTED so that screens would be set to the STARTED state when
+ // pushed. Otherwise we may run into issues in tests when popping screens, where they go
+ // from the INITIALIZED state to the DESTROYED state.
+ mCarContext.getLifecycleOwner().getRegistry().setCurrentState(Lifecycle.State.STARTED);
}
@Test
@@ -65,7 +69,6 @@
.containsExactly(screen1, screen2);
}
- @Ignore("b/218342974")
@Test
public void pop_getScreensRemoved() {
Screen screen1 = new TestScreen();
@@ -83,7 +86,6 @@
.containsExactly(screen4);
}
- @Ignore("b/218342974")
@Test
public void remove_getScreensRemoved() {
Screen screen1 = new TestScreen();
@@ -101,7 +103,6 @@
.containsExactly(screen2);
}
- @Ignore("b/218342974")
@Test
public void popTo_getScreensRemoved() {
Screen screen1 = new TestScreen();
@@ -122,7 +123,6 @@
.containsExactly(screen4, screen3);
}
- @Ignore("b/218342974")
@Test
public void popToRoot_getScreensRemoved() {
Screen screen1 = new TestScreen();
diff --git a/car/app/app/src/main/java/androidx/car/app/AppInfo.java b/car/app/app/src/main/java/androidx/car/app/AppInfo.java
index 1db3946..d74fc85 100644
--- a/car/app/app/src/main/java/androidx/car/app/AppInfo.java
+++ b/car/app/app/src/main/java/androidx/car/app/AppInfo.java
@@ -129,6 +129,7 @@
@RestrictTo(Scope.LIBRARY)
@VisibleForTesting
@CarAppApiLevel
+ @SuppressWarnings("deprecation")
public static int retrieveMinCarAppApiLevel(@NonNull Context context) {
try {
ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppMetadataHolderService.java b/car/app/app/src/main/java/androidx/car/app/CarAppMetadataHolderService.java
index f85125b..799e4c7 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppMetadataHolderService.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppMetadataHolderService.java
@@ -53,7 +53,7 @@
* Returns the {@link ServiceInfo} for the declared {@link CarAppMetadataHolderService}.
*/
@NonNull
- @SuppressWarnings("deprecation") // GET_DISABLED_COMPONENTS
+ @SuppressWarnings("deprecation") // GET_DISABLED_COMPONENTS, getServiceInfo
public static ServiceInfo getServiceInfo(@NonNull Context context) throws
PackageManager.NameNotFoundException {
int flags = PackageManager.GET_META_DATA;
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppPermission.java b/car/app/app/src/main/java/androidx/car/app/CarAppPermission.java
index 9349bb1..22913a2 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppPermission.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppPermission.java
@@ -104,6 +104,7 @@
*
* @throws SecurityException if the app does not have the required permission declared
*/
+ @SuppressWarnings("deprecation")
public static void checkHasLibraryPermission(
@NonNull Context context, @NonNull @LibraryPermission String permission) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppPermissionActivity.java b/car/app/app/src/main/java/androidx/car/app/CarAppPermissionActivity.java
index 664f64a..e759e55 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppPermissionActivity.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppPermissionActivity.java
@@ -55,6 +55,7 @@
processInternal(getIntent());
}
+ @SuppressWarnings("deprecation")
private void maybeSetCustomBackground() {
@StyleRes int themeId = Resources.ID_NULL;
ApplicationInfo applicationInfo;
diff --git a/car/app/app/src/main/java/androidx/car/app/notification/CarNotificationManager.java b/car/app/app/src/main/java/androidx/car/app/notification/CarNotificationManager.java
index e94496c..2a9c643 100644
--- a/car/app/app/src/main/java/androidx/car/app/notification/CarNotificationManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/notification/CarNotificationManager.java
@@ -434,6 +434,7 @@
}
@StyleRes
+ @SuppressWarnings("deprecation")
private static int loadThemeId(Context context) {
int theme = Resources.ID_NULL;
ApplicationInfo applicationInfo;
diff --git a/car/app/app/src/main/java/androidx/car/app/notification/CarPendingIntent.java b/car/app/app/src/main/java/androidx/car/app/notification/CarPendingIntent.java
index f321557..0c98298 100644
--- a/car/app/app/src/main/java/androidx/car/app/notification/CarPendingIntent.java
+++ b/car/app/app/src/main/java/androidx/car/app/notification/CarPendingIntent.java
@@ -119,6 +119,7 @@
* @see CarContext#startCarApp(Intent)
*/
@VisibleForTesting
+ @SuppressWarnings("deprecation")
static void validateIntent(Context context, Intent intent) {
String packageName = context.getPackageName();
String action = intent.getAction();
diff --git a/car/app/app/src/main/java/androidx/car/app/validation/HostValidator.java b/car/app/app/src/main/java/androidx/car/app/validation/HostValidator.java
index 2d52162..bb8fcd2 100644
--- a/car/app/app/src/main/java/androidx/car/app/validation/HostValidator.java
+++ b/car/app/app/src/main/java/androidx/car/app/validation/HostValidator.java
@@ -336,6 +336,7 @@
@DoNotInline
@NonNull
+ @SuppressWarnings("deprecation")
static PackageInfo getPackageInfo(@NonNull PackageManager packageManager,
@NonNull String packageName) throws PackageManager.NameNotFoundException {
return packageManager.getPackageInfo(packageName,
diff --git a/car/app/app/src/test/java/androidx/car/app/AppInfoTest.java b/car/app/app/src/test/java/androidx/car/app/AppInfoTest.java
index da37284..a7f9235 100644
--- a/car/app/app/src/test/java/androidx/car/app/AppInfoTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/AppInfoTest.java
@@ -48,6 +48,7 @@
private final ApplicationInfo mApplicationInfo = new ApplicationInfo();
@Before
+ @SuppressWarnings("deprecation")
public void setUp() throws PackageManager.NameNotFoundException {
MockitoAnnotations.initMocks(this);
diff --git a/car/app/app/src/test/java/androidx/car/app/CarContextTest.java b/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
index 6a74ac5..1883180 100644
--- a/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
@@ -53,7 +53,6 @@
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -129,6 +128,10 @@
ApplicationProvider.getApplicationContext(),
ApplicationProvider.getApplicationContext().getResources().getConfiguration());
mCarContext.setCarHost(mMockCarHost);
+ // Set the app's lifecycle to STARTED so that screens would be set to the STARTED state when
+ // pushed. Otherwise we may run into issues in tests when popping screens, where they go
+ // from the INITIALIZED state to the DESTROYED state.
+ mLifecycleOwner.mRegistry.setCurrentState(State.STARTED);
mScreen1 = new TestScreen(mCarContext, mMockScreen1);
mScreen2 = new TestScreen(mCarContext, mMockScreen2);
@@ -363,7 +366,6 @@
}
}
- @Ignore("b/218342974")
@Test
public void getOnBackPressedDispatcher_noListeners_popsAScreen() {
mCarContext.getCarService(ScreenManager.class).push(mScreen1);
@@ -382,9 +384,10 @@
OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
when(callback.isEnabled()).thenReturn(true);
- mLifecycleOwner.mRegistry.setCurrentState(State.STARTED);
- mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
+ TestLifecycleOwner callbackLifecycle = new TestLifecycleOwner();
+ callbackLifecycle.mRegistry.setCurrentState(State.STARTED);
+ mCarContext.getOnBackPressedDispatcher().addCallback(callbackLifecycle, callback);
mCarContext.getOnBackPressedDispatcher().onBackPressed();
verify(callback).handleOnBackPressed();
@@ -392,7 +395,6 @@
verify(mMockScreen2, never()).dispatchLifecycleEvent(Event.ON_DESTROY);
}
- @Ignore("b/218342974")
@Test
public void getOnBackPressedDispatcher_withAListenerThatIsNotStarted_popsAScreen() {
mCarContext.getCarService(ScreenManager.class).push(mScreen1);
@@ -400,9 +402,10 @@
OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
when(callback.isEnabled()).thenReturn(true);
- mLifecycleOwner.mRegistry.setCurrentState(State.CREATED);
- mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
+ TestLifecycleOwner callbackLifecycle = new TestLifecycleOwner();
+ callbackLifecycle.mRegistry.setCurrentState(State.CREATED);
+ mCarContext.getOnBackPressedDispatcher().addCallback(callbackLifecycle, callback);
mCarContext.getOnBackPressedDispatcher().onBackPressed();
verify(callback, never()).handleOnBackPressed();
@@ -410,7 +413,6 @@
verify(mMockScreen2).dispatchLifecycleEvent(Event.ON_DESTROY);
}
- @Ignore("b/218342974")
@Test
public void getOnBackPressedDispatcher_callsDefaultListenerWheneverTheAddedOneIsNotSTARTED() {
mCarContext.getCarService(ScreenManager.class).push(mScreen1);
@@ -418,15 +420,16 @@
OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
when(callback.isEnabled()).thenReturn(true);
- mLifecycleOwner.mRegistry.setCurrentState(State.CREATED);
- mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
+ TestLifecycleOwner callbackLifecycle = new TestLifecycleOwner();
+ callbackLifecycle.mRegistry.setCurrentState(State.CREATED);
+ mCarContext.getOnBackPressedDispatcher().addCallback(callbackLifecycle, callback);
mCarContext.getOnBackPressedDispatcher().onBackPressed();
verify(callback, never()).handleOnBackPressed();
verify(mMockScreen2).dispatchLifecycleEvent(Event.ON_DESTROY);
- mLifecycleOwner.mRegistry.setCurrentState(State.STARTED);
+ callbackLifecycle.mRegistry.setCurrentState(State.STARTED);
mCarContext.getOnBackPressedDispatcher().onBackPressed();
verify(callback).handleOnBackPressed();
diff --git a/car/app/app/src/test/java/androidx/car/app/validation/HostValidatorTestApi28.java b/car/app/app/src/test/java/androidx/car/app/validation/HostValidatorTestApi28.java
index b56b1e1..cdff5ce 100644
--- a/car/app/app/src/test/java/androidx/car/app/validation/HostValidatorTestApi28.java
+++ b/car/app/app/src/test/java/androidx/car/app/validation/HostValidatorTestApi28.java
@@ -100,6 +100,7 @@
assertThat(hostValidator.isValidHost(hostInfo)).isTrue();
}
+ @SuppressWarnings("deprecation")
private void installPackage(String packageName, Signature[] signatures) {
PackageInfo packageInfo = new PackageInfo();
packageInfo.applicationInfo = new ApplicationInfo();
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
index 2633800..42d5f19 100644
--- a/compose/foundation/foundation/api/current.ignore
+++ b/compose/foundation/foundation/api/current.ignore
@@ -5,6 +5,8 @@
Removed class androidx.compose.foundation.lazy.layout.LazyLayoutStateKt
RemovedClass: androidx.compose.foundation.lazy.list.IntervalListKt:
Removed class androidx.compose.foundation.lazy.list.IntervalListKt
+RemovedClass: androidx.compose.foundation.relocation.BringRectangleOnScreen_androidKt:
+ Removed class androidx.compose.foundation.relocation.BringRectangleOnScreen_androidKt
RemovedClass: androidx.compose.foundation.text.TextFieldMagnifierKt:
Removed class androidx.compose.foundation.text.TextFieldMagnifierKt
RemovedClass: androidx.compose.foundation.text.TextFieldMagnifier_androidKt:
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index f5a4468..defd30f 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -548,13 +548,16 @@
package androidx.compose.foundation.relocation {
+ public final class BringIntoViewKt {
+ }
+
public final class BringIntoViewRequesterKt {
}
public final class BringIntoViewResponderKt {
}
- public final class BringRectangleOnScreen_androidKt {
+ public final class BringIntoViewResponder_androidKt {
}
}
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 27cb31c..a5ad4a7 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -690,6 +690,9 @@
package androidx.compose.foundation.relocation {
+ public final class BringIntoViewKt {
+ }
+
@androidx.compose.foundation.ExperimentalFoundationApi public sealed interface BringIntoViewRequester {
method public suspend Object? bringIntoView(optional androidx.compose.ui.geometry.Rect? rect, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}
@@ -700,22 +703,15 @@
}
@androidx.compose.foundation.ExperimentalFoundationApi public interface BringIntoViewResponder {
- method public suspend Object? bringIntoView(androidx.compose.ui.geometry.Rect rect, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public androidx.compose.ui.geometry.Rect toLocalRect(androidx.compose.ui.geometry.Rect rect, androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates);
- field public static final androidx.compose.foundation.relocation.BringIntoViewResponder.Companion Companion;
- }
-
- @androidx.compose.foundation.ExperimentalFoundationApi public static final class BringIntoViewResponder.Companion {
- method public androidx.compose.ui.modifier.ProvidableModifierLocal<androidx.compose.foundation.relocation.BringIntoViewResponder> getModifierLocalBringIntoViewResponder();
- method public androidx.compose.foundation.relocation.BringIntoViewResponder getRootBringIntoViewResponder();
- property public final androidx.compose.ui.modifier.ProvidableModifierLocal<androidx.compose.foundation.relocation.BringIntoViewResponder> ModifierLocalBringIntoViewResponder;
- property public final androidx.compose.foundation.relocation.BringIntoViewResponder RootBringIntoViewResponder;
+ method @androidx.compose.foundation.ExperimentalFoundationApi public suspend Object? bringChildIntoView(androidx.compose.ui.geometry.Rect localRect, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.geometry.Rect calculateRectForParent(androidx.compose.ui.geometry.Rect localRect);
}
public final class BringIntoViewResponderKt {
+ method @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier bringIntoViewResponder(androidx.compose.ui.Modifier, androidx.compose.foundation.relocation.BringIntoViewResponder responder);
}
- public final class BringRectangleOnScreen_androidKt {
+ public final class BringIntoViewResponder_androidKt {
}
}
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
index 2633800..42d5f19 100644
--- a/compose/foundation/foundation/api/restricted_current.ignore
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -5,6 +5,8 @@
Removed class androidx.compose.foundation.lazy.layout.LazyLayoutStateKt
RemovedClass: androidx.compose.foundation.lazy.list.IntervalListKt:
Removed class androidx.compose.foundation.lazy.list.IntervalListKt
+RemovedClass: androidx.compose.foundation.relocation.BringRectangleOnScreen_androidKt:
+ Removed class androidx.compose.foundation.relocation.BringRectangleOnScreen_androidKt
RemovedClass: androidx.compose.foundation.text.TextFieldMagnifierKt:
Removed class androidx.compose.foundation.text.TextFieldMagnifierKt
RemovedClass: androidx.compose.foundation.text.TextFieldMagnifier_androidKt:
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index f5a4468..defd30f 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -548,13 +548,16 @@
package androidx.compose.foundation.relocation {
+ public final class BringIntoViewKt {
+ }
+
public final class BringIntoViewRequesterKt {
}
public final class BringIntoViewResponderKt {
}
- public final class BringRectangleOnScreen_androidKt {
+ public final class BringIntoViewResponder_androidKt {
}
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
index a21353b..cb41b94 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
@@ -18,8 +18,12 @@
import androidx.compose.foundation.demos.relocation.BringIntoViewAndroidInteropDemo
import androidx.compose.foundation.demos.relocation.BringIntoViewDemo
+import androidx.compose.foundation.demos.relocation.BringIntoViewResponderDemo
+import androidx.compose.foundation.demos.relocation.BringNestedIntoViewDemo
import androidx.compose.foundation.demos.relocation.BringRectangleIntoViewDemo
import androidx.compose.foundation.demos.relocation.RequestRectangleOnScreenDemo
+import androidx.compose.foundation.samples.BringIntoViewResponderSample
+import androidx.compose.foundation.samples.BringPartOfComposableIntoViewSample
import androidx.compose.foundation.samples.ControlledScrollableRowSample
import androidx.compose.foundation.samples.InteractionSourceFlowSample
import androidx.compose.foundation.samples.SimpleInteractionSourceSample
@@ -29,9 +33,13 @@
private val RelocationDemos = listOf(
ComposableDemo("Bring Into View") { BringIntoViewDemo() },
+ /** This gives [BringPartOfComposableIntoViewSample] some explanation text. */
ComposableDemo("Bring Rectangle Into View") { BringRectangleIntoViewDemo() },
+ /** This gives [BringIntoViewResponderSample] some explanation text. */
+ ComposableDemo("Custom responder") { BringIntoViewResponderDemo() },
ComposableDemo("Request Rectangle On Screen") { RequestRectangleOnScreenDemo() },
- ComposableDemo("Android view interop") { BringIntoViewAndroidInteropDemo() }
+ ComposableDemo("Android view interop") { BringIntoViewAndroidInteropDemo() },
+ ComposableDemo("Nested scrollables") { BringNestedIntoViewDemo() },
)
val FoundationDemos = DemoCategory(
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/BringNestedIntoViewDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/BringNestedIntoViewDemo.kt
new file mode 100644
index 0000000..cb26929
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/BringNestedIntoViewDemo.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("SameParameterValue")
+
+package androidx.compose.foundation.demos.relocation
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.relocation.BringIntoViewRequester
+import androidx.compose.foundation.relocation.bringIntoViewRequester
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.IconButton
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun BringNestedIntoViewDemo() {
+ Column {
+ val rows = 3
+ val columns = 3
+ val bringIntoViewRequesters = remember { List(rows * columns) { BringIntoViewRequester() } }
+
+ Text(
+ "This is a $rows x $columns grid of circles. The entire grid is vertically " +
+ "scrollable, and each row in the grid is horizontally scrollable. Click the " +
+ "buttons in the smaller grid below to bring the corresponding circle into view."
+ )
+
+ ScrollableGrid(rows, columns, bringIntoViewRequesters)
+ ControlGrid(rows, columns, bringIntoViewRequesters)
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun ScrollableGrid(rows: Int, columns: Int, requesters: List<BringIntoViewRequester>) {
+ Column(
+ Modifier
+ .border(3.dp, Color.Blue)
+ .size(200.dp, 250.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ repeat(rows) { row ->
+ Row(
+ Modifier
+ // Inner scrollable rows.
+ .border(3.dp, Color.Green)
+ .fillMaxWidth()
+ .height(200.dp)
+ .horizontalScroll(rememberScrollState())
+ .width(600.dp),
+ horizontalArrangement = Arrangement.SpaceAround,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // Circles
+ repeat(columns) { column ->
+ val index = row * columns + column
+ TextCircle(
+ index.toString(),
+ Modifier
+ .size(75.dp)
+ .bringIntoViewRequester(requesters[index])
+ )
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun ControlGrid(rows: Int, columns: Int, requesters: List<BringIntoViewRequester>) {
+ val coroutineScope = rememberCoroutineScope()
+ Column {
+ repeat(rows) { row ->
+ Row {
+ repeat(columns) { column ->
+ val requester = requesters[row * columns + column]
+ IconButton(
+ onClick = {
+ coroutineScope.launch {
+ requester.bringIntoView()
+ }
+ }
+ ) {
+ val index = row * columns + column
+ TextCircle(
+ index.toString(),
+ Modifier.size(50.dp)
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun TextCircle(text: String, modifier: Modifier = Modifier) {
+ Box(
+ modifier
+ .aspectRatio(1f)
+ .background(Color.Red, shape = CircleShape)
+ ) {
+ Text(
+ text = text,
+ color = Color.White,
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/BringRectangleIntoViewDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/BringRectangleIntoViewDemo.kt
deleted file mode 100644
index 423969e..0000000
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/BringRectangleIntoViewDemo.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.demos.relocation
-
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.border
-import androidx.compose.foundation.horizontalScroll
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.relocation.BringIntoViewRequester
-import androidx.compose.foundation.relocation.bringIntoViewRequester
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.material.Button
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Color.Companion.Red
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.launch
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun BringRectangleIntoViewDemo() {
- with(LocalDensity.current) {
- val bringIntoViewRequester = remember { BringIntoViewRequester() }
- val coroutineScope = rememberCoroutineScope()
- Column {
- Text(
- "This is a scrollable Box. Drag to scroll the Circle into view or click the " +
- "button to bring the circle into view."
- )
- Box(
- Modifier
- .border(2.dp, Color.Black)
- .size(500f.toDp())
- .horizontalScroll(rememberScrollState())
- ) {
- Canvas(
- Modifier
- .size(1500f.toDp(), 500f.toDp())
- .bringIntoViewRequester(bringIntoViewRequester)
- ) {
- drawCircle(color = Red, radius = 250f, center = Offset(750f, 250f))
- }
- }
- Button(
- onClick = {
- val circleCoordinates = Rect(500f, 0f, 1000f, 500f)
- coroutineScope.launch {
- bringIntoViewRequester.bringIntoView(circleCoordinates)
- }
- }
- ) {
- Text("Bring circle into View")
- }
- }
- }
-}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/BringRectangleIntoViewSampleWrappers.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/BringRectangleIntoViewSampleWrappers.kt
new file mode 100644
index 0000000..d825bd4
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/BringRectangleIntoViewSampleWrappers.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos.relocation
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.samples.BringIntoViewResponderSample
+import androidx.compose.foundation.samples.BringPartOfComposableIntoViewSample
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+
+@Composable
+fun BringRectangleIntoViewDemo() {
+ Column {
+ Text(
+ "This is a scrollable Box. Drag to scroll the Circle into view or click the " +
+ "button to bring the circle into view."
+ )
+ BringPartOfComposableIntoViewSample()
+ }
+}
+
+@Composable
+fun BringIntoViewResponderDemo() {
+ Column {
+ Text(
+ "Each cell in this box is focusable, use the arrow keys/tab/dpad to move focus " +
+ "around. The container will always put the last-requested rectangle in the top-" +
+ "left of itself."
+ )
+ BringIntoViewResponderSample()
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/RequestRectangleOnScreeenDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/RequestRectangleOnScreenDemo.kt
similarity index 100%
rename from compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/RequestRectangleOnScreeenDemo.kt
rename to compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/RequestRectangleOnScreenDemo.kt
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BringIntoViewSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BringIntoViewSamples.kt
index 49f37aa..7416de3 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BringIntoViewSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BringIntoViewSamples.kt
@@ -20,27 +20,41 @@
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.relocation.BringIntoViewRequester
+import androidx.compose.foundation.relocation.BringIntoViewResponder
import androidx.compose.foundation.relocation.bringIntoViewRequester
+import androidx.compose.foundation.relocation.bringIntoViewResponder
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@@ -110,3 +124,68 @@
}
}
}
+
+@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
+@Sampled
+@Composable
+fun BringIntoViewResponderSample() {
+ var offset: IntOffset by remember { mutableStateOf(IntOffset.Zero) }
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .layout { measurable, constraints ->
+ // Allow the content to be as big as it wants.
+ val placeable = measurable.measure(
+ constraints.copy(
+ maxWidth = Constraints.Infinity,
+ maxHeight = Constraints.Infinity
+ )
+ )
+
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ // Place the last-requested rectangle at the top-left of the box.
+ placeable.place(offset)
+ }
+ }
+ .bringIntoViewResponder(remember {
+ object : BringIntoViewResponder {
+ override fun calculateRectForParent(localRect: Rect): Rect {
+ // Ask our parent to bring our top-left corner into view, since that's where
+ // we're always going to position the requested content.
+ return Rect(Offset.Zero, localRect.size)
+ }
+
+ override suspend fun bringChildIntoView(localRect: Rect) {
+ // Offset the content right and down by the offset of the requested area so
+ // that it will always be aligned to the top-left of the box.
+ offset = -localRect.topLeft.round()
+ }
+ }
+ })
+ ) {
+ LargeContentWithFocusableChildren()
+ }
+}
+
+@Composable
+private fun LargeContentWithFocusableChildren() {
+ Column {
+ repeat(10) { row ->
+ Row {
+ repeat(10) { column ->
+ val interactionSource = remember { MutableInteractionSource() }
+ val isFocused by interactionSource.collectIsFocusedAsState()
+ Text(
+ "$row x $column",
+ Modifier
+ .focusable(interactionSource = interactionSource)
+ .then(
+ if (isFocused) Modifier.border(1.dp, Color.Blue) else Modifier
+ )
+ .padding(8.dp)
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyDslSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyDslSamples.kt
index c069d7e..7205812 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyDslSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyDslSamples.kt
@@ -23,19 +23,25 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.flow.collect
@Sampled
@Composable
@@ -116,3 +122,45 @@
}
}
}
+
+@Sampled
+@Composable
+fun UsingListScrollPositionForSideEffectSample() {
+ val listState = rememberLazyListState()
+ LaunchedEffect(listState) {
+ snapshotFlow { listState.firstVisibleItemIndex }
+ .collect {
+ // use the new index
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun UsingListScrollPositionInCompositionSample() {
+ val listState = rememberLazyListState()
+ val isAtTop by remember {
+ derivedStateOf {
+ listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0
+ }
+ }
+ if (!isAtTop) {
+ ScrollToTopButton(listState)
+ }
+}
+
+@Sampled
+@Composable
+fun UsingListLayoutInfoForSideEffectSample() {
+ val listState = rememberLazyListState()
+ LaunchedEffect(listState) {
+ snapshotFlow { listState.layoutInfo.totalItemsCount }
+ .collect {
+ // use the new items count
+ }
+ }
+}
+
+@Composable
+private fun ScrollToTopButton(@Suppress("UNUSED_PARAMETER") listState: LazyListState) {
+}
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyGridSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyGridSamples.kt
index 139feee..4b3da5e 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyGridSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyGridSamples.kt
@@ -24,14 +24,22 @@
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.GridItemSpan
+import androidx.compose.foundation.lazy.LazyGridState
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyGridState
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.flow.collect
@OptIn(ExperimentalFoundationApi::class)
@Sampled
@@ -91,3 +99,49 @@
}
}
}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Sampled
+@Composable
+fun UsingGridScrollPositionForSideEffectSample() {
+ val gridState = rememberLazyGridState()
+ LaunchedEffect(gridState) {
+ snapshotFlow { gridState.firstVisibleItemIndex }
+ .collect {
+ // use the new index
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Sampled
+@Composable
+fun UsingGridScrollPositionInCompositionSample() {
+ val gridState = rememberLazyGridState()
+ val isAtTop by remember {
+ derivedStateOf {
+ gridState.firstVisibleItemIndex == 0 && gridState.firstVisibleItemScrollOffset == 0
+ }
+ }
+ if (!isAtTop) {
+ ScrollToTopButton(gridState)
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Sampled
+@Composable
+fun UsingGridLayoutInfoForSideEffectSample() {
+ val gridState = rememberLazyGridState()
+ LaunchedEffect(gridState) {
+ snapshotFlow { gridState.layoutInfo.totalItemsCount }
+ .collect {
+ // use the new items count
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun ScrollToTopButton(@Suppress("UNUSED_PARAMETER") gridState: LazyGridState) {
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
index 17e6f3b..4eab756 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
@@ -73,6 +73,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
+import com.google.common.collect.Range
+import com.google.common.truth.IntegerSubject
import com.google.common.truth.Truth
import kotlinx.coroutines.runBlocking
import org.junit.Rule
@@ -985,3 +987,7 @@
.assertPixels { Color.Green }
}
}
+
+internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
+ isIn(Range.closed(expected - tolerance, expected + tolerance))
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
index 6e61140..2ac7d84 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.AutoTestFrameClock
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
@@ -67,12 +68,14 @@
private var itemSize: Dp = Dp.Infinity
private var smallPaddingSize: Dp = Dp.Infinity
+ private var itemSizePx = 50f
+ private var smallPaddingSizePx = 12f
@Before
fun before() {
with(rule.density) {
- itemSize = 50.toDp()
- smallPaddingSize = 12.toDp()
+ itemSize = itemSizePx.toDp()
+ smallPaddingSize = smallPaddingSizePx.toDp()
}
}
@@ -416,6 +419,52 @@
}
@Test
+ fun column_overscrollWithContentPadding() {
+ lateinit var state: LazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize + smallPaddingSize * 2)) {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(
+ vertical = smallPaddingSize
+ )
+ ) {
+ items(2) {
+ Box(Modifier.testTag("$it").height(itemSize))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(smallPaddingSize)
+ .assertHeightIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(smallPaddingSize + itemSize)
+ .assertHeightIsEqualTo(itemSize)
+
+ rule.runOnIdle {
+ runBlocking {
+ // itemSizePx is the maximum offset, plus if we overscroll the content padding
+ // the layout mechanism will decide the item 0 is not needed until we start
+ // filling the over scrolled gap.
+ state.scrollBy(value = itemSizePx + smallPaddingSizePx * 1.5f)
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(smallPaddingSize)
+ .assertHeightIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(smallPaddingSize - itemSize)
+ .assertHeightIsEqualTo(itemSize)
+ }
+
+ @Test
fun totalPaddingLargerParentSize_initialState() {
lateinit var state: LazyGridState
rule.setContent {
@@ -1089,6 +1138,51 @@
// rule.onNodeWithTag("0").assertIsNotDisplayed()
// }
+ // @Test
+ // fun row_overscrollWithContentPadding() {
+ // lateinit var state: LazyListState
+ // rule.setContent {
+ // state = rememberLazyListState()
+ // Box(modifier = Modifier.testTag(ContainerTag).size(itemSize + smallPaddingSize * 2)) {
+ // LazyRow(
+ // state = state,
+ // contentPadding = PaddingValues(
+ // horizontal = smallPaddingSize
+ // )
+ // ) {
+ // items(2) {
+ // Box(Modifier.testTag("$it").fillParentMaxSize())
+ // }
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(smallPaddingSize)
+ // .assertWidthIsEqualTo(itemSize)
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(smallPaddingSize + itemSize)
+ // .assertWidthIsEqualTo(itemSize)
+
+ // rule.runOnIdle {
+ // runBlocking {
+ // // itemSizePx is the maximum offset, plus if we overscroll the content padding
+ // // the layout mechanism will decide the item 0 is not needed until we start
+ // // filling the over scrolled gap.
+ // state.scrollBy(value = itemSizePx + smallPaddingSizePx * 1.5f)
+ // }
+ // }
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(smallPaddingSize)
+ // .assertWidthIsEqualTo(itemSize)
+
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(smallPaddingSize - itemSize)
+ // .assertWidthIsEqualTo(itemSize)
+ // }
+
private fun LazyGridState.scrollBy(offset: Dp) {
runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
index 6a697ff..3e855ad 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.lazy.grid
+import androidx.compose.animation.core.FloatSpringSpec
import androidx.compose.foundation.AutoTestFrameClock
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.animateScrollBy
@@ -27,19 +28,23 @@
import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyGridState
import androidx.compose.foundation.lazy.LazyVerticalGrid
-import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyGridState
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.Dp
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.Before
import org.junit.Rule
import org.junit.Test
-import kotlin.math.roundToInt
@MediumTest
// @RunWith(Parameterized::class)
@@ -51,26 +56,37 @@
private val vertical: Boolean
get() = true // orientation == Orientation.Vertical
- private val items = (1..20).toList()
+ private val itemsCount = 40
private lateinit var state: LazyGridState
+ private val itemSizePx = 100
+ private var itemSizeDp = Dp.Unspecified
+ private var containerSizeDp = Dp.Unspecified
+
+ lateinit var scope: CoroutineScope
+
@Before
fun setup() {
+ with(rule.density) {
+ itemSizeDp = itemSizePx.toDp()
+ containerSizeDp = itemSizeDp * 3
+ }
rule.setContent {
state = rememberLazyGridState()
+ scope = rememberCoroutineScope()
TestContent()
}
}
@Test
- fun testSetupWorks() {
+ fun setupWorks() {
assertThat(state.firstVisibleItemIndex).isEqualTo(0)
assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
assertThat(state.firstVisibleItemIndex).isEqualTo(0)
}
@Test
- fun snapToItemTest() = runBlocking {
+ fun scrollToItem() = runBlocking {
withContext(Dispatchers.Main + AutoTestFrameClock()) {
state.scrollToItem(2)
}
@@ -85,14 +101,57 @@
}
@Test
- fun smoothScrollByTest() = runBlocking {
- fun Int.dpToPx(): Int = with(rule.density) { dp.toPx().roundToInt() }
- val scrollDistance = 320.dpToPx()
- val itemSize = 101.dpToPx()
+ fun scrollToItemWithOffset() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(6, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(6)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ }
- val expectedLine = scrollDistance / itemSize // resolves to 3
+ @Test
+ fun scrollToItemWithNegativeOffset() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(6, -10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(4)
+ val item6Offset = state.layoutInfo.visibleItemsInfo.first { it.index == 6 }.offset.y
+ assertThat(item6Offset).isEqualTo(10)
+ }
+
+ @Test
+ fun scrollToItemWithPositiveOffsetLargerThanAvailableSize() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(itemsCount - 6, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not 10
+ }
+
+ @Test
+ fun scrollToItemWithNegativeOffsetLargerThanAvailableSize() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(1, -(itemSizePx + 10))
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not -10
+ }
+
+ @Test
+ fun scrollToItemWithIndexLargerThanItemsCount() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(itemsCount + 4)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
+ }
+
+ @Test
+ fun animateScrollBy() = runBlocking {
+ val scrollDistance = 320
+
+ val expectedLine = scrollDistance / itemSizePx // resolves to 3
val expectedItem = expectedLine * 2 // resolves to 6
- val expectedOffset = scrollDistance % itemSize // resolves to ~17.dp.toIntPx()
+ val expectedOffset = scrollDistance % itemSizePx // resolves to 20px
withContext(Dispatchers.Main + AutoTestFrameClock()) {
state.animateScrollBy(scrollDistance.toFloat())
@@ -102,25 +161,155 @@
}
@Test
- fun smoothScrollToItemTest() = runBlocking {
+ fun animateScrollToItem() = runBlocking {
withContext(Dispatchers.Main + AutoTestFrameClock()) {
state.animateScrollToItem(10, 10)
}
assertThat(state.firstVisibleItemIndex).isEqualTo(10)
assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ }
+
+ @Test
+ fun animateScrollToItemWithOffset() = runBlocking {
withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.animateScrollToItem(0, 10)
- state.animateScrollToItem(11, 10)
+ state.animateScrollToItem(6, 10)
}
- // assertThat(state.firstVisibleItemIndex).isEqualTo(10)
- // assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(6)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ }
+
+ @Test
+ fun animateScrollToItemWithNegativeOffset() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(6, -10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(4)
+ val item6Offset = state.layoutInfo.visibleItemsInfo.first { it.index == 6 }.offset.y
+ assertThat(item6Offset).isEqualTo(10)
+ }
+
+ @Test
+ fun animateScrollToItemWithPositiveOffsetLargerThanAvailableSize() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(itemsCount - 6, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not 10
+ }
+
+ @Test
+ fun animateScrollToItemWithNegativeOffsetLargerThanAvailableSize() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(2, -(itemSizePx + 10))
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not -10
+ }
+
+ @Test
+ fun animateScrollToItemWithIndexLargerThanItemsCount() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(itemsCount + 2)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
+ }
+
+ @Test
+ fun animatePerFrameForwardToVisibleItem() {
+ assertSpringAnimation(toIndex = 4)
+ }
+
+ @Test
+ fun animatePerFrameForwardToVisibleItemWithOffset() {
+ assertSpringAnimation(toIndex = 4, toOffset = 35)
+ }
+
+ @Test
+ fun animatePerFrameForwardToNotVisibleItem() {
+ assertSpringAnimation(toIndex = 16)
+ }
+
+ @Test
+ fun animatePerFrameForwardToNotVisibleItemWithOffset() {
+ assertSpringAnimation(toIndex = 20, toOffset = 35)
+ }
+
+ @Test
+ fun animatePerFrameBackward() {
+ assertSpringAnimation(toIndex = 2, fromIndex = 12)
+ }
+
+ @Test
+ fun animatePerFrameBackwardWithOffset() {
+ assertSpringAnimation(toIndex = 2, fromIndex = 10, fromOffset = 58)
+ }
+
+ @Test
+ fun animatePerFrameBackwardWithInitialOffset() {
+ assertSpringAnimation(toIndex = 0, toOffset = 40, fromIndex = 8)
+ }
+
+ private fun assertSpringAnimation(
+ toIndex: Int,
+ toOffset: Int = 0,
+ fromIndex: Int = 0,
+ fromOffset: Int = 0
+ ) {
+ if (fromIndex != 0 || fromOffset != 0) {
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(fromIndex, fromOffset)
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ assertThat(state.firstVisibleItemIndex).isEqualTo(fromIndex)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(fromOffset)
+
+ rule.mainClock.autoAdvance = false
+
+ scope.launch {
+ state.animateScrollToItem(toIndex, toOffset)
+ }
+
+ while (!state.isScrollInProgress) {
+ Thread.sleep(5)
+ }
+
+ val startOffset = (fromIndex / 2 * itemSizePx + fromOffset).toFloat()
+ val endOffset = (toIndex / 2 * itemSizePx + toOffset).toFloat()
+ val spec = FloatSpringSpec()
+
+ val duration =
+ TimeUnit.NANOSECONDS.toMillis(spec.getDurationNanos(startOffset, endOffset, 0f))
+ rule.mainClock.advanceTimeByFrame()
+ var expectedTime = rule.mainClock.currentTime
+ for (i in 0..duration step FrameDuration) {
+ val nanosTime = TimeUnit.MILLISECONDS.toNanos(i)
+ val expectedValue =
+ spec.getValueFromNanos(nanosTime, startOffset, endOffset, 0f)
+ val actualValue =
+ (state.firstVisibleItemIndex / 2 * itemSizePx + state.firstVisibleItemScrollOffset)
+ assertWithMessage(
+ "On animation frame at $i index=${state.firstVisibleItemIndex} " +
+ "offset=${state.firstVisibleItemScrollOffset} expectedValue=$expectedValue"
+ ).that(actualValue).isEqualTo(expectedValue.roundToInt(), tolerance = 1)
+
+ rule.mainClock.advanceTimeBy(FrameDuration)
+ expectedTime += FrameDuration
+ assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
+ rule.waitForIdle()
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(toIndex)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(toOffset)
}
@Composable
private fun TestContent() {
if (vertical) {
- LazyVerticalGrid(GridCells.Fixed(2), Modifier.height(300.dp), state) {
- items(items) {
+ LazyVerticalGrid(GridCells.Fixed(2), Modifier.height(containerSizeDp), state) {
+ items(itemsCount) {
ItemContent()
}
}
@@ -136,9 +325,9 @@
@Composable
private fun ItemContent() {
val modifier = if (vertical) {
- Modifier.height(101.dp)
+ Modifier.height(itemSizeDp)
} else {
- Modifier.width(101.dp)
+ Modifier.width(itemSizeDp)
}
Spacer(modifier)
}
@@ -149,3 +338,5 @@
// fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
// }
}
+
+private val FrameDuration = 16L
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
index bf3cf61..766053b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
@@ -32,7 +32,6 @@
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
@@ -142,11 +141,10 @@
@Test
fun animateScrollBy() = runBlocking {
- fun Int.dpToPx(): Int = with(rule.density) { dp.toPx().roundToInt() }
- val scrollDistance = 320.dpToPx()
+ val scrollDistance = 320
val expectedIndex = scrollDistance / itemSizePx // resolves to 3
- val expectedOffset = scrollDistance % itemSizePx // resolves to ~17.dp.toIntPx()
+ val expectedOffset = scrollDistance % itemSizePx // resolves to 20px
withContext(Dispatchers.Main + AutoTestFrameClock()) {
state.animateScrollBy(scrollDistance.toFloat())
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
index becdc7b..5d682d7 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
@@ -24,19 +24,12 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.relocation.BringIntoViewResponder.Companion.ModifierLocalBringIntoViewResponder
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.modifier.modifierLocalProvider
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.IntOffset
@@ -103,9 +96,9 @@
@Test
fun bringIntoView_callsViewRequestRectangleOnScreen_whenResponderPresent() {
val requesterOffset = IntOffset(1, 2)
- val scrollOffset = 3
+ val scrollOffset = Offset(3f, 4f)
val rectangleToRequest = Rect(Offset(10f, 20f), Size(30f, 40f))
- val expectedRectangle = AndroidRect(11, 38, 41, 78)
+ val expectedRectangle = AndroidRect(14, 26, 44, 66)
lateinit var scope: CoroutineScope
lateinit var parent: FakeScrollable
val bringIntoViewRequester = BringIntoViewRequester()
@@ -119,7 +112,7 @@
Box(
Modifier
.size(10.dp)
- .verticalScroll(rememberScrollState(scrollOffset))
+ .fakeScrollable(scrollOffset) {}
) {
Box(
Modifier
@@ -148,7 +141,7 @@
}
}
- @Ignore("b/216652644")
+ @Ignore("This use case can't be supported until BringIntoView is in ui: b/216652644")
@Test
fun bringIntoView_propagatesThroughIntermediateView() {
val requesterOffset = IntOffset(1, 2)
@@ -160,7 +153,9 @@
rule.setContent {
scope = rememberCoroutineScope()
AndroidView(
- modifier = Modifier.testBringIntoViewResponder { requests += it },
+ modifier = Modifier
+ // This offset needs to be non-zero or it won't see the request at all.
+ .fakeScrollable { requests += it },
factory = { context ->
val parent = FakeScrollable(context)
val child = ComposeView(context)
@@ -189,26 +184,6 @@
}
}
- private fun Modifier.testBringIntoViewResponder(onBringIntoView: (Rect) -> Unit): Modifier =
- composed {
- val screenRequester = remember { BringRectangleOnScreenRequester() }
- val responder = remember {
- object : BringIntoViewResponder {
- override suspend fun bringIntoView(rect: Rect) {
- onBringIntoView(rect)
- }
-
- override fun toLocalRect(
- rect: Rect,
- layoutCoordinates: LayoutCoordinates
- ): Rect = rect
- }
- }
-
- bringRectangleOnScreenRequester(screenRequester)
- .modifierLocalProvider(ModifierLocalBringIntoViewResponder) { responder }
- }
-
/** A view that records calls to [requestChildRectangleOnScreen] for testing. */
private class FakeScrollable(context: Context) : FrameLayout(context) {
val requests = mutableListOf<RectangleRequest>()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
index 59fd60c..814c13e 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
@@ -17,37 +17,30 @@
package androidx.compose.foundation.relocation
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.TestActivity
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.relocation.BringIntoViewResponder.Companion.ModifierLocalBringIntoViewResponder
-import androidx.compose.foundation.relocation.LayoutCoordinateSubject.Companion.assertThat
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.boundsInParent
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.layout.positionInParent
-import androidx.compose.ui.modifier.modifierLocalProvider
-import androidx.compose.foundation.TestActivity
-import androidx.compose.foundation.relocation.ScrollableResponder.Companion.LocalRect
import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.google.common.truth.Fact.simpleFact
-import com.google.common.truth.FailureMetadata
-import com.google.common.truth.Subject
-import com.google.common.truth.Subject.Factory
-import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
-import org.junit.Before
+import kotlinx.coroutines.test.TestCoroutineScope
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class BringIntoViewResponderTest {
@@ -55,28 +48,18 @@
@get:Rule
val rule = createAndroidComposeRule<TestActivity>()
- lateinit var density: Density
+ fun Float.toDp(): Dp = with(rule.density) { [email protected]() }
- @Before
- fun setup() {
- density = Density(rule.activity)
- }
-
- fun Float.toDp(): Dp = with(density) { [email protected]() }
-
- @OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
@Test
fun zeroSizedItem_zeroSizedParent_bringIntoView() {
// Arrange.
- lateinit var layoutCoordinates: LayoutCoordinates
val bringIntoViewRequester = BringIntoViewRequester()
- val scrollableParent = ScrollableResponder()
+ lateinit var requestedRect: Rect
rule.setContent {
Box(
Modifier
- .modifierLocalProvider(ModifierLocalBringIntoViewResponder) { scrollableParent }
+ .fakeScrollable { requestedRect = it }
.bringIntoViewRequester(bringIntoViewRequester)
- .onGloballyPositioned { layoutCoordinates = it }
)
}
@@ -84,27 +67,71 @@
runBlocking { bringIntoViewRequester.bringIntoView() }
// Assert.
- assertThat(scrollableParent.callersLayoutCoordinates).isEqualTo(layoutCoordinates)
- assertThat(scrollableParent.nonLocalRect).isEqualTo(Rect.Zero)
- assertThat(scrollableParent.requestedRect).isEqualTo(LocalRect)
+ rule.runOnIdle {
+ assertThat(requestedRect).isEqualTo(Rect.Zero)
+ }
}
- @OptIn(ExperimentalComposeUiApi::class,
- androidx.compose.foundation.ExperimentalFoundationApi::class
- )
@Test
- fun bringIntoView_itemWithSize() {
+ fun bringIntoView_rectInChild() {
// Arrange.
- lateinit var layoutCoordinates: LayoutCoordinates
val bringIntoViewRequester = BringIntoViewRequester()
- val scrollableParent = ScrollableResponder()
+ lateinit var requestedRect: Rect
rule.setContent {
Box(
Modifier
- .size(20f.toDp(), 10f.toDp())
- .modifierLocalProvider(ModifierLocalBringIntoViewResponder) { scrollableParent }
+ .fakeScrollable { requestedRect = it }
.bringIntoViewRequester(bringIntoViewRequester)
- .onGloballyPositioned { layoutCoordinates = it }
+ )
+ }
+
+ // Act.
+ runBlocking { bringIntoViewRequester.bringIntoView(Rect(1f, 2f, 3f, 4f)) }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(requestedRect).isEqualTo(Rect(1f, 2f, 3f, 4f))
+ }
+ }
+
+ @Test
+ fun bringIntoView_childWithSize() {
+ // Arrange.
+ val bringIntoViewRequester = BringIntoViewRequester()
+ lateinit var requestedRect: Rect
+ rule.setContent {
+ Box(Modifier) {
+ Box(
+ Modifier
+ .fakeScrollable { requestedRect = it }
+ .size(20f.toDp(), 10f.toDp())
+ .offset { IntOffset(40, 30) }
+ .bringIntoViewRequester(bringIntoViewRequester)
+ )
+ }
+ }
+
+ // Act.
+ runBlocking { bringIntoViewRequester.bringIntoView() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(requestedRect).isEqualTo(Rect(40f, 30f, 60f, 40f))
+ }
+ }
+
+ @Test
+ fun bringIntoView_childBiggerThanParent() {
+ // Arrange.
+ val bringIntoViewRequester = BringIntoViewRequester()
+ lateinit var requestedRect: Rect
+ rule.setContent {
+ Box(
+ Modifier
+ .size(1f.toDp())
+ .fakeScrollable { requestedRect = it }
+ .size(20f.toDp(), 10f.toDp())
+ .bringIntoViewRequester(bringIntoViewRequester)
)
}
@@ -112,68 +139,192 @@
runBlocking { bringIntoViewRequester.bringIntoView() }
// Assert.
+ rule.runOnIdle {
+ assertThat(requestedRect).isEqualTo(Rect(0f, 0f, 20f, 10f))
+ }
+ }
+
+ @Test
+ fun bringIntoView_propagatesToMultipleResponders() {
+ // Arrange.
+ lateinit var outerRequest: Rect
+ lateinit var innerRequest: Rect
+ val bringIntoViewRequester = BringIntoViewRequester()
+ rule.setContent {
+ Box(
+ Modifier
+ .fakeScrollable { outerRequest = it }
+ .offset(2f.toDp(), 1f.toDp())
+ .fakeScrollable { innerRequest = it }
+ .size(20f.toDp(), 10f.toDp())
+ .bringIntoViewRequester(bringIntoViewRequester)
+ )
+ }
+
+ // Act.
+ runBlocking { bringIntoViewRequester.bringIntoView() }
+
// Assert.
- assertThat(scrollableParent.callersLayoutCoordinates).isEqualTo(layoutCoordinates)
- assertThat(scrollableParent.nonLocalRect).isEqualTo(Rect(0f, 0f, 20f, 10f))
- assertThat(scrollableParent.requestedRect).isEqualTo(LocalRect)
+ rule.runOnIdle {
+ assertThat(innerRequest).isEqualTo(Rect(0f, 0f, 20f, 10f))
+ assertThat(outerRequest).isEqualTo(Rect(2f, 1f, 22f, 11f))
+ }
}
-}
-private class LayoutCoordinateSubject(metadata: FailureMetadata?, val actual: LayoutCoordinates?) :
- Subject(metadata, actual) {
-
- fun isEqualTo(expected: LayoutCoordinates?) {
- if (expected == null) {
- assertThat(actual).isNull()
- return
- }
- if (actual == null) {
- failWithActual(simpleFact("Expected non-null layout coordinates."))
- return
- }
- if (expected.size != actual.size) {
- failWithoutActual(
- simpleFact("Expected size be ${expected.size}"),
- simpleFact("But was ${actual.size}")
+ @Test
+ fun bringIntoView_onlyPropagatesUp() {
+ // Arrange.
+ lateinit var parentRequest: Rect
+ var childRequest: Rect? = null
+ val bringIntoViewRequester = BringIntoViewRequester()
+ rule.setContent {
+ Box(
+ Modifier
+ .fakeScrollable { parentRequest = it }
+ .bringIntoViewRequester(bringIntoViewRequester)
+ .fakeScrollable { childRequest = it }
)
}
- if (expected.positionInParent() != actual.positionInParent()) {
- failWithoutActual(
- simpleFact("Expected bounds in parent to be ${expected.boundsInParent()}"),
- simpleFact("But was ${actual.boundsInParent()}")
+
+ // Act.
+ runBlocking { bringIntoViewRequester.bringIntoView() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parentRequest).isEqualTo(Rect.Zero)
+ assertThat(childRequest).isNull()
+ }
+ }
+
+ @Test
+ fun bringIntoView_propagatesUp_whenRectForParentReturnsInput() {
+ // Arrange.
+ lateinit var parentRequest: Rect
+ var childRequest: Rect? = null
+ val bringIntoViewRequester = BringIntoViewRequester()
+ rule.setContent {
+ Box(
+ Modifier
+ .fakeScrollable { parentRequest = it }
+ .fakeScrollable { childRequest = it }
+ .bringIntoViewRequester(bringIntoViewRequester)
)
}
+
+ // Act.
+ runBlocking { bringIntoViewRequester.bringIntoView() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(parentRequest).isEqualTo(Rect.Zero)
+ assertThat(childRequest).isEqualTo(Rect.Zero)
+ }
}
- companion object {
- fun assertThat(layoutCoordinates: LayoutCoordinates?): LayoutCoordinateSubject {
- return Truth.assertAbout(subjectFactory).that(layoutCoordinates)
+ @Test
+ fun bringIntoView_translatesByCalculateRectForParent() {
+ // Arrange.
+ lateinit var requestedRect: Rect
+ val bringIntoViewRequester = BringIntoViewRequester()
+ rule.setContent {
+ Box(
+ Modifier
+ .fakeScrollable { requestedRect = it }
+ .fakeScrollable(Offset(2f, 3f)) {}
+ .bringIntoViewRequester(bringIntoViewRequester)
+ )
}
- private val subjectFactory =
- Factory<LayoutCoordinateSubject, LayoutCoordinates?> { metadata, actual ->
- LayoutCoordinateSubject(metadata, actual)
- }
- }
-}
+ // Act.
+ runBlocking { bringIntoViewRequester.bringIntoView() }
-@OptIn(ExperimentalFoundationApi::class)
-private class ScrollableResponder : BringIntoViewResponder {
- lateinit var requestedRect: Rect
- lateinit var nonLocalRect: Rect
- lateinit var callersLayoutCoordinates: LayoutCoordinates
-
- override suspend fun bringIntoView(rect: Rect) {
- requestedRect = rect
+ // Assert.
+ rule.runOnIdle {
+ assertThat(requestedRect).isEqualTo(Rect(2f, 3f, 2f, 3f))
+ }
}
- override fun toLocalRect(rect: Rect, layoutCoordinates: LayoutCoordinates): Rect {
- nonLocalRect = rect
- callersLayoutCoordinates = layoutCoordinates
- return LocalRect
+ @Test
+ fun bringChildIntoView_isCalled_whenRectForParentDoesNotReturnInput() {
+ // Arrange.
+ var requestedRect: Rect? = null
+ val bringIntoViewRequester = BringIntoViewRequester()
+ rule.setContent {
+ Box(
+ Modifier
+ .fakeScrollable(Offset.Zero) { requestedRect = it }
+ .bringIntoViewRequester(bringIntoViewRequester)
+ )
+ }
+
+ // Act.
+ runBlocking { bringIntoViewRequester.bringIntoView() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(requestedRect).isEqualTo(Rect.Zero)
+ }
}
- companion object {
- val LocalRect = Rect(10f, 10f, 10f, 10f)
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun bringChildIntoView_calledConcurrentlyOnAllResponders() {
+ // Arrange.
+ var childStarted = false
+ var parentStarted = false
+ var childFinished = false
+ var parentFinished = false
+ val bringIntoViewRequester = BringIntoViewRequester()
+ rule.setContent {
+ Box(
+ Modifier
+ .fakeScrollable {
+ parentStarted = true
+ try {
+ awaitCancellation()
+ } finally {
+ parentFinished = true
+ }
+ }
+ .fakeScrollable {
+ childStarted = true
+ try {
+ awaitCancellation()
+ } finally {
+ childFinished = true
+ }
+ }
+ .bringIntoViewRequester(bringIntoViewRequester)
+ )
+ }
+ val testScope = TestCoroutineScope().apply {
+ pauseDispatcher()
+ }
+ val requestJob = testScope.launch {
+ bringIntoViewRequester.bringIntoView()
+ }
+ rule.waitForIdle()
+
+ assertThat(childStarted).isFalse()
+ assertThat(parentStarted).isFalse()
+ assertThat(childFinished).isFalse()
+ assertThat(parentFinished).isFalse()
+
+ // Act.
+ testScope.advanceUntilIdle()
+
+ // Assert.
+ assertThat(childStarted).isTrue()
+ assertThat(parentStarted).isTrue()
+ assertThat(childFinished).isFalse()
+ assertThat(parentFinished).isFalse()
+
+ // Act.
+ requestJob.cancel()
+ testScope.advanceUntilIdle()
+
+ // Assert.
+ assertThat(childFinished).isTrue()
+ assertThat(parentFinished).isTrue()
}
-}
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/FakeScrollable.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/FakeScrollable.kt
new file mode 100644
index 0000000..131236c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/FakeScrollable.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.relocation
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+
+/**
+ * Returns a [bringIntoViewResponder] modifier that implements [BringIntoViewResponder] by
+ * offsetting the local rect by [parentOffset] and handling the request by calling
+ * [onBringIntoView]. Note that [onBringIntoView] will not be called if [parentOffset] is zero,
+ * since that means the scrollable doesn't actually need to scroll anything to satisfy the
+ * request.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal fun Modifier.fakeScrollable(
+ parentOffset: Offset = Offset.Zero,
+ onBringIntoView: suspend (Rect) -> Unit
+): Modifier = bringIntoViewResponder(
+ object : BringIntoViewResponder {
+ override fun calculateRectForParent(localRect: Rect): Rect =
+ localRect.translate(parentOffset)
+
+ override suspend fun bringChildIntoView(localRect: Rect) {
+ onBringIntoView(localRect)
+ }
+ })
+ .wrapContentSize(align = Alignment.TopStart, unbounded = true)
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt
new file mode 100644
index 0000000..b0b12cb
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.relocation
+
+import android.graphics.Rect as AndroidRect
+import android.view.View
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.LocalView
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal actual fun rememberDefaultBringIntoViewParent(): BringIntoViewParent {
+ val view = LocalView.current
+ return remember(view) { AndroidBringIntoViewParent(view) }
+}
+
+/**
+ * A [BringIntoViewParent] that delegates to the [View] hosting the composition.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+private class AndroidBringIntoViewParent(private val view: View) : BringIntoViewParent {
+ override suspend fun bringChildIntoView(rect: Rect, childCoordinates: LayoutCoordinates) {
+ val childOffset = childCoordinates.positionInRoot()
+ val rootRect = rect.translate(childOffset)
+ view.requestRectangleOnScreen(rootRect.toRect(), false)
+ }
+}
+
+private fun Rect.toRect() = AndroidRect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringRectangleOnScreen.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringRectangleOnScreen.android.kt
deleted file mode 100644
index 88c516a..0000000
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringRectangleOnScreen.android.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.relocation
-
-import android.view.View
-import androidx.compose.ui.Modifier
-import android.graphics.Rect
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.ui.composed
-import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.geometry.Rect as ComposeRect
-
-/**
- * Platform specific internal API to bring a rectangle into view.
- */
-internal actual class BringRectangleOnScreenRequester {
- internal var view: View? = null
- actual fun bringRectangleOnScreen(rect: ComposeRect) {
- view?.requestRectangleOnScreen(rect.toRect(), false)
- }
-}
-
-/**
- * Companion Modifier to [BringRectangleOnScreenRequester].
- */
-internal actual fun Modifier.bringRectangleOnScreenRequester(
- bringRectangleOnScreenRequester: BringRectangleOnScreenRequester
-): Modifier = composed(
- debugInspectorInfo {
- name = "bringRectangleOnScreenRequester"
- properties["bringRectangleOnScreenRequester"] = bringRectangleOnScreenRequester
- }
-) {
- val view = LocalView.current
- DisposableEffect(view) {
- bringRectangleOnScreenRequester.view = view
- onDispose {
- bringRectangleOnScreenRequester.view = null
- }
- }
- Modifier
-}
-
-private fun ComposeRect.toRect(): Rect {
- return Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
-}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 9334c0b..e50f84e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -26,10 +26,7 @@
import androidx.compose.foundation.gestures.Orientation.Vertical
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.relocation.BringIntoViewResponder
-import androidx.compose.foundation.relocation.BringIntoViewResponder.Companion.ModifierLocalBringIntoViewResponder
-import androidx.compose.foundation.relocation.BringRectangleOnScreenRequester
-import androidx.compose.foundation.relocation.bringIntoView
-import androidx.compose.foundation.relocation.bringRectangleOnScreenRequester
+import androidx.compose.foundation.relocation.bringIntoViewResponder
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
@@ -51,11 +48,8 @@
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.OnGloballyPositionedModifier
-import androidx.compose.ui.modifier.ModifierLocalConsumer
+import androidx.compose.ui.layout.OnRemeasuredModifier
import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.ModifierLocalReadScope
import androidx.compose.ui.modifier.modifierLocalOf
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Density
@@ -65,7 +59,6 @@
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastForEach
import kotlin.math.abs
-import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
/**
@@ -108,6 +101,7 @@
overScrollController = null
)
+@OptIn(ExperimentalFoundationApi::class)
internal fun Modifier.scrollable(
state: ScrollableState,
orientation: Orientation,
@@ -129,7 +123,7 @@
},
factory = {
val overscrollModifier = overScrollController?.let { Modifier.overScroll(it) } ?: Modifier
- val bringIntoViewModifier = remember(orientation, state, reverseDirection) {
+ val bringIntoViewResponder = remember(orientation, state, reverseDirection) {
BringIntoViewResponder(orientation, state, reverseDirection)
}
@@ -140,8 +134,7 @@
}
Modifier
- .then(bringIntoViewModifier)
- .bringRectangleOnScreenRequester(bringIntoViewModifier.bringRectangleOnScreenRequester)
+ .then(bringIntoViewResponder.modifier)
.then(overscrollModifier)
.pointerScrollable(
interactionSource,
@@ -468,89 +461,40 @@
private val orientation: Orientation,
private val scrollableState: ScrollableState,
private val reverseDirection: Boolean,
-) : ModifierLocalConsumer,
- ModifierLocalProvider<BringIntoViewResponder>,
- BringIntoViewResponder,
- OnGloballyPositionedModifier {
+) : BringIntoViewResponder, OnRemeasuredModifier {
+ private var size: IntSize = IntSize.Zero
- private fun Float.reverseIfNeeded(): Float = if (reverseDirection) this * -1 else this
+ val modifier = Modifier.bringIntoViewResponder(this)
+ .then(this)
- // Read the modifier local and save a reference to the parent.
- private lateinit var parent: BringIntoViewResponder
- override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
- parent = scope.run { ModifierLocalBringIntoViewResponder.current }
+ override fun onRemeasured(size: IntSize) {
+ this.size = size
}
- val bringRectangleOnScreenRequester = BringRectangleOnScreenRequester()
-
- // Populate the modifier local with this object.
- override val key = ModifierLocalBringIntoViewResponder
- override val value = this
-
- // LayoutCoordinates of this item.
- private lateinit var layoutCoordinates: LayoutCoordinates
- override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
- layoutCoordinates = coordinates
- }
-
- override suspend fun bringIntoView(rect: Rect) {
-
- val destRect = computeDestination(rect)
-
- // For the item to be visible, if needs to be in the viewport of all its ancestors.
- // Note: For now we run both of these in parallel, but in the future we could make this
- // configurable. (The child relocation could be executed before the parent, or parent
- // before the child).
- coroutineScope {
- // Bring the requested Child into this parent's view.
- launch {
- performBringIntoView(rect, destRect)
- }
-
- // If the parent is another BringIntoViewResponder, call bringIntoView.
- launch {
- parent.bringIntoView(
- parent.toLocalRect(destRect, [email protected]),
- bringRectangleOnScreenRequester
- )
- }
- }
- }
-
- override fun toLocalRect(rect: Rect, layoutCoordinates: LayoutCoordinates): Rect {
- // Translate the supplied layout coordinates into the coordinate system of this parent.
- val parentBoundingBox = this.layoutCoordinates.localBoundingBoxOf(layoutCoordinates, false)
-
- // Translate the rect to this parent's local coordinates.
- return rect.translate(parentBoundingBox.topLeft)
- }
-
- /**
- * Compute the destination given the source rectangle and current bounds.
- *
- * @param source The bounding box of the item that sent the request to be brought into view.
- * @return the destination rectangle.
- */
- fun computeDestination(source: Rect): Rect {
- val size = layoutCoordinates.size.toSize()
+ override fun calculateRectForParent(localRect: Rect): Rect {
+ val size = size.toSize()
return when (orientation) {
- Vertical ->
- source.translate(0f, relocationDistance(source.top, source.bottom, size.height))
- Horizontal ->
- source.translate(relocationDistance(source.left, source.right, size.width), 0f)
+ Vertical -> localRect.translate(
+ translateX = 0f,
+ translateY = relocationDistance(localRect.top, localRect.bottom, size.height)
+ )
+ Horizontal -> localRect.translate(
+ translateX = relocationDistance(localRect.left, localRect.right, size.width),
+ translateY = 0f
+ )
}
}
- /**
- * Using the source and destination bounds, perform an animated scroll.
- */
- suspend fun performBringIntoView(source: Rect, destination: Rect) {
+ override suspend fun bringChildIntoView(localRect: Rect) {
+ val destRect = calculateRectForParent(localRect)
val offset = when (orientation) {
- Vertical -> source.top - destination.top
- Horizontal -> source.left - destination.left
+ Vertical -> localRect.top - destRect.top
+ Horizontal -> localRect.left - destRect.left
}
scrollableState.animateScrollBy(offset.reverseIfNeeded())
}
+
+ private fun Float.reverseIfNeeded(): Float = if (reverseDirection) this * -1 else this
}
// Calculate the offset needed to bring one of the edges into view. The leadingEdge is the side
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGridState.kt
index e463d58..7c1f959 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGridState.kt
@@ -92,7 +92,18 @@
LazyGridScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset)
/**
- * The index of the first item that is visible
+ * The index of the first item that is visible.
+ *
+ * Note that this property is observable and if you use it in the composable function it will
+ * be recomposed on every change causing potential performance issues.
+ *
+ * If you want to run some side effects like sending an analytics event or updating a state
+ * based on this value consider using "snapshotFlow":
+ * @sample androidx.compose.foundation.samples.UsingGridScrollPositionForSideEffectSample
+ *
+ * If you need to use it in the composition then consider wrapping the calculation into a
+ * derived state in order to only have recompositions when the derived value changes:
+ * @sample androidx.compose.foundation.samples.UsingGridScrollPositionInCompositionSample
*/
val firstVisibleItemIndex: Int get() = scrollPosition.observableIndex
@@ -108,6 +119,15 @@
/**
* The object of [LazyGridLayoutInfo] calculated during the last layout pass. For example,
* you can use it to calculate what items are currently visible.
+ *
+ * Note that this property is observable and is updated after every scroll or remeasure.
+ * If you use it in the composable function it will be recomposed on every change causing
+ * potential performance issues including infinity recomposition loop.
+ * Therefore, avoid using it in the composition.
+ *
+ * If you want to run some side effects like sending an analytics event or updating a state
+ * based on this value consider using "snapshotFlow":
+ * @sample androidx.compose.foundation.samples.UsingGridLayoutInfoForSideEffectSample
*/
val layoutInfo: LazyGridLayoutInfo get() = layoutInfoState.value
@@ -145,6 +165,11 @@
/**
* Needed for [animateScrollToItem]. Updated on every measure.
*/
+ internal var slotsPerLine: Int = 0
+
+ /**
+ * Needed for [animateScrollToItem]. Updated on every measure.
+ */
internal var density: Density = Density(1f, 1f)
/**
@@ -204,17 +229,14 @@
* Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
* pixels.
*
- * Cancels the currently running scroll, if any, and suspends until the cancellation is
- * complete.
- *
- * @param index the data index to snap to. Must be between 0 and the number of elements.
- * @param scrollOffset the number of pixels past the start of the item to snap to. Must
- * not be negative.
+ * @param index the index to which to scroll. Must be non-negative.
+ * @param scrollOffset the offset that the item should end up after the scroll. Note that
+ * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
+ * scroll the item further upward (taking it partly offscreen).
*/
suspend fun scrollToItem(
/*@IntRange(from = 0)*/
index: Int,
- /*@IntRange(from = 0)*/
scrollOffset: Int = 0
) {
return scrollableState.scroll {
@@ -235,9 +257,6 @@
* performed within a [scroll] block (even if they don't call any other methods on this
* object) in order to guarantee that mutual exclusion is enforced.
*
- * Cancels the currently running scroll, if any, and suspends until the cancellation is
- * complete.
- *
* If [scroll] is called from elsewhere, this will be canceled.
*/
override suspend fun scroll(
@@ -332,19 +351,17 @@
/**
* Animate (smooth scroll) to the given item.
*
- * @param index the index to which to scroll
- * @param scrollOffset the offset that the item should end up after the scroll (same as
- * [scrollToItem]) - note that positive offset refers to forward scroll, so in a
- * top-to-bottom grid, positive offset will scroll the item further upward (taking it partly
- * offscreen)
+ * @param index the index to which to scroll. Must be non-negative.
+ * @param scrollOffset the offset that the item should end up after the scroll. Note that
+ * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
+ * scroll the item further upward (taking it partly offscreen).
*/
suspend fun animateScrollToItem(
/*@IntRange(from = 0)*/
index: Int,
- /*@IntRange(from = 0)*/
scrollOffset: Int = 0
) {
- doSmoothScrollToItem(index, scrollOffset)
+ doSmoothScrollToItem(index, scrollOffset, slotsPerLine)
}
/**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 7581a4f..87a7ac2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -88,13 +88,28 @@
LazyListScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset)
/**
- * The index of the first item that is visible
+ * The index of the first item that is visible.
+ *
+ * Note that this property is observable and if you use it in the composable function it will
+ * be recomposed on every change causing potential performance issues.
+ *
+ * If you want to run some side effects like sending an analytics event or updating a state
+ * based on this value consider using "snapshotFlow":
+ * @sample androidx.compose.foundation.samples.UsingListScrollPositionForSideEffectSample
+ *
+ * If you need to use it in the composition then consider wrapping the calculation into a
+ * derived state in order to only have recompositions when the derived value changes:
+ * @sample androidx.compose.foundation.samples.UsingListScrollPositionInCompositionSample
*/
val firstVisibleItemIndex: Int get() = scrollPosition.observableIndex
/**
* The scroll offset of the first visible item. Scrolling forward is positive - i.e., the
- * amount that the item is offset backwards
+ * amount that the item is offset backwards.
+ *
+ * Note that this property is observable and if you use it in the composable function it will
+ * be recomposed on every scroll causing potential performance issues.
+ * @see firstVisibleItemIndex for samples with the recommended usage patterns.
*/
val firstVisibleItemScrollOffset: Int get() = scrollPosition.observableScrollOffset
@@ -104,6 +119,15 @@
/**
* The object of [LazyListLayoutInfo] calculated during the last layout pass. For example,
* you can use it to calculate what items are currently visible.
+ *
+ * Note that this property is observable and is updated after every scroll or remeasure.
+ * If you use it in the composable function it will be recomposed on every change causing
+ * potential performance issues including infinity recomposition loop.
+ * Therefore, avoid using it in the composition.
+ *
+ * If you want to run some side effects like sending an analytics event or updating a state
+ * based on this value consider using "snapshotFlow":
+ * @sample androidx.compose.foundation.samples.UsingListLayoutInfoForSideEffectSample
*/
val layoutInfo: LazyListLayoutInfo get() = layoutInfoState.value
@@ -321,7 +345,7 @@
* @param index the index to which to scroll. Must be non-negative.
* @param scrollOffset the offset that the item should end up after the scroll. Note that
* positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
- * scroll the item further upward (taking it partly offscreen)
+ * scroll the item further upward (taking it partly offscreen).
*/
suspend fun animateScrollToItem(
/*@IntRange(from = 0)*/
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index 7cd6054..3d63d6d4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -221,14 +221,15 @@
val itemsProvider = stateOfItemsProvider.value
state.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
- // Update the state's cached Density
- state.density = this
-
val spanLayoutProvider = stateOfSpanLayoutProvider.value
// Resolve slotsPerLine.
val resolvedSlotsPerLine = slotsPerLine(constraints)
spanLayoutProvider.slotsPerLine = resolvedSlotsPerLine
+ // Update the state's cached Density and slotsPerLine
+ state.density = this
+ state.slotsPerLine = resolvedSlotsPerLine
+
val spaceBetweenLinesDp = if (isVertical) {
requireNotNull(verticalArrangement).spacing
} else {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 3e7e34b..5ed721c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -161,7 +161,9 @@
val toScrollBack = maxOffset - currentMainAxisOffset
currentFirstLineScrollOffset -= toScrollBack
currentMainAxisOffset += toScrollBack
- while (currentFirstLineScrollOffset < 0 && currentFirstLineIndex > LineIndex(0)) {
+ while (currentFirstLineScrollOffset < beforeContentPadding &&
+ currentFirstLineIndex > LineIndex(0)
+ ) {
val previousIndex = LineIndex(currentFirstLineIndex.value - 1)
val measuredLine = measuredLineProvider.getAndMeasure(previousIndex)
visibleLines.add(0, measuredLine)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt
index 50dcf3e..bd6578d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt
@@ -59,9 +59,11 @@
// state would be lost and overridden with zeros.
if (hadFirstNotEmptyLayout || measureResult.totalItemsCount > 0) {
hadFirstNotEmptyLayout = true
+ val scrollOffset = measureResult.firstVisibleLineScrollOffset
+ check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" }
update(
ItemIndex(measureResult.firstVisibleLine?.items?.firstOrNull()?.index?.value ?: 0),
- measureResult.firstVisibleLineScrollOffset
+ scrollOffset
)
}
}
@@ -71,7 +73,7 @@
* composing the items during the next measure pass and will be updated by the real
* position calculated during the measurement. This means that there is guarantee that
* exactly this index and offset will be applied as it is possible that:
- * a) there will no item at this index in reality
+ * a) there will be no item at this index in reality
* b) item at this index will be smaller than the asked scrollOffset, which means we would
* switch to the next item
* c) there will be not enough items to fill the viewport after the requested index, so we
@@ -96,7 +98,6 @@
private fun update(index: ItemIndex, scrollOffset: Int) {
require(index.value >= 0f) { "Index should be non-negative (${index.value})" }
- require(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" }
if (index != this.index) {
this.index = index
indexState.value = index.value
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrolling.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrolling.kt
index 52133b7..d14ce2fa 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrolling.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrolling.kt
@@ -16,19 +16,24 @@
package androidx.compose.foundation.lazy.grid
-import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.animateTo
-import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.copy
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyGridItemInfo
import androidx.compose.foundation.lazy.LazyGridState
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFirstOrNull
import kotlin.coroutines.cancellation.CancellationException
+import kotlin.math.abs
+import kotlin.math.max
@OptIn(ExperimentalFoundationApi::class)
-private class ItemFoundInScroll(val item: LazyGridItemInfo) : CancellationException()
+private class ItemFoundInScroll(
+ val item: LazyGridItemInfo,
+ val previousAnimation: AnimationState<Float, AnimationVector1D>
+) : CancellationException()
private val TargetDistance = 2500.dp
private val BoundDistance = 1500.dp
@@ -36,42 +41,94 @@
private const val DEBUG = false
private inline fun debugLog(generateMsg: () -> String) {
if (DEBUG) {
- println("LazyListScrolling: ${generateMsg()}")
+ println("LazyGridScrolling: ${generateMsg()}")
}
}
@OptIn(ExperimentalFoundationApi::class)
internal suspend fun LazyGridState.doSmoothScrollToItem(
index: Int,
- scrollOffset: Int
+ scrollOffset: Int,
+ slotsPerLine: Int
) {
- val animationSpec: AnimationSpec<Float> = spring()
+ require(index >= 0f) { "Index should be non-negative ($index)" }
fun getTargetItem() = layoutInfo.visibleItemsInfo.fastFirstOrNull {
it.index == index
}
scroll {
- val targetDistancePx = with(density) { TargetDistance.toPx() }
- val boundDistancePx = with(density) { BoundDistance.toPx() }
- var loop = true
- var prevVelocity = 0f
try {
+ val targetDistancePx = with(density) { TargetDistance.toPx() }
+ val boundDistancePx = with(density) { BoundDistance.toPx() }
+ var loop = true
+ var anim = AnimationState(0f)
val targetItemInitialInfo = getTargetItem()
if (targetItemInitialInfo != null) {
// It's already visible, just animate directly
- throw ItemFoundInScroll(targetItemInitialInfo)
+ throw ItemFoundInScroll(
+ targetItemInitialInfo,
+ anim
+ )
}
val forward = index > firstVisibleItemIndex
- val target = if (forward) targetDistancePx else -targetDistancePx
+
+ fun isOvershot(): Boolean {
+ // Did we scroll past the item?
+ @Suppress("RedundantIf") // It's way easier to understand the logic this way
+ return if (forward) {
+ if (firstVisibleItemIndex > index) {
+ true
+ } else if (
+ firstVisibleItemIndex == index &&
+ firstVisibleItemScrollOffset > scrollOffset
+ ) {
+ true
+ } else {
+ false
+ }
+ } else { // backward
+ if (firstVisibleItemIndex < index) {
+ true
+ } else if (
+ firstVisibleItemIndex == index &&
+ firstVisibleItemScrollOffset < scrollOffset
+ ) {
+ true
+ } else {
+ false
+ }
+ }
+ }
+
var loops = 1
- while (loop) {
- val anim = AnimationState(
- initialValue = 0f,
- initialVelocity = prevVelocity
+ while (loop && layoutInfo.totalItemsCount > 0) {
+ val visibleItems = layoutInfo.visibleItemsInfo
+ val averageLineMainAxisSize = calculateLineAverageMainAxisSize(
+ visibleItems,
+ true // TODO(b/191238807)
)
+ val before = index < firstVisibleItemIndex
+ val linesDiff =
+ (index - firstVisibleItemIndex + (slotsPerLine - 1) * if (before) -1 else 1) /
+ slotsPerLine
+
+ val expectedDistance = (averageLineMainAxisSize * linesDiff).toFloat() +
+ scrollOffset - firstVisibleItemScrollOffset
+ val target = if (abs(expectedDistance) < targetDistancePx) {
+ expectedDistance
+ } else {
+ if (forward) targetDistancePx else -targetDistancePx
+ }
+
+ debugLog {
+ "Scrolling to index=$index offset=$scrollOffset from " +
+ "index=$firstVisibleItemIndex offset=$firstVisibleItemScrollOffset with " +
+ "averageSize=$averageLineMainAxisSize and calculated target=$target"
+ }
+
+ anim = anim.copy(value = 0f)
var prevValue = 0f
anim.animateTo(
target,
- animationSpec = animationSpec,
sequentialAnimation = (anim.velocity != 0f)
) {
// If we haven't found the item yet, check if it's visible.
@@ -93,7 +150,7 @@
targetItem = getTargetItem()
if (targetItem != null) {
debugLog { "Found the item after performing scrollBy()" }
- } else {
+ } else if (!isOvershot()) {
if (delta != consumed) {
debugLog { "Hit end without finding the item" }
cancelAnimation()
@@ -117,11 +174,11 @@
if (forward) {
if (
loops >= 2 &&
- index - layoutInfo.visibleItemsInfo.last().index > 100
+ index - layoutInfo.visibleItemsInfo.last().index > 200
) {
// Teleport
debugLog { "Teleport forward" }
- snapToItemIndexInternal(index = index - 100, scrollOffset = 0)
+ snapToItemIndexInternal(index = index - 200, scrollOffset = 0)
}
} else {
if (
@@ -130,40 +187,15 @@
) {
// Teleport
debugLog { "Teleport backward" }
- snapToItemIndexInternal(index = index + 100, scrollOffset = 0)
+ snapToItemIndexInternal(index = index + 200, scrollOffset = 0)
}
}
}
}
- // Did we scroll past the item?
- @Suppress("RedundantIf") // It's way easier to understand the logic this way
- val overshot = if (forward) {
- if (firstVisibleItemIndex > index) {
- true
- } else if (
- firstVisibleItemIndex == index &&
- firstVisibleItemScrollOffset > scrollOffset
- ) {
- true
- } else {
- false
- }
- } else { // backward
- if (firstVisibleItemIndex < index) {
- true
- } else if (
- firstVisibleItemIndex == index &&
- firstVisibleItemScrollOffset < scrollOffset
- ) {
- true
- } else {
- false
- }
- }
// We don't throw ItemFoundInScroll when we snap, because once we've snapped to
// the final position, there's no need to animate to it.
- if (overshot) {
+ if (isOvershot()) {
debugLog { "Overshot" }
snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
loop = false
@@ -171,25 +203,24 @@
return@animateTo
} else if (targetItem != null) {
debugLog { "Found item" }
- throw ItemFoundInScroll(targetItem)
+ throw ItemFoundInScroll(
+ targetItem,
+ anim
+ )
}
}
- prevVelocity = anim.velocity
loops++
}
} catch (itemFound: ItemFoundInScroll) {
// We found it, animate to it
// Bring to the requested position - will be automatically stopped if not possible
- val anim = AnimationState(
- initialValue = 0f,
- initialVelocity = prevVelocity
- )
- // TODO(popam): hardcoded .y
+ val anim = itemFound.previousAnimation.copy(value = 0f)
+ // TODO(b/191238807)
val target = (itemFound.item.offset.y + scrollOffset).toFloat()
var prevValue = 0f
debugLog {
- "Seeking by $target at velocity $prevVelocity, sequential: ${anim.velocity != 0f}"
+ "Seeking by $target at velocity ${itemFound.previousAnimation.velocity}"
}
anim.animateTo(target, sequentialAnimation = (anim.velocity != 0f)) {
// Springs can overshoot their target, clamp to the desired range
@@ -222,4 +253,48 @@
snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
}
}
-}
\ No newline at end of file
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun calculateLineAverageMainAxisSize(
+ visibleItems: List<LazyGridItemInfo>,
+ isVertical: Boolean
+): Int {
+ val lineOf: (Int) -> Int = {
+ if (isVertical) visibleItems[it].row else visibleItems[it].column
+ }
+
+ var totalLinesMainAxisSize = 0
+ var linesCount = 0
+
+ var lineStartIndex = 0
+ while (lineStartIndex < visibleItems.size) {
+ val currentLine = lineOf(lineStartIndex)
+ if (currentLine == -1) {
+ // Filter out exiting items.
+ ++lineStartIndex
+ continue
+ }
+
+ var lineMainAxisSize = 0
+ var lineEndIndex = lineStartIndex
+ while (lineEndIndex < visibleItems.size && lineOf(lineEndIndex) == currentLine) {
+ lineMainAxisSize = max(
+ lineMainAxisSize,
+ if (isVertical) {
+ visibleItems[lineEndIndex].size.height
+ } else {
+ visibleItems[lineEndIndex].size.width
+ }
+ )
+ ++lineEndIndex
+ }
+
+ totalLinesMainAxisSize += lineMainAxisSize
+ ++linesCount
+
+ lineStartIndex = lineEndIndex
+ }
+
+ return totalLinesMainAxisSize / linesCount
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
new file mode 100644
index 0000000..35aa72c
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.foundation.relocation
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.OnGloballyPositionedModifier
+import androidx.compose.ui.modifier.ModifierLocalConsumer
+import androidx.compose.ui.modifier.ModifierLocalReadScope
+import androidx.compose.ui.modifier.modifierLocalOf
+
+/**
+ * The Key for the ModifierLocal that can be used to access the [BringIntoViewParent].
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal val ModifierLocalBringIntoViewParent = modifierLocalOf<BringIntoViewParent?> { null }
+
+/**
+ * Platform-specific "root" of the [BringIntoViewParent] chain to call into when there are no
+ * [ModifierLocalBringIntoViewParent]s above a [BringIntoViewChildModifier]. The value returned by
+ * this function should be passed to the [BringIntoViewChildModifier] constructor.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal expect fun rememberDefaultBringIntoViewParent(): BringIntoViewParent
+
+/**
+ * A node that can respond to [bringChildIntoView] requests from its children by scrolling its
+ * content.
+ */
+internal fun interface BringIntoViewParent {
+ /**
+ * Scrolls this node's content so that [rect] will be in visible bounds. Must ensure that the
+ * request is propagated up to the parent node.
+ *
+ * @param rect The rectangle to bring into view, relative to [childCoordinates].
+ * @param childCoordinates The [LayoutCoordinates] of the child node making the request. This
+ * parent can use these [LayoutCoordinates] to translate [rect] into its own coordinates.
+ */
+ suspend fun bringChildIntoView(rect: Rect, childCoordinates: LayoutCoordinates)
+}
+
+/**
+ * Common modifier logic shared between both requester and responder modifiers, namely recording
+ * the [LayoutCoordinates] of the modifier and providing access to the appropriate
+ * [BringIntoViewParent]: either one read from the [ModifierLocalBringIntoViewParent], or if no
+ * modifier local is specified then the [defaultParent].
+ *
+ * @param defaultParent The [BringIntoViewParent] to use if there is no
+ * [ModifierLocalBringIntoViewParent] available to read. This parent should always be obtained by
+ * calling [rememberDefaultBringIntoViewParent] to support platform-specific integration.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal abstract class BringIntoViewChildModifier(
+ private val defaultParent: BringIntoViewParent
+) : ModifierLocalConsumer,
+ OnGloballyPositionedModifier {
+
+ private var localParent: BringIntoViewParent? = null
+
+ /** The [LayoutCoordinates] of this modifier, if attached. */
+ protected var layoutCoordinates: LayoutCoordinates? = null
+ get() = field?.takeIf { it.isAttached }
+ private set
+
+ protected val parent: BringIntoViewParent
+ get() = localParent ?: defaultParent
+
+ override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
+ with(scope) {
+ localParent = ModifierLocalBringIntoViewParent.current
+ }
+ }
+
+ override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
+ layoutCoordinates = coordinates
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
index 786a038..3286e49 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
@@ -17,20 +17,13 @@
package androidx.compose.foundation.relocation
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.relocation.BringIntoViewResponder.Companion.ModifierLocalBringIntoViewResponder
-import androidx.compose.foundation.relocation.BringIntoViewResponder.Companion.RootBringIntoViewResponder
import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.toRect
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalReadScope
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.toSize
@@ -98,59 +91,55 @@
@ExperimentalFoundationApi
fun Modifier.bringIntoViewRequester(
bringIntoViewRequester: BringIntoViewRequester
-): Modifier = composed(
- inspectorInfo = debugInspectorInfo {
- name = "bringIntoViewRequester"
- properties["bringIntoViewRequester"] = bringIntoViewRequester
+): Modifier = composed(debugInspectorInfo {
+ name = "bringIntoViewRequester"
+ properties["bringIntoViewRequester"] = bringIntoViewRequester
+}) {
+ val defaultResponder = rememberDefaultBringIntoViewParent()
+ val modifier = remember(defaultResponder) {
+ BringIntoViewRequesterModifier(defaultResponder)
}
-) {
- val bringIntoViewData = remember { BringIntoViewData(BringRectangleOnScreenRequester()) }
if (bringIntoViewRequester is BringIntoViewRequesterImpl) {
DisposableEffect(bringIntoViewRequester) {
- bringIntoViewRequester.bringIntoViewUsages += bringIntoViewData
- onDispose { bringIntoViewRequester.bringIntoViewUsages -= bringIntoViewData }
+ bringIntoViewRequester.modifiers += modifier
+ onDispose { bringIntoViewRequester.modifiers -= modifier }
}
}
-
- Modifier
- .bringRectangleOnScreenRequester(bringIntoViewData.bringRectangleOnScreenRequester)
- .onGloballyPositioned { bringIntoViewData.layoutCoordinates = it }
- .then(
- remember {
- object : ModifierLocalConsumer {
- override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
- bringIntoViewData.parent = scope.run {
- ModifierLocalBringIntoViewResponder.current
- }
- }
- }
- }
- )
+ return@composed modifier
}
@ExperimentalFoundationApi
private class BringIntoViewRequesterImpl : BringIntoViewRequester {
- val bringIntoViewUsages: MutableVector<BringIntoViewData> = mutableVectorOf()
+ val modifiers = mutableVectorOf<BringIntoViewRequesterModifier>()
override suspend fun bringIntoView(rect: Rect?) {
- bringIntoViewUsages.forEach {
- val layoutCoordinates = it.layoutCoordinates
- if (layoutCoordinates == null || !layoutCoordinates.isAttached) return@forEach
-
- // If the rect is not specified, use a rectangle representing the entire composable.
- val sourceRect = rect ?: layoutCoordinates.size.toSize().toRect()
-
- // Convert the rect into parent coordinates.
- val rectInParentCoordinates = it.parent.toLocalRect(sourceRect, layoutCoordinates)
-
- it.parent.bringIntoView(rectInParentCoordinates, it.bringRectangleOnScreenRequester)
+ modifiers.forEach {
+ it.bringIntoView(rect)
}
}
}
+/**
+ * A modifier that holds state and modifier implementations for [bringIntoViewRequester]. It has
+ * access to the next [BringIntoViewParent] via [BringIntoViewChildModifier], and uses that parent
+ * to respond to requests to [bringIntoView].
+ */
@ExperimentalFoundationApi
-private data class BringIntoViewData(
- val bringRectangleOnScreenRequester: BringRectangleOnScreenRequester,
- var parent: BringIntoViewResponder = RootBringIntoViewResponder,
- var layoutCoordinates: LayoutCoordinates? = null
-)
+private class BringIntoViewRequesterModifier(
+ defaultParent: BringIntoViewParent
+) : BringIntoViewChildModifier(defaultParent) {
+
+ /**
+ * Requests that [rect] (if non-null) or the entire bounds of this modifier's node (if [rect]
+ * is null) be brought into view by the [parent] [BringIntoViewParent].
+ */
+ suspend fun bringIntoView(rect: Rect?) {
+ val layoutCoordinates = layoutCoordinates ?: return
+
+ // If the rect is not specified, use a rectangle representing the entire composable.
+ val sourceRect = rect ?: layoutCoordinates.size.toSize().toRect()
+
+ // Convert the rect into parent coordinates.
+ parent.bringChildIntoView(sourceRect, layoutCoordinates)
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
index 85a9e2e..6d72cd38 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
@@ -17,85 +17,152 @@
package androidx.compose.foundation.relocation
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.relocation.BringIntoViewResponder.Companion.ModifierLocalBringIntoViewResponder
-import androidx.compose.foundation.relocation.BringIntoViewResponder.Companion.RootBringIntoViewResponder
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.modifier.ModifierLocalProvider
import androidx.compose.ui.modifier.ProvidableModifierLocal
-import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.platform.debugInspectorInfo
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
/**
- * A parent that can respond to [bringIntoView] requests from its children, and scroll so that the
- * item is visible on screen.
+ * A parent that can respond to [bringChildIntoView] requests from its children, and scroll so that
+ * the item is visible on screen. To apply a responder to an element, pass it to the
+ * [bringIntoViewResponder] modifier.
*
* When a component calls [BringIntoViewRequester.bringIntoView], the
- * [BringIntoView ModifierLocal][ModifierLocalBringIntoViewResponder] is read to gain access to the
- * [BringIntoViewResponder], which is responsible for performing a scroll to bring the requesting
- * component into view and then send a [bringIntoView] request to its parent.
+ * [BringIntoView ModifierLocal][ModifierLocalBringIntoViewParent] is read to gain access to the
+ * [BringIntoViewResponder], which is responsible for, in order:
*
+ * 1. Calculating a rectangle that its parent responder should bring into view by returning it from
+ * [calculateRectForParent].
+ * 2. Performing any scroll or other layout adjustments needed to ensure the requested rectangle is
+ * brought into view in [bringChildIntoView].
+ *
+ * Here is a sample defining a custom [BringIntoViewResponder]:
+ * @sample androidx.compose.foundation.samples.BringIntoViewResponderSample
+ *
+ * Here is a sample where a composable is brought into view:
* @sample androidx.compose.foundation.samples.BringIntoViewSample
*
+ * Here is a sample where a part of a composable is brought into view:
+ * @sample androidx.compose.foundation.samples.BringPartOfComposableIntoViewSample
+ *
* @see BringIntoViewRequester
*/
@ExperimentalFoundationApi
interface BringIntoViewResponder {
+
/**
- * Bring this specified rectangle into bounds by making all the scrollable parents scroll
+ * Return the rectangle in this node that should be brought into view by this node's parent,
+ * in coordinates relative to this node. If this node needs to adjust itself to bring
+ * [localRect] into view, the returned rectangle should be the destination rectangle that
+ * [localRect] will eventually occupy once this node's content is adjusted.
+ *
+ * @param localRect The rectangle that should be brought into view, relative to this node. This
+ * will be the same rectangle passed to [bringChildIntoView].
+ * @return The rectangle in this node that should be brought into view itself, relative to this
+ * node. If this node needs to scroll to bring [localRect] into view, the returned rectangle
+ * should be the destination rectangle that [localRect] will eventually occupy, once the
+ * scrolling animation is finished.
+ */
+ @ExperimentalFoundationApi
+ fun calculateRectForParent(localRect: Rect): Rect
+
+ /**
+ * Bring this specified rectangle into bounds by making this scrollable parent scroll
* appropriately.
*
- * @param rect The rectangle that should be brought into view. If you
- * don't specify the coordinates, the entire component is brought into view.
- *
- *
- * Here is a sample where a composable is brought into view:
- * @sample androidx.compose.foundation.samples.BringIntoViewSample
- *
- * Here is a sample where a part of a composable is brought into view:
- * @sample androidx.compose.foundation.samples.BringPartOfComposableIntoViewSample
+ * @param localRect The rectangle that should be brought into view, relative to this node. This
+ * is the same rectangle that will have been passed to [calculateRectForParent].
*/
- suspend fun bringIntoView(rect: Rect)
+ @ExperimentalFoundationApi
+ suspend fun bringChildIntoView(localRect: Rect)
+}
+
+/**
+ * A parent that can respond to [BringIntoViewRequester] requests from its children, and scroll so
+ * that the item is visible on screen. See [BringIntoViewResponder] for more details about how
+ * this mechanism works.
+ *
+ * @sample androidx.compose.foundation.samples.BringIntoViewResponderSample
+ * @sample androidx.compose.foundation.samples.BringIntoViewSample
+ *
+ * @see BringIntoViewRequester
+ */
+@ExperimentalFoundationApi
+fun Modifier.bringIntoViewResponder(
+ responder: BringIntoViewResponder
+): Modifier = composed(debugInspectorInfo {
+ name = "bringIntoViewResponder"
+ properties["responder"] = responder
+}) {
+ val defaultParent = rememberDefaultBringIntoViewParent()
+ val modifier = remember(defaultParent) {
+ BringIntoViewResponderModifier(defaultParent)
+ }
+ modifier.responder = responder
+ return@composed modifier
+}
+
+/**
+ * A modifier that holds state and modifier implementations for [bringIntoViewResponder]. It has
+ * access to the next [BringIntoViewParent] via [BringIntoViewChildModifier] and additionally
+ * provides itself as the [BringIntoViewParent] for subsequent modifiers. This class is responsible
+ * for recursively propagating requests up the responder chain.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+private class BringIntoViewResponderModifier(
+ defaultParent: BringIntoViewParent
+) : BringIntoViewChildModifier(defaultParent),
+ ModifierLocalProvider<BringIntoViewParent?>,
+ BringIntoViewParent {
+
+ lateinit var responder: BringIntoViewResponder
+
+ override val key: ProvidableModifierLocal<BringIntoViewParent?>
+ get() = ModifierLocalBringIntoViewParent
+ override val value: BringIntoViewParent
+ get() = this
/**
- * Convert a Rect into the layoutCoordinates of this [BringIntoViewResponder].
+ * Responds to a child's request by first converting [rect] into this node's [LayoutCoordinates]
+ * and then, concurrently, calling the [responder] and the [parent] to handle the request.
*/
- fun toLocalRect(rect: Rect, layoutCoordinates: LayoutCoordinates): Rect
+ override suspend fun bringChildIntoView(rect: Rect, childCoordinates: LayoutCoordinates) {
+ val layoutCoordinates = layoutCoordinates ?: return
+ if (!childCoordinates.isAttached) return
+ val localRect = layoutCoordinates.localRectOf(childCoordinates, rect)
+ val parentRect = responder.calculateRectForParent(localRect)
- @ExperimentalFoundationApi
- companion object {
- /**
- * The Key for the ModifierLocal that can be used to access the [BringIntoViewResponder].
- */
- val ModifierLocalBringIntoViewResponder: ProvidableModifierLocal<BringIntoViewResponder> =
- modifierLocalOf { RootBringIntoViewResponder }
+ // For the item to be visible, if needs to be in the viewport of all its ancestors.
+ // Note: For now we run both of these concurrently, but in the future we could make this
+ // configurable. (The child relocation could be executed before the parent, or parent
+ // before the child).
+ coroutineScope {
+ // Bring the requested Child into this parent's view.
+ launch {
+ responder.bringChildIntoView(localRect)
+ }
- /**
- * The Root [BringIntoViewResponder]. If you read this value for the
- * [ModifierLocalBringIntoViewResponder], it means that this is the topmost
- * [BringIntoViewResponder] in this hierarchy.
- */
- val RootBringIntoViewResponder = object : BringIntoViewResponder {
- override suspend fun bringIntoView(rect: Rect) {}
- override fun toLocalRect(rect: Rect, layoutCoordinates: LayoutCoordinates) =
- Rect(layoutCoordinates.localToRoot(rect.topLeft), rect.size)
+ parent.bringChildIntoView(parentRect, layoutCoordinates)
}
}
}
/**
- * Brings [rectInParentCoordinates] into view by either calling this [BringIntoViewResponder] or,
- * if this is the [RootBringIntoViewResponder], then it delegates to
- * [bringRectangleOnScreenRequester]. Implementations of [BringIntoViewResponder] should call this
- * to propagate the request to their parent to ensure that the request is actually propagated.
+ * Translates [rect], specified in [sourceCoordinates], into this [LayoutCoordinates].
*/
-@OptIn(ExperimentalFoundationApi::class)
-internal suspend fun BringIntoViewResponder.bringIntoView(
- rectInParentCoordinates: Rect,
- bringRectangleOnScreenRequester: BringRectangleOnScreenRequester
-) {
- if (this == RootBringIntoViewResponder) {
- // Use the platform specific API to bring the rectangle on screen.
- bringRectangleOnScreenRequester.bringRectangleOnScreen(rectInParentCoordinates)
- } else {
- bringIntoView(rectInParentCoordinates)
- }
-}
+private fun LayoutCoordinates.localRectOf(
+ sourceCoordinates: LayoutCoordinates,
+ rect: Rect
+): Rect {
+ // Translate the supplied layout coordinates into the coordinate system of this parent.
+ val localRect = localBoundingBoxOf(sourceCoordinates, clipBounds = false)
+
+ // Translate the rect to this parent's local coordinates.
+ return rect.translate(localRect.topLeft)
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringRectangleOnScreen.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringRectangleOnScreen.kt
deleted file mode 100644
index f80d946..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringRectangleOnScreen.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.relocation
-
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Rect
-
-/**
- * Platform specific internal API to bring a rectangle into view.
- */
-internal expect class BringRectangleOnScreenRequester() {
- fun bringRectangleOnScreen(rect: Rect)
-}
-
-/**
- * Companion Modifier to [BringRectangleOnScreenRequester].
- */
-internal expect fun Modifier.bringRectangleOnScreenRequester(
- bringRectangleOnScreenRequester: BringRectangleOnScreenRequester
-): Modifier
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringRectangleOnScreen.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.desktop.kt
similarity index 64%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringRectangleOnScreen.desktop.kt
rename to compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.desktop.kt
index 93e1cdd..ea02955 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringRectangleOnScreen.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.desktop.kt
@@ -16,24 +16,17 @@
package androidx.compose.foundation.relocation
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Rect as ComposeRect
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Composable
/**
* Platform specific internal API to bring a rectangle into view.
*/
-internal actual class BringRectangleOnScreenRequester {
- @Suppress("UNUSED_PARAMETER")
- actual fun bringRectangleOnScreen(rect: ComposeRect) {
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal actual fun rememberDefaultBringIntoViewParent(): BringIntoViewParent {
+ return BringIntoViewParent { _, _ ->
// TODO(b/203204124): Implement this if desktop has a
// concept similar to Android's View.requestRectangleOnScreen.
}
-}
-
-/**
- * Companion Modifier to [BringRectangleOnScreenRequester].
- */
-@Suppress("UNUSED_PARAMETER")
-internal actual fun Modifier.bringRectangleOnScreenRequester(
- bringRectangleOnScreenRequester: BringRectangleOnScreenRequester
-): Modifier = this
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index 987464d3..f0ee239 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -62,6 +62,7 @@
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":compose:material:material"))
+ androidTestImplementation("androidx.fragment:fragment-testing:1.4.1")
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.truth)
@@ -124,6 +125,7 @@
androidAndroidTest.dependencies {
implementation(project(":compose:test-utils"))
implementation(project(":compose:material:material"))
+ implementation("androidx.fragment:fragment-testing:1.4.1")
implementation(libs.testRules)
implementation(libs.testRunner)
implementation(libs.truth)
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeInFragmentTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeInFragmentTest.kt
new file mode 100644
index 0000000..5433960
--- /dev/null
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeInFragmentTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test.junit4
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import androidx.compose.material.Text
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.test.onNodeWithText
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.testing.launchFragmentInContainer
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ComposeInFragmentTest {
+ @get:Rule
+ val rule = createEmptyComposeRule()
+
+ @Test
+ fun test() {
+ launchFragmentInContainer<CustomFragment>()
+ rule.onNodeWithText("Hello Compose").assertExists()
+ }
+
+ class CustomFragment : Fragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return container?.let {
+ ComposeView(container.context).apply {
+ layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ (view as ComposeView).setContent {
+ Text("Hello Compose")
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui-text-google-fonts/api/current.txt b/compose/ui/ui-text-google-fonts/api/current.txt
new file mode 100644
index 0000000..925f1b8
--- /dev/null
+++ b/compose/ui/ui-text-google-fonts/api/current.txt
@@ -0,0 +1,8 @@
+// Signature format: 4.0
+package androidx.compose.ui.text.googlefonts {
+
+ public final class GoogleFontKt {
+ }
+
+}
+
diff --git a/compose/ui/ui-text-google-fonts/api/public_plus_experimental_current.txt b/compose/ui/ui-text-google-fonts/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..882734b
--- /dev/null
+++ b/compose/ui/ui-text-google-fonts/api/public_plus_experimental_current.txt
@@ -0,0 +1,13 @@
+// Signature format: 4.0
+package androidx.compose.ui.text.googlefonts {
+
+ public final class GoogleFontKt {
+ method @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font GoogleFont(String name, androidx.compose.ui.text.googlefonts.GoogleFontProvider fontProvider, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional boolean bestEffort);
+ }
+
+ @androidx.compose.ui.text.ExperimentalTextApi public final class GoogleFontProvider {
+ ctor public GoogleFontProvider(String providerAuthority, String providerPackage, java.util.List<? extends java.util.List<byte[]>> certificates);
+ }
+
+}
+
diff --git a/compose/ui/ui-text-google-fonts/api/res-current.txt b/compose/ui/ui-text-google-fonts/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/compose/ui/ui-text-google-fonts/api/res-current.txt
diff --git a/compose/ui/ui-text-google-fonts/api/restricted_current.txt b/compose/ui/ui-text-google-fonts/api/restricted_current.txt
new file mode 100644
index 0000000..925f1b8
--- /dev/null
+++ b/compose/ui/ui-text-google-fonts/api/restricted_current.txt
@@ -0,0 +1,8 @@
+// Signature format: 4.0
+package androidx.compose.ui.text.googlefonts {
+
+ public final class GoogleFontKt {
+ }
+
+}
+
diff --git a/compose/ui/ui-text-google-fonts/build.gradle b/compose/ui/ui-text-google-fonts/build.gradle
new file mode 100644
index 0000000..a70217c
--- /dev/null
+++ b/compose/ui/ui-text-google-fonts/build.gradle
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ kotlinPlugin(project(":compose:compiler:compiler"))
+
+ implementation(libs.kotlinStdlib)
+
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:ui:ui-text"))
+ implementation(project(":core:core"))
+
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.espressoCore)
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.truth)
+}
+
+androidx {
+ name = "AndroidX Compose Google Fonts integration"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.COMPOSE_UI
+ inceptionYear = "2022"
+ description = "Compose Downloadable Fonts integration for Google Fonts"
+}
diff --git a/compose/ui/ui-text-google-fonts/src/androidTest/AndroidManifest.xml b/compose/ui/ui-text-google-fonts/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..f8bb6461
--- /dev/null
+++ b/compose/ui/ui-text-google-fonts/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest package="androidx.compose.ui.text.googlefonts" />
diff --git a/compose/ui/ui-text-google-fonts/src/androidTest/java/androidx/compose/ui/text/googlefonts/GoogleFontTest.kt b/compose/ui/ui-text-google-fonts/src/androidTest/java/androidx/compose/ui/text/googlefonts/GoogleFontTest.kt
new file mode 100644
index 0000000..e3a09c0
--- /dev/null
+++ b/compose/ui/ui-text-google-fonts/src/androidTest/java/androidx/compose/ui/text/googlefonts/GoogleFontTest.kt
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text.googlefonts
+
+import android.content.Context
+import android.graphics.Typeface
+import android.os.Handler
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.font.AndroidFont
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontLoadingStrategy
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.core.provider.FontRequest
+import androidx.core.provider.FontsContractCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalTextApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GoogleFontTest {
+
+ val context = InstrumentationRegistry.getInstrumentation().context
+
+ private val TestProvider = GoogleFontProvider(
+ "providerAuthority",
+ "providerPackage",
+ listOf(listOf(ByteArray(100) { it.toByte() }))
+ )
+
+ @Test
+ fun GoogleFont_create_ComposeFont() {
+ val font = GoogleFont("Test font", TestProvider)
+ assertThat(font).isInstanceOf(Font::class.java)
+ }
+
+ @OptIn(ExperimentalTextApi::class)
+ @Test
+ fun GoogleFont_is_AsyncFont() {
+ val font = GoogleFont("Test font", TestProvider)
+ assertThat(font.loadingStrategy).isEqualTo(FontLoadingStrategy.Async)
+ }
+
+ @Test
+ fun GoogleFont_default_W400() {
+ val font = GoogleFont("Test", TestProvider)
+ assertThat(font.weight).isEqualTo(FontWeight.W400)
+ }
+
+ @Test
+ fun GoogleFont_default_isNormal() {
+ val font = GoogleFont("Test", TestProvider)
+ assertThat(font.style).isEqualTo(FontStyle.Normal)
+ }
+
+ @Test
+ fun GoogleFont_default_bestEffort_true() {
+ val font = GoogleFont("best effort", TestProvider) as GoogleFontImpl
+ assertThat(font.bestEffort).isTrue()
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun GoogleFont_throwsOn_emptyName() {
+ GoogleFont("", TestProvider)
+ }
+
+ @Test
+ fun GoogleFont_keepsUrlEncodingRequiredNames() {
+ val expected = "!@#$%^&*(){}'<>PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ~~`1234567890[]/=\\-;:,."
+ val font = GoogleFont(expected, TestProvider) as GoogleFontImpl
+ assertThat(font.name).isEqualTo(expected)
+ }
+
+ @Test
+ fun GoogleFontImpl_fontRequest_containsName() {
+ val font = GoogleFont("Test Name", TestProvider) as GoogleFontImpl
+ assertThat(font.toFontRequest().query).contains("name=Test+Name")
+ }
+
+ @Test
+ fun GoogleFontImpl_fontRequest_containsWeight() {
+ val font = GoogleFont("a", TestProvider, weight = FontWeight.W800) as GoogleFontImpl
+ assertThat(font.toFontRequest().query).contains("weight=800")
+ }
+
+ @Test
+ fun GoogleFontImpl_fontRequest_containsStyle_normal() {
+ val font = GoogleFont("a", TestProvider) as GoogleFontImpl
+ assertThat(font.toFontRequest().query).contains("italic=0")
+ }
+
+ @Test
+ fun GoogleFontImpl_fontRequest_containsStyle_italic() {
+ val font = GoogleFont("a", TestProvider, style = FontStyle.Italic) as GoogleFontImpl
+ assertThat(font.toFontRequest().query).contains("italic=1")
+ }
+
+ @Test
+ fun GoogleFontImpl_fontRequest_bestEffort() {
+ val font = GoogleFont("a", TestProvider) as GoogleFontImpl
+ assertThat(font.toFontRequest().query).contains("besteffort=true")
+ }
+
+ @Test
+ fun GoogleFontImpl_fontRequest_bestEffort_false() {
+ val font = GoogleFont("a", TestProvider, bestEffort = false) as GoogleFontImpl
+ assertThat(font.toFontRequest().query).contains("besteffort=false")
+ }
+
+ @Test
+ fun GoogleFontImpl_providerAuthority_passedDown() {
+ val font = GoogleFont("a", TestProvider) as GoogleFontImpl
+ assertThat(font.toFontRequest().providerAuthority).isEqualTo(TestProvider.providerAuthority)
+ }
+
+ @Test
+ fun GoogleFontImpl_providerPackage_passedDown() {
+ val font = GoogleFont("a", TestProvider) as GoogleFontImpl
+ assertThat(font.toFontRequest().providerPackage).isEqualTo(TestProvider.providerPackage)
+ }
+
+ @Test
+ fun GoogleFontImpl_providerCerts_passedDown() {
+ val font = GoogleFont("a", TestProvider) as GoogleFontImpl
+ assertThat(font.toFontRequest().certificates).isEqualTo(TestProvider.certificates)
+ }
+
+ @Test
+ fun GoogleFontImpl_TypefaceStyle_Normal() {
+ val font = GoogleFont("a", TestProvider) as GoogleFontImpl
+ assertThat(font.toTypefaceStyle()).isEqualTo(Typeface.NORMAL)
+ }
+
+ @Test
+ fun GoogleFontImpl_TypefaceStyle_Italic() {
+ val font = GoogleFont("a", TestProvider, style = FontStyle.Italic) as GoogleFontImpl
+ assertThat(font.toTypefaceStyle()).isEqualTo(Typeface.ITALIC)
+ }
+
+ @Test
+ fun GoogleFontImpl_TypefaceStyle_Bold() {
+ val font = GoogleFont("a", TestProvider, weight = FontWeight.Bold) as GoogleFontImpl
+ assertThat(font.toTypefaceStyle()).isEqualTo(Typeface.BOLD)
+ }
+
+ @Test
+ fun GoogleFontImpl_TypefaceStyle_BoldItalic() {
+ val font = GoogleFont(
+ "a",
+ TestProvider,
+ weight = FontWeight.Bold,
+ style = FontStyle.Italic
+ ) as GoogleFontImpl
+ assertThat(font.toTypefaceStyle()).isEqualTo(Typeface.BOLD_ITALIC)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun GoogleFont_TypefaceLoader_resumesOnCompletion() {
+ val compatLoader = CapturingFontsContractCompatLoader()
+ runBlockingTest {
+ val deferred = async {
+ GoogleFontTypefaceLoader.awaitLoad(
+ context,
+ GoogleFont("Foo", TestProvider) as AndroidFont,
+ compatLoader
+ )
+ }
+ compatLoader.callback?.onTypefaceRetrieved(Typeface.MONOSPACE)
+ assertThat(deferred.await()).isEqualTo(Typeface.MONOSPACE)
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun GoogleFont_TypefaceLoader_throwsOnError() {
+ val compatLoader = CapturingFontsContractCompatLoader()
+ runBlockingTest {
+ val deferred = async {
+ GoogleFontTypefaceLoader.awaitLoad(
+ context,
+ GoogleFont("Foo", TestProvider) as AndroidFont,
+ compatLoader
+ )
+ }
+ compatLoader.callback?.onTypefaceRequestFailed(42)
+ var exception: IllegalStateException? = null
+ try {
+ assertThat(deferred.await())
+ } catch (ex: IllegalStateException) {
+ exception = ex
+ }
+ assertThat(exception?.message).contains("reason=42")
+ assertThat(exception?.message).contains("GoogleFont(name=\"Foo\"")
+ }
+ }
+
+ @Test
+ fun GoogleFont_toString() {
+ val font = GoogleFont("Font Family", TestProvider)
+ assertThat(font.toString()).isEqualTo(
+ "GoogleFont(name=\"Font Family\", weight=FontWeight(weight=400), " +
+ "style=Normal, bestEffort=true)"
+ )
+ }
+
+ private class CapturingFontsContractCompatLoader : FontsContractCompatLoader {
+ var callback: FontsContractCompat.FontRequestCallback? = null
+
+ override fun requestFont(
+ context: Context,
+ fontRequest: FontRequest,
+ typefaceStyle: Int,
+ handler: Handler,
+ callback: FontsContractCompat.FontRequestCallback
+ ) {
+ this.callback = callback
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-text-google-fonts/src/main/AndroidManifest.xml b/compose/ui/ui-text-google-fonts/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5597ebf
--- /dev/null
+++ b/compose/ui/ui-text-google-fonts/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<!--
+ Copyright 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest package="androidx.compose.ui.text.googlefonts" />
diff --git a/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/GoogleFont.kt b/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/GoogleFont.kt
new file mode 100644
index 0000000..e0a6ba9
--- /dev/null
+++ b/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/GoogleFont.kt
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2022 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.
+ */
+
+// this file provides integration with fonts.google.com, which is called Google Fonts
+@file:Suppress("MentionsGoogle")
+
+package androidx.compose.ui.text.googlefonts
+
+import android.content.Context
+import android.graphics.Typeface
+import android.os.Handler
+import android.os.Looper
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.font.AndroidFont
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontLoadingStrategy
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.core.provider.FontRequest
+import androidx.core.provider.FontsContractCompat
+import java.lang.IllegalStateException
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * Load a font from Google Fonts via Downloadable Fonts.
+ *
+ * To learn more about the features supported by Google Fonts, see
+ * [Get Started with the Google Fonts for Android](https://developers.google.com/fonts/docs/android)
+ *
+ * @param name of font such as "Roboto" or "Open Sans"
+ * @param fontProvider configuration for downloadable font provider
+ * @param weight font weight to load, or weight to closest match if [bestEffort] is true
+ * @param style italic or normal font
+ * @param bestEffort If besteffort is true and your query specifies a valid family name but the
+ * requested width/weight/italic value is not supported Google Fonts will return the best match it
+ * can find within the family. If false, exact matches will be returned only.
+ */
+// contains Google in name because this function provides integration with fonts.google.com
+@Suppress("MentionsGoogle")
+@ExperimentalTextApi
+fun GoogleFont(
+ name: String,
+ fontProvider: GoogleFontProvider,
+ weight: FontWeight = FontWeight.W400,
+ style: FontStyle = FontStyle.Normal,
+ bestEffort: Boolean = true
+): Font {
+ require(name.isNotEmpty()) { "name cannot be empty" }
+ return GoogleFontImpl(
+ name = name,
+ fontProvider = fontProvider,
+ weight = weight,
+ style = style,
+ bestEffort = bestEffort
+ )
+}
+
+/**
+ * Attributes used to create a [FontRequest] for a [GoogleFont].
+ *
+ * @see FontRequest
+ */
+@ExperimentalTextApi
+// contains Google in name because this function provides integration with fonts.google.com
+@Suppress("MentionsGoogle")
+class GoogleFontProvider(
+ internal val providerAuthority: String,
+ internal val providerPackage: String,
+ internal val certificates: List<List<ByteArray>>
+) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as GoogleFontProvider
+
+ if (providerAuthority != other.providerAuthority) return false
+ if (providerPackage != other.providerPackage) return false
+ if (certificates != other.certificates) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = providerAuthority.hashCode()
+ result = 31 * result + providerPackage.hashCode()
+ result = 31 * result + certificates.hashCode()
+ return result
+ }
+}
+
+@ExperimentalTextApi
+internal data class GoogleFontImpl constructor(
+ val name: String,
+ private val fontProvider: GoogleFontProvider,
+ override val weight: FontWeight,
+ override val style: FontStyle,
+ val bestEffort: Boolean
+) : AndroidFont(FontLoadingStrategy.Async) {
+ override val typefaceLoader: TypefaceLoader
+ get() = GoogleFontTypefaceLoader
+
+ fun toFontRequest(): FontRequest {
+ val query = "name=${name.encode()}&weight=${weight.weight}" +
+ "&italic=${style.toQueryParam()}&besteffort=${bestEffortQueryParam()}"
+ return FontRequest(
+ fontProvider.providerAuthority,
+ fontProvider.providerPackage,
+ query,
+ fontProvider.certificates
+ )
+ }
+
+ private fun bestEffortQueryParam() = if (bestEffort) "true" else "false"
+ private fun FontStyle.toQueryParam(): Int = if (this == FontStyle.Italic) 1 else 0
+ private fun String.encode() = URLEncoder.encode(this, StandardCharsets.UTF_8.toString())
+
+ fun toTypefaceStyle(): Int {
+ val isItalic = style == FontStyle.Italic
+ val isBold = weight >= FontWeight.Bold
+ return when {
+ isItalic && isBold -> Typeface.BOLD_ITALIC
+ isItalic -> Typeface.ITALIC
+ isBold -> Typeface.BOLD
+ else -> Typeface.NORMAL
+ }
+ }
+
+ override fun toString(): String {
+ return "GoogleFont(name=\"$name\", weight=$weight, style=$style, bestEffort=$bestEffort)"
+ }
+}
+
+@ExperimentalTextApi
+internal object GoogleFontTypefaceLoader : AndroidFont.TypefaceLoader {
+ override fun loadBlocking(context: Context, font: AndroidFont): Typeface? {
+ error("GoogleFont only support async loading: $font")
+ }
+
+ override suspend fun awaitLoad(context: Context, font: AndroidFont): Typeface? {
+ return awaitLoad(context, font, DefaultFontsContractCompatLoader)
+ }
+
+ internal suspend fun awaitLoad(
+ context: Context,
+ font: AndroidFont,
+ loader: FontsContractCompatLoader
+ ): Typeface? {
+ require(font is GoogleFontImpl) { "Only GoogleFontImpl supported (actual $font)" }
+ val fontRequest = font.toFontRequest()
+ val typefaceStyle = font.toTypefaceStyle()
+
+ return suspendCancellableCoroutine { continuation ->
+ val callback = object : FontsContractCompat.FontRequestCallback() {
+ override fun onTypefaceRetrieved(typeface: Typeface?) {
+ // this is entered from any thread
+ continuation.resume(typeface)
+ }
+
+ override fun onTypefaceRequestFailed(reason: Int) {
+ // this is entered from any thread
+ continuation.cancel(
+ IllegalStateException("Failed to load $font (reason=$reason)")
+ )
+ }
+ }
+
+ loader.requestFont(
+ context = context,
+ fontRequest = fontRequest,
+ typefaceStyle = typefaceStyle,
+ handler = asyncHandlerForCurrentThreadOrMainIfNoLooper(),
+ callback = callback
+ )
+ }
+ }
+
+ private fun asyncHandlerForCurrentThreadOrMainIfNoLooper(): Handler {
+ val looper = Looper.myLooper() ?: Looper.getMainLooper()
+ return HandlerHelper.createAsync(looper)
+ }
+}
+
+/**
+ * To allow mocking for tests
+ */
+internal interface FontsContractCompatLoader {
+ fun requestFont(
+ context: Context,
+ fontRequest: FontRequest,
+ typefaceStyle: Int,
+ handler: Handler,
+ callback: FontsContractCompat.FontRequestCallback
+ )
+}
+
+/**
+ * Actual implementation of requestFont using androidx.core
+ */
+private object DefaultFontsContractCompatLoader : FontsContractCompatLoader {
+ override fun requestFont(
+ context: Context,
+ fontRequest: FontRequest,
+ typefaceStyle: Int,
+ handler: Handler,
+ callback: FontsContractCompat.FontRequestCallback
+ ) {
+ FontsContractCompat.requestFont(
+ context,
+ fontRequest,
+ typefaceStyle,
+ false, /* isBlockingFetch*/
+ 0, /* timeout - not used when isBlockingFetch=false */
+ handler,
+ callback
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/HandlerHelper.kt b/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/HandlerHelper.kt
new file mode 100644
index 0000000..782d2d4
--- /dev/null
+++ b/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/HandlerHelper.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text.googlefonts
+
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+
+/**
+ * Handler Helper helps make handlers.
+ *
+ * (with Async support API28+)
+ */
+internal object HandlerHelper {
+
+ /**
+ * @return handler, with createAsync if API level supports it.
+ */
+ fun createAsync(looper: Looper): Handler {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ Handler28Impl.createAsync(looper)
+ } else {
+ Handler(looper)
+ }
+ }
+
+ @RequiresApi(28)
+ internal object Handler28Impl {
+ @DoNotInline
+ fun createAsync(looper: Looper): Handler {
+ return Handler.createAsync(looper)
+ }
+ }
+}
diff --git a/compose/ui/ui-text/api/current.ignore b/compose/ui/ui-text/api/current.ignore
index 360cd29..b062517 100644
--- a/compose/ui/ui-text/api/current.ignore
+++ b/compose/ui/ui-text/api/current.ignore
@@ -1,3 +1,7 @@
// Baseline format: 1.0
+InvalidNullConversion: androidx.compose.ui.text.font.FontKt#Font(int, androidx.compose.ui.text.font.FontWeight, int):
+ Attempted to remove @NonNull annotation from method androidx.compose.ui.text.font.FontKt.Font(int,androidx.compose.ui.text.font.FontWeight,int)
+
+
RemovedClass: androidx.compose.ui.text.TextLayoutResultKt:
Removed class androidx.compose.ui.text.TextLayoutResultKt
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index efd3293..63de602 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -616,7 +616,8 @@
}
public final class FontKt {
- method @androidx.compose.runtime.Stable public static androidx.compose.ui.text.font.Font Font(int resId, optional androidx.compose.ui.text.font.FontWeight weight, optional int style);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.text.font.Font Font(int resId, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional int loadingStrategy);
+ method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.text.font.Font! Font(int resId, optional androidx.compose.ui.text.font.FontWeight weight, optional int style);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.text.font.FontFamily toFontFamily(androidx.compose.ui.text.font.Font);
}
@@ -628,6 +629,21 @@
public final class FontListFontFamilyTypefaceAdapterKt {
}
+ @kotlin.jvm.JvmInline public final value class FontLoadingStrategy {
+ method public int getValue();
+ property public final int value;
+ field public static final androidx.compose.ui.text.font.FontLoadingStrategy.Companion Companion;
+ }
+
+ public static final class FontLoadingStrategy.Companion {
+ method public int getAsync();
+ method public int getBlocking();
+ method public int getOptionalLocal();
+ property public final int Async;
+ property public final int Blocking;
+ property public final int OptionalLocal;
+ }
+
public final inline class FontStyle {
ctor public FontStyle();
method public int getValue();
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index bd2a8d8..c2c70dc 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -635,8 +635,8 @@
}
public final class FontKt {
- method @androidx.compose.runtime.Stable public static androidx.compose.ui.text.font.Font Font(int resId, optional androidx.compose.ui.text.font.FontWeight weight, optional int style);
- method @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(int resId, int loadingStrategy, optional androidx.compose.ui.text.font.FontWeight weight, optional int style);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.text.font.Font Font(int resId, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional int loadingStrategy);
+ method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.text.font.Font! Font(int resId, optional androidx.compose.ui.text.font.FontWeight weight, optional int style);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.text.font.FontFamily toFontFamily(androidx.compose.ui.text.font.Font);
}
@@ -648,7 +648,7 @@
public final class FontListFontFamilyTypefaceAdapterKt {
}
- @androidx.compose.ui.text.ExperimentalTextApi @kotlin.jvm.JvmInline public final value class FontLoadingStrategy {
+ @kotlin.jvm.JvmInline public final value class FontLoadingStrategy {
method public int getValue();
property public final int value;
field public static final androidx.compose.ui.text.font.FontLoadingStrategy.Companion Companion;
diff --git a/compose/ui/ui-text/api/restricted_current.ignore b/compose/ui/ui-text/api/restricted_current.ignore
index 360cd29..b062517 100644
--- a/compose/ui/ui-text/api/restricted_current.ignore
+++ b/compose/ui/ui-text/api/restricted_current.ignore
@@ -1,3 +1,7 @@
// Baseline format: 1.0
+InvalidNullConversion: androidx.compose.ui.text.font.FontKt#Font(int, androidx.compose.ui.text.font.FontWeight, int):
+ Attempted to remove @NonNull annotation from method androidx.compose.ui.text.font.FontKt.Font(int,androidx.compose.ui.text.font.FontWeight,int)
+
+
RemovedClass: androidx.compose.ui.text.TextLayoutResultKt:
Removed class androidx.compose.ui.text.TextLayoutResultKt
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index efd3293..63de602 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -616,7 +616,8 @@
}
public final class FontKt {
- method @androidx.compose.runtime.Stable public static androidx.compose.ui.text.font.Font Font(int resId, optional androidx.compose.ui.text.font.FontWeight weight, optional int style);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.text.font.Font Font(int resId, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional int loadingStrategy);
+ method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.text.font.Font! Font(int resId, optional androidx.compose.ui.text.font.FontWeight weight, optional int style);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.text.font.FontFamily toFontFamily(androidx.compose.ui.text.font.Font);
}
@@ -628,6 +629,21 @@
public final class FontListFontFamilyTypefaceAdapterKt {
}
+ @kotlin.jvm.JvmInline public final value class FontLoadingStrategy {
+ method public int getValue();
+ property public final int value;
+ field public static final androidx.compose.ui.text.font.FontLoadingStrategy.Companion Companion;
+ }
+
+ public static final class FontLoadingStrategy.Companion {
+ method public int getAsync();
+ method public int getBlocking();
+ method public int getOptionalLocal();
+ property public final int Async;
+ property public final int Blocking;
+ property public final int OptionalLocal;
+ }
+
public final inline class FontStyle {
ctor public FontStyle();
method public int getValue();
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/Font.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/Font.kt
index 6aea6a3..5aa7e6f 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/Font.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/Font.kt
@@ -232,10 +232,6 @@
* frame they are used until the font is loaded. This is the correct behavior for small fonts
* available locally.
*
- * To load fonts from a remote resource, it is recommended to call
- * [Font](Int, FontLoadingStrategy, ...) and provide [FontLoadingStrategy.Async] as the second
- * parameter.
- *
* @param resId The resource ID of the font file in font resources. i.e. "R.font.myfont".
* @param weight The weight of the font. The system uses this to match a font to a font request
* that is given in a [androidx.compose.ui.text.SpanStyle].
@@ -247,10 +243,14 @@
*
* @see FontFamily
*/
-// TODO: When FontLoadingStrategy is stable, deprecate HIDDEN this for binary compatible overload
-// and add a default parameter to other overload for source compatible (deferred because adding an
-// optional experimental parameters makes call sites experimental).
-@OptIn(ExperimentalTextApi::class)
+// TODO(b/219783755): Remove this when safe after Compose 1.3
+@Deprecated(
+ "Maintained for binary compatibility until Compose 1.3.",
+ replaceWith = ReplaceWith(
+ "Font(resId, weight, style)"
+ ),
+ DeprecationLevel.HIDDEN
+)
@Stable
fun Font(
resId: Int,
@@ -270,25 +270,20 @@
* [FontLoadingStrategy.Async].
*
* @param resId The resource ID of the font file in font resources. i.e. "R.font.myfont".
- * @param loadingStrategy Load strategy for this font, may be async for async resource fonts
* @param weight The weight of the font. The system uses this to match a font to a font request
* that is given in a [androidx.compose.ui.text.SpanStyle].
* @param style The style of the font, normal or italic. The system uses this to match a font to a
* font request that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param loadingStrategy Load strategy for this font, may be async for async resource fonts
*
* @see FontFamily
*/
-// TODO: When FontLoadingStrategy is stable, move fontLoad parameter to last position, add default,
-// and promote to stable API in the same release. This maintains source compatibility with the
-// original overload's positional ordering as well as adding a default param (deferred because new
-// default parameters of experimental type mark call site as experimental)
-@ExperimentalTextApi
@Stable
fun Font(
resId: Int,
- loadingStrategy: FontLoadingStrategy,
weight: FontWeight = FontWeight.Normal,
- style: FontStyle = FontStyle.Normal
+ style: FontStyle = FontStyle.Normal,
+ loadingStrategy: FontLoadingStrategy = FontLoadingStrategy.Blocking
): Font = ResourceFont(resId, weight, style, loadingStrategy)
/**
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontLoadingStrategy.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontLoadingStrategy.kt
index f41a259..ad4c017 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontLoadingStrategy.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontLoadingStrategy.kt
@@ -16,8 +16,6 @@
package androidx.compose.ui.text.font
-import androidx.compose.ui.text.ExperimentalTextApi
-
/**
* Font loading strategy for a [Font] in a [FontListFontFamily].
*
@@ -25,8 +23,6 @@
*
* For more information about font family resolution see [FontFamily].
*/
-// TODO: When making stable, resolve Font(resId) != Font(resId, fontLoad) in Font.kt
-@ExperimentalTextApi
@kotlin.jvm.JvmInline
value class FontLoadingStrategy private constructor(val value: Int) {
override fun toString(): String {
diff --git a/core/OWNERS b/core/OWNERS
index 6a8e25d..d39071c 100644
--- a/core/OWNERS
+++ b/core/OWNERS
@@ -17,3 +17,6 @@
# For shortcut related files
[email protected]
[email protected]
+
+# Per-file for platform ownership
+per-file src/main/java/androidx/core/content/FileProvider.java = [email protected]
diff --git a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java
index 82dba71..f85edaa 100644
--- a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java
+++ b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java
@@ -204,7 +204,7 @@
int startColor = 0xFFFF8080;
int endColor = 0xFF8080FF;
- Integer[] values = {new Integer(startColor), new Integer(endColor)};
+ Integer[] values = {startColor, endColor};
ArgbEvaluator evaluator = ArgbEvaluator.getInstance();
final ObjectAnimator colorAnimator = ObjectAnimator.ofObject(object, property,
evaluator, values);
diff --git a/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java b/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
index f01b379..90b69bc 100644
--- a/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
+++ b/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
@@ -631,6 +631,7 @@
@SdkSuppress(minSdkVersion = 29)
@LargeTest
@Test
+ @SuppressWarnings("deprecation")
public void testFixedAllFileChecksumsDirectExecutor() throws Exception {
installPackage(TEST_FIXED_APK);
assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
diff --git a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
index 750ac3d..dbb0098 100644
--- a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
+++ b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
@@ -106,6 +106,7 @@
* @throws PackageManager.NameNotFoundException if a package with the given name cannot be
* found on the system.
*/
+ @SuppressWarnings("deprecation")
public static @NonNull ListenableFuture<Checksum[]> getChecksums(@NonNull Context context,
@NonNull String packageName, boolean includeSplits, final @Checksum.Type int required,
@NonNull List<Certificate> trustedInstallers, @NonNull Executor executor)
diff --git a/core/core-appdigest/src/main/java/androidx/core/appdigest/ChecksumsApiSImpl.java b/core/core-appdigest/src/main/java/androidx/core/appdigest/ChecksumsApiSImpl.java
index 6916edb..d883448 100644
--- a/core/core-appdigest/src/main/java/androidx/core/appdigest/ChecksumsApiSImpl.java
+++ b/core/core-appdigest/src/main/java/androidx/core/appdigest/ChecksumsApiSImpl.java
@@ -134,6 +134,7 @@
}
@SuppressLint("WrongConstant")
+ @SuppressWarnings("deprecation")
static void getInstallerChecksums(@NonNull Context context,
String split, File file,
@Checksum.Type int required,
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/TrampolineActivityTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/TrampolineActivityTest.java
index b14761a..fd7b0b4 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/TrampolineActivityTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/TrampolineActivityTest.java
@@ -126,6 +126,7 @@
@Test
@SmallTest
+ @SuppressWarnings("deprecation")
public void testManifest_canDiscoverMetadata() {
PackageManager packageManager = mContext.getPackageManager();
Intent activityIntent = new Intent(SHORTCUT_LISTENER_INTENT_FILTER_ACTION);
diff --git a/core/core-remoteviews/src/main/AndroidManifest.xml b/core/core-remoteviews/src/main/AndroidManifest.xml
index 9a694b6..9435eb8 100644
--- a/core/core-remoteviews/src/main/AndroidManifest.xml
+++ b/core/core-remoteviews/src/main/AndroidManifest.xml
@@ -14,9 +14,16 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<manifest package="androidx.core.remoteviews" xmlns:android="http://schemas.android.com/apk/res/android">
+<manifest
+ package="androidx.core.remoteviews"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
<application>
- <service android:name="androidx.core.widget.RemoteViewsCompatService"
- android:permission="android.permission.BIND_REMOTEVIEWS"/>
+ <service
+ android:name="androidx.core.widget.RemoteViewsCompatService"
+ android:permission="android.permission.BIND_REMOTEVIEWS"
+ tools:ignore="MissingServiceExportedEqualsTrue" />
</application>
+
</manifest>
diff --git a/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompat.kt b/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompat.kt
index ab0550c..8fa7f2f 100644
--- a/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompat.kt
+++ b/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompat.kt
@@ -64,6 +64,7 @@
* @param items The items to display in the [android.widget.AdapterView].
*/
@JvmStatic
+ @Suppress("DEPRECATION")
public fun setRemoteAdapter(
context: Context,
remoteViews: RemoteViews,
@@ -3933,4 +3934,4 @@
rv.setFloatDimenAttr(id, method, resId)
}
}
-}
\ No newline at end of file
+}
diff --git a/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompatService.kt b/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompatService.kt
index 46b257b..34a449d 100644
--- a/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompatService.kt
+++ b/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompatService.kt
@@ -222,6 +222,7 @@
}
}
+ @Suppress("DEPRECATION")
internal fun getVersionCode(context: Context): Long? {
val packageManager = context.packageManager
val packageInfo = try {
@@ -306,4 +307,4 @@
RemoteViewsCompatServiceData.create(context, items).save(context, appWidgetId, viewId)
}
}
-}
\ No newline at end of file
+}
diff --git a/core/core/lint-baseline.xml b/core/core/lint-baseline.xml
index 0864e15..26689c9 100644
--- a/core/core/lint-baseline.xml
+++ b/core/core/lint-baseline.xml
@@ -1048,6 +1048,17 @@
<issue
id="WrongConstant"
+ message="Must be one or more of: AccessibilityEventCompat.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION, AccessibilityEventCompat.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION, AccessibilityEventCompat.CONTENT_CHANGE_TYPE_SUBTREE, AccessibilityEventCompat.CONTENT_CHANGE_TYPE_TEXT, AccessibilityEventCompat.CONTENT_CHANGE_TYPE_UNDEFINED"
+ errorLine1=" return event.getContentChangeTypes();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java"
+ line="344"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="WrongConstant"
message="Must be one of: Action.SEMANTIC_ACTION_NONE, Action.SEMANTIC_ACTION_REPLY, Action.SEMANTIC_ACTION_MARK_AS_READ, Action.SEMANTIC_ACTION_MARK_AS_UNREAD, Action.SEMANTIC_ACTION_DELETE, Action.SEMANTIC_ACTION_ARCHIVE, Action.SEMANTIC_ACTION_MUTE, Action.SEMANTIC_ACTION_UNMUTE, Action.SEMANTIC_ACTION_THUMBS_UP, Action.SEMANTIC_ACTION_THUMBS_DOWN, Action.SEMANTIC_ACTION_CALL"
errorLine1=" builder.setSemanticAction(action.getSemanticAction());"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
diff --git a/emoji2/emoji2/build.gradle b/emoji2/emoji2/build.gradle
index 13daaf4..9c5c245 100644
--- a/emoji2/emoji2/build.gradle
+++ b/emoji2/emoji2/build.gradle
@@ -24,7 +24,7 @@
api("androidx.startup:startup-runtime:1.0.0")
implementation("androidx.collection:collection:1.1.0")
implementation("androidx.annotation:annotation:1.2.0")
- implementation("androidx.lifecycle:lifecycle-process:2.4.0-rc01")
+ implementation("androidx.lifecycle:lifecycle-process:2.4.1")
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/DefaultEmojiCompatConfigTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/DefaultEmojiCompatConfigTest.java
index 79cb95c..8d936cb 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/DefaultEmojiCompatConfigTest.java
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/DefaultEmojiCompatConfigTest.java
@@ -71,6 +71,7 @@
}
}
+ @SuppressWarnings("deprecation")
private boolean providerOnSystem() {
if (Build.VERSION.SDK_INT < 19) {
return false;
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/DefaultEmojiCompatConfig.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/DefaultEmojiCompatConfig.java
index dbb4ea4..c874512 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/DefaultEmojiCompatConfig.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/DefaultEmojiCompatConfig.java
@@ -303,6 +303,7 @@
extends DefaultEmojiCompatConfigHelper {
@NonNull
@Override
+ @SuppressWarnings("deprecation")
public List<ResolveInfo> queryIntentContentProviders(@NonNull PackageManager packageManager,
@NonNull Intent intent, int flags) {
return packageManager.queryIntentContentProviders(intent, flags);
diff --git a/enterprise/enterprise-feedback/src/main/java/androidx/enterprise/feedback/DefaultKeyedAppStatesReporter.java b/enterprise/enterprise-feedback/src/main/java/androidx/enterprise/feedback/DefaultKeyedAppStatesReporter.java
index 9f71a7d..c6b4aa4 100644
--- a/enterprise/enterprise-feedback/src/main/java/androidx/enterprise/feedback/DefaultKeyedAppStatesReporter.java
+++ b/enterprise/enterprise-feedback/src/main/java/androidx/enterprise/feedback/DefaultKeyedAppStatesReporter.java
@@ -220,6 +220,7 @@
return false;
}
+ @SuppressWarnings("deprecation")
private Collection<ServiceInfo> getServiceInfoInPackages(
Intent intent, Collection<String> acceptablePackageNames) {
PackageManager packageManager = mContext.getPackageManager();
diff --git a/fragment/fragment-ktx/api/api_lint.ignore b/fragment/fragment-ktx/api/api_lint.ignore
index a8ac758..e42cfe7 100644
--- a/fragment/fragment-ktx/api/api_lint.ignore
+++ b/fragment/fragment-ktx/api/api_lint.ignore
@@ -5,7 +5,7 @@
Missing nullability on method `add` return
MissingNullability: androidx.fragment.app.FragmentTransactionKt#replace(androidx.fragment.app.FragmentTransaction, int, String, android.os.Bundle):
Missing nullability on method `replace` return
-MissingNullability: androidx.fragment.app.FragmentViewModelLazyKt#activityViewModels(androidx.fragment.app.Fragment, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>):
+MissingNullability: androidx.fragment.app.FragmentViewModelLazyKt#activityViewModels(androidx.fragment.app.Fragment, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>):
Missing nullability on method `activityViewModels` return
-MissingNullability: androidx.fragment.app.FragmentViewModelLazyKt#viewModels(androidx.fragment.app.Fragment, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>):
+MissingNullability: androidx.fragment.app.FragmentViewModelLazyKt#viewModels(androidx.fragment.app.Fragment, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>):
Missing nullability on method `viewModels` return
diff --git a/fragment/fragment-ktx/api/current.ignore b/fragment/fragment-ktx/api/current.ignore
new file mode 100644
index 0000000..a75bd4a
--- /dev/null
+++ b/fragment/fragment-ktx/api/current.ignore
@@ -0,0 +1,7 @@
+// Baseline format: 1.0
+ChangedType: androidx.fragment.app.FragmentViewModelLazyKt#createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>):
+ Method androidx.fragment.app.FragmentViewModelLazyKt.createViewModelLazy has changed return type from kotlin.Lazy<VM> to kotlin.Lazy<? extends VM>
+
+
+InvalidNullConversion: androidx.fragment.app.FragmentViewModelLazyKt#createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>):
+ Attempted to remove @NonNull annotation from method androidx.fragment.app.FragmentViewModelLazyKt.createViewModelLazy(androidx.fragment.app.Fragment,kotlin.reflect.KClass<VM>,kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore>,kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>)
diff --git a/fragment/fragment-ktx/api/current.txt b/fragment/fragment-ktx/api/current.txt
index 946af54..6d0a8d3 100644
--- a/fragment/fragment-ktx/api/current.txt
+++ b/fragment/fragment-ktx/api/current.txt
@@ -21,10 +21,12 @@
}
public final class FragmentViewModelLazyKt {
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! activityViewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
- method @MainThread public static <VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
- method @MainThread public static <VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras> extrasProducer);
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! activityViewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! activityViewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static <VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras> extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static <VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
}
public final class ViewKt {
diff --git a/fragment/fragment-ktx/api/public_plus_experimental_current.txt b/fragment/fragment-ktx/api/public_plus_experimental_current.txt
index 946af54..6d0a8d3 100644
--- a/fragment/fragment-ktx/api/public_plus_experimental_current.txt
+++ b/fragment/fragment-ktx/api/public_plus_experimental_current.txt
@@ -21,10 +21,12 @@
}
public final class FragmentViewModelLazyKt {
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! activityViewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
- method @MainThread public static <VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
- method @MainThread public static <VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras> extrasProducer);
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! activityViewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! activityViewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static <VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras> extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static <VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
}
public final class ViewKt {
diff --git a/fragment/fragment-ktx/api/restricted_current.ignore b/fragment/fragment-ktx/api/restricted_current.ignore
new file mode 100644
index 0000000..a75bd4a
--- /dev/null
+++ b/fragment/fragment-ktx/api/restricted_current.ignore
@@ -0,0 +1,7 @@
+// Baseline format: 1.0
+ChangedType: androidx.fragment.app.FragmentViewModelLazyKt#createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>):
+ Method androidx.fragment.app.FragmentViewModelLazyKt.createViewModelLazy has changed return type from kotlin.Lazy<VM> to kotlin.Lazy<? extends VM>
+
+
+InvalidNullConversion: androidx.fragment.app.FragmentViewModelLazyKt#createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>):
+ Attempted to remove @NonNull annotation from method androidx.fragment.app.FragmentViewModelLazyKt.createViewModelLazy(androidx.fragment.app.Fragment,kotlin.reflect.KClass<VM>,kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore>,kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>)
diff --git a/fragment/fragment-ktx/api/restricted_current.txt b/fragment/fragment-ktx/api/restricted_current.txt
index 946af54..6d0a8d3 100644
--- a/fragment/fragment-ktx/api/restricted_current.txt
+++ b/fragment/fragment-ktx/api/restricted_current.txt
@@ -21,10 +21,12 @@
}
public final class FragmentViewModelLazyKt {
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! activityViewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
- method @MainThread public static <VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
- method @MainThread public static <VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras> extrasProducer);
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! activityViewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! activityViewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static <VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras> extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static <VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! createViewModelLazy(androidx.fragment.app.Fragment, kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! viewModels(androidx.fragment.app.Fragment, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
}
public final class ViewKt {
diff --git a/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/FragmentViewModelLazyTest.kt b/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/FragmentViewModelLazyTest.kt
index 295e15cd..5ab1dda 100644
--- a/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/FragmentViewModelLazyTest.kt
+++ b/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/FragmentViewModelLazyTest.kt
@@ -52,6 +52,8 @@
assertThat(fragment.activityVM).isEqualTo(activityRule.activity.vm)
assertThat(fragment.activityVM2).isEqualTo(activityRule.activity.vm2)
assertThat(fragment.savedStateViewModel.defaultValue).isEqualTo("value")
+ assertThat(fragment.activityVMCE.defaultValue).isEqualTo("value2")
+ assertThat(fragment.savedStateViewModelCE.defaultValue).isEqualTo("value3")
}
class TestVMFragment : Fragment() {
@@ -62,6 +64,26 @@
val activityVM: TestActivityViewModel by activityViewModels()
val activityVM2: TestActivityViewModel2 by viewModels({ requireActivity() })
val savedStateViewModel: TestSavedStateViewModel by viewModels({ requireActivity() })
+ val activityVMCE: TestActivityViewModelCE by activityViewModels(
+ extrasProducer = {
+ MutableCreationExtras().apply {
+ set(SAVED_STATE_REGISTRY_OWNER_KEY, requireActivity())
+ set(VIEW_MODEL_STORE_OWNER_KEY, requireActivity())
+ set(DEFAULT_ARGS_KEY, bundleOf("test" to "value2"))
+ }
+ }
+ )
+ val savedStateViewModelCE: TestSavedStateViewModelCE by viewModels(
+ ownerProducer = { requireActivity() },
+ extrasProducer = {
+ MutableCreationExtras().apply {
+ set(SAVED_STATE_REGISTRY_OWNER_KEY, requireActivity())
+ set(VIEW_MODEL_STORE_OWNER_KEY, requireActivity())
+ set(DEFAULT_ARGS_KEY, bundleOf("test" to "value3"))
+ }
+ }
+ )
+
override fun onCreate(savedInstanceState: Bundle?) {
injectedFactory = VMFactory("dagger")
super.onCreate(savedInstanceState)
@@ -99,6 +121,12 @@
class TestSavedStateViewModel(handle: SavedStateHandle) : ViewModel() {
val defaultValue = handle.get<String>("test")
}
+ class TestActivityViewModelCE(handle: SavedStateHandle) : ViewModel() {
+ val defaultValue = handle.get<String>("test")
+ }
+ class TestSavedStateViewModelCE(handle: SavedStateHandle) : ViewModel() {
+ val defaultValue = handle.get<String>("test")
+ }
private class VMFactory(val prop: String) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
diff --git a/fragment/fragment-ktx/src/main/java/androidx/fragment/app/FragmentViewModelLazy.kt b/fragment/fragment-ktx/src/main/java/androidx/fragment/app/FragmentViewModelLazy.kt
index e281eef..12b7299 100644
--- a/fragment/fragment-ktx/src/main/java/androidx/fragment/app/FragmentViewModelLazy.kt
+++ b/fragment/fragment-ktx/src/main/java/androidx/fragment/app/FragmentViewModelLazy.kt
@@ -53,6 +53,10 @@
* This property can be accessed only after this Fragment is attached i.e., after
* [Fragment.onAttach()], and access prior to that will result in IllegalArgumentException.
*/
+@Deprecated(
+ "Superseded by viewModels that takes a CreationExtras producer",
+ level = DeprecationLevel.HIDDEN
+)
@MainThread
public inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
@@ -62,16 +66,93 @@
return createViewModelLazy(
VM::class,
{ owner.viewModelStore },
+ {
+ (owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelCreationExtras
+ ?: CreationExtras.Empty
+ },
factoryProducer ?: {
(owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelProviderFactory
?: defaultViewModelProviderFactory
- }) {
- (owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelCreationExtras
- ?: CreationExtras.Empty
- }
+ })
}
/**
+ * Returns a property delegate to access [ViewModel] by **default** scoped to this [Fragment]:
+ * ```
+ * class MyFragment : Fragment() {
+ * val viewmodel: MyViewModel by viewmodels()
+ * }
+ * ```
+ *
+ * Custom [ViewModelProvider.Factory] can be defined via [factoryProducer] parameter,
+ * factory returned by it will be used to create [ViewModel]:
+ * ```
+ * class MyFragment : Fragment() {
+ * val viewmodel: MyViewModel by viewmodels { myFactory }
+ * }
+ * ```
+ *
+ * Default scope may be overridden with parameter [ownerProducer]:
+ * ```
+ * class MyFragment : Fragment() {
+ * val viewmodel: MyViewModel by viewmodels ({requireParentFragment()})
+ * }
+ * ```
+ *
+ * This property can be accessed only after this Fragment is attached i.e., after
+ * [Fragment.onAttach()], and access prior to that will result in IllegalArgumentException.
+ */
+@MainThread
+public inline fun <reified VM : ViewModel> Fragment.viewModels(
+ noinline ownerProducer: () -> ViewModelStoreOwner = { this },
+ noinline extrasProducer: (() -> CreationExtras)? = null,
+ noinline factoryProducer: (() -> Factory)? = null
+): Lazy<VM> {
+ val owner by lazy(LazyThreadSafetyMode.NONE) { ownerProducer() }
+ return createViewModelLazy(
+ VM::class,
+ { owner.viewModelStore },
+ {
+ extrasProducer?.invoke()
+ ?: (owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelCreationExtras
+ ?: CreationExtras.Empty
+ },
+ factoryProducer ?: {
+ (owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelProviderFactory
+ ?: defaultViewModelProviderFactory
+ })
+}
+
+/**
+ * Returns a property delegate to access parent activity's [ViewModel],
+ * if [factoryProducer] is specified then [ViewModelProvider.Factory]
+ * returned by it will be used to create [ViewModel] first time. Otherwise, the activity's
+ * [androidx.activity.ComponentActivity.getDefaultViewModelProviderFactory](default factory)
+ * will be used.
+ *
+ * ```
+ * class MyFragment : Fragment() {
+ * val viewmodel: MyViewModel by activityViewModels()
+ * }
+ * ```
+ *
+ * This property can be accessed only after this Fragment is attached i.e., after
+ * [Fragment.onAttach()], and access prior to that will result in IllegalArgumentException.
+ */
+@Deprecated(
+ "Superseded by activityViewModels that takes a CreationExtras producer",
+ level = DeprecationLevel.HIDDEN
+)
+@MainThread
+public inline fun <reified VM : ViewModel> Fragment.activityViewModels(
+ noinline factoryProducer: (() -> Factory)? = null
+): Lazy<VM> = createViewModelLazy(
+ VM::class, { requireActivity().viewModelStore },
+ { requireActivity().defaultViewModelCreationExtras },
+ factoryProducer ?: { requireActivity().defaultViewModelProviderFactory }
+)
+
+/**
* Returns a property delegate to access parent activity's [ViewModel],
* if [factoryProducer] is specified then [ViewModelProvider.Factory]
* returned by it will be used to create [ViewModel] first time. Otherwise, the activity's
@@ -89,17 +170,23 @@
*/
@MainThread
public inline fun <reified VM : ViewModel> Fragment.activityViewModels(
+ noinline extrasProducer: (() -> CreationExtras)? = null,
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> = createViewModelLazy(
VM::class, { requireActivity().viewModelStore },
- factoryProducer ?: { requireActivity().defaultViewModelProviderFactory },
- { requireActivity().defaultViewModelCreationExtras }
+ { extrasProducer?.invoke() ?: requireActivity().defaultViewModelCreationExtras },
+ factoryProducer ?: { requireActivity().defaultViewModelProviderFactory }
+
)
/**
* Helper method for creation of [ViewModelLazy], that resolves `null` passed as [factoryProducer]
* to default factory.
*/
+@Deprecated(
+ "Superseded by createViewModelLazy that takes a CreationExtras producer",
+ level = DeprecationLevel.HIDDEN
+)
@MainThread
public fun <VM : ViewModel> Fragment.createViewModelLazy(
viewModelClass: KClass<VM>,
@@ -108,8 +195,9 @@
): Lazy<VM> = createViewModelLazy(
viewModelClass,
storeProducer,
+ { defaultViewModelCreationExtras },
factoryProducer
-) { defaultViewModelCreationExtras }
+)
/**
* Helper method for creation of [ViewModelLazy], that resolves `null` passed as [factoryProducer]
@@ -122,8 +210,9 @@
public fun <VM : ViewModel> Fragment.createViewModelLazy(
viewModelClass: KClass<VM>,
storeProducer: () -> ViewModelStore,
- factoryProducer: (() -> Factory)? = null,
- extrasProducer: () -> CreationExtras = { defaultViewModelCreationExtras }
+ extrasProducer: () -> CreationExtras = { defaultViewModelCreationExtras },
+ factoryProducer: (() -> Factory)? = null
+
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
index 6e0b227..4664d0c 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
@@ -13,8 +13,10 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.glance.appwidget.demos">
+<manifest
+ package="androidx.glance.appwidget.demos"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="false"
@@ -34,7 +36,9 @@
</activity>
<activity android:name=".ActionDemoActivity" />
- <service android:name=".ActionDemoService" />
+ <service
+ android:name=".ActionDemoService"
+ tools:ignore="MissingServiceExportedEqualsTrue" />
<activity android:name=".ListClickDestinationActivity" android:exported="false" />
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/SingleEntityWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/SingleEntityWidget.kt
index d36cc28..043e781 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/SingleEntityWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/SingleEntityWidget.kt
@@ -31,9 +31,10 @@
import androidx.template.appwidget.GlanceTemplateAppWidget
import androidx.template.template.TemplateImageWithDescription
import androidx.template.template.SingleEntityTemplate
+import androidx.template.template.TemplateText
import androidx.template.template.TemplateTextButton
-/** A [SingleEntityTemplate] implementation that sets the header given widget state */
+/** A [SingleEntityTemplate] implementation that sets the title given widget state */
class MyWidgetTemplate : SingleEntityTemplate() {
override fun getData(state: Any?): Data {
require(state is Preferences)
@@ -59,16 +60,16 @@
override val glanceAppWidget: GlanceAppWidget = SingleEntityWidget()
}
-private fun createData(header: String) = SingleEntityTemplate.Data(
- header = header,
+private fun createData(title: String) = SingleEntityTemplate.Data(
+ header = TemplateText("Single Entity demo"),
headerIcon = TemplateImageWithDescription(
ImageProvider(R.drawable.compose),
"Header icon"
),
- title = "",
- bodyText = "",
+ title = TemplateText(title, 1),
+ subtitle = TemplateText("Subtitle test", 2),
button = TemplateTextButton(actionRunCallback<ButtonAction>(), "toggle"),
- mainImage = TemplateImageWithDescription(
+ image = TemplateImageWithDescription(
ImageProvider(R.drawable.compose),
"Compose image"
),
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 870260b..7552c20 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -438,62 +438,6 @@
<sha256 value="02c12c3c2ae12dd475219ff691c82a4d9ea21f44bc594a181295bf6d43dcfbb0" origin="Generated by Gradle"/>
</artifact>
</component>
- <component group="com.google.devtools.ksp" name="symbol-processing" version="1.5.30-1.0.0">
- <artifact name="symbol-processing-1.5.30-1.0.0.jar">
- <sha256 value="abb95c72ad7a76eca777fcc94c83d1f907b5d70895f6571a1a9313d3ecb05588" origin="Generated by Gradle"/>
- </artifact>
- <artifact name="symbol-processing-1.5.30-1.0.0.pom">
- <sha256 value="91423e540336a6cefa4bbbf772da55bc477f4e0b72e547c66a81cf4f5acabbcb" origin="Generated by Gradle"/>
- </artifact>
- </component>
- <component group="com.google.devtools.ksp" name="symbol-processing" version="1.5.31-1.0.0">
- <artifact name="symbol-processing-1.5.31-1.0.0.jar">
- <sha256 value="0a9928423e04bcd642623d4c997232f6f60c7e4523f01d5963fae66bddd004fd" origin="Generated by Gradle"/>
- </artifact>
- <artifact name="symbol-processing-1.5.31-1.0.0.pom">
- <sha256 value="bd9c11c632b591f8159c6b8b958040bbdbb4895ae6799d38227b0769fbf6e09a" origin="Generated by Gradle"/>
- </artifact>
- </component>
- <component group="com.google.devtools.ksp" name="symbol-processing-api" version="1.5.20-1.0.0-beta03">
- <artifact name="symbol-processing-api-1.5.20-1.0.0-beta03.jar">
- <sha256 value="4812505df0a6e08bf8c167d358b31abf96c8a3784e6d4074587919955f2df4bd" origin="Generated by Gradle"/>
- </artifact>
- <artifact name="symbol-processing-api-1.5.20-1.0.0-beta03.module">
- <sha256 value="2e2e1673a07dc04a39a3161cddd5cf860b125a507ca20c2f89db6252f59c6487" origin="Generated by Gradle"/>
- </artifact>
- </component>
- <component group="com.google.devtools.ksp" name="symbol-processing-api" version="1.5.30-1.0.0">
- <artifact name="symbol-processing-api-1.5.30-1.0.0.jar">
- <sha256 value="bf4a6875af46917174b087d03840456685e115954d926ce88fd04b9d2f254df3" origin="Generated by Gradle"/>
- </artifact>
- <artifact name="symbol-processing-api-1.5.30-1.0.0.module">
- <sha256 value="c82a98246729a2186e6c3b431a4d04bf957612707e11fadc392f23e4d85328ff" origin="Generated by Gradle"/>
- </artifact>
- </component>
- <component group="com.google.devtools.ksp" name="symbol-processing-api" version="1.5.31-1.0.0">
- <artifact name="symbol-processing-api-1.5.31-1.0.0.jar">
- <sha256 value="2dc570ace6f7452e7196c090a3f86df7b6c5fa9755bd0212a687ae060cf523f6" origin="Generated by Gradle"/>
- </artifact>
- <artifact name="symbol-processing-api-1.5.31-1.0.0.module">
- <sha256 value="9b200e651b4b6dd21553ea857e3b4ad2fbdf1a6b724127189c16ef5fa8e5fed2" origin="Generated by Gradle"/>
- </artifact>
- </component>
- <component group="com.google.devtools.ksp" name="symbol-processing-gradle-plugin" version="1.5.30-1.0.0">
- <artifact name="symbol-processing-gradle-plugin-1.5.30-1.0.0.jar">
- <sha256 value="e829abac4f87293784aed54614335386e980c2c0871c0c4d48caf4e2c36321f0" origin="Generated by Gradle"/>
- </artifact>
- <artifact name="symbol-processing-gradle-plugin-1.5.30-1.0.0.module">
- <sha256 value="d67e2f8b403d24a13b594c50e64ca248f38650f9e7dfb6d7c97813593141e92e" origin="Generated by Gradle"/>
- </artifact>
- </component>
- <component group="com.google.devtools.ksp" name="symbol-processing-gradle-plugin" version="1.5.31-1.0.0">
- <artifact name="symbol-processing-gradle-plugin-1.5.31-1.0.0.jar">
- <sha256 value="42e755966bc5abaf54b247f9648e682d7311d4568d9268e67dd9ba44afbb92e5" origin="Generated by Gradle"/>
- </artifact>
- <artifact name="symbol-processing-gradle-plugin-1.5.31-1.0.0.module">
- <sha256 value="30f37be681a129b1feb0a83d46ef4f3e48e015c4bf2c9927234f549d191059bd" origin="Generated by Gradle"/>
- </artifact>
- </component>
<component group="com.google.firebase" name="firebase-appindexing" version="20.0.0">
<artifact name="firebase-appindexing-20.0.0.aar">
<sha256 value="c07aee785d8e0644f38895955f6d4e8808bb43f44c5660b05ff4b828700ac96c" origin="Generated by Gradle"/>
diff --git a/health/health-data-client/api/current.txt b/health/health-data-client/api/current.txt
index e6f50d0..50a64a6a 100644
--- a/health/health-data-client/api/current.txt
+++ b/health/health-data-client/api/current.txt
@@ -1 +1,31 @@
// Signature format: 4.0
+package androidx.health.data.client.metadata {
+
+ public final class Device {
+ ctor public Device(optional String? identifier, optional String? manufacturer, optional String? model, optional String? type);
+ method public String? getIdentifier();
+ method public String? getManufacturer();
+ method public String? getModel();
+ method public String? getType();
+ property public final String? identifier;
+ property public final String? manufacturer;
+ property public final String? model;
+ property public final String? type;
+ }
+
+ public final class Metadata {
+ ctor public Metadata(optional String? uid, optional java.time.Instant lastModifiedTime, optional String? clientId, optional long clientVersion, optional androidx.health.data.client.metadata.Device? device);
+ method public String? getClientId();
+ method public long getClientVersion();
+ method public androidx.health.data.client.metadata.Device? getDevice();
+ method public java.time.Instant getLastModifiedTime();
+ method public String? getUid();
+ property public final String? clientId;
+ property public final long clientVersion;
+ property public final androidx.health.data.client.metadata.Device? device;
+ property public final java.time.Instant lastModifiedTime;
+ property public final String? uid;
+ }
+
+}
+
diff --git a/health/health-data-client/api/public_plus_experimental_current.txt b/health/health-data-client/api/public_plus_experimental_current.txt
index e6f50d0..50a64a6a 100644
--- a/health/health-data-client/api/public_plus_experimental_current.txt
+++ b/health/health-data-client/api/public_plus_experimental_current.txt
@@ -1 +1,31 @@
// Signature format: 4.0
+package androidx.health.data.client.metadata {
+
+ public final class Device {
+ ctor public Device(optional String? identifier, optional String? manufacturer, optional String? model, optional String? type);
+ method public String? getIdentifier();
+ method public String? getManufacturer();
+ method public String? getModel();
+ method public String? getType();
+ property public final String? identifier;
+ property public final String? manufacturer;
+ property public final String? model;
+ property public final String? type;
+ }
+
+ public final class Metadata {
+ ctor public Metadata(optional String? uid, optional java.time.Instant lastModifiedTime, optional String? clientId, optional long clientVersion, optional androidx.health.data.client.metadata.Device? device);
+ method public String? getClientId();
+ method public long getClientVersion();
+ method public androidx.health.data.client.metadata.Device? getDevice();
+ method public java.time.Instant getLastModifiedTime();
+ method public String? getUid();
+ property public final String? clientId;
+ property public final long clientVersion;
+ property public final androidx.health.data.client.metadata.Device? device;
+ property public final java.time.Instant lastModifiedTime;
+ property public final String? uid;
+ }
+
+}
+
diff --git a/health/health-data-client/api/restricted_current.txt b/health/health-data-client/api/restricted_current.txt
index e6f50d0..50a64a6a 100644
--- a/health/health-data-client/api/restricted_current.txt
+++ b/health/health-data-client/api/restricted_current.txt
@@ -1 +1,31 @@
// Signature format: 4.0
+package androidx.health.data.client.metadata {
+
+ public final class Device {
+ ctor public Device(optional String? identifier, optional String? manufacturer, optional String? model, optional String? type);
+ method public String? getIdentifier();
+ method public String? getManufacturer();
+ method public String? getModel();
+ method public String? getType();
+ property public final String? identifier;
+ property public final String? manufacturer;
+ property public final String? model;
+ property public final String? type;
+ }
+
+ public final class Metadata {
+ ctor public Metadata(optional String? uid, optional java.time.Instant lastModifiedTime, optional String? clientId, optional long clientVersion, optional androidx.health.data.client.metadata.Device? device);
+ method public String? getClientId();
+ method public long getClientVersion();
+ method public androidx.health.data.client.metadata.Device? getDevice();
+ method public java.time.Instant getLastModifiedTime();
+ method public String? getUid();
+ property public final String? clientId;
+ property public final long clientVersion;
+ property public final androidx.health.data.client.metadata.Device? device;
+ property public final java.time.Instant lastModifiedTime;
+ property public final String? uid;
+ }
+
+}
+
diff --git a/health/health-data-client/build.gradle b/health/health-data-client/build.gradle
index 6bf816c..63cbf4c 100644
--- a/health/health-data-client/build.gradle
+++ b/health/health-data-client/build.gradle
@@ -27,6 +27,12 @@
// Add dependencies here
}
+android {
+ defaultConfig {
+ minSdkVersion 26
+ }
+}
+
androidx {
name = "AndroidX Health Data Client Library"
type = LibraryType.PUBLISHED_LIBRARY
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/metadata/Device.kt b/health/health-data-client/src/main/java/androidx/health/data/client/metadata/Device.kt
new file mode 100644
index 0000000..1a66ac4
--- /dev/null
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/metadata/Device.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2022 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.health.data.client.metadata
+
+/**
+ * The physical device (such as phone, watch, scale, or chest strap) that captured associated health
+ * record.
+ *
+ * [Device] needs to be populated by users of the API. Fields not provided by clients will remain
+ * absent. Two devices with corresponding unknown fields will compare as equal, but may represent
+ * different devices.
+ */
+public class Device(
+ public val identifier: String? = null,
+ public val manufacturer: String? = null,
+ public val model: String? = null,
+ public val type: String? = null
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Device) return false
+
+ if (identifier != other.identifier) return false
+ if (manufacturer != other.manufacturer) return false
+ if (model != other.model) return false
+ if (type != other.type) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = identifier?.hashCode() ?: 0
+ result = 31 * result + (manufacturer?.hashCode() ?: 0)
+ result = 31 * result + (model?.hashCode() ?: 0)
+ result = 31 * result + (type?.hashCode() ?: 0)
+ return result
+ }
+}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/metadata/Metadata.kt b/health/health-data-client/src/main/java/androidx/health/data/client/metadata/Metadata.kt
new file mode 100644
index 0000000..b5720d7
--- /dev/null
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/metadata/Metadata.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2022 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.health.data.client.metadata
+
+import java.time.Instant
+
+/** Set of shared metadata fields for [androidx.health.data.client.records.Record]. */
+public class Metadata(
+ /**
+ * Unique identifier of this data, assigned by the Android Health Platform at insertion time.
+ */
+ public val uid: String? = null,
+
+ /** Automatically populated to when data was last modified (or originally created). */
+ public val lastModifiedTime: Instant = Instant.EPOCH,
+
+ /** Optional client supplied unique data identifier associated with the data. */
+ public val clientId: String? = null,
+
+ /** Optional client supplied version associated with the data. */
+ public val clientVersion: Long = 0,
+
+ /** Optional client supplied device information associated with the data. */
+ public val device: Device? = null,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Metadata) return false
+
+ if (uid != other.uid) return false
+ if (lastModifiedTime != other.lastModifiedTime) return false
+ if (clientId != other.clientId) return false
+ if (clientVersion != other.clientVersion) return false
+ if (device != other.device) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = uid?.hashCode() ?: 0
+ result = 31 * result + lastModifiedTime.hashCode()
+ result = 31 * result + (clientId?.hashCode() ?: 0)
+ result = 31 * result + clientVersion.hashCode()
+ result = 31 * result + (device?.hashCode() ?: 0)
+ return result
+ }
+
+ internal companion object {
+ /** A default instance of metadata with no fields initialised. */
+ @JvmField internal val EMPTY = Metadata()
+ }
+}
diff --git a/lifecycle/lifecycle-viewmodel-compose/api/current.txt b/lifecycle/lifecycle-viewmodel-compose/api/current.txt
index 7ab89d3..7d21e88 100644
--- a/lifecycle/lifecycle-viewmodel-compose/api/current.txt
+++ b/lifecycle/lifecycle-viewmodel-compose/api/current.txt
@@ -13,6 +13,7 @@
method @Deprecated @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM! viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM! viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
method @Deprecated @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM! viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+ method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM! viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
}
}
diff --git a/lifecycle/lifecycle-viewmodel-compose/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-viewmodel-compose/api/public_plus_experimental_current.txt
index 7ab89d3..7d21e88 100644
--- a/lifecycle/lifecycle-viewmodel-compose/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-viewmodel-compose/api/public_plus_experimental_current.txt
@@ -13,6 +13,7 @@
method @Deprecated @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM! viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM! viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
method @Deprecated @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM! viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+ method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM! viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
}
}
diff --git a/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.txt b/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.txt
index 7ab89d3..7d21e88 100644
--- a/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.txt
+++ b/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.txt
@@ -13,6 +13,7 @@
method @Deprecated @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM! viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM! viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
method @Deprecated @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM! viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+ method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM! viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String key, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
}
}
diff --git a/lifecycle/lifecycle-viewmodel-compose/samples/src/main/java/androidx/lifecycle/viewmodel/compose/samples/LifecycleViewModelSamples.kt b/lifecycle/lifecycle-viewmodel-compose/samples/src/main/java/androidx/lifecycle/viewmodel/compose/samples/LifecycleViewModelSamples.kt
index c6d903ce..e228f0b 100644
--- a/lifecycle/lifecycle-viewmodel-compose/samples/src/main/java/androidx/lifecycle/viewmodel/compose/samples/LifecycleViewModelSamples.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/samples/src/main/java/androidx/lifecycle/viewmodel/compose/samples/LifecycleViewModelSamples.kt
@@ -22,11 +22,14 @@
import androidx.core.os.bundleOf
import androidx.lifecycle.DEFAULT_ARGS_KEY
import androidx.lifecycle.HasDefaultViewModelProviderFactory
+import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
+import androidx.lifecycle.viewmodel.compose.viewModel
@Sampled
@Composable
@@ -55,4 +58,23 @@
viewModel.args
}
+@Sampled
+@Composable
+fun CreationExtrasViewModelInitializer() {
+ // Just like any call to viewModel(), the default owner is the LocalViewModelStoreOwner.current.
+ // The lambda is only called the first time the ViewModel needs to be created.
+ val viewModel = viewModel {
+ // Within the lambda, you have direct access to the CreationExtras which allows you to call
+ // extension methods on CreationExtras such as createSavedStateHandle()
+ val handle = createSavedStateHandle()
+ // You can send any custom parameter, repository, etc. to your ViewModel.
+ SavedStateViewModel(handle, "custom_value")
+ }
+ // The handle and parameter are now available from the ViewModel
+ viewModel.handle
+ viewModel.value
+}
+
class TestViewModel(val args: String?) : ViewModel()
+
+class SavedStateViewModel(val handle: SavedStateHandle, val value: String) : ViewModel()
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
index b89fd5c..fbd860e 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
@@ -135,6 +135,31 @@
}
@Test
+ public fun viewModelCreatedCreationExtrasInitializer() {
+ val owner = FakeViewModelStoreOwner()
+ var createdInComposition: Any? = null
+ val extrasKey = object : CreationExtras.Key<String> { }
+ rule.setContent {
+ CompositionLocalProvider(LocalViewModelStoreOwner provides owner) {
+ createdInComposition = viewModel(
+ key = "test",
+ initializer = {
+ TestViewModel(MutableCreationExtras(this).apply {
+ set(extrasKey, "value")
+ })
+ }
+ )
+ }
+ }
+
+ assertThat(owner.factory.createCalled).isFalse()
+ val createdManually =
+ ViewModelProvider(owner)["test", TestViewModel::class.java]
+ assertThat(createdInComposition).isEqualTo(createdManually)
+ assertThat(createdManually.extras[extrasKey]).isEqualTo("value")
+ }
+
+ @Test
public fun viewModelCreatedViaDefaultFactoryWithCustomOwner() {
val customOwner = FakeViewModelStoreOwner()
val owner = FakeViewModelStoreOwner()
@@ -223,7 +248,7 @@
}
}
-private class TestViewModel : ViewModel()
+private class TestViewModel(val extras: CreationExtras = CreationExtras.Empty) : ViewModel()
private class FakeViewModelProviderFactory : ViewModelProvider.Factory {
var createCalled = false
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/main/java/androidx/lifecycle/viewmodel/compose/ViewModel.kt b/lifecycle/lifecycle-viewmodel-compose/src/main/java/androidx/lifecycle/viewmodel/compose/ViewModel.kt
index 8978113..b08ebf1 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/main/java/androidx/lifecycle/viewmodel/compose/ViewModel.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/main/java/androidx/lifecycle/viewmodel/compose/ViewModel.kt
@@ -22,6 +22,8 @@
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewmodel.CreationExtras
+import androidx.lifecycle.viewmodel.initializer
+import androidx.lifecycle.viewmodel.viewModelFactory
/**
* Returns an existing [ViewModel] or creates a new one in the given owner (usually, a fragment or
@@ -153,6 +155,43 @@
}
): VM = viewModelStoreOwner.get(modelClass, key, factory, extras)
+/**
+ * Returns an existing [ViewModel] or creates a new one in the scope (usually, a fragment or
+ * an activity)
+ *
+ * The created [ViewModel] is associated with the given [viewModelStoreOwner] and will be retained
+ * as long as the scope is alive (e.g. if it is an activity, until it is
+ * finished or process is killed).
+ *
+ * If the [viewModelStoreOwner] implements [HasDefaultViewModelProviderFactory] its default
+ * [CreationExtras] are the ones that will be provided to the receiver scope from the [initializer]
+ *
+ * @param viewModelStoreOwner The scope that the created [ViewModel] should be associated with.
+ * @param key The key to use to identify the [ViewModel].
+ * @param initializer lambda used to create an instance of the ViewModel class
+ * @return A [ViewModel] that is an instance of the given [VM] type.
+ *
+ * @sample androidx.lifecycle.viewmodel.compose.samples.CreationExtrasViewModelInitializer
+ */
+@Composable
+public inline fun <reified VM : ViewModel> viewModel(
+ viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
+ "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
+ },
+ key: String? = null,
+ noinline initializer: CreationExtras.() -> VM
+): VM = viewModel(
+ VM::class.java,
+ viewModelStoreOwner,
+ key,
+ viewModelFactory { initializer(initializer) },
+ if (viewModelStoreOwner is HasDefaultViewModelProviderFactory) {
+ viewModelStoreOwner.defaultViewModelCreationExtras
+ } else {
+ CreationExtras.Empty
+ }
+)
+
private fun <VM : ViewModel> ViewModelStoreOwner.get(
javaClass: Class<VM>,
key: String? = null,
diff --git a/lifecycle/lifecycle-viewmodel/api/current.txt b/lifecycle/lifecycle-viewmodel/api/current.txt
index 5b11ead..5b564eb 100644
--- a/lifecycle/lifecycle-viewmodel/api/current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/current.txt
@@ -13,6 +13,8 @@
public abstract class ViewModel {
ctor public ViewModel();
+ ctor public ViewModel(java.io.Closeable!...);
+ method public void addCloseable(java.io.Closeable);
method protected void onCleared();
}
diff --git a/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
index 5b11ead..5b564eb 100644
--- a/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
@@ -13,6 +13,8 @@
public abstract class ViewModel {
ctor public ViewModel();
+ ctor public ViewModel(java.io.Closeable!...);
+ method public void addCloseable(java.io.Closeable);
method protected void onCleared();
}
diff --git a/lifecycle/lifecycle-viewmodel/api/restricted_current.txt b/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
index 5b11ead..5b564eb 100644
--- a/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
@@ -13,6 +13,8 @@
public abstract class ViewModel {
ctor public ViewModel();
+ ctor public ViewModel(java.io.Closeable!...);
+ method public void addCloseable(java.io.Closeable);
method protected void onCleared();
}
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
index 1dc643c..2370b740 100644
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
@@ -17,12 +17,16 @@
package androidx.lifecycle;
import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.Closeable;
import java.io.IOException;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.LinkedHashSet;
import java.util.Map;
+import java.util.Set;
/**
* ViewModel is a class that is responsible for preparing and managing the data for
@@ -108,9 +112,49 @@
// Can't use ConcurrentHashMap, because it can lose values on old apis (see b/37042460)
@Nullable
private final Map<String, Object> mBagOfTags = new HashMap<>();
+ @Nullable
+ private final Set<Closeable> mCloseables = new LinkedHashSet<>();
private volatile boolean mCleared = false;
/**
+ * Construct a new ViewModel instance.
+ * <p>
+ * You should <strong>never</strong> manually construct a ViewModel outside of a
+ * {@link ViewModelProvider.Factory}.
+ */
+ public ViewModel() {
+ }
+
+ /**
+ * Construct a new ViewModel instance. Any {@link Closeable} objects provided here
+ * will be closed directly before {@link #onCleared()} is called.
+ * <p>
+ * You should <strong>never</strong> manually construct a ViewModel outside of a
+ * {@link ViewModelProvider.Factory}.
+ */
+ public ViewModel(@NonNull Closeable... closeables) {
+ mCloseables.addAll(Arrays.asList(closeables));
+ }
+
+ /**
+ * Add a new {@link Closeable} object that will be closed directly before
+ * {@link #onCleared()} is called.
+ *
+ * @param closeable The object that should be {@link Closeable#close() closed} directly before
+ * {@link #onCleared()} is called.
+ */
+ public void addCloseable(@NonNull Closeable closeable) {
+ // As this method is final, it will still be called on mock objects even
+ // though mCloseables won't actually be created...we'll just not do anything
+ // in that case.
+ if (mCloseables != null) {
+ synchronized (mCloseables) {
+ mCloseables.add(closeable);
+ }
+ }
+ }
+
+ /**
* This method will be called when this ViewModel is no longer used and will be destroyed.
* <p>
* It is useful when ViewModel observes some data and you need to clear this subscription to
@@ -135,6 +179,14 @@
}
}
}
+ // We need the same null check here
+ if (mCloseables != null) {
+ synchronized (mCloseables) {
+ for (Closeable closeable : mCloseables) {
+ closeWithRuntimeException(closeable);
+ }
+ }
+ }
onCleared();
}
diff --git a/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelTest.java b/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelTest.java
index 1b04323..76fd7b0 100644
--- a/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelTest.java
+++ b/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelTest.java
@@ -21,6 +21,8 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertTrue;
+import androidx.annotation.NonNull;
+
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -39,7 +41,13 @@
}
}
- class ViewModel extends androidx.lifecycle.ViewModel {
+ static class ViewModel extends androidx.lifecycle.ViewModel {
+ }
+
+ static class ConstructorArgViewModel extends androidx.lifecycle.ViewModel {
+ ConstructorArgViewModel(@NonNull Closeable closeable) {
+ super(closeable);
+ }
}
@Test
@@ -73,4 +81,29 @@
ViewModel vm = Mockito.mock(ViewModel.class);
assertThat(vm.getTag("Careless mocks =|"), nullValue());
}
+
+ @Test
+ public void testAddCloseable() {
+ ViewModel vm = new ViewModel();
+ CloseableImpl impl = new CloseableImpl();
+ vm.addCloseable(impl);
+ vm.clear();
+ assertTrue(impl.mWasClosed);
+ }
+
+ @Test
+ public void testConstructorCloseable() {
+ CloseableImpl impl = new CloseableImpl();
+ ConstructorArgViewModel vm = new ConstructorArgViewModel(impl);
+ vm.clear();
+ assertTrue(impl.mWasClosed);
+ }
+
+ @Test
+ public void testMockedAddCloseable() {
+ ViewModel vm = Mockito.mock(ViewModel.class);
+ CloseableImpl impl = new CloseableImpl();
+ // This shouldn't crash, even on a mocked object
+ vm.addCloseable(impl);
+ }
}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/CameraXMissingQuirkSummaryJava.java b/lint-checks/integration-tests/src/main/java/androidx/CameraXMissingQuirkSummaryJava.java
new file mode 100644
index 0000000..57d06d3
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/CameraXMissingQuirkSummaryJava.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 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;
+
+/**
+ * A quirk where crop rect is wrong on samsung devices.
+ */
+public class CameraXMissingQuirkSummaryJava implements Quirk {
+
+}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/Quirk.kt b/lint-checks/integration-tests/src/main/java/androidx/Quirk.kt
new file mode 100644
index 0000000..7abe5d9
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/Quirk.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2022 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
+
+interface Quirk
diff --git a/lint-checks/src/main/java/androidx/build/lint/AndroidManifestServiceExportedDetector.kt b/lint-checks/src/main/java/androidx/build/lint/AndroidManifestServiceExportedDetector.kt
new file mode 100644
index 0000000..12e1e056
--- /dev/null
+++ b/lint-checks/src/main/java/androidx/build/lint/AndroidManifestServiceExportedDetector.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022 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.build.lint
+
+import com.android.SdkConstants
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Incident
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.XmlContext
+import com.android.tools.lint.detector.api.XmlScanner
+import org.w3c.dom.Element
+
+@Suppress("UnstableApiUsage")
+class AndroidManifestServiceExportedDetector : Detector(), XmlScanner {
+
+ override fun getApplicableElements(): Collection<String> {
+ return listOf(SdkConstants.TAG_SERVICE)
+ }
+
+ override fun visitElement(context: XmlContext, element: Element) {
+ val attrExported = element.getAttribute("android:${SdkConstants.ATTR_EXPORTED}")
+ if (attrExported != "true") {
+ val incident = Incident(context, ISSUE)
+ .message("Missing exported=true in <service> tag")
+ .at(element)
+ context.report(incident)
+ }
+ }
+
+ companion object {
+ val ISSUE = Issue.create(
+ id = "MissingServiceExportedEqualsTrue",
+ briefDescription = "Missing exported=true declaration in the <service> tag inside" +
+ " the library manifest",
+ explanation = "Library-defined services should set the exported attribute to true.",
+ category = Category.CORRECTNESS,
+ priority = 5,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ AndroidManifestServiceExportedDetector::class.java,
+ Scope.MANIFEST_SCOPE
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
index 0ac4c28..74fcf64 100644
--- a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
@@ -38,6 +38,7 @@
companion object {
val Issues get(): List<Issue> {
return listOf(
+ AndroidManifestServiceExportedDetector.ISSUE,
BanParcelableUsage.ISSUE,
BanConcurrentHashMap.ISSUE,
BanInappropriateExperimentalUsage.ISSUE,
@@ -60,6 +61,7 @@
PrivateConstructorForUtilityClassDetector.ISSUE,
ClassVerificationFailureDetector.ISSUE,
IdeaSuppressionDetector.ISSUE,
+ CameraXQuirksClassDetector.ISSUE
)
}
}
diff --git a/lint-checks/src/main/java/androidx/build/lint/CameraXQuirksClassDetector.kt b/lint-checks/src/main/java/androidx/build/lint/CameraXQuirksClassDetector.kt
new file mode 100644
index 0000000..11591b4
--- /dev/null
+++ b/lint-checks/src/main/java/androidx/build/lint/CameraXQuirksClassDetector.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2022 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.build.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import org.jetbrains.uast.UClass
+
+/**
+ * Detector to apply lint rules for CameraX quirks. The rule is to enforce a javadoc template to
+ * describe the bug id, issue description and device info. This detector is disabled by default.
+ * Only CameraX modules will enable the detector.
+ */
+class CameraXQuirksClassDetector : Detector(), Detector.UastScanner {
+
+ override fun getApplicableUastTypes() = listOf(UClass::class.java)
+
+ override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
+
+ override fun visitClass(node: UClass) {
+ val elements = node.implementsList?.getReferenceElements()
+ var isQuirk = false
+ if (elements != null) {
+ for (element in elements) {
+ if (("Quirk").equals(element.referenceName)) {
+ isQuirk = true
+ }
+ }
+ }
+
+ if (isQuirk) {
+ val comments = node.comments
+ val sb = StringBuilder()
+ comments.forEach { sb.append(it.text) }
+ val comment = sb.append("\n").toString()
+
+ if (!comment.contains("@QuirkSummary")) {
+ val implForInsertion = """
+ * @QuirkSummary
+ * Bug Id:
+ * Description:
+ * Device(s):
+ """.trimIndent()
+
+ context.report(
+ CameraXQuirksClassDetector.ISSUE, node,
+ context.getNameLocation(node),
+ "CameraX quirks should include this template in the javadoc:" +
+ "\n\n$implForInsertion\n\n"
+ )
+ }
+ }
+ }
+ }
+
+ companion object {
+ val ISSUE = Issue.create(
+ id = "CameraXQuirksClassDetector",
+ briefDescription = "CameraQuirks include @QuirkSummary in the javadoc",
+ explanation = "CameraX quirks should include @QuirkSummary in the javadoc.",
+ category = Category.CORRECTNESS,
+ priority = 5,
+ severity = Severity.ERROR,
+ enabledByDefault = false,
+ implementation = Implementation(
+ CameraXQuirksClassDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/lint-checks/src/test/java/androidx/build/lint/AndroidManifestServiceExportedDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/AndroidManifestServiceExportedDetectorTest.kt
new file mode 100644
index 0000000..1a482ac
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/AndroidManifestServiceExportedDetectorTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.build.lint
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class AndroidManifestServiceExportedDetectorTest : AbstractLintDetectorTest(
+ useDetector = AndroidManifestServiceExportedDetector(),
+ useIssues = listOf(AndroidManifestServiceExportedDetector.ISSUE),
+) {
+
+ @Test
+ fun `Detect missing exported=true declaration in service tag`() {
+ val input = arrayOf(
+ manifestSample()
+ )
+
+ /* ktlint-disable max-line-length */
+ val expected = """
+AndroidManifest.xml:22: Error: Missing exported=true in <service> tag [MissingServiceExportedEqualsTrue]
+ <service android:name="androidx.core.app.JobIntentService">
+ ^
+1 errors, 0 warnings
+ """.trimIndent()
+ /* ktlint-enable max-line-length */
+
+ check(*input).expect(expected)
+ }
+
+ @Test
+ fun `Detect present exported=true declaration in service tag`() {
+ val input = xml(
+ "AndroidManifest.xml",
+ """
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <application>
+ <service
+ android:name="androidx.service"
+ android:exported="true" />
+ </application>
+</manifest>
+ """.trimIndent()
+ )
+
+ check(input).expectClean()
+ }
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/CameraXQuirksClassDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/CameraXQuirksClassDetectorTest.kt
new file mode 100644
index 0000000..2f66733
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/CameraXQuirksClassDetectorTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.build.lint
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class CameraXQuirksClassDetectorTest : AbstractLintDetectorTest(
+ useDetector = CameraXQuirksClassDetector(),
+ useIssues = listOf(CameraXQuirksClassDetector.ISSUE)
+) {
+
+ @Test
+ fun `Detection of CameraX Quirks in Java`() {
+ val input = arrayOf(
+ javaSample("androidx.CameraXMissingQuirkSummaryJava")
+ )
+
+ /* ktlint-disable max-line-length */
+ val expected = """
+ src/androidx/CameraXMissingQuirkSummaryJava.java:22: Error: CameraX quirks should include this template in the javadoc:
+
+ * @QuirkSummary
+ * Bug Id:
+ * Description:
+ * Device(s):
+
+ [CameraXQuirksClassDetector]
+ public class CameraXMissingQuirkSummaryJava implements Quirk {
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent()
+ /* ktlint-enable max-line-length */
+
+ check(*input).expect(expected)
+ }
+}
\ No newline at end of file
diff --git a/media/media/src/main/java/androidx/media/MediaSessionManagerImplBase.java b/media/media/src/main/java/androidx/media/MediaSessionManagerImplBase.java
index a5d7bc5..6a38578 100644
--- a/media/media/src/main/java/androidx/media/MediaSessionManagerImplBase.java
+++ b/media/media/src/main/java/androidx/media/MediaSessionManagerImplBase.java
@@ -53,6 +53,7 @@
}
@Override
+ @SuppressWarnings("deprecation")
public boolean isTrustedForMediaControl(
@NonNull MediaSessionManager.RemoteUserInfoImpl userInfo) {
try {
diff --git a/media/media/src/main/java/androidx/media/session/MediaButtonReceiver.java b/media/media/src/main/java/androidx/media/session/MediaButtonReceiver.java
index f131f65..10d515c 100644
--- a/media/media/src/main/java/androidx/media/session/MediaButtonReceiver.java
+++ b/media/media/src/main/java/androidx/media/session/MediaButtonReceiver.java
@@ -294,6 +294,7 @@
return null;
}
+ @SuppressWarnings("deprecation")
private static ComponentName getServiceComponentByAction(Context context, String action) {
PackageManager pm = context.getPackageManager();
Intent queryIntent = new Intent(action);
diff --git a/media2/integration-tests/testapp/src/main/AndroidManifest.xml b/media2/integration-tests/testapp/src/main/AndroidManifest.xml
index a2959fa..efb4e45 100644
--- a/media2/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/media2/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -47,8 +47,11 @@
</intent-filter>
</activity>
- <service android:name=".VideoSessionService" android:exported="false"
- android:process=":VideoSessionService">
+ <service
+ android:name=".VideoSessionService"
+ android:exported="false"
+ android:process=":VideoSessionService"
+ tools:ignore="MissingServiceExportedEqualsTrue">
<intent-filter>
<action android:name="androidx.media2.session.MediaSessionService" />
</intent-filter>
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
index 20d9a96..34f5ad0 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
@@ -1153,6 +1153,7 @@
}
@Nullable
+ @SuppressWarnings("deprecation")
private ComponentName getServiceComponentByAction(@NonNull String action) {
PackageManager pm = mContext.getPackageManager();
Intent queryIntent = new Intent(action);
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionManager.java b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionManager.java
index c689c10..d628d7b 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionManager.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionManager.java
@@ -102,6 +102,7 @@
* @return set of tokens
*/
@NonNull
+ @SuppressWarnings("deprecation")
public Set<SessionToken> getSessionServiceTokens() {
ArraySet<SessionToken> sessionServiceTokens = new ArraySet<>();
PackageManager pm = mContext.getPackageManager();
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/SessionToken.java b/media2/media2-session/src/main/java/androidx/media2/session/SessionToken.java
index 3cf63b6..df3d6da 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/SessionToken.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/SessionToken.java
@@ -353,6 +353,7 @@
}
}
+ @SuppressWarnings("deprecation")
private static boolean isInterfaceDeclared(PackageManager manager, String serviceInterface,
ComponentName serviceComponent) {
Intent serviceIntent = new Intent(serviceInterface);
@@ -378,6 +379,7 @@
return false;
}
+ @SuppressWarnings("deprecation")
private static int getUid(PackageManager manager, String packageName) {
try {
return manager.getApplicationInfo(packageName, 0).uid;
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteButton.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteButton.java
index 43c29f4..88a95b7 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteButton.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteButton.java
@@ -412,6 +412,7 @@
return result;
}
+ @SuppressWarnings("deprecation")
private boolean showOutputSwitcherForAndroidR() {
Context context = getContext();
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
index cb299313..d392178 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
@@ -95,6 +95,7 @@
}
}
+ @SuppressWarnings("deprecation")
void scanPackages() {
if (!mRunning) {
return;
@@ -164,6 +165,7 @@
@RequiresApi(Build.VERSION_CODES.R)
@NonNull
+ @SuppressWarnings("deprecation")
List<ServiceInfo> getMediaRoute2ProviderServices() {
Intent intent = new Intent(MediaRoute2ProviderService.SERVICE_INTERFACE);
diff --git a/navigation/navigation-fragment/api/api_lint.ignore b/navigation/navigation-fragment/api/api_lint.ignore
index ff18170..b57faea 100644
--- a/navigation/navigation-fragment/api/api_lint.ignore
+++ b/navigation/navigation-fragment/api/api_lint.ignore
@@ -3,9 +3,9 @@
Method FragmentNavArgsLazyKt.navArgs appears to be throwing java.lang.IllegalStateException; this should be listed in the documentation; see https://android.github.io/kotlin-guides/interop.html#document-exceptions
-MissingNullability: androidx.navigation.NavGraphViewModelLazyKt#navGraphViewModels(androidx.fragment.app.Fragment, String, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>):
+MissingNullability: androidx.navigation.NavGraphViewModelLazyKt#navGraphViewModels(androidx.fragment.app.Fragment, String, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>):
Missing nullability on method `navGraphViewModels` return
-MissingNullability: androidx.navigation.NavGraphViewModelLazyKt#navGraphViewModels(androidx.fragment.app.Fragment, int, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>):
+MissingNullability: androidx.navigation.NavGraphViewModelLazyKt#navGraphViewModels(androidx.fragment.app.Fragment, int, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>):
Missing nullability on method `navGraphViewModels` return
MissingNullability: androidx.navigation.fragment.FragmentNavArgsLazyKt#navArgs(androidx.fragment.app.Fragment):
Missing nullability on method `navArgs` return
diff --git a/navigation/navigation-fragment/api/current.txt b/navigation/navigation-fragment/api/current.txt
index 98ee0fc..52fc882 100644
--- a/navigation/navigation-fragment/api/current.txt
+++ b/navigation/navigation-fragment/api/current.txt
@@ -2,8 +2,10 @@
package androidx.navigation {
public final class NavGraphViewModelLazyKt {
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, @IdRes int navGraphId, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, String navGraphRoute, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, @IdRes int navGraphId, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, @IdRes int navGraphId, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, String navGraphRoute, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, String navGraphRoute, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
}
}
diff --git a/navigation/navigation-fragment/api/public_plus_experimental_current.txt b/navigation/navigation-fragment/api/public_plus_experimental_current.txt
index 98ee0fc..52fc882 100644
--- a/navigation/navigation-fragment/api/public_plus_experimental_current.txt
+++ b/navigation/navigation-fragment/api/public_plus_experimental_current.txt
@@ -2,8 +2,10 @@
package androidx.navigation {
public final class NavGraphViewModelLazyKt {
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, @IdRes int navGraphId, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, String navGraphRoute, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, @IdRes int navGraphId, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, @IdRes int navGraphId, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, String navGraphRoute, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, String navGraphRoute, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
}
}
diff --git a/navigation/navigation-fragment/api/restricted_current.txt b/navigation/navigation-fragment/api/restricted_current.txt
index 98ee0fc..52fc882 100644
--- a/navigation/navigation-fragment/api/restricted_current.txt
+++ b/navigation/navigation-fragment/api/restricted_current.txt
@@ -2,8 +2,10 @@
package androidx.navigation {
public final class NavGraphViewModelLazyKt {
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, @IdRes int navGraphId, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, String navGraphRoute, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, @IdRes int navGraphId, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, @IdRes int navGraphId, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, String navGraphRoute, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM>! navGraphViewModels(androidx.fragment.app.Fragment, String navGraphRoute, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
}
}
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavGraphViewModelLazyTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavGraphViewModelLazyTest.kt
index 70496b2..07ce168f 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavGraphViewModelLazyTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavGraphViewModelLazyTest.kt
@@ -21,13 +21,19 @@
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.fragment.app.testing.withFragment
+import androidx.lifecycle.DEFAULT_ARGS_KEY
import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.SavedStateViewModelFactory
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
+import androidx.lifecycle.viewmodel.CreationExtras
+import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.navigation.NavHostController
import androidx.navigation.Navigation
import androidx.navigation.createGraph
@@ -82,6 +88,30 @@
}
@Test
+ fun vmInitializationRoute() {
+ val scenario = launchFragmentInContainer<TestRouteVMFragment>()
+ navController.setViewModelStore(ViewModelStore())
+ scenario.onFragment { fragment ->
+ Navigation.setViewNavController(fragment.requireView(), navController)
+ }
+ val navGraph = navController.navigatorProvider.navigation(
+ route = "vm_graph",
+ startDestination = "start_destination"
+ ) {
+ test("start_destination")
+ }
+ scenario.withFragment {
+ navController.setGraph(navGraph, null)
+ }
+
+ scenario.onFragment { fragment ->
+ assertThat(fragment.viewModel).isNotNull()
+ assertThat(fragment.savedStateViewModelCE).isNotNull()
+ assertThat(fragment.savedStateViewModelCE.defaultValue).isEqualTo("value")
+ }
+ }
+
+ @Test
fun sameViewModelAcrossFragments() {
with(ActivityScenario.launch(NavGraphActivity::class.java)) {
val navController = withActivity { findNavController(R.id.nav_host_fragment) }
@@ -238,6 +268,9 @@
class TestRouteVMFragment : Fragment() {
val viewModel: TestViewModel by navGraphViewModels("vm_graph")
val savedStateViewModel: TestSavedStateViewModel by navGraphViewModels("vm_graph")
+ val savedStateViewModelCE: TestSavedStateViewModel by navGraphViewModels("vm_graph",
+ extrasProducer = { defaultViewModelCreationExtras }
+ )
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -245,7 +278,19 @@
): View? {
return View(activity)
}
+
+ override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
+ return SavedStateViewModelFactory()
+ }
+
+ override fun getDefaultViewModelCreationExtras(): CreationExtras {
+ val extras = MutableCreationExtras(super.getDefaultViewModelCreationExtras())
+ extras[DEFAULT_ARGS_KEY] = bundleOf("test" to "value")
+ return extras
+ }
}
class TestViewModel : ViewModel()
-class TestSavedStateViewModel(val savedStateHandle: SavedStateHandle) : ViewModel()
+class TestSavedStateViewModel(val savedStateHandle: SavedStateHandle) : ViewModel() {
+ val defaultValue = savedStateHandle.get<String>("test")
+}
diff --git a/navigation/navigation-fragment/src/main/java/androidx/navigation/NavGraphViewModelLazy.kt b/navigation/navigation-fragment/src/main/java/androidx/navigation/NavGraphViewModelLazy.kt
index 08e165e..79eb8ac 100644
--- a/navigation/navigation-fragment/src/main/java/androidx/navigation/NavGraphViewModelLazy.kt
+++ b/navigation/navigation-fragment/src/main/java/androidx/navigation/NavGraphViewModelLazy.kt
@@ -23,11 +23,12 @@
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
+import androidx.lifecycle.viewmodel.CreationExtras
import androidx.navigation.fragment.findNavController
/**
* Returns a property delegate to access a [ViewModel] scoped to a navigation graph present on the
- * {@link NavController} back stack:
+ * [NavController] back stack:
* ```
* class MyFragment : Fragment() {
* val viewmodel: MainViewModel by navGraphViewModels(R.id.main)
@@ -46,7 +47,14 @@
* and an attempt access prior to that will result in an IllegalArgumentException.
*
* @param navGraphId ID of a NavGraph that exists on the [NavController] back stack
+ * @param factoryProducer lambda that will be called during initialization to return
+ * [ViewModelProvider.Factory]. If none is provided, this will use the factory from the
+ * [NavBackStackEntry] referenced by the [navGraphId].
*/
+@Deprecated(
+ "Superseded by navGraphViewModels that takes a CreationExtras producer",
+ level = DeprecationLevel.HIDDEN
+)
@MainThread
public inline fun <reified VM : ViewModel> Fragment.navGraphViewModels(
@IdRes navGraphId: Int,
@@ -60,13 +68,61 @@
}
return createViewModelLazy(
VM::class, storeProducer,
+ { backStackEntry.defaultViewModelCreationExtras },
factoryProducer ?: { backStackEntry.defaultViewModelProviderFactory }
- ) { backStackEntry.defaultViewModelCreationExtras }
+ )
}
/**
* Returns a property delegate to access a [ViewModel] scoped to a navigation graph present on the
- * {@link NavController} back stack:
+ * [NavController] back stack:
+ * ```
+ * class MyFragment : Fragment() {
+ * val viewmodel: MainViewModel by navGraphViewModels(R.id.main)
+ * }
+ * ```
+ *
+ * Custom [ViewModelProvider.Factory] can be defined via [factoryProducer] parameter,
+ * factory returned by it will be used to create [ViewModel]:
+ * ```
+ * class MyFragment : Fragment() {
+ * val viewmodel: MainViewModel by navGraphViewModels(R.id.main) { myFactory }
+ * }
+ * ```
+ *
+ * This property can be accessed only after this NavGraph is on the NavController back stack,
+ * and an attempt access prior to that will result in an IllegalArgumentException.
+ *
+ * @param navGraphId ID of a NavGraph that exists on the [NavController] back stack
+ * @param extrasProducer lambda that will be called during initialization to return
+ * [CreationExtras]. If none is provided, this will use the extras from the [NavBackStackEntry]
+ * referenced by the [navGraphId].
+ * @param factoryProducer lambda that will be called during initialization to return
+ * [ViewModelProvider.Factory]. If none is provided, this will use the factory from the
+ * [NavBackStackEntry] referenced by the [navGraphId].
+ */
+@MainThread
+public inline fun <reified VM : ViewModel> Fragment.navGraphViewModels(
+ @IdRes navGraphId: Int,
+ noinline extrasProducer: (() -> CreationExtras)? = null,
+ noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
+): Lazy<VM> {
+ val backStackEntry by lazy {
+ findNavController().getBackStackEntry(navGraphId)
+ }
+ val storeProducer: () -> ViewModelStore = {
+ backStackEntry.viewModelStore
+ }
+ return createViewModelLazy(
+ VM::class, storeProducer,
+ { extrasProducer?.invoke() ?: backStackEntry.defaultViewModelCreationExtras },
+ factoryProducer ?: { backStackEntry.defaultViewModelProviderFactory }
+ )
+}
+
+/**
+ * Returns a property delegate to access a [ViewModel] scoped to a navigation graph present on the
+ * [NavController] back stack:
* ```
* class MyFragment : Fragment() {
* val viewModel: MainViewModel by navGraphViewModels("main")
@@ -87,7 +143,14 @@
* @param navGraphRoute [NavDestination.route] of a NavGraph that exists on the [NavController]
* back stack. If a [NavDestination] with the given route does not exist on the back stack, an
* [IllegalArgumentException] will be thrown.
+ * @param factoryProducer lambda that will be called during initialization to return
+ * [ViewModelProvider.Factory]. If none is provided, this will use the factory from the
+ * [NavBackStackEntry] referenced by the [navGraphRoute].
*/
+@Deprecated(
+ "Superseded by navGraphViewModels that takes a CreationExtras producer",
+ level = DeprecationLevel.HIDDEN
+)
@MainThread
public inline fun <reified VM : ViewModel> Fragment.navGraphViewModels(
navGraphRoute: String,
@@ -101,6 +164,56 @@
}
return createViewModelLazy(
VM::class, storeProducer,
+ { backStackEntry.defaultViewModelCreationExtras },
factoryProducer ?: { backStackEntry.defaultViewModelProviderFactory }
- ) { backStackEntry.defaultViewModelCreationExtras }
-}
\ No newline at end of file
+ )
+}
+
+/**
+ * Returns a property delegate to access a [ViewModel] scoped to a navigation graph present on the
+ * [NavController] back stack:
+ * ```
+ * class MyFragment : Fragment() {
+ * val viewModel: MainViewModel by navGraphViewModels("main")
+ * }
+ * ```
+ *
+ * Custom [ViewModelProvider.Factory] can be defined via [factoryProducer] parameter,
+ * factory returned by it will be used to create [ViewModel]:
+ * ```
+ * class MyFragment : Fragment() {
+ * val viewModel: MainViewModel by navGraphViewModels("main") { myFactory }
+ * }
+ * ```
+ *
+ * This property can be accessed only after this NavGraph is on the NavController back stack,
+ * and an attempt access prior to that will result in an IllegalArgumentException.
+ *
+ * @param navGraphRoute [NavDestination.route] of a NavGraph that exists on the [NavController]
+ * back stack. If a [NavDestination] with the given route does not exist on the back stack, an
+ * [IllegalArgumentException] will be thrown.
+ * @param extrasProducer lambda that will be called during initialization to return
+ * [CreationExtras]. If none is provided, this will use the extras from the [NavBackStackEntry]
+ * referenced by the [navGraphRoute].
+ * @param factoryProducer lambda that will be called during initialization to return
+ * [ViewModelProvider.Factory]. If none is provided, this will use the factory from the
+ * [NavBackStackEntry] referenced by the [navGraphRoute].
+ */
+@MainThread
+public inline fun <reified VM : ViewModel> Fragment.navGraphViewModels(
+ navGraphRoute: String,
+ noinline extrasProducer: (() -> CreationExtras)? = null,
+ noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
+): Lazy<VM> {
+ val backStackEntry by lazy {
+ findNavController().getBackStackEntry(navGraphRoute)
+ }
+ val storeProducer: () -> ViewModelStore = {
+ backStackEntry.viewModelStore
+ }
+ return createViewModelLazy(
+ VM::class, storeProducer,
+ { extrasProducer?.invoke() ?: backStackEntry.defaultViewModelCreationExtras },
+ factoryProducer ?: { backStackEntry.defaultViewModelProviderFactory }
+ )
+}
diff --git a/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt b/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
index 83a768a..f6fe2f6 100644
--- a/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
+++ b/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
@@ -32,6 +32,7 @@
import java.io.File
import java.util.Locale
import javax.inject.Inject
+import org.jetbrains.kotlin.gradle.utils.property
private const val PLUGIN_DIRNAME = "navigation-args"
internal const val GENERATED_PATH = "generated/source/$PLUGIN_DIRNAME"
@@ -102,7 +103,18 @@
providerFactory.provider { variant.applicationId }
}
)
- task.rFilePackage.set(variant.rFilePackage())
+ val rPackage = variant.rFilePackage(project)
+ task.rFilePackage.set(
+ // If a package name is available we use that by default to ensure we
+ // continue to support different productFlavors
+ if (rPackage.get().isNotEmpty()) {
+ rPackage
+ } else {
+ // otherwise, we fall back to the applicationId set on the task to ensure
+ // we support namespaces as well.
+ task.applicationId
+ }
+ )
task.navigationFiles.setFrom(navigationFiles(variant, project))
task.outputDir.set(File(project.buildDir, "$GENERATED_PATH/${variant.dirName}"))
task.incrementalFolder.set(File(project.buildDir, "$INCREMENTAL_PATH/${task.name}"))
@@ -124,12 +136,16 @@
}
@Suppress("DEPRECATION") // For BaseVariant should be replaced in later studio versions
- private fun com.android.build.gradle.api.BaseVariant.rFilePackage() = providerFactory.provider {
+ private fun com.android.build.gradle.api.BaseVariant.rFilePackage(
+ project: Project
+ ): Provider<String> = project.objects.property(String::class.java).apply {
val mainSourceSet = sourceSets.find { it.name == "main" }
val sourceSet = mainSourceSet ?: sourceSets[0]
val manifest = sourceSet.manifestFile
val parsed = XmlSlurper(false, false).parse(manifest)
- parsed.getProperty("@package").toString()
+ set(parsed.getProperty("@package").toString())
+ disallowChanges()
+ finalizeValueOnRead()
}
@Suppress("DEPRECATION") // For BaseVariant should be replaced in later studio versions
diff --git a/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt b/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt
index cc612c6..5e42841 100644
--- a/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt
+++ b/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt
@@ -89,6 +89,9 @@
}
""".trimIndent(),
suffix = """
+ android {
+ namespace 'androidx.navigation.testapp'
+ }
dependencies {
implementation "${projectSetup.props.navigationRuntime}"
}
@@ -134,6 +137,9 @@
}
""".trimIndent(),
suffix = """
+ android {
+ namespace 'androidx.navigation.testapp'
+ }
dependencies {
implementation "${projectSetup.props.kotlinStblib}"
implementation "${projectSetup.props.navigationRuntime}"
diff --git a/navigation/navigation-safe-args-gradle-plugin/src/test/test-data/app-project-kotlin/src/main/AndroidManifest.xml b/navigation/navigation-safe-args-gradle-plugin/src/test/test-data/app-project-kotlin/src/main/AndroidManifest.xml
index 8cf7a88..ec8ad26 100644
--- a/navigation/navigation-safe-args-gradle-plugin/src/test/test-data/app-project-kotlin/src/main/AndroidManifest.xml
+++ b/navigation/navigation-safe-args-gradle-plugin/src/test/test-data/app-project-kotlin/src/main/AndroidManifest.xml
@@ -14,6 +14,5 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.navigation.testapp">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
diff --git a/paging/integration-tests/testapp/build.gradle b/paging/integration-tests/testapp/build.gradle
index d5e19f0..bb064965 100644
--- a/paging/integration-tests/testapp/build.gradle
+++ b/paging/integration-tests/testapp/build.gradle
@@ -61,3 +61,9 @@
freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
}
}
+
+// Enable parameter names to support Room incremental when its a project() dep.
+// See b/198431380
+tasks.withType(JavaCompile) {
+ options.compilerArgs << "-parameters"
+}
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index 0329de3..ff1bc3f 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -25,7 +25,7 @@
kotlin.code.style=official
# Disable docs
androidx.enableDocumentation=false
-androidx.playground.snapshotBuildId=8145911
+androidx.playground.snapshotBuildId=8187875
androidx.playground.metalavaBuildId=8073933
androidx.playground.dokkaBuildId=7472101
androidx.studio.type=playground
diff --git a/profileinstaller/profileinstaller/api/current.txt b/profileinstaller/profileinstaller/api/current.txt
index 4fb75af..4d5e335 100644
--- a/profileinstaller/profileinstaller/api/current.txt
+++ b/profileinstaller/profileinstaller/api/current.txt
@@ -5,6 +5,7 @@
ctor public ProfileInstallReceiver();
method public void onReceive(android.content.Context, android.content.Intent?);
field public static final String ACTION_INSTALL_PROFILE = "androidx.profileinstaller.action.INSTALL_PROFILE";
+ field public static final String ACTION_SKIP_FILE = "androidx.profileinstaller.action.SKIP_FILE";
}
public class ProfileInstaller {
@@ -16,7 +17,9 @@
field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
field public static final int RESULT_BASELINE_PROFILE_NOT_FOUND = 6; // 0x6
+ field public static final int RESULT_DELETE_SKIP_FILE_SUCCESS = 11; // 0xb
field public static final int RESULT_DESIRED_FORMAT_UNSUPPORTED = 5; // 0x5
+ field public static final int RESULT_INSTALL_SKIP_FILE_SUCCESS = 10; // 0xa
field public static final int RESULT_INSTALL_SUCCESS = 1; // 0x1
field public static final int RESULT_IO_EXCEPTION = 7; // 0x7
field public static final int RESULT_META_FILE_REQUIRED_BUT_NOT_FOUND = 9; // 0x9
diff --git a/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt b/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
index 4fb75af..4d5e335 100644
--- a/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
+++ b/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
@@ -5,6 +5,7 @@
ctor public ProfileInstallReceiver();
method public void onReceive(android.content.Context, android.content.Intent?);
field public static final String ACTION_INSTALL_PROFILE = "androidx.profileinstaller.action.INSTALL_PROFILE";
+ field public static final String ACTION_SKIP_FILE = "androidx.profileinstaller.action.SKIP_FILE";
}
public class ProfileInstaller {
@@ -16,7 +17,9 @@
field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
field public static final int RESULT_BASELINE_PROFILE_NOT_FOUND = 6; // 0x6
+ field public static final int RESULT_DELETE_SKIP_FILE_SUCCESS = 11; // 0xb
field public static final int RESULT_DESIRED_FORMAT_UNSUPPORTED = 5; // 0x5
+ field public static final int RESULT_INSTALL_SKIP_FILE_SUCCESS = 10; // 0xa
field public static final int RESULT_INSTALL_SUCCESS = 1; // 0x1
field public static final int RESULT_IO_EXCEPTION = 7; // 0x7
field public static final int RESULT_META_FILE_REQUIRED_BUT_NOT_FOUND = 9; // 0x9
diff --git a/profileinstaller/profileinstaller/api/restricted_current.txt b/profileinstaller/profileinstaller/api/restricted_current.txt
index 4fb75af..4d5e335 100644
--- a/profileinstaller/profileinstaller/api/restricted_current.txt
+++ b/profileinstaller/profileinstaller/api/restricted_current.txt
@@ -5,6 +5,7 @@
ctor public ProfileInstallReceiver();
method public void onReceive(android.content.Context, android.content.Intent?);
field public static final String ACTION_INSTALL_PROFILE = "androidx.profileinstaller.action.INSTALL_PROFILE";
+ field public static final String ACTION_SKIP_FILE = "androidx.profileinstaller.action.SKIP_FILE";
}
public class ProfileInstaller {
@@ -16,7 +17,9 @@
field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
field public static final int RESULT_BASELINE_PROFILE_NOT_FOUND = 6; // 0x6
+ field public static final int RESULT_DELETE_SKIP_FILE_SUCCESS = 11; // 0xb
field public static final int RESULT_DESIRED_FORMAT_UNSUPPORTED = 5; // 0x5
+ field public static final int RESULT_INSTALL_SKIP_FILE_SUCCESS = 10; // 0xa
field public static final int RESULT_INSTALL_SUCCESS = 1; // 0x1
field public static final int RESULT_IO_EXCEPTION = 7; // 0x7
field public static final int RESULT_META_FILE_REQUIRED_BUT_NOT_FOUND = 9; // 0x9
diff --git a/profileinstaller/profileinstaller/src/main/AndroidManifest.xml b/profileinstaller/profileinstaller/src/main/AndroidManifest.xml
index 04b8330..bd17478 100644
--- a/profileinstaller/profileinstaller/src/main/AndroidManifest.xml
+++ b/profileinstaller/profileinstaller/src/main/AndroidManifest.xml
@@ -35,6 +35,9 @@
<intent-filter>
<action android:name="androidx.profileinstaller.action.INSTALL_PROFILE" />
</intent-filter>
+ <intent-filter>
+ <action android:name="androidx.profileinstaller.action.SKIP_FILE" />
+ </intent-filter>
</receiver>
</application>
</manifest>
\ No newline at end of file
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstallReceiver.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstallReceiver.java
index 2532545..6de83a3 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstallReceiver.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstallReceiver.java
@@ -19,6 +19,7 @@
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -34,17 +35,50 @@
*/
public class ProfileInstallReceiver extends BroadcastReceiver {
/**
- * This is the action constant that this broadcast receiver responds to.
+ * This is the action constant that this broadcast receiver responds to and installs a profile.
*/
public static final @NonNull String ACTION_INSTALL_PROFILE =
"androidx.profileinstaller.action.INSTALL_PROFILE";
+ /**
+ * This is an action constant which requests that {@link ProfileInstaller} manipulate the
+ * skip file used during profile installation. This is only useful when the app is being
+ * instrumented when using Jetpack Macrobenchmarks.
+ */
+ public static final @NonNull String ACTION_SKIP_FILE =
+ "androidx.profileinstaller.action.SKIP_FILE";
+
+ /**
+ * This is the key in the {@link Bundle} of extras, which provides additional information on
+ * the operation to be performed.
+ */
+ private static final @NonNull String EXTRA_SKIP_FILE_OPERATION = "EXTRA_SKIP_FILE_OPERATION";
+
+ /**
+ * The value that requests that a skip file be written.
+ */
+ private static final @NonNull String EXTRA_SKIP_FILE_OPERATION_WRITE = "WRITE_SKIP_FILE";
+ /**
+ * The value that requests that a skip file be deleted.
+ */
+ private static final @NonNull String EXTRA_SKIP_FILE_OPERATION_DELETE = "DELETE_SKIP_FILE";
+
@Override
public void onReceive(@NonNull Context context, @Nullable Intent intent) {
if (intent == null) return;
- if (!ACTION_INSTALL_PROFILE.equals(intent.getAction())) return;
- ProfileInstaller.writeProfile(context, Runnable::run,
- new ResultDiagnostics(), /* forceWriteProfile */true);
+ String action = intent.getAction();
+ if (ACTION_INSTALL_PROFILE.equals(action)) {
+ ProfileInstaller.writeProfile(context, Runnable::run,
+ new ResultDiagnostics(), /* forceWriteProfile */true);
+ } else if (ACTION_SKIP_FILE.equals(action)) {
+ Bundle extras = intent.getExtras();
+ String operation = extras.getString(EXTRA_SKIP_FILE_OPERATION);
+ if (EXTRA_SKIP_FILE_OPERATION_WRITE.equals(operation)) {
+ ProfileInstaller.writeSkipFile(context, Runnable::run, new ResultDiagnostics());
+ } else if (EXTRA_SKIP_FILE_OPERATION_DELETE.equals(operation)) {
+ ProfileInstaller.deleteSkipFile(context, Runnable::run, new ResultDiagnostics());
+ }
+ }
}
class ResultDiagnostics implements ProfileInstaller.DiagnosticsCallback {
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
index 4312dd1..83ca965 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
@@ -159,6 +159,10 @@
break;
case RESULT_PARSE_EXCEPTION: msg = "RESULT_PARSE_EXCEPTION";
break;
+ case RESULT_INSTALL_SKIP_FILE_SUCCESS: msg = "RESULT_INSTALL_SKIP_FILE_SUCCESS";
+ break;
+ case RESULT_DELETE_SKIP_FILE_SUCCESS: msg = "RESULT_DELETE_SKIP_FILE_SUCCESS";
+ break;
}
switch (code) {
@@ -227,7 +231,9 @@
RESULT_BASELINE_PROFILE_NOT_FOUND,
RESULT_IO_EXCEPTION,
RESULT_PARSE_EXCEPTION,
- RESULT_META_FILE_REQUIRED_BUT_NOT_FOUND
+ RESULT_META_FILE_REQUIRED_BUT_NOT_FOUND,
+ RESULT_INSTALL_SKIP_FILE_SUCCESS,
+ RESULT_DELETE_SKIP_FILE_SUCCESS
})
public @interface ResultCode {}
@@ -288,6 +294,16 @@
@ResultCode public static final int RESULT_META_FILE_REQUIRED_BUT_NOT_FOUND = 9;
/**
+ * Indicates that a skip file was successfully written and profile installation will be skipped.
+ */
+ @ResultCode public static final int RESULT_INSTALL_SKIP_FILE_SUCCESS = 10;
+
+ /**
+ * Indicates that a skip file was successfully deleted and profile installation will resume.
+ */
+ @ResultCode public static final int RESULT_DELETE_SKIP_FILE_SUCCESS = 11;
+
+ /**
* Check if we've already installed a profile for this app installation.
*
* @hide
@@ -324,7 +340,11 @@
return result;
}
- static void noteProfileWrittenFor(PackageInfo packageInfo, File appFilesDir) {
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ static void noteProfileWrittenFor(@NonNull PackageInfo packageInfo, @NonNull File appFilesDir) {
File skipFile = new File(appFilesDir, PROFILE_INSTALLER_SKIP_FILE_NAME);
try (DataOutputStream os = new DataOutputStream(new FileOutputStream(skipFile))) {
os.writeLong(packageInfo.lastUpdateTime);
@@ -334,6 +354,15 @@
}
/**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ static boolean deleteProfileWrittenFor(@NonNull File appFilesDir) {
+ File skipFile = new File(appFilesDir, PROFILE_INSTALLER_SKIP_FILE_NAME);
+ return skipFile.delete();
+ }
+
+ /**
* Transcode the source file to an appropriate destination format for this OS version, and
* write it to the ART aot directory.
* @param assets the asset manager to read source file from dexopt/baseline.prof
@@ -342,7 +371,6 @@
* @param filesDir for noting successful installation
* @param apkName The apk file name the profile is targeting
* @param diagnostics The diagnostics callback to pass diagnostics to
- * @return true iff the profile was successfully written
*/
private static void transcodeAndWrite(
@NonNull AssetManager assets,
@@ -468,6 +496,7 @@
*
*/
@WorkerThread
+ @SuppressWarnings("deprecation")
static void writeProfile(
@NonNull Context context,
@NonNull Executor executor,
@@ -494,4 +523,54 @@
diagnostics);
}
}
+
+ /**
+ * Writes a profile installation skip file, which makes {@link ProfileInstaller} skip profile
+ * installation. This is being done so that Macrobenchmarks can request a skip file for
+ * `CompilationMode.None()`, and avoid any interference from {@link ProfileInstaller}.
+ *
+ * @param context context to read assets from
+ * @param diagnostics an object which will receive diagnostic information
+ * @param executor the executor to run the diagnostic events through
+ */
+ @WorkerThread
+ @SuppressWarnings("deprecation")
+ static void writeSkipFile(
+ @NonNull Context context,
+ @NonNull Executor executor,
+ @NonNull DiagnosticsCallback diagnostics
+ ) {
+ Context appContext = context.getApplicationContext();
+ String packageName = appContext.getPackageName();
+ PackageManager packageManager = context.getPackageManager();
+ PackageInfo packageInfo;
+ try {
+ packageInfo = packageManager.getPackageInfo(packageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ result(executor, diagnostics, RESULT_IO_EXCEPTION, e);
+ return;
+ }
+ File filesDir = context.getFilesDir();
+ ProfileInstaller.noteProfileWrittenFor(packageInfo, filesDir);
+ result(executor, diagnostics, RESULT_INSTALL_SKIP_FILE_SUCCESS, null);
+ }
+
+ /**
+ * Deletes a profile installation skip so profile installation can continue after
+ * CompilationMode.None()`.
+ *
+ * @param context context to read assets from
+ * @param diagnostics an object which will receive diagnostic information
+ * @param executor the executor to run the diagnostic events through
+ */
+ @WorkerThread
+ static void deleteSkipFile(
+ @NonNull Context context,
+ @NonNull Executor executor,
+ @NonNull DiagnosticsCallback diagnostics
+ ) {
+ File filesDir = context.getFilesDir();
+ ProfileInstaller.deleteProfileWrittenFor(filesDir);
+ result(executor, diagnostics, RESULT_DELETE_SKIP_FILE_SUCCESS, null);
+ }
}
diff --git a/profileinstaller/profileinstaller/src/test/java/androidx/profileinstaller/ProfileInstallerTest.java b/profileinstaller/profileinstaller/src/test/java/androidx/profileinstaller/ProfileInstallerTest.java
index f04d352..b6b2ca5 100644
--- a/profileinstaller/profileinstaller/src/test/java/androidx/profileinstaller/ProfileInstallerTest.java
+++ b/profileinstaller/profileinstaller/src/test/java/androidx/profileinstaller/ProfileInstallerTest.java
@@ -21,7 +21,6 @@
import android.content.pm.PackageInfo;
import android.os.Build;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -45,7 +44,6 @@
@RunWith(JUnit4.class)
public class ProfileInstallerTest extends TestCase {
- @NonNull
private Path mTmpDir;
@Before
@@ -123,14 +121,37 @@
packageInfo.lastUpdateTime = 5L;
TraceDiagnostics diagnosticsCallback = new TraceDiagnostics();
File appFilesDir = mTmpDir.toFile();
- boolean result = ProfileInstaller.hasAlreadyWrittenProfileForThisInstall(packageInfo,
- appFilesDir, diagnosticsCallback);
-
+ ProfileInstaller.hasAlreadyWrittenProfileForThisInstall(
+ packageInfo,
+ appFilesDir,
+ diagnosticsCallback
+ );
assertThat(diagnosticsCallback.mResults).isEmpty();
}
- class TraceDiagnostics implements ProfileInstaller.DiagnosticsCallback {
+ @Test
+ public void verifySkipFileDeleted() {
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.lastUpdateTime = 5L;
+ TraceDiagnostics diagnosticsCallback = new TraceDiagnostics();
+ File appFilesDir = mTmpDir.toFile();
+ ProfileInstaller.noteProfileWrittenFor(packageInfo, appFilesDir);
+ boolean result = ProfileInstaller.hasAlreadyWrittenProfileForThisInstall(
+ packageInfo,
+ appFilesDir,
+ diagnosticsCallback
+ );
+ assertTrue(result);
+ ProfileInstaller.deleteProfileWrittenFor(appFilesDir);
+ result = ProfileInstaller.hasAlreadyWrittenProfileForThisInstall(
+ packageInfo,
+ appFilesDir,
+ diagnosticsCallback
+ );
+ assertFalse(result);
+ }
+ static class TraceDiagnostics implements ProfileInstaller.DiagnosticsCallback {
List<Integer> mDiagnostics = new ArrayList<>();
List<Integer> mResults = new ArrayList<>();
diff --git a/remotecallback/remotecallback/src/main/java/androidx/remotecallback/CallbackHandlerRegistry.java b/remotecallback/remotecallback/src/main/java/androidx/remotecallback/CallbackHandlerRegistry.java
index d98ae28..bf253c7 100644
--- a/remotecallback/remotecallback/src/main/java/androidx/remotecallback/CallbackHandlerRegistry.java
+++ b/remotecallback/remotecallback/src/main/java/androidx/remotecallback/CallbackHandlerRegistry.java
@@ -72,6 +72,7 @@
}
}
+ @SuppressWarnings("deprecation")
private String determineAuthority(Context context, String authority, Class<?> aClass) {
if (authority != null) {
return authority;
diff --git a/room/integration-tests/testapp/src/main/AndroidManifest.xml b/room/integration-tests/testapp/src/main/AndroidManifest.xml
index 45c55ab..abd3c54 100644
--- a/room/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/room/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -14,8 +14,10 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.room.integration.testapp">
+<manifest
+ package="androidx.room.integration.testapp"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
<application
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="true"
@@ -24,6 +26,7 @@
android:name=".SampleDatabaseService"
android:label="Multi-process SampleDatabase"
android:exported="false"
- android:process="androidx.room.integration.testapp.service"/>
+ android:process="androidx.room.integration.testapp.service"
+ tools:ignore="MissingServiceExportedEqualsTrue" />
</application>
</manifest>
diff --git a/room/room-compiler-processing/OWNERS b/room/room-compiler-processing/OWNERS
new file mode 100644
index 0000000..23a1553
--- /dev/null
+++ b/room/room-compiler-processing/OWNERS
@@ -0,0 +1 @@
[email protected]
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumEntry.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumEntry.kt
index c271190..c0d53adf 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumEntry.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumEntry.kt
@@ -31,6 +31,15 @@
* The parent enum type declaration that holds all entries for this enum type..
*/
override val enclosingElement: XEnumTypeElement
+
+ /**
+ * The parent enum type declaration that holds all entries for this enum type..
+ */
+ @Deprecated(message = "use XEnumEntry#enclosingElement() instead.",
+ replaceWith = ReplaceWith("enclosingElement")
+ )
+ val enumTypeElement: XEnumTypeElement
+ get() = enclosingElement
}
fun XElement.isEnumEntry(): Boolean {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XExecutableParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XExecutableParameterElement.kt
index b4b85df..b5148d4 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XExecutableParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XExecutableParameterElement.kt
@@ -27,6 +27,15 @@
override val enclosingElement: XExecutableElement
/**
+ * The enclosing [XExecutableElement] this parameter belongs to.
+ */
+ @Deprecated(message = "use XExecutableParameterElement#enclosingElement() instead.",
+ replaceWith = ReplaceWith("enclosingElement")
+ )
+ val enclosingMethodElement: XExecutableElement
+ get() = enclosingElement
+
+ /**
* `true` if the parameter has a default value, `false` otherwise.
*
* Note that when @JvmOverloads is used in a kotlin function with KAPT, only the original
diff --git a/room/room-runtime/src/main/AndroidManifest.xml b/room/room-runtime/src/main/AndroidManifest.xml
index a595d1c..5d8771f 100644
--- a/room/room-runtime/src/main/AndroidManifest.xml
+++ b/room/room-runtime/src/main/AndroidManifest.xml
@@ -14,14 +14,17 @@
~ limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.room">
+<manifest
+ package="androidx.room"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
<application>
<service
- android:name=".MultiInstanceInvalidationService"
- android:directBootAware="true"
- android:exported="false"/>
+ android:name=".MultiInstanceInvalidationService"
+ android:directBootAware="true"
+ android:exported="false"
+ tools:ignore="MissingServiceExportedEqualsTrue" />
</application>
</manifest>
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/Support4Demos.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/Support4Demos.java
index 9d9a3ca..5a90e7f 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/Support4Demos.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/Support4Demos.java
@@ -52,6 +52,7 @@
getListView().setTextFilterEnabled(true);
}
+ @SuppressWarnings("deprecation")
protected List<Map<String, Object>> getData(String prefix) {
List<Map<String, Object>> myData = new ArrayList<Map<String, Object>>();
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/PackageValidator.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/PackageValidator.java
index 0c24101..368cd2f 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/PackageValidator.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/PackageValidator.java
@@ -90,7 +90,7 @@
/**
* @return false if the caller is not authorized to get data from this MediaBrowserService
*/
- @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ @SuppressWarnings({"BooleanMethodIsAlwaysInverted", "deprecation"})
public boolean isCallerAllowed(Context context, String callingPackage, int callingUid) {
// Always allow calls from the framework, self app or development environment.
if (Process.SYSTEM_UID == callingUid || Process.myUid() == callingUid) {
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/utils/ResourceHelper.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/utils/ResourceHelper.java
index 2083b0c..233e85f 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/utils/ResourceHelper.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/utils/ResourceHelper.java
@@ -33,7 +33,7 @@
* @param defaultColor default to use.
* @return color value
*/
- @SuppressWarnings("CatchAndPrintStackTrace")
+ @SuppressWarnings({"CatchAndPrintStackTrace", "deprecation"})
public static int getThemeColor(Context context, int attribute, int defaultColor) {
int themeColor = 0;
String packageName = context.getPackageName();
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/Support7Demos.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/Support7Demos.java
index 3feb3f3..2223ccf 100644
--- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/Support7Demos.java
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/Support7Demos.java
@@ -52,6 +52,7 @@
getListView().setTextFilterEnabled(true);
}
+ @SuppressWarnings("deprecation")
protected List<Map<String, Object>> getData(String prefix) {
List<Map<String, Object>> myData = new ArrayList<Map<String, Object>>();
diff --git a/samples/SupportAnimationDemos/src/main/java/com/example/android/support/animation/BrowseActivity.java b/samples/SupportAnimationDemos/src/main/java/com/example/android/support/animation/BrowseActivity.java
index 4065625..a44ff7a 100644
--- a/samples/SupportAnimationDemos/src/main/java/com/example/android/support/animation/BrowseActivity.java
+++ b/samples/SupportAnimationDemos/src/main/java/com/example/android/support/animation/BrowseActivity.java
@@ -55,6 +55,7 @@
getListView().setTextFilterEnabled(true);
}
+ @SuppressWarnings("deprecation")
protected List<Map<String, Object>> getData(String prefix) {
List<Map<String, Object>> myData = new ArrayList<Map<String, Object>>();
diff --git a/samples/SupportLeanbackDemos/src/main/AndroidManifest.xml b/samples/SupportLeanbackDemos/src/main/AndroidManifest.xml
index ecd3b3e..5838c3f 100644
--- a/samples/SupportLeanbackDemos/src/main/AndroidManifest.xml
+++ b/samples/SupportLeanbackDemos/src/main/AndroidManifest.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
package="com.example.android.leanback"
android:versionCode="1"
android:versionName="1.0">
@@ -204,7 +205,10 @@
android:name=".MusicExampleActivity"
android:exported="true"/>
- <service android:exported="false" android:name=".MediaSessionService"/>
+ <service
+ android:exported="false"
+ android:name=".MediaSessionService"
+ tools:ignore="MissingServiceExportedEqualsTrue" />
<activity
android:name=".DatePickerActivity"
diff --git a/samples/SupportPreferenceDemos/src/main/java/com/example/androidx/preference/MainActivity.java b/samples/SupportPreferenceDemos/src/main/java/com/example/androidx/preference/MainActivity.java
index 05aaf32..f75c81e 100644
--- a/samples/SupportPreferenceDemos/src/main/java/com/example/androidx/preference/MainActivity.java
+++ b/samples/SupportPreferenceDemos/src/main/java/com/example/androidx/preference/MainActivity.java
@@ -58,6 +58,7 @@
}
@NonNull
+ @SuppressWarnings("deprecation")
protected List<Map<String, Object>> getActivityList() {
List<Map<String, Object>> activityList = new ArrayList<>();
diff --git a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceSelectionDialog.java b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceSelectionDialog.java
index 46c3792..f793a7a 100644
--- a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceSelectionDialog.java
+++ b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceSelectionDialog.java
@@ -148,6 +148,7 @@
}).start();
}
+ @SuppressWarnings("deprecation")
private static void showSliceList(Context context, Consumer<Uri> selectedCallback,
ProviderInfo provider, String label) {
ProgressDialog dialog = ProgressDialog.show(context, null, "Loading...");
diff --git a/samples/SupportTransitionDemos/src/main/java/com/example/android/support/transition/SupportTransitionDemos.java b/samples/SupportTransitionDemos/src/main/java/com/example/android/support/transition/SupportTransitionDemos.java
index 39b36f5..3d08f1e 100644
--- a/samples/SupportTransitionDemos/src/main/java/com/example/android/support/transition/SupportTransitionDemos.java
+++ b/samples/SupportTransitionDemos/src/main/java/com/example/android/support/transition/SupportTransitionDemos.java
@@ -52,6 +52,7 @@
getListView().setTextFilterEnabled(true);
}
+ @SuppressWarnings("deprecation")
protected List<Map<String, Object>> getData(String prefix) {
List<Map<String, Object>> myData = new ArrayList<>();
diff --git a/security/security-app-authenticator/src/androidTest/java/androidx/security/app/authenticator/AppAuthenticatorUtilsTest.java b/security/security-app-authenticator/src/androidTest/java/androidx/security/app/authenticator/AppAuthenticatorUtilsTest.java
index 22b8025..f03d98f 100644
--- a/security/security-app-authenticator/src/androidTest/java/androidx/security/app/authenticator/AppAuthenticatorUtilsTest.java
+++ b/security/security-app-authenticator/src/androidTest/java/androidx/security/app/authenticator/AppAuthenticatorUtilsTest.java
@@ -79,6 +79,7 @@
}
@Test
+ @SuppressWarnings("deprecation")
public void getUidForPackage_returnsExpectedUid() throws Exception {
// The AppAuthenticatorUtils provides an instance method to obtain the UID of the
// specified package to facilitate tests; this test verifies a base AppAuthenticatorUtils
diff --git a/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppAuthenticatorUtils.java b/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppAuthenticatorUtils.java
index 210789f..cdeae4f 100644
--- a/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppAuthenticatorUtils.java
+++ b/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppAuthenticatorUtils.java
@@ -70,6 +70,7 @@
*
* @see ApplicationInfo#uid
*/
+ @SuppressWarnings("deprecation")
int getUidForPackage(String packageName) throws PackageManager.NameNotFoundException {
ApplicationInfo appInfo = mContext.getPackageManager().getApplicationInfo(packageName, 0);
return appInfo.uid;
diff --git a/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppSignatureVerifier.java b/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppSignatureVerifier.java
index 3df7674..29321cc 100644
--- a/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppSignatureVerifier.java
+++ b/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppSignatureVerifier.java
@@ -312,6 +312,7 @@
* @throws AppSignatureVerifierException if the specified package is not found, or if the
* {@code SigningInfo} is not returned for the package.
*/
+ @SuppressWarnings("deprecation")
static AppSigningInfo getAppSigningInfo(PackageManager packageManager,
String packageName) throws AppSignatureVerifierException {
PackageInfo packageInfo;
diff --git a/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppSignatureVerifierTest.java b/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppSignatureVerifierTest.java
index 1854c03..31a1e90 100644
--- a/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppSignatureVerifierTest.java
+++ b/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppSignatureVerifierTest.java
@@ -173,6 +173,7 @@
}
@Test
+ @SuppressWarnings("deprecation")
public void verifySigningIdentity_unknownPackageName() throws Exception {
// When a package name is specified that is not on the device the #getPackageInfo call
// should result in a NameNotFoundException; when this is caught the verifier should
@@ -553,6 +554,7 @@
return this;
}
+ @SuppressWarnings("deprecation")
private AppSignatureVerifier build() throws Exception {
if (mCurrentSigners.isEmpty()) {
throw new IllegalArgumentException("At least one current signer must be specified");
diff --git a/settings.gradle b/settings.gradle
index 9bf36a2..72bd6a5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -434,6 +434,7 @@
includeProject(":compose:ui:ui-test-manifest", "compose/ui/ui-test-manifest", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-test-manifest:integration-tests:testapp", "compose/ui/ui-test-manifest/integration-tests/testapp", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-text", "compose/ui/ui-text", [BuildType.COMPOSE])
+includeProject(":compose:ui:ui-text-google-fonts", "compose/ui/ui-text-google-fonts", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-text:ui-text-benchmark", "compose/ui/ui-text/benchmark", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-text:ui-text-samples", "compose/ui/ui-text/samples", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-tooling", "compose/ui/ui-tooling", [BuildType.COMPOSE])
diff --git a/sharetarget/sharetarget/src/main/java/androidx/sharetarget/ShareTargetXmlParser.java b/sharetarget/sharetarget/src/main/java/androidx/sharetarget/ShareTargetXmlParser.java
index ed99123..7ff445d 100644
--- a/sharetarget/sharetarget/src/main/java/androidx/sharetarget/ShareTargetXmlParser.java
+++ b/sharetarget/sharetarget/src/main/java/androidx/sharetarget/ShareTargetXmlParser.java
@@ -82,6 +82,7 @@
/* Hide the constructor */
}
+ @SuppressWarnings("deprecation")
private static ArrayList<ShareTargetCompat> parseShareTargets(Context context) {
ArrayList<ShareTargetCompat> targets = new ArrayList<>();
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java b/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java
index 68a80dc..8a72034 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java
@@ -384,6 +384,7 @@
* Get string describing permission request.
*/
@RequiresApi(19)
+ @SuppressWarnings("deprecation")
private static CharSequence getPermissionString(Context context, String callingPackage) {
PackageManager pm = context.getPackageManager();
try {
diff --git a/slice/slice-core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java b/slice/slice-core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
index bfd3368..a659a18 100644
--- a/slice/slice-core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
+++ b/slice/slice-core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
@@ -56,6 +56,7 @@
private AlertDialog mDialog;
@Override
+ @SuppressWarnings("deprecation")
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
diff --git a/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderCompat.java b/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
index 6da7d06..547c821 100644
--- a/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
+++ b/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
@@ -377,6 +377,7 @@
* Compat version of {@link Slice#bindSlice}.
*/
@Nullable
+ @SuppressWarnings("deprecation")
public static Slice bindSlice(@NonNull Context context, @NonNull Intent intent,
@NonNull Set<SliceSpec> supportedSpecs) {
Preconditions.checkNotNull(intent, "intent");
@@ -540,6 +541,7 @@
* Compat version of {@link android.app.slice.SliceManager#mapIntentToUri}.
*/
@Nullable
+ @SuppressWarnings("deprecation")
public static Uri mapIntentToUri(@NonNull Context context, @NonNull Intent intent) {
Preconditions.checkNotNull(intent, "intent");
Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
diff --git a/slice/slice-view/src/androidTest/java/androidx/slice/SliceViewManagerTest.java b/slice/slice-view/src/androidTest/java/androidx/slice/SliceViewManagerTest.java
index ca207d4..c276a95 100644
--- a/slice/slice-view/src/androidTest/java/androidx/slice/SliceViewManagerTest.java
+++ b/slice/slice-view/src/androidTest/java/androidx/slice/SliceViewManagerTest.java
@@ -210,6 +210,7 @@
@Test
@SdkSuppress(minSdkVersion = 28)
+ @SuppressWarnings("deprecation")
public void testSuspended() throws PackageManager.NameNotFoundException {
Uri uri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerWrapper.java b/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerWrapper.java
index 1367f4d..5c9989d 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerWrapper.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerWrapper.java
@@ -134,6 +134,7 @@
return isPackageSuspended(pkg);
}
+ @SuppressWarnings("deprecation")
private boolean isPackageSuspended(String pkg) {
Boolean isSuspended = mCachedSuspendFlags.get(pkg);
if (isSuspended == null) {
diff --git a/startup/startup-runtime/src/main/java/androidx/startup/AppInitializer.java b/startup/startup-runtime/src/main/java/androidx/startup/AppInitializer.java
index 1a85102..db4dc87 100644
--- a/startup/startup-runtime/src/main/java/androidx/startup/AppInitializer.java
+++ b/startup/startup-runtime/src/main/java/androidx/startup/AppInitializer.java
@@ -195,6 +195,7 @@
}
}
+ @SuppressWarnings("deprecation")
void discoverAndInitialize() {
try {
Trace.beginSection(SECTION_NAME);
diff --git a/template/template-appwidget/integration-tests/demos/src/main/java/androidx/template/appwidget/demos/TemplateInputActivity.kt b/template/template-appwidget/integration-tests/demos/src/main/java/androidx/template/appwidget/demos/TemplateInputActivity.kt
index a1e817d..a3a8398 100644
--- a/template/template-appwidget/integration-tests/demos/src/main/java/androidx/template/appwidget/demos/TemplateInputActivity.kt
+++ b/template/template-appwidget/integration-tests/demos/src/main/java/androidx/template/appwidget/demos/TemplateInputActivity.kt
@@ -71,6 +71,7 @@
import androidx.template.template.GlanceTemplate
import androidx.template.template.TemplateImageWithDescription
import androidx.template.template.SingleEntityTemplate
+import androidx.template.template.TemplateText
import androidx.template.template.TemplateTextButton
import androidx.template.template.TemplateImageButton
import kotlinx.coroutines.CoroutineScope
@@ -390,16 +391,13 @@
body: String,
background: ColorProvider
) = SingleEntityTemplate.Data(
- header = "Single Entity Example",
- headerIcon = TemplateImageWithDescription(
- ImageProvider(R.drawable.compose),
- "Header icon"
- ),
- title = title,
- subtitle = subtitle,
- bodyText = body,
+ header = TemplateText("Template demo"),
+ headerIcon = TemplateImageWithDescription(ImageProvider(R.drawable.compose), "Header icon"),
+ title = TemplateText(title, 1),
+ subtitle = TemplateText(subtitle, 2),
+ body = TemplateText(body, 3),
button = TemplateTextButton(actionRunCallback<TemplateButtonAction>(), "Apply"),
- mainImage = TemplateImageWithDescription(ImageProvider(R.drawable.compose), "Compose image"),
+ image = TemplateImageWithDescription(ImageProvider(R.drawable.compose), "Compose image"),
backgroundColor = background
)
@@ -409,13 +407,10 @@
background: ColorProvider,
backgroundImage: ImageProvider?
) = FreeformTemplate.Data(
- header = "Freeform Example",
- headerIcon = TemplateImageWithDescription(
- ImageProvider(R.drawable.compose),
- "Header icon"
- ),
- title = title,
- subtitle = subtitle,
+ header = TemplateText("Freeform Example"),
+ headerIcon = TemplateImageWithDescription(ImageProvider(R.drawable.compose), "Header icon"),
+ title = TemplateText(title),
+ subtitle = TemplateText(subtitle),
actionIcon = TemplateImageButton(
actionRunCallback<TemplateButtonAction>(),
TemplateImageWithDescription(ImageProvider(R.drawable.ic_favorite), "Apply")
diff --git a/template/template/api/current.txt b/template/template/api/current.txt
index 2c16bc5..eab29e6 100644
--- a/template/template/api/current.txt
+++ b/template/template/api/current.txt
@@ -9,21 +9,21 @@
}
public static final class FreeformTemplate.Data {
- ctor public FreeformTemplate.Data(androidx.glance.unit.ColorProvider backgroundColor, androidx.template.template.TemplateImageWithDescription headerIcon, androidx.template.template.TemplateImageButton? actionIcon, optional String? header, optional String? title, optional String? subtitle, optional androidx.glance.ImageProvider? backgroundImage);
+ ctor public FreeformTemplate.Data(androidx.glance.unit.ColorProvider backgroundColor, androidx.template.template.TemplateImageWithDescription headerIcon, androidx.template.template.TemplateImageButton? actionIcon, optional androidx.template.template.TemplateText? header, optional androidx.template.template.TemplateText? title, optional androidx.template.template.TemplateText? subtitle, optional androidx.glance.ImageProvider? backgroundImage);
method public androidx.template.template.TemplateImageButton? getActionIcon();
method public androidx.glance.unit.ColorProvider getBackgroundColor();
method public androidx.glance.ImageProvider? getBackgroundImage();
- method public String? getHeader();
+ method public androidx.template.template.TemplateText? getHeader();
method public androidx.template.template.TemplateImageWithDescription getHeaderIcon();
- method public String? getSubtitle();
- method public String? getTitle();
+ method public androidx.template.template.TemplateText? getSubtitle();
+ method public androidx.template.template.TemplateText? getTitle();
property public final androidx.template.template.TemplateImageButton? actionIcon;
property public final androidx.glance.unit.ColorProvider backgroundColor;
property public final androidx.glance.ImageProvider? backgroundImage;
- property public final String? header;
+ property public final androidx.template.template.TemplateText? header;
property public final androidx.template.template.TemplateImageWithDescription headerIcon;
- property public final String? subtitle;
- property public final String? title;
+ property public final androidx.template.template.TemplateText? subtitle;
+ property public final androidx.template.template.TemplateText? title;
}
public abstract class GlanceTemplate<T> {
@@ -45,23 +45,23 @@
}
public static final class SingleEntityTemplate.Data {
- ctor public SingleEntityTemplate.Data(androidx.glance.unit.ColorProvider backgroundColor, androidx.template.template.TemplateImageWithDescription headerIcon, optional String? header, optional String? title, optional String? subtitle, optional String? bodyText, optional androidx.template.template.TemplateButton? button, optional androidx.template.template.TemplateImageWithDescription? mainImage);
- method public androidx.glance.unit.ColorProvider getBackgroundColor();
- method public String? getBodyText();
+ ctor public SingleEntityTemplate.Data(androidx.template.template.TemplateImageWithDescription headerIcon, optional androidx.template.template.TemplateText? header, optional androidx.template.template.TemplateText? title, optional androidx.template.template.TemplateText? subtitle, optional androidx.template.template.TemplateText? body, optional androidx.template.template.TemplateButton? button, optional androidx.template.template.TemplateImageWithDescription? image, optional androidx.glance.unit.ColorProvider? backgroundColor);
+ method public androidx.glance.unit.ColorProvider? getBackgroundColor();
+ method public androidx.template.template.TemplateText? getBody();
method public androidx.template.template.TemplateButton? getButton();
- method public String? getHeader();
+ method public androidx.template.template.TemplateText? getHeader();
method public androidx.template.template.TemplateImageWithDescription getHeaderIcon();
- method public androidx.template.template.TemplateImageWithDescription? getMainImage();
- method public String? getSubtitle();
- method public String? getTitle();
- property public final androidx.glance.unit.ColorProvider backgroundColor;
- property public final String? bodyText;
+ method public androidx.template.template.TemplateImageWithDescription? getImage();
+ method public androidx.template.template.TemplateText? getSubtitle();
+ method public androidx.template.template.TemplateText? getTitle();
+ property public final androidx.glance.unit.ColorProvider? backgroundColor;
+ property public final androidx.template.template.TemplateText? body;
property public final androidx.template.template.TemplateButton? button;
- property public final String? header;
+ property public final androidx.template.template.TemplateText? header;
property public final androidx.template.template.TemplateImageWithDescription headerIcon;
- property public final androidx.template.template.TemplateImageWithDescription? mainImage;
- property public final String? subtitle;
- property public final String? title;
+ property public final androidx.template.template.TemplateImageWithDescription? image;
+ property public final androidx.template.template.TemplateText? subtitle;
+ property public final androidx.template.template.TemplateText? title;
}
public abstract sealed class TemplateButton {
@@ -83,6 +83,16 @@
property public final androidx.glance.ImageProvider image;
}
+ public final class TemplateText {
+ ctor public TemplateText(String text, optional int priority, optional androidx.glance.unit.ColorProvider? color);
+ method public androidx.glance.unit.ColorProvider? getColor();
+ method public int getPriority();
+ method public String getText();
+ property public final androidx.glance.unit.ColorProvider? color;
+ property public final int priority;
+ property public final String text;
+ }
+
public final class TemplateTextButton extends androidx.template.template.TemplateButton {
ctor public TemplateTextButton(androidx.glance.action.Action action, String text);
method public String getText();
diff --git a/template/template/api/public_plus_experimental_current.txt b/template/template/api/public_plus_experimental_current.txt
index 2c16bc5..eab29e6 100644
--- a/template/template/api/public_plus_experimental_current.txt
+++ b/template/template/api/public_plus_experimental_current.txt
@@ -9,21 +9,21 @@
}
public static final class FreeformTemplate.Data {
- ctor public FreeformTemplate.Data(androidx.glance.unit.ColorProvider backgroundColor, androidx.template.template.TemplateImageWithDescription headerIcon, androidx.template.template.TemplateImageButton? actionIcon, optional String? header, optional String? title, optional String? subtitle, optional androidx.glance.ImageProvider? backgroundImage);
+ ctor public FreeformTemplate.Data(androidx.glance.unit.ColorProvider backgroundColor, androidx.template.template.TemplateImageWithDescription headerIcon, androidx.template.template.TemplateImageButton? actionIcon, optional androidx.template.template.TemplateText? header, optional androidx.template.template.TemplateText? title, optional androidx.template.template.TemplateText? subtitle, optional androidx.glance.ImageProvider? backgroundImage);
method public androidx.template.template.TemplateImageButton? getActionIcon();
method public androidx.glance.unit.ColorProvider getBackgroundColor();
method public androidx.glance.ImageProvider? getBackgroundImage();
- method public String? getHeader();
+ method public androidx.template.template.TemplateText? getHeader();
method public androidx.template.template.TemplateImageWithDescription getHeaderIcon();
- method public String? getSubtitle();
- method public String? getTitle();
+ method public androidx.template.template.TemplateText? getSubtitle();
+ method public androidx.template.template.TemplateText? getTitle();
property public final androidx.template.template.TemplateImageButton? actionIcon;
property public final androidx.glance.unit.ColorProvider backgroundColor;
property public final androidx.glance.ImageProvider? backgroundImage;
- property public final String? header;
+ property public final androidx.template.template.TemplateText? header;
property public final androidx.template.template.TemplateImageWithDescription headerIcon;
- property public final String? subtitle;
- property public final String? title;
+ property public final androidx.template.template.TemplateText? subtitle;
+ property public final androidx.template.template.TemplateText? title;
}
public abstract class GlanceTemplate<T> {
@@ -45,23 +45,23 @@
}
public static final class SingleEntityTemplate.Data {
- ctor public SingleEntityTemplate.Data(androidx.glance.unit.ColorProvider backgroundColor, androidx.template.template.TemplateImageWithDescription headerIcon, optional String? header, optional String? title, optional String? subtitle, optional String? bodyText, optional androidx.template.template.TemplateButton? button, optional androidx.template.template.TemplateImageWithDescription? mainImage);
- method public androidx.glance.unit.ColorProvider getBackgroundColor();
- method public String? getBodyText();
+ ctor public SingleEntityTemplate.Data(androidx.template.template.TemplateImageWithDescription headerIcon, optional androidx.template.template.TemplateText? header, optional androidx.template.template.TemplateText? title, optional androidx.template.template.TemplateText? subtitle, optional androidx.template.template.TemplateText? body, optional androidx.template.template.TemplateButton? button, optional androidx.template.template.TemplateImageWithDescription? image, optional androidx.glance.unit.ColorProvider? backgroundColor);
+ method public androidx.glance.unit.ColorProvider? getBackgroundColor();
+ method public androidx.template.template.TemplateText? getBody();
method public androidx.template.template.TemplateButton? getButton();
- method public String? getHeader();
+ method public androidx.template.template.TemplateText? getHeader();
method public androidx.template.template.TemplateImageWithDescription getHeaderIcon();
- method public androidx.template.template.TemplateImageWithDescription? getMainImage();
- method public String? getSubtitle();
- method public String? getTitle();
- property public final androidx.glance.unit.ColorProvider backgroundColor;
- property public final String? bodyText;
+ method public androidx.template.template.TemplateImageWithDescription? getImage();
+ method public androidx.template.template.TemplateText? getSubtitle();
+ method public androidx.template.template.TemplateText? getTitle();
+ property public final androidx.glance.unit.ColorProvider? backgroundColor;
+ property public final androidx.template.template.TemplateText? body;
property public final androidx.template.template.TemplateButton? button;
- property public final String? header;
+ property public final androidx.template.template.TemplateText? header;
property public final androidx.template.template.TemplateImageWithDescription headerIcon;
- property public final androidx.template.template.TemplateImageWithDescription? mainImage;
- property public final String? subtitle;
- property public final String? title;
+ property public final androidx.template.template.TemplateImageWithDescription? image;
+ property public final androidx.template.template.TemplateText? subtitle;
+ property public final androidx.template.template.TemplateText? title;
}
public abstract sealed class TemplateButton {
@@ -83,6 +83,16 @@
property public final androidx.glance.ImageProvider image;
}
+ public final class TemplateText {
+ ctor public TemplateText(String text, optional int priority, optional androidx.glance.unit.ColorProvider? color);
+ method public androidx.glance.unit.ColorProvider? getColor();
+ method public int getPriority();
+ method public String getText();
+ property public final androidx.glance.unit.ColorProvider? color;
+ property public final int priority;
+ property public final String text;
+ }
+
public final class TemplateTextButton extends androidx.template.template.TemplateButton {
ctor public TemplateTextButton(androidx.glance.action.Action action, String text);
method public String getText();
diff --git a/template/template/api/restricted_current.txt b/template/template/api/restricted_current.txt
index 2c16bc5..eab29e6 100644
--- a/template/template/api/restricted_current.txt
+++ b/template/template/api/restricted_current.txt
@@ -9,21 +9,21 @@
}
public static final class FreeformTemplate.Data {
- ctor public FreeformTemplate.Data(androidx.glance.unit.ColorProvider backgroundColor, androidx.template.template.TemplateImageWithDescription headerIcon, androidx.template.template.TemplateImageButton? actionIcon, optional String? header, optional String? title, optional String? subtitle, optional androidx.glance.ImageProvider? backgroundImage);
+ ctor public FreeformTemplate.Data(androidx.glance.unit.ColorProvider backgroundColor, androidx.template.template.TemplateImageWithDescription headerIcon, androidx.template.template.TemplateImageButton? actionIcon, optional androidx.template.template.TemplateText? header, optional androidx.template.template.TemplateText? title, optional androidx.template.template.TemplateText? subtitle, optional androidx.glance.ImageProvider? backgroundImage);
method public androidx.template.template.TemplateImageButton? getActionIcon();
method public androidx.glance.unit.ColorProvider getBackgroundColor();
method public androidx.glance.ImageProvider? getBackgroundImage();
- method public String? getHeader();
+ method public androidx.template.template.TemplateText? getHeader();
method public androidx.template.template.TemplateImageWithDescription getHeaderIcon();
- method public String? getSubtitle();
- method public String? getTitle();
+ method public androidx.template.template.TemplateText? getSubtitle();
+ method public androidx.template.template.TemplateText? getTitle();
property public final androidx.template.template.TemplateImageButton? actionIcon;
property public final androidx.glance.unit.ColorProvider backgroundColor;
property public final androidx.glance.ImageProvider? backgroundImage;
- property public final String? header;
+ property public final androidx.template.template.TemplateText? header;
property public final androidx.template.template.TemplateImageWithDescription headerIcon;
- property public final String? subtitle;
- property public final String? title;
+ property public final androidx.template.template.TemplateText? subtitle;
+ property public final androidx.template.template.TemplateText? title;
}
public abstract class GlanceTemplate<T> {
@@ -45,23 +45,23 @@
}
public static final class SingleEntityTemplate.Data {
- ctor public SingleEntityTemplate.Data(androidx.glance.unit.ColorProvider backgroundColor, androidx.template.template.TemplateImageWithDescription headerIcon, optional String? header, optional String? title, optional String? subtitle, optional String? bodyText, optional androidx.template.template.TemplateButton? button, optional androidx.template.template.TemplateImageWithDescription? mainImage);
- method public androidx.glance.unit.ColorProvider getBackgroundColor();
- method public String? getBodyText();
+ ctor public SingleEntityTemplate.Data(androidx.template.template.TemplateImageWithDescription headerIcon, optional androidx.template.template.TemplateText? header, optional androidx.template.template.TemplateText? title, optional androidx.template.template.TemplateText? subtitle, optional androidx.template.template.TemplateText? body, optional androidx.template.template.TemplateButton? button, optional androidx.template.template.TemplateImageWithDescription? image, optional androidx.glance.unit.ColorProvider? backgroundColor);
+ method public androidx.glance.unit.ColorProvider? getBackgroundColor();
+ method public androidx.template.template.TemplateText? getBody();
method public androidx.template.template.TemplateButton? getButton();
- method public String? getHeader();
+ method public androidx.template.template.TemplateText? getHeader();
method public androidx.template.template.TemplateImageWithDescription getHeaderIcon();
- method public androidx.template.template.TemplateImageWithDescription? getMainImage();
- method public String? getSubtitle();
- method public String? getTitle();
- property public final androidx.glance.unit.ColorProvider backgroundColor;
- property public final String? bodyText;
+ method public androidx.template.template.TemplateImageWithDescription? getImage();
+ method public androidx.template.template.TemplateText? getSubtitle();
+ method public androidx.template.template.TemplateText? getTitle();
+ property public final androidx.glance.unit.ColorProvider? backgroundColor;
+ property public final androidx.template.template.TemplateText? body;
property public final androidx.template.template.TemplateButton? button;
- property public final String? header;
+ property public final androidx.template.template.TemplateText? header;
property public final androidx.template.template.TemplateImageWithDescription headerIcon;
- property public final androidx.template.template.TemplateImageWithDescription? mainImage;
- property public final String? subtitle;
- property public final String? title;
+ property public final androidx.template.template.TemplateImageWithDescription? image;
+ property public final androidx.template.template.TemplateText? subtitle;
+ property public final androidx.template.template.TemplateText? title;
}
public abstract sealed class TemplateButton {
@@ -83,6 +83,16 @@
property public final androidx.glance.ImageProvider image;
}
+ public final class TemplateText {
+ ctor public TemplateText(String text, optional int priority, optional androidx.glance.unit.ColorProvider? color);
+ method public androidx.glance.unit.ColorProvider? getColor();
+ method public int getPriority();
+ method public String getText();
+ property public final androidx.glance.unit.ColorProvider? color;
+ property public final int priority;
+ property public final String text;
+ }
+
public final class TemplateTextButton extends androidx.template.template.TemplateButton {
ctor public TemplateTextButton(androidx.glance.action.Action action, String text);
method public String getText();
diff --git a/template/template/src/main/java/androidx/template/template/FreeformTemplate.kt b/template/template/src/main/java/androidx/template/template/FreeformTemplate.kt
index 223137c..3c821cc 100644
--- a/template/template/src/main/java/androidx/template/template/FreeformTemplate.kt
+++ b/template/template/src/main/java/androidx/template/template/FreeformTemplate.kt
@@ -50,8 +50,8 @@
horizontalAlignment = Alignment.CenterHorizontally
) {
// TODO: Text block ordering
- it.title?.let { title -> Title(title) }
- it.subtitle?.let { subtitle -> Subtitle(subtitle) }
+ it.title?.let { title -> Title(title.text) }
+ it.subtitle?.let { subtitle -> Subtitle(subtitle.text) }
}
Action(it.actionIcon)
}
@@ -65,8 +65,8 @@
horizontalAlignment = Alignment.CenterHorizontally
) {
TemplateHeader(it.headerIcon, it.header)
- it.title?.let { title -> Title(title) }
- it.subtitle?.let { subtitle -> Subtitle(subtitle) }
+ it.title?.let { title -> Title(title.text) }
+ it.subtitle?.let { subtitle -> Subtitle(subtitle.text) }
}
Action(it.actionIcon)
}
@@ -80,8 +80,8 @@
horizontalAlignment = Alignment.CenterHorizontally
) {
TemplateHeader(it.headerIcon, it.header)
- it.title?.let { title -> Title(title) }
- it.subtitle?.let { subtitle -> Subtitle(subtitle) }
+ it.title?.let { title -> Title(title.text) }
+ it.subtitle?.let { subtitle -> Subtitle(subtitle.text) }
}
Action(it.actionIcon)
}
@@ -138,17 +138,17 @@
* @param headerIcon Logo icon, displayed in the glanceable header
* @param actionIcon Action icon button
* @param header Main header text
- * @param title Text section main title
- * @param subtitle Text section subtitle
+ * @param title Text section main title, priority ordered
+ * @param subtitle Text section subtitle, priority ordered
* @param backgroundImage Background image, if set replaces the glanceable background
*/
class Data(
val backgroundColor: ColorProvider,
val headerIcon: TemplateImageWithDescription,
val actionIcon: TemplateImageButton?,
- val header: String? = null,
- val title: String? = null,
- val subtitle: String? = null,
+ val header: TemplateText? = null,
+ val title: TemplateText? = null,
+ val subtitle: TemplateText? = null,
val backgroundImage: ImageProvider? = null,
) {
diff --git a/template/template/src/main/java/androidx/template/template/GlanceTemplate.kt b/template/template/src/main/java/androidx/template/template/GlanceTemplate.kt
index cd74697..ef4ad09 100644
--- a/template/template/src/main/java/androidx/template/template/GlanceTemplate.kt
+++ b/template/template/src/main/java/androidx/template/template/GlanceTemplate.kt
@@ -17,7 +17,6 @@
package androidx.template.template
import androidx.compose.runtime.Composable
-import androidx.glance.ImageProvider
import androidx.glance.action.Action
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -26,16 +25,25 @@
import androidx.glance.layout.Alignment
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
-import androidx.glance.layout.fillMaxWidth
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.TextUnit
+import androidx.glance.Button
+import androidx.glance.ImageProvider
+import androidx.glance.LocalSize
+import androidx.glance.action.clickable
+import androidx.glance.background
+import androidx.glance.layout.Column
import androidx.glance.layout.height
import androidx.glance.layout.width
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
+import androidx.glance.unit.ColorProvider
/** Transforms semantic data into a composable layout for any glanceable */
abstract class GlanceTemplate<T> {
- /** Defines the data associated with this template */
+ /** Defines the data associated with this template. */
abstract fun getData(state: Any?): T
/** Default layout implementation for AppWidget glanceable, at "collapsed" display size. */
@@ -54,12 +62,46 @@
}
/**
+ * Contains the information required to display a string on a template.
+ *
+ * @param text string to be displayed
+ * @param priority the priority order for this string, in orderable blocks
+ * @param color text color
+ */
+public class TemplateText(
+ val text: String,
+ val priority: Int = 0,
+ val color: ColorProvider? = null
+) {
+
+ override fun hashCode(): Int {
+ var result = text.hashCode()
+ result = 31 * result + priority
+ result = 31 * result + (color?.hashCode() ?: 0)
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as TemplateText
+
+ if (text != other.text) return false
+ if (priority != other.priority) return false
+ if (color != other.color) return false
+
+ return true
+ }
+}
+
+/**
* Contains the information required to display an image on a template.
*
* @param image The image to display
* @param description The image description, usually used as alt text
*/
-class TemplateImageWithDescription(val image: ImageProvider, val description: String) {
+public class TemplateImageWithDescription(val image: ImageProvider, val description: String) {
override fun hashCode(): Int = 31 * image.hashCode() + description.hashCode()
@@ -77,7 +119,7 @@
*
* @param action The onClick action
*/
-sealed class TemplateButton(val action: Action) {
+public sealed class TemplateButton(val action: Action) {
override fun hashCode(): Int = action.hashCode()
@@ -95,7 +137,7 @@
* @param action The onClick action
* @param text The button display text
*/
-class TemplateTextButton(action: Action, val text: String) : TemplateButton(action) {
+public class TemplateTextButton(action: Action, val text: String) : TemplateButton(action) {
override fun hashCode(): Int = 31 * super.hashCode() + text.hashCode()
@@ -114,7 +156,7 @@
* @param action The onClick action
* @param image The button image
*/
-class TemplateImageButton(
+public class TemplateImageButton(
action: Action,
val image: TemplateImageWithDescription
) : TemplateButton(action) {
@@ -140,10 +182,10 @@
@Composable
internal fun TemplateHeader(
headerIcon: TemplateImageWithDescription,
- header: String? = null
+ header: TemplateText?
) {
Row(
- modifier = GlanceModifier.fillMaxWidth(),
+ modifier = GlanceModifier.background(Color.Transparent),
verticalAlignment = Alignment.CenterVertically
) {
Image(
@@ -153,12 +195,132 @@
)
header?.let {
Spacer(modifier = GlanceModifier.width(8.dp))
- // TODO: Text color customization
+ val size =
+ textSize(TemplateTextType.Title, DisplaySize.fromDpSize(LocalSize.current))
+
Text(
- text = header,
- style = TextStyle(fontSize = 20.sp),
+ modifier = GlanceModifier.defaultWeight(),
+ text = header.text,
+ style = TextStyle(fontSize = size, color = it.color),
maxLines = 1
)
}
}
}
+
+/**
+ * Default glanceable text section layout. Displays a list of reorderable text fields, ordered by
+ * priority and styled according to the [TemplateTextType] of each field.
+ *
+ * @param textList the list of text fields to display in the block
+ */
+@Composable
+internal fun TextSection(textList: List<TypedTemplateText>) {
+ if (textList.isEmpty()) return
+
+ val sorted = textList.sortedBy { it.text.priority }
+ Column(modifier = GlanceModifier.background(Color.Transparent)) {
+ sorted.forEach {
+ // TODO: Spacing
+ val size = textSize(it.textType, DisplaySize.fromDpSize(LocalSize.current))
+ Text(
+ it.text.text,
+ style = TextStyle(fontSize = size, color = it.text.color),
+ maxLines = maxLines(it.textType),
+ modifier = GlanceModifier.background(Color.Transparent)
+ )
+ }
+ }
+}
+
+/**
+ * Displays a [TemplateButton]
+ */
+@Composable
+internal fun TemplateButton(button: TemplateButton) {
+ when (button) {
+ is TemplateImageButton -> {
+ // TODO: Specify sizing for image button
+ val image = button.image
+ Image(
+ provider = image.image,
+ contentDescription = image.description,
+ modifier = GlanceModifier.clickable(button.action)
+ )
+ }
+ is TemplateTextButton -> {
+ Button(text = button.text, onClick = button.action)
+ }
+ }
+}
+
+/**
+ * A [TemplateText] tagged with the [TemplateTextType] of the field. Templates can use the
+ * text type to assign appropriate styling.
+ *
+ * @param text base text field
+ * @param textType text field type type
+ */
+internal data class TypedTemplateText(val text: TemplateText, val textType: TemplateTextType)
+
+/**
+ * The text types that can be used with templates
+ */
+internal enum class TemplateTextType {
+ Display,
+ Title,
+ Label,
+ Body
+}
+
+private enum class DisplaySize {
+ Small,
+ Medium,
+ Large;
+
+ companion object {
+ fun fromDpSize(dpSize: DpSize): DisplaySize =
+ if (dpSize.width < 180.dp && dpSize.height < 120.dp) {
+ Small
+ } else if (dpSize.width < 280.dp && dpSize.height < 180.dp) {
+ Medium
+ } else {
+ Large
+ }
+ }
+}
+
+private fun textSize(textClass: TemplateTextType, displaySize: DisplaySize): TextUnit =
+ when (textClass) {
+ // TODO: Does display scale?
+ TemplateTextType.Display -> 45.sp
+ TemplateTextType.Title -> {
+ when (displaySize) {
+ DisplaySize.Small -> 14.sp
+ DisplaySize.Medium -> 16.sp
+ DisplaySize.Large -> 22.sp
+ }
+ }
+ TemplateTextType.Body -> {
+ when (displaySize) {
+ DisplaySize.Small -> 12.sp
+ DisplaySize.Medium -> 14.sp
+ DisplaySize.Large -> 14.sp
+ }
+ }
+ TemplateTextType.Label -> {
+ when (displaySize) {
+ DisplaySize.Small -> 11.sp
+ DisplaySize.Medium -> 12.sp
+ DisplaySize.Large -> 14.sp
+ }
+ }
+ }
+
+private fun maxLines(textClass: TemplateTextType): Int =
+ when (textClass) {
+ TemplateTextType.Display -> 1
+ TemplateTextType.Title -> 3
+ TemplateTextType.Body -> 3
+ TemplateTextType.Label -> 1
+ }
diff --git a/template/template/src/main/java/androidx/template/template/SingleEntityTemplate.kt b/template/template/src/main/java/androidx/template/template/SingleEntityTemplate.kt
index a210b9d..5fb4e90 100644
--- a/template/template/src/main/java/androidx/template/template/SingleEntityTemplate.kt
+++ b/template/template/src/main/java/androidx/template/template/SingleEntityTemplate.kt
@@ -16,23 +16,24 @@
package androidx.template.template
-import android.util.Log
import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.glance.Button
import androidx.glance.GlanceModifier
import androidx.glance.Image
-import androidx.glance.action.clickable
+import androidx.glance.LocalSize
import androidx.glance.background
import androidx.glance.currentState
import androidx.glance.layout.Column
+import androidx.glance.layout.ContentScale
import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxHeight
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.fillMaxWidth
+import androidx.glance.layout.height
import androidx.glance.layout.padding
-import androidx.glance.text.Text
-import androidx.glance.text.TextStyle
+import androidx.glance.layout.width
import androidx.glance.unit.ColorProvider
/**
@@ -43,12 +44,15 @@
@Composable
override fun WidgetLayoutCollapsed() {
getData(currentState()).let {
- Column(
- modifier = GlanceModifier.fillMaxSize().padding(8.dp).background(it.backgroundColor)
- ) {
+ var modifier = GlanceModifier.fillMaxSize().padding(16.dp)
+ it.backgroundColor?.let { color -> modifier = modifier.background(color) }
+ it.image?.let { image ->
+ modifier = modifier.background(image.image, ContentScale.Crop)
+ }
+ Column(modifier = modifier) {
TemplateHeader(it.headerIcon, it.header)
- TextBlock(it.title, it.subtitle, null)
- Button(it)
+ Spacer(modifier = GlanceModifier.defaultWeight())
+ TextSection(textList(it.title, it.subtitle))
}
}
}
@@ -56,15 +60,25 @@
@Composable
override fun WidgetLayoutVertical() {
getData(currentState()).let {
- Column(
- modifier = GlanceModifier.fillMaxSize().padding(8.dp).background(it.backgroundColor)
- ) {
+ var modifier = GlanceModifier.fillMaxSize().padding(16.dp)
+ it.backgroundColor?.let { color -> modifier = modifier.background(color) }
+ Column(modifier = modifier) {
TemplateHeader(it.headerIcon, it.header)
- TextBlock(it.title, it.subtitle, it.bodyText)
- it.mainImage?.let { image ->
- Image(provider = image.image, contentDescription = image.description)
+ Spacer(modifier = GlanceModifier.height(16.dp))
+ it.image?.let { image ->
+ Image(
+ provider = image.image,
+ contentDescription = image.description,
+ modifier = GlanceModifier.fillMaxWidth().defaultWeight(),
+ contentScale = ContentScale.Crop
+ )
+ Spacer(modifier = GlanceModifier.height(16.dp))
}
- Button(it)
+ Row(modifier = GlanceModifier.fillMaxWidth()) {
+ TextSection(textList(it.title, it.subtitle))
+ Spacer(modifier = GlanceModifier.defaultWeight())
+ it.button?.let { button -> TemplateButton(button) }
+ }
}
}
}
@@ -72,123 +86,113 @@
@Composable
override fun WidgetLayoutHorizontal() {
getData(currentState()).let {
- Column(
- modifier = GlanceModifier.fillMaxSize().padding(8.dp).background(it.backgroundColor)
- ) {
- Row(modifier = GlanceModifier.fillMaxWidth()) {
+ var modifier = GlanceModifier.fillMaxSize().padding(16.dp)
+ it.backgroundColor?.let { color -> modifier = modifier.background(color) }
+ Row(modifier = modifier) {
+ Column(
+ modifier =
+ GlanceModifier.fillMaxHeight().background(Color.Transparent).defaultWeight()
+ ) {
TemplateHeader(it.headerIcon, it.header)
+ Spacer(modifier = GlanceModifier.height(16.dp))
+ Spacer(modifier = GlanceModifier.defaultWeight())
+
+ // TODO: Extract small height as template constant
+ val body =
+ if (LocalSize.current.height > 240.dp) {
+ it.body
+ } else {
+ null
+ }
+ TextSection(textList(it.title, it.subtitle, body))
+ it.button?.let { button ->
+ Spacer(modifier = GlanceModifier.height(16.dp))
+ TemplateButton(button)
+ }
}
- Row(modifier = GlanceModifier.fillMaxWidth()) {
- Column(modifier = GlanceModifier.defaultWeight()) {
- TextBlock(it.title, it.subtitle, it.bodyText)
- Button(it)
- }
- it.mainImage?.let { image ->
- Image(
- provider = image.image,
- contentDescription = image.description,
- modifier = GlanceModifier.defaultWeight()
- )
- }
+ it.image?.let { image ->
+ Spacer(modifier = GlanceModifier.width(16.dp))
+ Image(
+ provider = image.image,
+ contentDescription = image.description,
+ modifier = GlanceModifier.fillMaxHeight().defaultWeight(),
+ contentScale = ContentScale.Crop
+ )
}
}
}
}
- @Composable
- private fun TextBlock(title: String?, subtitle: String?, body: String?) {
- // TODO: Text color customization
- val color = ColorProvider(R.color.text_default)
- Column {
- title?.let {
- Text(title, style = TextStyle(fontSize = 36.sp, color = color), maxLines = 2)
- }
- subtitle?.let {
- Text(it, style = TextStyle(fontSize = 20.sp, color = color), maxLines = 2)
- }
- body?.let { Text(it, style = TextStyle(fontSize = 18.sp, color = color), maxLines = 5) }
+ private fun textList(
+ title: TemplateText? = null,
+ subtitle: TemplateText? = null,
+ body: TemplateText? = null
+ ): List<TypedTemplateText> {
+ val result = mutableListOf<TypedTemplateText>()
+ title?.let {
+ result.add(TypedTemplateText(it, TemplateTextType.Title))
}
- }
+ subtitle?.let {
+ result.add(TypedTemplateText(it, TemplateTextType.Label))
+ }
+ body?.let {
+ result.add(TypedTemplateText(it, TemplateTextType.Body))
+ }
- @Composable
- private fun Button(data: Data) {
- // If a button image is provided, use an image button
- when (data.button) {
- is TemplateImageButton -> {
- // TODO: specify sizing for image button
- val image = data.button.image
- Image(
- provider = image.image,
- contentDescription = image.description,
- modifier = GlanceModifier.clickable(data.button.action)
- )
- }
- is TemplateTextButton -> {
- Button(text = data.button.text, onClick = data.button.action)
- }
- else -> {
- Log.e(TAG, "Unrecognized button type: ${data.button}")
- }
- }
+ return result
}
/**
* The semantic data required to build [SingleEntityTemplate] layouts.
*
- * @param backgroundColor Glanceable background color
* @param headerIcon Logo icon, displayed in the glanceable header
- * @param header Main header text
- * @param title Text section main title
- * @param subtitle Text section subtitle
- * @param bodyText Text section body text
+ * @param header Main header text, text priority is ignored in default layouts
+ * @param title Text section main title, priority ordered
+ * @param subtitle Text section subtitle, priority ordered
+ * @param body Text section body text, priority ordered
* @param button Action button
- * @param mainImage Main image content
+ * @param image Main image content
+ * @param backgroundColor Glanceable background color
*/
- // TODO: add optional ordering for text vs image sections, determine granularity level for
- // setting text colors
class Data(
- val backgroundColor: ColorProvider,
val headerIcon: TemplateImageWithDescription,
- val header: String? = null,
- val title: String? = null,
- val subtitle: String? = null,
- val bodyText: String? = null,
+ val header: TemplateText? = null,
+ val title: TemplateText? = null,
+ val subtitle: TemplateText? = null,
+ val body: TemplateText? = null,
val button: TemplateButton? = null,
- val mainImage: TemplateImageWithDescription? = null,
+ val image: TemplateImageWithDescription? = null,
+ val backgroundColor: ColorProvider? = null
) {
+ override fun hashCode(): Int {
+ var result = headerIcon.hashCode()
+ result = 31 * result + (header?.hashCode() ?: 0)
+ result = 31 * result + (title?.hashCode() ?: 0)
+ result = 31 * result + (subtitle?.hashCode() ?: 0)
+ result = 31 * result + (body?.hashCode() ?: 0)
+ result = 31 * result + (button?.hashCode() ?: 0)
+ result = 31 * result + (image?.hashCode() ?: 0)
+ result = 31 * result + (backgroundColor?.hashCode() ?: 0)
+ return result
+ }
+
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Data
- if (backgroundColor != other.backgroundColor) return false
if (headerIcon != other.headerIcon) return false
if (header != other.header) return false
if (title != other.title) return false
if (subtitle != other.subtitle) return false
- if (bodyText != other.bodyText) return false
+ if (body != other.body) return false
if (button != other.button) return false
- if (mainImage != other.mainImage) return false
+ if (image != other.image) return false
+ if (backgroundColor != other.backgroundColor) return false
return true
}
-
- override fun hashCode(): Int {
- var result = backgroundColor.hashCode()
- result = 31 * result + headerIcon.hashCode()
- result = 31 * result + (header?.hashCode() ?: 0)
- result = 31 * result + (title?.hashCode() ?: 0)
- result = 31 * result + (subtitle?.hashCode() ?: 0)
- result = 31 * result + (bodyText?.hashCode() ?: 0)
- result = 31 * result + (button?.hashCode() ?: 0)
- result = 31 * result + (mainImage?.hashCode() ?: 0)
- return result
- }
- }
-
- private companion object {
- private const val TAG = "SingleEntityTemplate"
}
}
diff --git a/template/template/src/main/java/androidx/template/template/TemplateTranslator.kt b/template/template/src/main/java/androidx/template/template/TemplateTranslator.kt
index d005c82..669aaea 100644
--- a/template/template/src/main/java/androidx/template/template/TemplateTranslator.kt
+++ b/template/template/src/main/java/androidx/template/template/TemplateTranslator.kt
@@ -31,7 +31,7 @@
// TODO: pass in host context and get layout for display params, including surface type etc.
val height = LocalSize.current.height
val width = LocalSize.current.width
- if (height < Dp(240f) && width < Dp(240f)) {
+ if (height <= Dp(240f) && width <= Dp(240f)) {
template.WidgetLayoutCollapsed()
} else if ((width / height) < (3.0 / 2.0)) {
template.WidgetLayoutVertical()
diff --git a/textclassifier/textclassifier/src/androidTest/java/androidx/textclassifier/MatchMakerImplTest.java b/textclassifier/textclassifier/src/androidTest/java/androidx/textclassifier/MatchMakerImplTest.java
index e385cef..81dd12f 100644
--- a/textclassifier/textclassifier/src/androidTest/java/androidx/textclassifier/MatchMakerImplTest.java
+++ b/textclassifier/textclassifier/src/androidTest/java/androidx/textclassifier/MatchMakerImplTest.java
@@ -74,6 +74,7 @@
private String mSms;
@Before
+ @SuppressWarnings("deprecation")
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
mPackageManager = mock(PackageManager.class);
@@ -134,6 +135,7 @@
}
@Test
+ @SuppressWarnings("deprecation")
public void noMatchingApp() throws Exception {
final PackageManager packageManager = mock(PackageManager.class);
when(packageManager.resolveActivity(any(Intent.class), anyInt())).thenReturn(null);
@@ -147,6 +149,7 @@
}
@Test
+ @SuppressWarnings("deprecation")
public void noMatchingActivity() throws Exception {
final PackageManager packageManager = mock(PackageManager.class);
when(packageManager.resolveActivity(any(Intent.class), anyInt()))
diff --git a/textclassifier/textclassifier/src/main/java/androidx/textclassifier/LegacyTextClassifier.java b/textclassifier/textclassifier/src/main/java/androidx/textclassifier/LegacyTextClassifier.java
index adb2397..180d92a 100644
--- a/textclassifier/textclassifier/src/main/java/androidx/textclassifier/LegacyTextClassifier.java
+++ b/textclassifier/textclassifier/src/main/java/androidx/textclassifier/LegacyTextClassifier.java
@@ -329,6 +329,7 @@
}
@Nullable
+ @SuppressWarnings("deprecation")
private RemoteActionCompat createRemoteAction(
Intent intent, String title, String description, int requestCode) {
final ResolveInfo resolveInfo = mPackageManager.resolveActivity(intent, 0);
@@ -364,6 +365,7 @@
}
@Nullable
+ @SuppressWarnings("deprecation")
private PendingIntent createPendingIntent(Intent intent, int requestCode) {
final ResolveInfo resolveInfo = mPackageManager.resolveActivity(intent, 0);
final int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
diff --git a/vectordrawable/integration-tests/testapp/src/main/java/com/example/android/support/vectordrawable/app/SupportVectorDrawableDemos.java b/vectordrawable/integration-tests/testapp/src/main/java/com/example/android/support/vectordrawable/app/SupportVectorDrawableDemos.java
index c07087f..6b895f9 100644
--- a/vectordrawable/integration-tests/testapp/src/main/java/com/example/android/support/vectordrawable/app/SupportVectorDrawableDemos.java
+++ b/vectordrawable/integration-tests/testapp/src/main/java/com/example/android/support/vectordrawable/app/SupportVectorDrawableDemos.java
@@ -55,6 +55,7 @@
getListView().setTextFilterEnabled(true);
}
+ @SuppressWarnings("deprecation")
protected List<Map<String, Object>> getData(String prefix) {
List<Map<String, Object>> myData = new ArrayList<Map<String, Object>>();
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index 752bbdee..d9022a7 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -300,12 +300,18 @@
}
public interface ScalingLazyListLayoutInfo {
+ method public androidx.compose.foundation.gestures.Orientation getOrientation();
+ method public boolean getReverseLayout();
method public int getTotalItemsCount();
method public int getViewportEndOffset();
+ method public long getViewportSize();
method public int getViewportStartOffset();
method public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
+ property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+ property public abstract boolean reverseLayout;
property public abstract int totalItemsCount;
property public abstract int viewportEndOffset;
+ property public abstract long viewportSize;
property public abstract int viewportStartOffset;
property public abstract java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> visibleItemsInfo;
}
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index 58e2e73..b67df1d 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -326,12 +326,18 @@
}
public interface ScalingLazyListLayoutInfo {
+ method public androidx.compose.foundation.gestures.Orientation getOrientation();
+ method public boolean getReverseLayout();
method public int getTotalItemsCount();
method public int getViewportEndOffset();
+ method public long getViewportSize();
method public int getViewportStartOffset();
method public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
+ property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+ property public abstract boolean reverseLayout;
property public abstract int totalItemsCount;
property public abstract int viewportEndOffset;
+ property public abstract long viewportSize;
property public abstract int viewportStartOffset;
property public abstract java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> visibleItemsInfo;
}
@@ -514,7 +520,6 @@
method @androidx.compose.runtime.Composable public void CurvedTextSeparator(androidx.wear.compose.foundation.CurvedRowScope, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.CurvedTextStyle curvedTextStyle, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding);
method @androidx.compose.runtime.Composable public void TextSeparator(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.layout.PaddingValues contentPadding);
method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
- method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.CurvedTextStyle timeCurvedTextStyle(optional long color, optional long background, optional long fontSize);
method @androidx.compose.runtime.Composable public String timeFormat();
method public androidx.wear.compose.material.TimeSource timeSource(String timeFormat);
method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long color, optional long background, optional long fontSize);
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index 752bbdee..d9022a7 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -300,12 +300,18 @@
}
public interface ScalingLazyListLayoutInfo {
+ method public androidx.compose.foundation.gestures.Orientation getOrientation();
+ method public boolean getReverseLayout();
method public int getTotalItemsCount();
method public int getViewportEndOffset();
+ method public long getViewportSize();
method public int getViewportStartOffset();
method public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
+ property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+ property public abstract boolean reverseLayout;
property public abstract int totalItemsCount;
property public abstract int viewportEndOffset;
+ property public abstract long viewportSize;
property public abstract int viewportStartOffset;
property public abstract java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> visibleItemsInfo;
}
diff --git a/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/TimeTextBenchmark.kt b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/TimeTextBenchmark.kt
index 8f9a295..91ddcbb 100644
--- a/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/TimeTextBenchmark.kt
+++ b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/TimeTextBenchmark.kt
@@ -27,7 +27,7 @@
import androidx.compose.testutils.benchmark.benchmarkLayoutPerf
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
-import androidx.wear.compose.foundation.BasicCurvedText
+import androidx.wear.compose.material.CurvedText
import androidx.wear.compose.material.ExperimentalWearMaterialApi
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
@@ -100,18 +100,16 @@
)
},
leadingCurvedContent = {
- BasicCurvedText(
- text = "Leading content",
- style = TimeTextDefaults.timeCurvedTextStyle()
+ CurvedText(
+ text = "Leading content"
)
},
textCurvedSeparator = {
CurvedTextSeparator()
},
trailingCurvedContent = {
- BasicCurvedText(
- text = "Trailing content",
- style = TimeTextDefaults.timeCurvedTextStyle()
+ CurvedText(
+ text = "Trailing content"
)
},
)
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/TimeTextSample.kt b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/TimeTextSample.kt
index 9f7451f..f3d7874 100644
--- a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/TimeTextSample.kt
+++ b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/TimeTextSample.kt
@@ -24,6 +24,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.ArcPaddingValues
+import androidx.wear.compose.foundation.CurvedTextStyle
import androidx.wear.compose.material.CurvedText
import androidx.wear.compose.material.ExperimentalWearMaterialApi
import androidx.wear.compose.material.Text
@@ -35,42 +36,45 @@
@Sampled
@Composable
fun TimeTextWithCustomSeparator() {
+ val leadingTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Green)
+ val trailingTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
+ val separatorTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Magenta)
TimeText(
leadingLinearContent = {
Text(
text = "Leading content",
- style = TimeTextDefaults.timeTextStyle(color = Color.Green)
+ style = leadingTextStyle
)
},
leadingCurvedContent = {
CurvedText(
text = "Leading content",
- style = TimeTextDefaults.timeCurvedTextStyle(color = Color.Green)
+ style = CurvedTextStyle(leadingTextStyle)
)
},
trailingLinearContent = {
Text(
text = "Trailing content",
- style = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
+ style = trailingTextStyle
)
},
trailingCurvedContent = {
CurvedText(
text = "Trailing content",
- style = TimeTextDefaults.timeCurvedTextStyle(color = Color.Yellow)
+ style = CurvedTextStyle(trailingTextStyle)
)
},
textLinearSeparator = {
Text(
modifier = Modifier.padding(horizontal = 8.dp),
text = "***",
- style = TimeTextDefaults.timeTextStyle(color = Color.Magenta)
+ style = separatorTextStyle
)
},
textCurvedSeparator = {
CurvedText(
text = "***",
- style = TimeTextDefaults.timeCurvedTextStyle(color = Color.Magenta),
+ style = CurvedTextStyle(separatorTextStyle),
contentArcPadding = ArcPaddingValues(angular = 8.dp)
)
}
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
index 02d89f9..bda8c48 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
@@ -413,7 +413,6 @@
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
- contentPadding = PaddingValues(vertical = 100.dp)
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
index 051684e..7db086f 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
@@ -16,6 +16,7 @@
package androidx.wear.compose.material
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Arrangement
@@ -34,6 +35,7 @@
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
@@ -94,6 +96,85 @@
}
}
+ @Test
+ fun orientationIsCorrect() {
+ lateinit var state: ScalingLazyListState
+ rule.setContent {
+ ScalingLazyColumn(
+ state = rememberScalingLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(
+ itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+ ),
+ autoCentering = true,
+ contentPadding = PaddingValues(all = 0.dp)
+ ) {
+ items(5) {
+ Box(Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ }
+
+ // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+ rule.waitUntil { state.initialized.value }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.orientation).isEqualTo(Orientation.Vertical)
+ }
+ }
+
+ @Test
+ fun reverseLayoutIsCorrectWhenNotReversed() {
+ lateinit var state: ScalingLazyListState
+ rule.setContent {
+ ScalingLazyColumn(
+ state = rememberScalingLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(
+ itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+ ),
+ autoCentering = true,
+ contentPadding = PaddingValues(all = 0.dp)
+ ) {
+ items(5) {
+ Box(Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ }
+
+ // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+ rule.waitUntil { state.initialized.value }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.reverseLayout).isEqualTo(false)
+ }
+ }
+
+ @Test
+ fun reverseLayoutIsCorrectWhenReversed() {
+ lateinit var state: ScalingLazyListState
+ rule.setContent {
+ ScalingLazyColumn(
+ state = rememberScalingLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(
+ itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+ ),
+ autoCentering = true,
+ contentPadding = PaddingValues(all = 0.dp),
+ reverseLayout = true
+ ) {
+ items(5) {
+ Box(Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ }
+
+ // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
+ rule.waitUntil { state.initialized.value }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.reverseLayout).isEqualTo(true)
+ }
+ }
+
@FlakyTest(bugId = 217762274)
@Test
fun visibleItemsAreCorrectSetExplicitInitialItemIndex() {
@@ -492,7 +573,6 @@
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
- contentPadding = PaddingValues(vertical = 100.dp),
) {
items(5) {
Box(
@@ -585,7 +665,6 @@
itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
),
scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
- contentPadding = PaddingValues(vertical = 100.dp)
) {
items(5) {
Box(Modifier.requiredSize(itemSizeDp))
@@ -876,7 +955,7 @@
}
@Test
- fun viewportOffsetsAreCorrect() {
+ fun viewportOffsetsAndSizeAreCorrect() {
val sizePx = 45
val sizeDp = with(rule.density) { sizePx.toDp() }
lateinit var state: ScalingLazyListState
@@ -896,11 +975,12 @@
rule.runOnIdle {
assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0)
assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx)
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx, sizePx))
}
}
@Test
- fun viewportOffsetsAreCorrectWithContentPadding() {
+ fun viewportOffsetsAndSizeAreCorrectWithContentPadding() {
val sizePx = 45
val startPaddingPx = 10
val endPaddingPx = 15
@@ -925,6 +1005,7 @@
rule.runOnIdle {
assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx, sizePx))
}
}
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt
index 535e985..9fce750 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt
@@ -29,13 +29,14 @@
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.sp
-import androidx.wear.compose.foundation.BasicCurvedText
import androidx.wear.compose.material.TimeTextDefaults.CurvedTextSeparator
+import java.util.Calendar
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
-import java.util.Calendar
@ExperimentalWearMaterialApi
class TimeTextTest {
@@ -105,10 +106,9 @@
)
},
leadingCurvedContent = {
- BasicCurvedText(
+ CurvedText(
modifier = Modifier.testTag(CURVED_ITEM_TAG),
- text = "Leading content",
- style = TimeTextDefaults.timeCurvedTextStyle()
+ text = "Leading content"
)
}
)
@@ -131,10 +131,9 @@
)
},
leadingCurvedContent = {
- BasicCurvedText(
+ CurvedText(
modifier = Modifier.testTag(CURVED_ITEM_TAG),
- text = "Leading content",
- style = TimeTextDefaults.timeCurvedTextStyle()
+ text = "Leading content"
)
}
)
@@ -156,10 +155,9 @@
)
},
trailingCurvedContent = {
- BasicCurvedText(
+ CurvedText(
modifier = Modifier.testTag(CURVED_ITEM_TAG),
- text = "Trailing content",
- style = TimeTextDefaults.timeCurvedTextStyle()
+ text = "Trailing content"
)
}
)
@@ -181,10 +179,9 @@
)
},
trailingCurvedContent = {
- BasicCurvedText(
+ CurvedText(
modifier = Modifier.testTag(CURVED_ITEM_TAG),
- text = "Leading content",
- style = TimeTextDefaults.timeCurvedTextStyle()
+ text = "Leading content"
)
}
)
@@ -330,9 +327,8 @@
ConfiguredShapeScreen(true) {
TimeText(
trailingCurvedContent = {
- BasicCurvedText(
- text = "Trailing content",
- style = TimeTextDefaults.timeCurvedTextStyle()
+ CurvedText(
+ text = "Trailing content"
)
},
textLinearSeparator = {
@@ -391,15 +387,13 @@
ConfiguredShapeScreen(true) {
TimeText(
leadingCurvedContent = {
- BasicCurvedText(
- text = "Leading content",
- style = TimeTextDefaults.timeCurvedTextStyle()
+ CurvedText(
+ text = "Leading content"
)
},
trailingCurvedContent = {
- BasicCurvedText(
- text = "Trailing content",
- style = TimeTextDefaults.timeCurvedTextStyle()
+ CurvedText(
+ text = "Trailing content"
)
},
textLinearSeparator = {
@@ -423,7 +417,7 @@
// for CurvedText
@Test
fun changes_timeTextStyle_on_square_device() {
- val timeState = mutableStateOf("testState")
+ val timeText = "testTime"
val testTextStyle = TextStyle(
color = Color.Green,
@@ -436,17 +430,52 @@
timeSource = object : TimeSource {
override val currentTime: String
@Composable
- get() = timeState.value
+ get() = timeText
},
timeTextStyle = testTextStyle
)
}
}
- val actualStyle = rule.textStyleOf(timeState.value)
+ val actualStyle = rule.textStyleOf(timeText)
assertEquals(testTextStyle.color, actualStyle.color)
assertEquals(testTextStyle.background, actualStyle.background)
assertEquals(testTextStyle.fontSize, actualStyle.fontSize)
}
+
+ @Test
+ fun changes_material_theme_on_square_device() {
+ val timeText = "testTime"
+
+ val testTextStyle = TextStyle(
+ color = Color.Green,
+ background = Color.Black,
+ fontStyle = FontStyle.Italic,
+ fontSize = 25.sp,
+ fontFamily = FontFamily.SansSerif
+ )
+ rule.setContent {
+ MaterialTheme(
+ typography = MaterialTheme.typography.copy(
+ caption1 = testTextStyle)
+ ) {
+ ConfiguredShapeScreen(false) {
+ TimeText(
+ timeSource = object : TimeSource {
+ override val currentTime: String
+ @Composable
+ get() = timeText
+ }
+ )
+ }
+ }
+ }
+ val actualStyle = rule.textStyleOf(timeText)
+ assertEquals(testTextStyle.color, actualStyle.color)
+ assertEquals(testTextStyle.background, actualStyle.background)
+ assertEquals(testTextStyle.fontSize, actualStyle.fontSize)
+ assertEquals(testTextStyle.fontStyle, actualStyle.fontStyle)
+ assertEquals(testTextStyle.fontFamily, actualStyle.fontFamily)
+ }
}
@ExperimentalWearMaterialApi
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
index f224436..3ef1130 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
@@ -40,6 +40,7 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
@@ -54,6 +55,7 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset
+import kotlinx.coroutines.flow.first
/**
* Receiver scope which is used by [ScalingLazyColumn].
@@ -363,6 +365,12 @@
}
if (initialized) {
LaunchedEffect(state) {
+ snapshotFlow {
+ (state.layoutInfo as DefaultScalingLazyListLayoutInfo)
+ .readyForInitialization
+ }.first {
+ it
+ }
state.scrollToInitialItem()
}
}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
index edbb214..7f748f3 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
@@ -17,10 +17,12 @@
package androidx.wear.compose.material
import androidx.compose.animation.core.Easing
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.lerp
import kotlin.math.min
import kotlin.math.roundToInt
@@ -395,7 +397,16 @@
override val viewportEndOffset: Int,
override val totalItemsCount: Int,
val centerItemIndex: Int,
- val centerItemScrollOffset: Int
+ val centerItemScrollOffset: Int,
+ override val reverseLayout: Boolean,
+ override val orientation: Orientation,
+ override val viewportSize: IntSize,
+ // Flag to indicate that we are ready for the second stage of initialization. Note that this
+ // flag will be false once initialization is complete and initialized == true.
+ internal val readyForInitialization: Boolean,
+ // Flag to indicate that initialization is complete and initial scroll index and offset have
+ // been set.
+ internal val initialized: Boolean
) : ScalingLazyListLayoutInfo
internal class DefaultScalingLazyListItemInfo(
@@ -405,7 +416,7 @@
override val offset: Int,
override val size: Int,
override val scale: Float,
- override val alpha: Float,
+ override val alpha: Float
) : ScalingLazyListItemInfo {
override fun toString(): String {
return "DefaultScalingLazyListItemInfo(index=$index, key=$key, " +
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt
index 83ef1a8..02d3c62 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt
@@ -15,6 +15,9 @@
*/
package androidx.wear.compose.material
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.IntSize
+
/**
* Contains useful information about the currently displayed layout state of [ScalingLazyColumn].
* For example you can get the list of currently displayed item.
@@ -48,4 +51,19 @@
* The total count of items passed to [ScalingLazyColumn].
*/
val totalItemsCount: Int
+
+ /**
+ * The size of the viewport. It is the lazy list layout size including all the content paddings.
+ */
+ val viewportSize: IntSize
+
+ /**
+ * The orientation of the lazy list.
+ */
+ val orientation: Orientation
+
+ /**
+ * True if the direction of scrolling and layout is reversed.
+ */
+ val reverseLayout: Boolean
}
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
index 951201d..56768ac 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
@@ -17,6 +17,7 @@
package androidx.wear.compose.material
import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.lazy.LazyListItemInfo
@@ -30,6 +31,7 @@
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.unit.IntSize
import kotlin.math.roundToInt
/**
@@ -102,17 +104,14 @@
gapBetweenItemsPx.value == null || viewportHeightPx.value == null ||
anchorType.value == null || reverseLayout.value == null ||
beforeContentPaddingPx.value == null || autoCentering.value == null ||
- layoutInfo.visibleItemsInfo.isEmpty()
+ !autoCentering.value!! || layoutInfo.visibleItemsInfo.isEmpty()
) {
0
} else {
- if (autoCentering.value!! && layoutInfo.visibleItemsInfo.first().index == 0) {
- if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
- viewportHeightPx.value!! / 2f
- } else {
- viewportHeightPx.value!! / 2f -
- layoutInfo.visibleItemsInfo.first().unadjustedSize() / 2f
- }.roundToInt() - gapBetweenItemsPx.value!!
+ if (layoutInfo.visibleItemsInfo.first().index == 0) {
+ calculateTopAutoCenteringPaddingPx(
+ layoutInfo.visibleItemsInfo.first().unadjustedSize()
+ )
} else {
0
}
@@ -156,6 +155,8 @@
} else {
val visibleItemsInfo = mutableListOf<ScalingLazyListItemInfo>()
val viewportHeightPx = viewportHeightPx.value!!
+ var newCenterItemIndex = 0
+ var newCenterItemScrollOffset = 0
// The verticalAdjustment is used to allow for the extraPadding that the
// ScalingLazyColumn employs to ensure that there are sufficient list items composed
@@ -189,8 +190,8 @@
centerItemInfo
)
- val newCenterItemIndex = centerItemInfo.index
- val newCenterItemScrollOffset = -centerItemInfo.offset
+ newCenterItemIndex = centerItemInfo.index
+ newCenterItemScrollOffset = -centerItemInfo.offset
// Find the adjusted position of the central item in the coordinate system of the
// underlying LazyColumn by adjusting for any scaling
@@ -270,27 +271,57 @@
return@forEach
}
}
-
- val totalItemsCount =
- if (autoCentering.value!!) {
- (lazyListState.layoutInfo.totalItemsCount - 2).coerceAtLeast(0)
- } else {
- lazyListState.layoutInfo.totalItemsCount
- }
-
- DefaultScalingLazyListLayoutInfo(
- visibleItemsInfo = visibleItemsInfo,
- totalItemsCount = totalItemsCount,
- viewportStartOffset = lazyListState.layoutInfo.viewportStartOffset +
- extraPaddingPx.value!!,
- viewportEndOffset = lazyListState.layoutInfo.viewportEndOffset -
- extraPaddingPx.value!!,
- centerItemIndex = if (initialized.value) newCenterItemIndex else 0,
- centerItemScrollOffset = if (initialized.value) newCenterItemScrollOffset else 0
- )
- } else {
- EmptyScalingLazyListLayoutInfo
}
+ val totalItemsCount =
+ if (autoCentering.value!!) {
+ (lazyListState.layoutInfo.totalItemsCount - 2).coerceAtLeast(0)
+ } else {
+ lazyListState.layoutInfo.totalItemsCount
+ }
+
+ // Decide if we are ready for the 2nd stage of initialization
+ // 1. autoCentering is off or
+ // 2. The list has no items or
+ // 3. the before content autoCentering Spacer has been sized.
+ // NOTE: 3a. It is possible, if the first real item in the list is large, that the size
+ // of the Spacer is 0.
+ val readyForInitialization =
+ // Not already initialized
+ !initialized.value && (
+ // Not autoCentering
+ !autoCentering.value!! || (
+ lazyListState.layoutInfo.visibleItemsInfo.size >= 2 && (
+ // or Empty list (other than the 2 spacers)
+ lazyListState.layoutInfo.visibleItemsInfo.size == 2 ||
+ // or first item is non-zero size
+ lazyListState.layoutInfo.visibleItemsInfo.first().size > 0 ||
+ // or first item is supposed to be zero size
+ calculateTopAutoCenteringPaddingPx(
+ lazyListState.layoutInfo.visibleItemsInfo[1].size
+ ) == 0
+ )
+ )
+ )
+
+ DefaultScalingLazyListLayoutInfo(
+ visibleItemsInfo = visibleItemsInfo,
+ totalItemsCount = totalItemsCount,
+ viewportStartOffset = lazyListState.layoutInfo.viewportStartOffset +
+ extraPaddingPx.value!!,
+ viewportEndOffset = lazyListState.layoutInfo.viewportEndOffset -
+ extraPaddingPx.value!!,
+ centerItemIndex = if (initialized.value) newCenterItemIndex else 0,
+ centerItemScrollOffset = if (initialized.value) newCenterItemScrollOffset else 0,
+ reverseLayout = reverseLayout.value!!,
+ orientation = lazyListState.layoutInfo.orientation,
+ viewportSize = IntSize(
+ width = lazyListState.layoutInfo.viewportSize.width,
+ height = lazyListState.layoutInfo.viewportSize.height -
+ extraPaddingPx.value!! * 2
+ ),
+ readyForInitialization = readyForInitialization,
+ initialized = initialized.value
+ )
}
}
@@ -455,6 +486,15 @@
private fun discardAutoCenteringListItem(item: LazyListItemInfo): Boolean =
autoCentering.value!! &&
(item.index == 0 || item.index == lazyListState.layoutInfo.totalItemsCount - 1)
+
+ private fun calculateTopAutoCenteringPaddingPx(unadjustedFirstItemSize: Int): Int {
+ return (if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
+ viewportHeightPx.value!! / 2f
+ } else {
+ viewportHeightPx.value!! / 2f -
+ unadjustedFirstItemSize / 2f
+ }.roundToInt() - gapBetweenItemsPx.value!!).coerceAtLeast(0)
+ }
}
private fun LazyListLayoutInfo.findItemInfoWithIndex(index: Int): LazyListItemInfo? {
@@ -474,4 +514,7 @@
override val viewportStartOffset = 0
override val viewportEndOffset = 0
override val totalItemsCount = 0
+ override val viewportSize = IntSize.Zero
+ override val orientation = Orientation.Vertical
+ override val reverseLayout = false
}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
index 04d8099..f189c2a 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
@@ -73,7 +73,7 @@
*/
@ExperimentalWearMaterialApi
@Composable
-fun TimeText(
+public fun TimeText(
modifier: Modifier = Modifier,
timeSource: TimeSource = TimeTextDefaults.timeSource(timeFormat()),
timeTextStyle: TextStyle = TimeTextDefaults.timeTextStyle(),
@@ -162,7 +162,7 @@
/**
* Creates a [TextStyle] with default parameters used for showing time
- * on square screens
+ * on square screens. By default a copy of MaterialTheme.typography.caption1 style is created
*
* @param color The main color
* @param background The background color
@@ -170,33 +170,11 @@
*/
@Composable
public fun timeTextStyle(
- color: Color = Color.White,
- background: Color = Color.Transparent,
- fontSize: TextUnit = MaterialTheme.typography.button.fontSize,
- ) = TextStyle(
- color = color,
- background = background,
- fontSize = fontSize
- )
-
- /**
- * Creates a [CurvedTextStyle] with default parameters used for showing time
- * on round screens
- *
- * @param color The main color
- * @param background The background color
- * @param fontSize The font size
- */
- @Composable
- public fun timeCurvedTextStyle(
- color: Color = Color.White,
- background: Color = Color.Transparent,
- fontSize: TextUnit = MaterialTheme.typography.button.fontSize,
- ) = CurvedTextStyle(
- color = color,
- background = background,
- fontSize = fontSize
- )
+ color: Color = Color.Unspecified,
+ background: Color = Color.Unspecified,
+ fontSize: TextUnit = TextUnit.Unspecified,
+ ) = MaterialTheme.typography.caption1 +
+ TextStyle(color = color, background = background, fontSize = fontSize)
/**
* A default implementation of Separator shown between trailing/leading content and the time
@@ -228,7 +206,7 @@
@Composable
public fun CurvedRowScope.CurvedTextSeparator(
modifier: Modifier = Modifier,
- curvedTextStyle: CurvedTextStyle = timeCurvedTextStyle(),
+ curvedTextStyle: CurvedTextStyle = CurvedTextStyle(timeTextStyle()),
contentArcPadding: ArcPaddingValues = ArcPaddingValues(angular = 4.dp)
) {
CurvedText(
@@ -260,9 +238,6 @@
fun timeSource(timeFormat: String): TimeSource = DefaultTimeSource(timeFormat)
}
-@ExperimentalWearMaterialApi
-internal expect class DefaultTimeSource(timeFormat: String) : TimeSource
-
/**
* An interface which is responsible for retrieving a formatted time.
*/
@@ -276,3 +251,6 @@
val currentTime: String
@Composable get
}
+
+@ExperimentalWearMaterialApi
+internal expect class DefaultTimeSource(timeFormat: String) : TimeSource
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/TimeTextDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/TimeTextDemo.kt
index 73ced52..2d7364f 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/TimeTextDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/TimeTextDemo.kt
@@ -22,6 +22,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.BasicCurvedText
+import androidx.wear.compose.foundation.CurvedTextStyle
import androidx.wear.compose.material.ExperimentalWearMaterialApi
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.TimeText
@@ -37,17 +38,18 @@
@OptIn(ExperimentalWearMaterialApi::class)
@Composable
fun TimeTextWithLeadingText() {
+ val textStyle = TimeTextDefaults.timeTextStyle(color = Color.Green)
TimeText(
leadingLinearContent = {
Text(
text = "Leading content",
- style = TimeTextDefaults.timeTextStyle(color = Color.Green)
+ style = textStyle
)
},
leadingCurvedContent = {
BasicCurvedText(
text = "Leading content",
- style = TimeTextDefaults.timeCurvedTextStyle(color = Color.Green)
+ style = CurvedTextStyle(textStyle)
)
}
)
@@ -56,17 +58,18 @@
@OptIn(ExperimentalWearMaterialApi::class)
@Composable
fun TimeTextWithTrailingText() {
+ val textStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
TimeText(
trailingLinearContent = {
Text(
text = "Trailing content",
- style = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
+ style = textStyle
)
},
trailingCurvedContent = {
BasicCurvedText(
text = "Trailing content",
- style = TimeTextDefaults.timeCurvedTextStyle(color = Color.Yellow)
+ style = CurvedTextStyle(textStyle)
)
}
)
@@ -75,29 +78,31 @@
@OptIn(ExperimentalWearMaterialApi::class)
@Composable
fun TimeTextWithLeadingAndTrailingText() {
+ val leadingTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Green)
+ val trailingTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
TimeText(
leadingLinearContent = {
Text(
text = "Leading content",
- style = TimeTextDefaults.timeTextStyle(color = Color.Green)
+ style = leadingTextStyle
)
},
leadingCurvedContent = {
BasicCurvedText(
text = "Leading content",
- style = TimeTextDefaults.timeCurvedTextStyle(color = Color.Green)
+ style = CurvedTextStyle(leadingTextStyle)
)
},
trailingLinearContent = {
Text(
text = "Trailing content",
- style = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
+ style = trailingTextStyle
)
},
trailingCurvedContent = {
BasicCurvedText(
text = "Trailing content",
- style = TimeTextDefaults.timeCurvedTextStyle(color = Color.Yellow)
+ style = CurvedTextStyle(trailingTextStyle)
)
}
)
@@ -106,29 +111,31 @@
@OptIn(ExperimentalWearMaterialApi::class)
@Composable
fun TimeTextWithPadding() {
+ val leadingTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Green)
+ val trailingTextStyle = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
TimeText(
leadingLinearContent = {
Text(
text = "Leading content",
- style = TimeTextDefaults.timeTextStyle(color = Color.Green)
+ style = leadingTextStyle
)
},
leadingCurvedContent = {
BasicCurvedText(
text = "Leading content",
- style = TimeTextDefaults.timeCurvedTextStyle(color = Color.Green)
+ style = CurvedTextStyle(leadingTextStyle)
)
},
trailingLinearContent = {
Text(
text = "Trailing content",
- style = TimeTextDefaults.timeTextStyle(color = Color.Yellow)
+ style = trailingTextStyle
)
},
trailingCurvedContent = {
BasicCurvedText(
text = "Trailing content",
- style = TimeTextDefaults.timeCurvedTextStyle(color = Color.Yellow)
+ style = CurvedTextStyle(trailingTextStyle)
)
},
contentPadding = PaddingValues(8.dp)
diff --git a/wear/tiles/tiles-material/api/current.txt b/wear/tiles/tiles-material/api/current.txt
index 42f4d21..38c920e 100644
--- a/wear/tiles/tiles-material/api/current.txt
+++ b/wear/tiles/tiles-material/api/current.txt
@@ -20,8 +20,8 @@
method public androidx.wear.tiles.material.Button.Builder setImageContent(String);
method public androidx.wear.tiles.material.Button.Builder setSize(androidx.wear.tiles.DimensionBuilders.DpProp);
method public androidx.wear.tiles.material.Button.Builder setSize(@Dimension(unit=androidx.annotation.Dimension.DP) float);
- method public androidx.wear.tiles.material.Button.Builder setTextContent(String, androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
- method public androidx.wear.tiles.material.Button.Builder setTextContent(String, androidx.wear.tiles.LayoutElementBuilders.FontStyle);
+ method public androidx.wear.tiles.material.Button.Builder setTextContent(String);
+ method public androidx.wear.tiles.material.Button.Builder setTextContent(String, int);
}
public class ButtonColors {
@@ -153,6 +153,25 @@
field public static final float GAP_START_ANGLE = -156.1f;
}
+ public class Text implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getColor();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle getFontStyle();
+ method public float getLineHeight();
+ method public float getMaxLines();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers getModifiers();
+ method public int getMultilineAlignment();
+ method public int getOverflow();
+ method public String getText();
+ }
+
+ public static final class Text.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public Text.Builder();
+ method public androidx.wear.tiles.material.Text build();
+ method public androidx.wear.tiles.material.Text.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.material.Text.Builder setText(String);
+ method public androidx.wear.tiles.material.Text.Builder setTypography(int);
+ }
+
public class TitleChip implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
method public androidx.wear.tiles.ActionBuilders.Action getAction();
method public androidx.wear.tiles.material.ChipColors getChipColors();
@@ -172,6 +191,21 @@
method public androidx.wear.tiles.material.TitleChip.Builder setWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
}
+ public class Typography {
+ field public static final int TYPOGRAPHY_BODY1 = 7; // 0x7
+ field public static final int TYPOGRAPHY_BODY2 = 8; // 0x8
+ field public static final int TYPOGRAPHY_BUTTON = 9; // 0x9
+ field public static final int TYPOGRAPHY_CAPTION1 = 10; // 0xa
+ field public static final int TYPOGRAPHY_CAPTION2 = 11; // 0xb
+ field public static final int TYPOGRAPHY_CAPTION3 = 12; // 0xc
+ field public static final int TYPOGRAPHY_DISPLAY1 = 1; // 0x1
+ field public static final int TYPOGRAPHY_DISPLAY2 = 2; // 0x2
+ field public static final int TYPOGRAPHY_DISPLAY3 = 3; // 0x3
+ field public static final int TYPOGRAPHY_TITLE1 = 4; // 0x4
+ field public static final int TYPOGRAPHY_TITLE2 = 5; // 0x5
+ field public static final int TYPOGRAPHY_TITLE3 = 6; // 0x6
+ }
+
}
package androidx.wear.tiles.material.layouts {
diff --git a/wear/tiles/tiles-material/api/public_plus_experimental_current.txt b/wear/tiles/tiles-material/api/public_plus_experimental_current.txt
index 42f4d21..38c920e 100644
--- a/wear/tiles/tiles-material/api/public_plus_experimental_current.txt
+++ b/wear/tiles/tiles-material/api/public_plus_experimental_current.txt
@@ -20,8 +20,8 @@
method public androidx.wear.tiles.material.Button.Builder setImageContent(String);
method public androidx.wear.tiles.material.Button.Builder setSize(androidx.wear.tiles.DimensionBuilders.DpProp);
method public androidx.wear.tiles.material.Button.Builder setSize(@Dimension(unit=androidx.annotation.Dimension.DP) float);
- method public androidx.wear.tiles.material.Button.Builder setTextContent(String, androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
- method public androidx.wear.tiles.material.Button.Builder setTextContent(String, androidx.wear.tiles.LayoutElementBuilders.FontStyle);
+ method public androidx.wear.tiles.material.Button.Builder setTextContent(String);
+ method public androidx.wear.tiles.material.Button.Builder setTextContent(String, int);
}
public class ButtonColors {
@@ -153,6 +153,25 @@
field public static final float GAP_START_ANGLE = -156.1f;
}
+ public class Text implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getColor();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle getFontStyle();
+ method public float getLineHeight();
+ method public float getMaxLines();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers getModifiers();
+ method public int getMultilineAlignment();
+ method public int getOverflow();
+ method public String getText();
+ }
+
+ public static final class Text.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public Text.Builder();
+ method public androidx.wear.tiles.material.Text build();
+ method public androidx.wear.tiles.material.Text.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.material.Text.Builder setText(String);
+ method public androidx.wear.tiles.material.Text.Builder setTypography(int);
+ }
+
public class TitleChip implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
method public androidx.wear.tiles.ActionBuilders.Action getAction();
method public androidx.wear.tiles.material.ChipColors getChipColors();
@@ -172,6 +191,21 @@
method public androidx.wear.tiles.material.TitleChip.Builder setWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
}
+ public class Typography {
+ field public static final int TYPOGRAPHY_BODY1 = 7; // 0x7
+ field public static final int TYPOGRAPHY_BODY2 = 8; // 0x8
+ field public static final int TYPOGRAPHY_BUTTON = 9; // 0x9
+ field public static final int TYPOGRAPHY_CAPTION1 = 10; // 0xa
+ field public static final int TYPOGRAPHY_CAPTION2 = 11; // 0xb
+ field public static final int TYPOGRAPHY_CAPTION3 = 12; // 0xc
+ field public static final int TYPOGRAPHY_DISPLAY1 = 1; // 0x1
+ field public static final int TYPOGRAPHY_DISPLAY2 = 2; // 0x2
+ field public static final int TYPOGRAPHY_DISPLAY3 = 3; // 0x3
+ field public static final int TYPOGRAPHY_TITLE1 = 4; // 0x4
+ field public static final int TYPOGRAPHY_TITLE2 = 5; // 0x5
+ field public static final int TYPOGRAPHY_TITLE3 = 6; // 0x6
+ }
+
}
package androidx.wear.tiles.material.layouts {
diff --git a/wear/tiles/tiles-material/api/restricted_current.txt b/wear/tiles/tiles-material/api/restricted_current.txt
index 42f4d21..38c920e 100644
--- a/wear/tiles/tiles-material/api/restricted_current.txt
+++ b/wear/tiles/tiles-material/api/restricted_current.txt
@@ -20,8 +20,8 @@
method public androidx.wear.tiles.material.Button.Builder setImageContent(String);
method public androidx.wear.tiles.material.Button.Builder setSize(androidx.wear.tiles.DimensionBuilders.DpProp);
method public androidx.wear.tiles.material.Button.Builder setSize(@Dimension(unit=androidx.annotation.Dimension.DP) float);
- method public androidx.wear.tiles.material.Button.Builder setTextContent(String, androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
- method public androidx.wear.tiles.material.Button.Builder setTextContent(String, androidx.wear.tiles.LayoutElementBuilders.FontStyle);
+ method public androidx.wear.tiles.material.Button.Builder setTextContent(String);
+ method public androidx.wear.tiles.material.Button.Builder setTextContent(String, int);
}
public class ButtonColors {
@@ -153,6 +153,25 @@
field public static final float GAP_START_ANGLE = -156.1f;
}
+ public class Text implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getColor();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle getFontStyle();
+ method public float getLineHeight();
+ method public float getMaxLines();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers getModifiers();
+ method public int getMultilineAlignment();
+ method public int getOverflow();
+ method public String getText();
+ }
+
+ public static final class Text.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public Text.Builder();
+ method public androidx.wear.tiles.material.Text build();
+ method public androidx.wear.tiles.material.Text.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.material.Text.Builder setText(String);
+ method public androidx.wear.tiles.material.Text.Builder setTypography(int);
+ }
+
public class TitleChip implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
method public androidx.wear.tiles.ActionBuilders.Action getAction();
method public androidx.wear.tiles.material.ChipColors getChipColors();
@@ -172,6 +191,21 @@
method public androidx.wear.tiles.material.TitleChip.Builder setWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
}
+ public class Typography {
+ field public static final int TYPOGRAPHY_BODY1 = 7; // 0x7
+ field public static final int TYPOGRAPHY_BODY2 = 8; // 0x8
+ field public static final int TYPOGRAPHY_BUTTON = 9; // 0x9
+ field public static final int TYPOGRAPHY_CAPTION1 = 10; // 0xa
+ field public static final int TYPOGRAPHY_CAPTION2 = 11; // 0xb
+ field public static final int TYPOGRAPHY_CAPTION3 = 12; // 0xc
+ field public static final int TYPOGRAPHY_DISPLAY1 = 1; // 0x1
+ field public static final int TYPOGRAPHY_DISPLAY2 = 2; // 0x2
+ field public static final int TYPOGRAPHY_DISPLAY3 = 3; // 0x3
+ field public static final int TYPOGRAPHY_TITLE1 = 4; // 0x4
+ field public static final int TYPOGRAPHY_TITLE2 = 5; // 0x5
+ field public static final int TYPOGRAPHY_TITLE3 = 6; // 0x6
+ }
+
}
package androidx.wear.tiles.material.layouts {
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Button.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Button.java
index 440e987..60b0090 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Button.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Button.java
@@ -34,21 +34,20 @@
import androidx.annotation.RestrictTo.Scope;
import androidx.wear.tiles.ActionBuilders.Action;
import androidx.wear.tiles.ColorBuilders.ColorProp;
-import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
import androidx.wear.tiles.DimensionBuilders.ContainerDimension;
import androidx.wear.tiles.DimensionBuilders.DpProp;
+import androidx.wear.tiles.LayoutElementBuilders;
import androidx.wear.tiles.LayoutElementBuilders.Box;
import androidx.wear.tiles.LayoutElementBuilders.ColorFilter;
import androidx.wear.tiles.LayoutElementBuilders.FontStyle;
-import androidx.wear.tiles.LayoutElementBuilders.FontStyles;
import androidx.wear.tiles.LayoutElementBuilders.Image;
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
-import androidx.wear.tiles.LayoutElementBuilders.Text;
import androidx.wear.tiles.ModifiersBuilders;
import androidx.wear.tiles.ModifiersBuilders.Background;
import androidx.wear.tiles.ModifiersBuilders.Clickable;
import androidx.wear.tiles.ModifiersBuilders.Corner;
import androidx.wear.tiles.ModifiersBuilders.Modifiers;
+import androidx.wear.tiles.material.Typography.TypographyName;
import androidx.wear.tiles.proto.LayoutElementProto;
import java.lang.annotation.Retention;
@@ -92,14 +91,15 @@
@NonNull private String mContentDescription = "";
@NonNull private DpProp mSize = DEFAULT_BUTTON_SIZE;
@Nullable private String mText = null;
- @Nullable private FontStyle mFontStyle = null;
+ private @TypographyName int mTypographyName =
+ getDefaultTypographyForSize(DEFAULT_BUTTON_SIZE);
+ private boolean mIsTypographyNameSet = false;
@Nullable private String mIcon = null;
@Nullable private DpProp mIconSize = null;
@Nullable private String mImage = null;
@NonNull private ButtonColors mButtonColors = PRIMARY_BUTTON_COLORS;
private @ButtonType int mType = NOT_SET;
private boolean mDefaultSize = false;
- @Nullable private DeviceParameters mDeviceParameters = null;
/**
* Creates a builder for the {@link Button} from the given content. Custom content should be
@@ -216,39 +216,38 @@
* Sets the content of this Button to be the given text with the default font for the set
* size (for the {@link ButtonDefaults#DEFAULT_BUTTON_SIZE}, {@link
* ButtonDefaults#LARGE_BUTTON_SIZE} and {@link ButtonDefaults#EXTRA_LARGE_BUTTON_SIZE} is
- * {@link FontStyles#title2}, {@link FontStyles#title1} and {@link FontStyles#display3}
- * respectively). Any previously added content will be overridden. Text should contain no
- * more than 3 characters, otherwise it will overflow from the edges.
+ * {@link Typography#TYPOGRAPHY_TITLE2}, {@link Typography#TYPOGRAPHY_TITLE1} and {@link
+ * Typography#TYPOGRAPHY_DISPLAY3} respectively). Any previously added content will be
+ * overridden. Text should contain no more than 3 characters, otherwise it will overflow
+ * from the edges.
*/
// There are multiple methods to set different type of content, but there is general getter
// getContent that will return LayoutElement set by any of them. b/217197259
@NonNull
@SuppressWarnings("MissingGetterMatchingBuilder")
- public Builder setTextContent(
- @NonNull String text, @NonNull DeviceParameters deviceParameters) {
+ public Builder setTextContent(@NonNull String text) {
resetContent();
this.mText = text;
- this.mDeviceParameters = deviceParameters;
this.mType = TEXT;
this.mDefaultSize = true;
return this;
}
/**
- * Sets the content of this Button to be the given text with the given font. You shouldn't
- * add color to your font as it will be overridden by the {@link
- * ButtonColors#getContentColor}. Only {@link #setButtonColors} should be used for
- * customizing the colors of the button. Any previously added content will be overridden.
- * Text should contain no more than 3 characters, otherwise it will overflow from the edges.
+ * Sets the content of this Button to be the given text with the given font. If you need
+ * more font related customization, consider using {@link #setContent} with {@link Text}
+ * component. Any previously added content will be overridden. Text should contain no more
+ * than 3 characters, otherwise it will overflow from the edges.
*/
// There are multiple methods to set different type of content, but there is general getter
// getContent that will return LayoutElement set by any of them. b/217197259
@NonNull
@SuppressWarnings("MissingGetterMatchingBuilder")
- public Builder setTextContent(@NonNull String text, @NonNull FontStyle font) {
+ public Builder setTextContent(@NonNull String text, @TypographyName int typographyName) {
resetContent();
this.mText = text;
- this.mFontStyle = font;
+ this.mTypographyName = typographyName;
+ this.mIsTypographyNameSet = true;
this.mType = TEXT;
this.mDefaultSize = false;
return this;
@@ -274,7 +273,7 @@
private void resetContent() {
this.mText = null;
- this.mFontStyle = null;
+ this.mIsTypographyNameSet = false;
this.mIcon = null;
this.mImage = null;
this.mCustomContent = null;
@@ -345,28 +344,16 @@
}
case TEXT:
{
- FontStyle fontStyle;
- if (mFontStyle != null) {
- fontStyle =
- FontStyle.fromProto(
- mFontStyle.toProto().toBuilder()
- .setColor(
- mButtonColors
- .getContentColor()
- .toProto())
- .build());
- } else {
- fontStyle =
- getDefaultFontForSize(mSize, checkNotNull(mDeviceParameters))
- .setColor(mButtonColors.getContentColor())
- .build();
- }
-
+ @TypographyName
+ int typographyName =
+ mIsTypographyNameSet
+ ? mTypographyName : getDefaultTypographyForSize(mSize);
mContent =
new Text.Builder()
- .setText(checkNotNull(mText))
- .setMaxLines(1)
- .setFontStyle(fontStyle);
+ .setText(checkNotNull(mText))
+ .setMaxLines(1)
+ .setTypography(typographyName)
+ .setColor(mButtonColors.getContentColor());
return mContent.build();
}
@@ -413,37 +400,33 @@
}
}
- @NonNull
- private FontStyle.Builder getDefaultFontForSize(
- @NonNull DpProp size, @NonNull DeviceParameters deviceParameters) {
+ private @TypographyName int getDefaultTypographyForSize(@NonNull DpProp size) {
if (size.getValue() == LARGE_BUTTON_SIZE.getValue()) {
- return FontStyles.title1(deviceParameters);
+ return Typography.TYPOGRAPHY_TITLE1;
} else {
if (size.getValue() == EXTRA_LARGE_BUTTON_SIZE.getValue()) {
- return FontStyles.display3(deviceParameters);
+ return Typography.TYPOGRAPHY_DISPLAY3;
} else {
- return FontStyles.title2(deviceParameters);
+ return Typography.TYPOGRAPHY_TITLE2;
}
}
}
}
- /** Returns the content of this Button. Intended for testing purposes only. */
+ /** Returns the content of this Button. */
@NonNull
public LayoutElement getContent() {
return checkNotNull(mElement.getContents().get(0));
}
- /**
- * Returns click event action associated with this Button. Intended for testing purposes only.
- */
+ /** Returns click event action associated with this Button. */
@NonNull
public Action getAction() {
return checkNotNull(
checkNotNull(checkNotNull(mElement.getModifiers()).getClickable()).getOnClick());
}
- /** Returns content description for this Button. Intended for testing purposes only. */
+ /** Returns content description for this Button. */
@NonNull
public String getContentDescription() {
return checkNotNull(
@@ -451,7 +434,7 @@
.getContentDescription());
}
- /** Returns size for this Button. Intended for testing purposes only. */
+ /** Returns size for this Button. */
@NonNull
public ContainerDimension getSize() {
return checkNotNull(mElement.getWidth());
@@ -462,7 +445,7 @@
checkNotNull(checkNotNull(mElement.getModifiers()).getBackground()).getColor());
}
- /** Returns button color of this Button. Intended for testing purposes only. */
+ /** Returns button color of this Button. */
@NonNull
public ButtonColors getButtonColors() {
ColorProp backgroundColor = getBackgroundColor();
@@ -477,7 +460,11 @@
break;
case Builder.TEXT:
contentColor =
- checkNotNull(checkNotNull(((Text) mainElement).getFontStyle()).getColor());
+ checkNotNull(
+ checkNotNull(
+ ((LayoutElementBuilders.Text) mainElement)
+ .getFontStyle())
+ .getColor());
break;
case Builder.IMAGE:
case Builder.CUSTOM_CONTENT:
@@ -502,8 +489,9 @@
* have color.
*/
private @Builder.ButtonType int getType(LayoutElement element) {
- if (element instanceof Text) {
- FontStyle fontStyle = ((Text) element).getFontStyle();
+ // To elementary Text class as Material Text when it goes to proto disappears.
+ if (element instanceof LayoutElementBuilders.Text) {
+ FontStyle fontStyle = ((LayoutElementBuilders.Text) element).getFontStyle();
if (fontStyle != null && fontStyle.getColor() != null) {
return Builder.TEXT;
}
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Chip.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Chip.java
index 0391a36..9c74f0d 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Chip.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Chip.java
@@ -43,20 +43,18 @@
import androidx.wear.tiles.LayoutElementBuilders.Box;
import androidx.wear.tiles.LayoutElementBuilders.ColorFilter;
import androidx.wear.tiles.LayoutElementBuilders.Column;
-import androidx.wear.tiles.LayoutElementBuilders.FontStyle;
-import androidx.wear.tiles.LayoutElementBuilders.FontStyles;
import androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignment;
import androidx.wear.tiles.LayoutElementBuilders.Image;
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
import androidx.wear.tiles.LayoutElementBuilders.Row;
import androidx.wear.tiles.LayoutElementBuilders.Spacer;
-import androidx.wear.tiles.LayoutElementBuilders.Text;
import androidx.wear.tiles.ModifiersBuilders;
import androidx.wear.tiles.ModifiersBuilders.Background;
import androidx.wear.tiles.ModifiersBuilders.Clickable;
import androidx.wear.tiles.ModifiersBuilders.Corner;
import androidx.wear.tiles.ModifiersBuilders.Modifiers;
import androidx.wear.tiles.ModifiersBuilders.Padding;
+import androidx.wear.tiles.material.Typography.TypographyName;
import androidx.wear.tiles.proto.LayoutElementProto;
import java.lang.annotation.Retention;
@@ -68,10 +66,10 @@
* optional icon or with custom content.
*
* <p>The Chip is Stadium shape and has a max height designed to take no more than two lines of text
- * of {@link FontStyles#button} style. The {@link Chip} can have an icon horizontally parallel to
- * the two lines of text. Width of chip can very, and the recommended size is screen dependent with
- * the recommended margin being defined in {@link ChipDefaults#DEFAULT_MARGIN_PERCENT} which is set
- * by default.
+ * of {@link Typography#TYPOGRAPHY_BUTTON} style. The {@link Chip} can have an icon horizontally
+ * parallel to the two lines of text. Width of chip can very, and the recommended size is screen
+ * dependent with the recommended margin being defined in
+ * {@link ChipDefaults#DEFAULT_MARGIN_PERCENT} which is set by default.
*
* <p>The recommended set of {@link ChipColors} styles can be obtained from {@link ChipDefaults}.,
* e.g. {@link ChipDefaults#PRIMARY} to get a color scheme for a primary {@link Chip} which by
@@ -105,14 +103,13 @@
@Nullable private String mLabelText = null;
@NonNull private final Action mAction;
@NonNull private final String mClickableId;
- @NonNull private final DeviceParameters mDeviceParameters;
@NonNull private String mContentDescription = "";
@NonNull private ContainerDimension mWidth;
@NonNull private DpProp mHeight = DEFAULT_HEIGHT;
@NonNull private ChipColors mChipColors = PRIMARY;
private @ChipType int mType = NOT_SET;
private @HorizontalAlignment int mHorizontalAlign = HORIZONTAL_ALIGN_START;
- @NonNull private FontStyle mPrimaryTextFont;
+ @TypographyName private int mPrimaryTextTypography;
@NonNull private DpProp mHorizontalPadding = HORIZONTAL_PADDING;
@NonNull private DpProp mVerticalPadding = VERTICAL_PADDING;
@@ -123,7 +120,7 @@
* @param action Associated Actions for click events. When the Chip is clicked it will fire
* the associated action.
* @param clickableId The ID associated with the given action's clickable.
- * @param deviceParameters The device parameters used for styling text.
+ * @param deviceParameters The device parameters used to derive defaults for this Chip.
*/
@SuppressWarnings("LambdaLast")
public Builder(
@@ -132,13 +129,12 @@
@NonNull DeviceParameters deviceParameters) {
mAction = action;
mClickableId = clickableId;
- mDeviceParameters = deviceParameters;
mWidth =
dp(
(100 - 2 * DEFAULT_MARGIN_PERCENT)
* deviceParameters.getScreenWidthDp()
/ 100);
- mPrimaryTextFont = FontStyles.button(deviceParameters).build();
+ mPrimaryTextTypography = Typography.TYPOGRAPHY_BUTTON;
}
/**
@@ -203,8 +199,8 @@
* <p>Sets the font for the primary text and should only be used internally.
*/
@NonNull
- Builder setPrimaryTextFontStyle(@NonNull FontStyle fontStyle) {
- this.mPrimaryTextFont = fontStyle;
+ Builder setPrimaryTextTypography(@TypographyName int typography) {
+ this.mPrimaryTextTypography = typography;
return this;
}
@@ -276,7 +272,6 @@
return this;
}
- // TODO(b/207350548): In RTL mode, should icon still be on the left.
/**
* Sets the horizontal alignment in the chip. It is strongly recommended that the content of
* the chip is start-aligned if there is more than primary text in it. If not set, {@link
@@ -372,7 +367,7 @@
if (mType == NOT_SET) {
throw new IllegalStateException(
"No content set. Use setPrimaryTextContent or similar method to add"
- + " content");
+ + " content");
}
if (mType == CUSTOM_CONTENT) {
return checkNotNull(mCustomContent);
@@ -380,7 +375,8 @@
Text mainTextElement =
new Text.Builder()
.setText(mPrimaryText)
- .setFontStyle(customizeFontStyle())
+ .setTypography(mPrimaryTextTypography)
+ .setColor(mChipColors.getContentColor())
.setMaxLines(getCorrectMaxLines())
.setOverflow(LayoutElementBuilders.TEXT_OVERFLOW_TRUNCATE)
.setMultilineAlignment(LayoutElementBuilders.TEXT_ALIGN_START)
@@ -395,10 +391,8 @@
Text labelTextElement =
new Text.Builder()
.setText(mLabelText)
- .setFontStyle(
- FontStyles.caption2(mDeviceParameters)
- .setColor(mChipColors.getSecondaryContentColor())
- .build())
+ .setTypography(Typography.TYPOGRAPHY_CAPTION2)
+ .setColor(mChipColors.getSecondaryContentColor())
.setMaxLines(1)
.setOverflow(LayoutElementBuilders.TEXT_OVERFLOW_TRUNCATE)
.setMultilineAlignment(LayoutElementBuilders.TEXT_ALIGN_START)
@@ -431,13 +425,6 @@
}
}
- private FontStyle customizeFontStyle() {
- return FontStyle.fromProto(
- mPrimaryTextFont.toProto().toBuilder()
- .setColor(mChipColors.getContentColor().toProto())
- .build());
- }
-
private int getCorrectMaxLines() {
if (mMaxLines > 0) {
return mMaxLines;
@@ -503,13 +490,17 @@
if (contents.size() == 1 || contents.size() == 2) {
// This is potentially our chip and this part contains 1 or 2 lines of text.
LayoutElement element = contents.get(0);
- if (element instanceof Text) {
- contentColor = getTextColorFromContent((Text) element);
+ // To elementary Text class as Material Text when it goes to proto disappears.
+ if (element instanceof LayoutElementBuilders.Text) {
+ contentColor = getTextColorFromContent((LayoutElementBuilders.Text) element);
if (contents.size() == 2) {
element = contents.get(1);
- if (element instanceof Text) {
- secondaryContentColor = getTextColorFromContent((Text) element);
+ // To elementary Text class as Material Text when it goes to proto
+ // disappears.
+ if (element instanceof LayoutElementBuilders.Text) {
+ secondaryContentColor =
+ getTextColorFromContent((LayoutElementBuilders.Text) element);
}
}
}
@@ -530,7 +521,7 @@
return new ChipColors(backgroundColor, iconTintColor, contentColor, secondaryContentColor);
}
- private ColorProp getTextColorFromContent(Text text) {
+ private ColorProp getTextColorFromContent(LayoutElementBuilders.Text text) {
ColorProp color = new ColorProp.Builder().build();
if (text.getFontStyle() != null && text.getFontStyle().getColor() != null) {
color = checkNotNull(checkNotNull(text.getFontStyle()).getColor());
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/CircularProgressIndicator.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/CircularProgressIndicator.java
index 1f4eea9..8455984 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/CircularProgressIndicator.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/CircularProgressIndicator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * Copyright 2022 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.
@@ -45,15 +45,14 @@
/**
* Tiles component {@link CircularProgressIndicator} that represents circular progress indicator
- * which supports a gap in the circular track between startAngle and endAngle.
- * [Progress Indicator doc]
- * (https://developer.android.com/training/wearables/components/progress-indicator)
+ * which supports a gap in the circular track between startAngle and endAngle. [Progress Indicator
+ * doc] (https://developer.android.com/training/wearables/components/progress-indicator)
*
* <p>The CircularProgressIndicator is a colored arc around the edge of the screen with the given
* start and end angles, which can describe a full or partial circle. Behind it is an arc with
- * optional gap representing full progress. The recommended sizes are defined in
- * {@link ProgressIndicatorDefaults}. Unless specified, the CircularProgressIndicator will have
- * the full length.
+ * optional gap representing full progress. The recommended sizes are defined in {@link
+ * ProgressIndicatorDefaults}. Unless specified, the CircularProgressIndicator will have the full
+ * length.
*
* <p>The recommended set of {@link ProgressIndicatorColors} can be obtained from {@link
* ProgressIndicatorDefaults}., e.g. {@link ProgressIndicatorDefaults#DEFAULT_COLOR} to get a color
@@ -192,8 +191,8 @@
.addContent(
new ArcLine.Builder()
.setColor(
- mCircularProgressIndicatorColors.getTrackColor()
- )
+ mCircularProgressIndicatorColors
+ .getTrackColor())
.setThickness(mStrokeWidth)
.setLength(length)
.build())
@@ -207,8 +206,7 @@
new ArcLine.Builder()
.setColor(
mCircularProgressIndicatorColors
- .getIndicatorColor()
- )
+ .getIndicatorColor())
.setThickness(mStrokeWidth)
.setLength(degrees(mProgress * length.getValue()))
.build());
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/CompactChip.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/CompactChip.java
index b7021fb..55b9eaa 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/CompactChip.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/CompactChip.java
@@ -28,7 +28,6 @@
import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
import androidx.wear.tiles.DimensionBuilders.ContainerDimension;
import androidx.wear.tiles.DimensionBuilders.WrappedDimensionProp;
-import androidx.wear.tiles.LayoutElementBuilders.FontStyles;
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
import androidx.wear.tiles.proto.LayoutElementProto;
@@ -36,7 +35,8 @@
* Tiles component {@link CompactChip} that represents clickable object with the text.
*
* <p>The Chip is Stadium shape and has a max height designed to take no more than one line of text
- * of {@link FontStyles#caption1} style. Width of the chip is adjustable to the text size.
+ * of {@link Typography#TYPOGRAPHY_CAPTION1} style. Width of the chip is adjustable to the text
+ * size.
*
* <p>The recommended set of {@link ChipColors} styles can be obtained from {@link ChipDefaults}.,
* e.g. {@link ChipDefaults#COMPACT_PRIMARY} to get a color scheme for a primary {@link CompactChip}
@@ -105,8 +105,7 @@
.setHeight(COMPACT_HEIGHT)
.setHorizontalPadding(COMPACT_HORIZONTAL_PADDING)
.setPrimaryTextContent(mText)
- .setPrimaryTextFontStyle(
- FontStyles.caption1(mDeviceParameters).build());
+ .setPrimaryTextTypography(Typography.TYPOGRAPHY_CAPTION1);
return new CompactChip(chipBuilder.build());
}
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Helper.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Helper.java
index 85adda7..08c5d74 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Helper.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Helper.java
@@ -37,7 +37,10 @@
/**
* Returns given value if not null or throws {@code NullPointerException} otherwise.
+ *
+ * @hide
*/
+ @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public static <T> T checkNotNull(@Nullable T value) {
if (value == null) {
@@ -53,7 +56,10 @@
/**
* Returns true if the given DeviceParameters belong to the round screen device.
+ *
+ * @hide
*/
+ @RestrictTo(Scope.LIBRARY_GROUP)
public static boolean isRoundDevice(@NonNull DeviceParameters deviceParameters) {
return deviceParameters.getScreenShape() == DeviceParametersBuilders.SCREEN_SHAPE_ROUND;
}
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Text.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Text.java
new file mode 100644
index 0000000..1d94bb5
--- /dev/null
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Text.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2022 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.wear.tiles.material;
+
+import static androidx.wear.tiles.ColorBuilders.argb;
+import static androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_CENTER;
+import static androidx.wear.tiles.LayoutElementBuilders.TEXT_OVERFLOW_TRUNCATE;
+import static androidx.wear.tiles.material.Helper.checkNotNull;
+import static androidx.wear.tiles.material.Typography.TYPOGRAPHY_DISPLAY1;
+import static androidx.wear.tiles.material.Typography.getFontStyleBuilder;
+import static androidx.wear.tiles.material.Typography.getLineHeightForTypography;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.tiles.ColorBuilders.ColorProp;
+import androidx.wear.tiles.LayoutElementBuilders;
+import androidx.wear.tiles.LayoutElementBuilders.FontStyle;
+import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
+import androidx.wear.tiles.LayoutElementBuilders.TextAlignment;
+import androidx.wear.tiles.LayoutElementBuilders.TextOverflow;
+import androidx.wear.tiles.ModifiersBuilders.Modifiers;
+import androidx.wear.tiles.material.Typography.TypographyName;
+import androidx.wear.tiles.proto.LayoutElementProto;
+
+/**
+ * Tiles component {@link Text} that represents text object holding any information.
+ *
+ * <p>There are pre-built typography styles that can be obtained from constants in
+ * {@link Typography}.
+ */
+public class Text implements LayoutElement {
+ @NonNull private final LayoutElementBuilders.Text mText;
+
+ Text(@NonNull LayoutElementBuilders.Text mText) {
+ this.mText = mText;
+ }
+
+ /** Builder class for {@link Text}. */
+ public static final class Builder implements LayoutElement.Builder {
+ @NonNull private String mTextContent = "";
+ @NonNull private ColorProp mColor = argb(Colors.ON_PRIMARY);
+ private @TypographyName int mTypographyName = TYPOGRAPHY_DISPLAY1;
+ private boolean mItalic = false;
+ private int mMaxLines = 1;
+ private boolean mUnderline = false;
+ private @TextAlignment int mMultilineAlignment = TEXT_ALIGN_CENTER;
+ @NonNull private Modifiers mModifiers = new Modifiers.Builder().build();
+ private @TextOverflow int mOverflow = TEXT_OVERFLOW_TRUNCATE;
+
+ /** Sets the text content for the {@link Text}. */
+ @NonNull
+ public Builder setText(@NonNull String text) {
+ this.mTextContent = text;
+ return this;
+ }
+
+ /**
+ * Sets the typography for the {@link Text}. If not set,
+ * {@link Typography#TYPOGRAPHY_TITLE1} will be used.
+ */
+ @NonNull
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ // There is getFontStyle matching getter for this setter as the serialized format of the
+ // ProtoLayouts do not allow for a direct reconstruction of the all arguments, but it has
+ // FontStyle object of that text.
+ public Builder setTypography(@TypographyName int typography) {
+ this.mTypographyName = typography;
+ return this;
+ }
+
+ /**
+ * Sets the color for the {@link Text}. If not set, {@link Colors#ON_PRIMARY} will be
+ * used.
+ */
+ @NonNull
+ public Builder setColor(@NonNull ColorProp color) {
+ this.mColor = color;
+ return this;
+ }
+
+ /** Sets the text to be italic. */
+ @NonNull
+ Builder setItalic(boolean italic) {
+ this.mItalic = italic;
+ return this;
+ }
+
+ /** Sets the text to be underlined. */
+ @NonNull
+ Builder setUnderline(boolean underline) {
+ this.mUnderline = underline;
+ return this;
+ }
+
+ /** Sets the maximum lines of text. If not set, 1 will be used. */
+ @NonNull
+ Builder setMaxLines(@IntRange(from = 1) int maxLines) {
+ this.mMaxLines = maxLines;
+ return this;
+ }
+
+ /**
+ * Sets the mutliline alignenment for text. If not set, {@link
+ * TextAlignment#TEXT_ALIGN_CENTER} will be used.
+ */
+ @NonNull
+ Builder setMultilineAlignment(@TextAlignment int multilineAlignment) {
+ this.mMultilineAlignment = multilineAlignment;
+ return this;
+ }
+
+ /** Sets the modifiers of text. */
+ @NonNull
+ Builder setModifiers(@NonNull Modifiers modifiers) {
+ this.mModifiers = modifiers;
+ return this;
+ }
+
+ /**
+ * Sets the overflow for text. If not set, {@link TextAlignment#TEXT_OVERFLOW_TRUNCATE} will
+ * be used.
+ */
+ @NonNull
+ Builder setOverflow(@TextOverflow int overflow) {
+ this.mOverflow = overflow;
+ return this;
+ }
+
+ /** Constructs and returns {@link Text} with the provided content and look. */
+ @NonNull
+ @Override
+ public Text build() {
+ LayoutElementBuilders.Text.Builder text =
+ new LayoutElementBuilders.Text.Builder()
+ .setText(mTextContent)
+ .setFontStyle(
+ getFontStyleBuilder(mTypographyName)
+ .setColor(mColor)
+ .setItalic(mItalic)
+ .setUnderline(mUnderline)
+ .build())
+ .setLineHeight(getLineHeightForTypography(mTypographyName))
+ .setMaxLines(mMaxLines)
+ .setMultilineAlignment(mMultilineAlignment)
+ .setModifiers(mModifiers)
+ .setOverflow(mOverflow);
+ return new Text(text.build());
+ }
+ }
+
+ /** Returns the text of this Text element. */
+ @NonNull
+ public String getText() {
+ return checkNotNull(checkNotNull(mText.getText()).getValue());
+ }
+
+ /** Returns the color of this Text element. */
+ @NonNull
+ public ColorProp getColor() {
+ return checkNotNull(checkNotNull(mText.getFontStyle()).getColor());
+ }
+
+ /** Returns the font style of this Text element. */
+ @NonNull
+ public FontStyle getFontStyle() {
+ return checkNotNull(mText.getFontStyle());
+ }
+
+ /** Returns the line height of this Text element. */
+ public float getLineHeight() {
+ return checkNotNull(mText.getLineHeight()).getValue();
+ }
+
+ /** Returns the max lines of text of this Text element. */
+ public float getMaxLines() {
+ return checkNotNull(mText.getMaxLines()).getValue();
+ }
+
+ /** Returns the multiline alignment of this Text element. */
+ public @TextAlignment int getMultilineAlignment() {
+ return checkNotNull(mText.getMultilineAlignment()).getValue();
+ }
+
+ /** Returns the modifiers of this Text element. */
+ @NonNull
+ public Modifiers getModifiers() {
+ return checkNotNull(mText.getModifiers());
+ }
+
+ /** Returns the overflow of this Text element. */
+ public @TextOverflow int getOverflow() {
+ return checkNotNull(mText.getOverflow()).getValue();
+ }
+
+ /** @hide */
+ @NonNull
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public LayoutElementProto.LayoutElement toLayoutElementProto() {
+ return mText.toLayoutElementProto();
+ }
+}
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/TitleChip.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/TitleChip.java
index 37a565a..7efa8b4 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/TitleChip.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/TitleChip.java
@@ -32,7 +32,6 @@
import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
import androidx.wear.tiles.DimensionBuilders.ContainerDimension;
import androidx.wear.tiles.DimensionBuilders.DpProp;
-import androidx.wear.tiles.LayoutElementBuilders.FontStyles;
import androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignment;
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
import androidx.wear.tiles.proto.LayoutElementProto;
@@ -41,7 +40,7 @@
* Tiles component {@link TitleChip} that represents clickable object with the text.
*
* <p>The Title Chip is Stadium shaped object with a larger height then standard Chip and it will
- * take one line of text of {@link FontStyles#title2} style.
+ * take one line of text of {@link Typography#TYPOGRAPHY_TITLE2} style.
*
* <p>The recommended set of {@link ChipColors} styles can be obtained from {@link ChipDefaults},
* e.g. {@link ChipDefaults#TITLE_PRIMARY} to get a color scheme for a primary {@link TitleChip}
@@ -143,7 +142,7 @@
.setMaxLines(1)
.setHorizontalPadding(TITLE_HORIZONTAL_PADDING)
.setPrimaryTextContent(mText)
- .setPrimaryTextFontStyle(FontStyles.title2(mDeviceParameters).build());
+ .setPrimaryTextTypography(Typography.TYPOGRAPHY_TITLE2);
if (mWidth != null) {
chipBuilder.setWidth(mWidth);
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Typography.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Typography.java
new file mode 100644
index 0000000..6137980d
--- /dev/null
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Typography.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2022 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.wear.tiles.material;
+
+import static androidx.annotation.Dimension.SP;
+import static androidx.wear.tiles.DimensionBuilders.sp;
+import static androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_BOLD;
+import static androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM;
+import static androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_NORMAL;
+import static androidx.wear.tiles.material.Helper.checkNotNull;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.Dimension;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.wear.tiles.DimensionBuilders;
+import androidx.wear.tiles.DimensionBuilders.SpProp;
+import androidx.wear.tiles.LayoutElementBuilders.FontStyle;
+import androidx.wear.tiles.LayoutElementBuilders.FontWeight;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Typography styles, currently set up to match Wear's styling. */
+public class Typography {
+ /** Typography for large display text. */
+ public static final int TYPOGRAPHY_DISPLAY1 = 1;
+
+ /** Typography for medium display text. */
+ public static final int TYPOGRAPHY_DISPLAY2 = 2;
+
+ /** Typography for small display text. */
+ public static final int TYPOGRAPHY_DISPLAY3 = 3;
+
+ /** Typography for large title text. */
+ public static final int TYPOGRAPHY_TITLE1 = 4;
+
+ /** Typography for medium title text. */
+ public static final int TYPOGRAPHY_TITLE2 = 5;
+
+ /** Typography for small title text. */
+ public static final int TYPOGRAPHY_TITLE3 = 6;
+
+ /** Typography for large body text. */
+ public static final int TYPOGRAPHY_BODY1 = 7;
+
+ /** Typography for medium body text. */
+ public static final int TYPOGRAPHY_BODY2 = 8;
+
+ /** Typography for bold button text. */
+ public static final int TYPOGRAPHY_BUTTON = 9;
+
+ /** Typography for large caption text. */
+ public static final int TYPOGRAPHY_CAPTION1 = 10;
+
+ /** Typography for medium caption text. */
+ public static final int TYPOGRAPHY_CAPTION2 = 11;
+
+ /** Typography for small caption text. */
+ public static final int TYPOGRAPHY_CAPTION3 = 12;
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ TYPOGRAPHY_DISPLAY1,
+ TYPOGRAPHY_DISPLAY2,
+ TYPOGRAPHY_DISPLAY3,
+ TYPOGRAPHY_TITLE1,
+ TYPOGRAPHY_TITLE2,
+ TYPOGRAPHY_TITLE3,
+ TYPOGRAPHY_BODY1,
+ TYPOGRAPHY_BODY2,
+ TYPOGRAPHY_BUTTON,
+ TYPOGRAPHY_CAPTION1,
+ TYPOGRAPHY_CAPTION2,
+ TYPOGRAPHY_CAPTION3
+ })
+ public @interface TypographyName {}
+
+ /** Mapping for line height for different typography. */
+ @NonNull
+ private static final Map<Integer, Float> FONT_STYLE_TO_LINE_HEIGHT_SP = new HashMap<>();
+
+ static {
+ FONT_STYLE_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_DISPLAY1, 46f);
+ FONT_STYLE_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_DISPLAY2, 40f);
+ FONT_STYLE_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_DISPLAY3, 36f);
+ FONT_STYLE_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_TITLE1, 28f);
+ FONT_STYLE_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_TITLE2, 24f);
+ FONT_STYLE_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_TITLE3, 20f);
+ FONT_STYLE_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_BODY1, 20f);
+ FONT_STYLE_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_BODY2, 18f);
+ FONT_STYLE_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_BUTTON, 19f);
+ FONT_STYLE_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_CAPTION1, 18f);
+ FONT_STYLE_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_CAPTION2, 16f);
+ FONT_STYLE_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_CAPTION3, 14f);
+ }
+
+ private Typography() {}
+
+ /**
+ * Returns the {@link FontStyle.Builder} for the given Typography name with the recommended
+ * size, weight and letter spacing.
+ */
+ @NonNull
+ static FontStyle.Builder getFontStyleBuilder(@TypographyName int typographyCode) {
+ switch (typographyCode) {
+ case TYPOGRAPHY_BODY1:
+ return body1();
+ case TYPOGRAPHY_BODY2:
+ return body2();
+ case TYPOGRAPHY_BUTTON:
+ return button();
+ case TYPOGRAPHY_CAPTION1:
+ return caption1();
+ case TYPOGRAPHY_CAPTION2:
+ return caption2();
+ case TYPOGRAPHY_CAPTION3:
+ return caption3();
+ case TYPOGRAPHY_DISPLAY1:
+ return display1();
+ case TYPOGRAPHY_DISPLAY2:
+ return display2();
+ case TYPOGRAPHY_DISPLAY3:
+ return display3();
+ case TYPOGRAPHY_TITLE1:
+ return title1();
+ case TYPOGRAPHY_TITLE2:
+ return title2();
+ case TYPOGRAPHY_TITLE3:
+ return title3();
+ default:
+ // Shouldn't happen.
+ throw new IllegalArgumentException(
+ "Typography name " + typographyCode + " doesn't exist.");
+ }
+ }
+
+ /**
+ * Returns the recommended line height for the given Typography to be added to the Text
+ * component.
+ */
+ @NonNull
+ static SpProp getLineHeightForTypography(@TypographyName int typography) {
+ if (!FONT_STYLE_TO_LINE_HEIGHT_SP.containsKey(typography)) {
+ throw new IllegalArgumentException("Typography " + typography + " doesn't exist.");
+ }
+ return sp(checkNotNull(FONT_STYLE_TO_LINE_HEIGHT_SP.get(typography)).intValue());
+ }
+
+ // The @Dimension(unit = SP) on sp() is seemingly being ignored, so lint complains that we're
+ // passing SP to something expecting PX. Just suppress the warning for now.
+ @SuppressLint("ResourceType")
+ private static FontStyle.Builder createFontStyleBuilder(
+ @Dimension(unit = SP) int size, @FontWeight int weight, float letterSpacing) {
+ return new FontStyle.Builder()
+ .setSize(DimensionBuilders.sp(size))
+ .setLetterSpacing(DimensionBuilders.em(letterSpacing))
+ .setWeight(weight);
+ }
+
+ /** Font style for large display text. */
+ @NonNull
+ private static FontStyle.Builder display1() {
+ return createFontStyleBuilder(40, FONT_WEIGHT_MEDIUM, 0.01f);
+ }
+
+ /** Font style for medium display text. */
+ @NonNull
+ private static FontStyle.Builder display2() {
+ return createFontStyleBuilder(34, FONT_WEIGHT_MEDIUM, 0.03f);
+ }
+
+ /** Font style for small display text. */
+ @NonNull
+ private static FontStyle.Builder display3() {
+ return createFontStyleBuilder(30, FONT_WEIGHT_MEDIUM, 0.03f);
+ }
+
+ /** Font style for large title text. */
+ @NonNull
+ private static FontStyle.Builder title1() {
+ return createFontStyleBuilder(24, FONT_WEIGHT_MEDIUM, 0.008f);
+ }
+
+ /** Font style for medium title text. */
+ @NonNull
+ private static FontStyle.Builder title2() {
+ return createFontStyleBuilder(20, FONT_WEIGHT_MEDIUM, 0.01f);
+ }
+
+ /** Font style for small title text. */
+ @NonNull
+ private static FontStyle.Builder title3() {
+ return createFontStyleBuilder(16, FONT_WEIGHT_MEDIUM, 0.01f);
+ }
+
+ /** Font style for normal body text. */
+ @NonNull
+ private static FontStyle.Builder body1() {
+ return createFontStyleBuilder(16, FONT_WEIGHT_NORMAL, 0.01f);
+ }
+
+ /** Font style for small body text. */
+ @NonNull
+ private static FontStyle.Builder body2() {
+ return createFontStyleBuilder(14, FONT_WEIGHT_NORMAL, 0.014f);
+ }
+
+ /** Font style for bold button text. */
+ @NonNull
+ private static FontStyle.Builder button() {
+ return createFontStyleBuilder(15, FONT_WEIGHT_BOLD, 0.03f);
+ }
+
+ /** Font style for large caption text. */
+ @NonNull
+ private static FontStyle.Builder caption1() {
+ return createFontStyleBuilder(14, FONT_WEIGHT_MEDIUM, 0.01f);
+ }
+
+ /** Font style for medium caption text. */
+ @NonNull
+ private static FontStyle.Builder caption2() {
+ return createFontStyleBuilder(12, FONT_WEIGHT_MEDIUM, 0.01f);
+ }
+
+ /** Font style for small caption text. */
+ @NonNull
+ private static FontStyle.Builder caption3() {
+ return createFontStyleBuilder(10, FONT_WEIGHT_MEDIUM, 0.01f);
+ }
+}
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/LayoutDefaults.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/LayoutDefaults.java
index b482f48..c7c132a 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/LayoutDefaults.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/LayoutDefaults.java
@@ -25,26 +25,24 @@
private LayoutDefaults() {}
/**
- * The default percentage for the bottom margin for primary chip in the {@link
- * PrimaryLayout}.
+ * The default percentage for the bottom margin for primary chip in the {@link PrimaryLayout}.
*/
static final float PRIMARY_LAYOUT_MARGIN_BOTTOM_ROUND_PERCENT = 6.3f / 100;
/**
- * The default percentage for the bottom margin for primary chip in the {@link
- * PrimaryLayout}.
+ * The default percentage for the bottom margin for primary chip in the {@link PrimaryLayout}.
*/
static final float PRIMARY_LAYOUT_MARGIN_BOTTOM_SQUARE_PERCENT = 2.2f / 100;
/**
- * The default percentage for the top margin for primary chip in the {@link PrimaryLayout}
- * on round devices.
+ * The default percentage for the top margin for primary chip in the {@link PrimaryLayout} on
+ * round devices.
*/
static final float PRIMARY_LAYOUT_MARGIN_TOP_ROUND_PERCENT = 16.7f / 100;
/**
- * The default percentage for the top margin for primary chip in the {@link PrimaryLayout}
- * on square devices.
+ * The default percentage for the top margin for primary chip in the {@link PrimaryLayout} on
+ * square devices.
*/
static final float PRIMARY_LAYOUT_MARGIN_TOP_SQUARE_PERCENT = 15.6f / 100;
@@ -81,9 +79,9 @@
*/
public static final float PROGRESS_INDICATOR_LAYOUT_PADDING_BELOW_MAIN_CONTENT_DP = 8;
- /** The default spacer width for slots in the {@link MultiSlotLayout}. */
+ /** The default spacer width for slots in a {@link MultiSlotLayout}. */
public static final DpProp MULTI_SLOT_LAYOUT_HORIZONTAL_SPACER_WIDTH = dp(8);
- /** The recommended space between slots in the {@link MultiSlotLayout} and additional labels. */
+ /** The recommended space between slots in a {@link MultiSlotLayout} and additional labels. */
public static final DpProp MULTI_SLOT_LAYOUT_VERTICAL_SPACER_HEIGHT = dp(8);
}
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/MultiSlotLayout.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/MultiSlotLayout.java
index f7ddb6d..a71012c 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/MultiSlotLayout.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/MultiSlotLayout.java
@@ -75,10 +75,9 @@
@NonNull private DpProp mVerticalSpacerHeight = MULTI_SLOT_LAYOUT_VERTICAL_SPACER_HEIGHT;
/**
- * Creates a builder for the {@link MultiSlotLayout} from the given content. Custom content
- * inside of it can later be set with {@link #addSlotContent}, {@link
- * #setPrimaryChipContent}, {@link #setPrimaryLabelTextContent}, {@link
- * #setSecondaryLabelTextContent}.
+ * Creates a builder for the {@link MultiSlotLayout}. Content inside of it can later be set
+ * with {@link #addSlotContent}, {@link #setPrimaryChipContent}, {@link
+ * #setPrimaryLabelTextContent} and {@link #setSecondaryLabelTextContent}.
*/
public Builder(@NonNull DeviceParameters deviceParameters) {
this.mDeviceParameters = deviceParameters;
@@ -123,14 +122,14 @@
// There is no direct matching getter for this setter as the serialized format of the
// ProtoLayouts do not allow for a direct reconstruction of the arguments. Instead there are
// methods to get the contents a whole for rendering.
- public Builder addSlotContent(@NonNull LayoutElement slotsContent) {
- mSlotsContent.add(slotsContent);
+ public Builder addSlotContent(@NonNull LayoutElement slotContent) {
+ mSlotsContent.add(slotContent);
return this;
}
/**
- * Sets the horizontal spacer width which is used as a space between slots if there is
- * more than one slot. If not set, {@link
+ * Sets the horizontal spacer width which is used as a space between slots if there is more
+ * than one slot. If not set, {@link
* LayoutDefaults#MULTI_SLOT_LAYOUT_HORIZONTAL_SPACER_WIDTH} will be used.
*/
@NonNull
@@ -144,8 +143,8 @@
}
/**
- * Sets the vertical spacer height which is used as a space between all slots and primary
- * or secondary label if there is any. If not set, {@link
+ * Sets the vertical spacer height which is used as a space between all slots and primary or
+ * secondary label if there is any. If not set, {@link
* LayoutDefaults#MULTI_SLOT_LAYOUT_VERTICAL_SPACER_HEIGHT} will be used.
*/
@NonNull
@@ -161,8 +160,7 @@
@NonNull
@Override
public MultiSlotLayout build() {
- PrimaryLayout.Builder layoutBuilder =
- new PrimaryLayout.Builder(mDeviceParameters);
+ PrimaryLayout.Builder layoutBuilder = new PrimaryLayout.Builder(mDeviceParameters);
if (mPrimaryChip != null) {
layoutBuilder.setCompactChipContent(mPrimaryChip);
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/PrimaryLayout.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/PrimaryLayout.java
index 58c2652..f0766ed 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/PrimaryLayout.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/PrimaryLayout.java
@@ -50,7 +50,7 @@
/**
* Tiles layout that represents a suggested layout style for Material Tiles with the primary
- * (compact) chip at the bottom with the given content in a center and the recommended margin and
+ * (compact) chip at the bottom with the given content in the center and the recommended margin and
* padding applied.
*/
// TODO(b/215323986): Link visuals.
@@ -69,8 +69,8 @@
@NonNull private LayoutElement mContent = new Box.Builder().build();
/**
- * Creates a builder for the {@link PrimaryLayout} from the given content. Custom
- * content inside of it can later be set with ({@link #setContent}.
+ * Creates a builder for the {@link PrimaryLayout} from the given content. Custom content
+ * inside of it can later be set with ({@link #setContent}.
*/
public Builder(@NonNull DeviceParameters deviceParameters) {
this.mDeviceParameters = deviceParameters;
@@ -118,8 +118,7 @@
float primaryChipHeight =
mPrimaryChip != null
- ? (COMPACT_HEIGHT.getValue()
- + PRIMARY_LAYOUT_SPACER_HEIGHT.getValue())
+ ? (COMPACT_HEIGHT.getValue() + PRIMARY_LAYOUT_SPACER_HEIGHT.getValue())
: 0;
DpProp mainContentHeight =
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/ProgressIndicatorLayout.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/ProgressIndicatorLayout.java
index 0497de9..06e41d0 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/ProgressIndicatorLayout.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/ProgressIndicatorLayout.java
@@ -191,7 +191,7 @@
/** Get the inner content from this layout. */
@NonNull
public LayoutElement getContent() {
- return checkNotNull(((Box) ((Box) mElement).getContents().get(0))).getContents().get(0);
+ return checkNotNull(((Box) ((Box) mElement).getContents().get(0)).getContents().get(0));
}
/** @hide */
diff --git a/wear/tiles/tiles/api/current.txt b/wear/tiles/tiles/api/current.txt
index d0a67d7..37c11ec 100644
--- a/wear/tiles/tiles/api/current.txt
+++ b/wear/tiles/tiles/api/current.txt
@@ -151,6 +151,7 @@
method public static androidx.wear.tiles.DimensionBuilders.DegreesProp degrees(float);
method public static androidx.wear.tiles.DimensionBuilders.DpProp dp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
method public static androidx.wear.tiles.DimensionBuilders.EmProp em(int);
+ method public static androidx.wear.tiles.DimensionBuilders.EmProp em(float);
method public static androidx.wear.tiles.DimensionBuilders.ExpandedDimensionProp expand();
method public static androidx.wear.tiles.DimensionBuilders.SpProp sp(@Dimension(unit=androidx.annotation.Dimension.SP) float);
method public static androidx.wear.tiles.DimensionBuilders.WrappedDimensionProp wrap();
diff --git a/wear/tiles/tiles/api/public_plus_experimental_current.txt b/wear/tiles/tiles/api/public_plus_experimental_current.txt
index ce5af10..161cb59 100644
--- a/wear/tiles/tiles/api/public_plus_experimental_current.txt
+++ b/wear/tiles/tiles/api/public_plus_experimental_current.txt
@@ -151,6 +151,7 @@
method public static androidx.wear.tiles.DimensionBuilders.DegreesProp degrees(float);
method public static androidx.wear.tiles.DimensionBuilders.DpProp dp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
method public static androidx.wear.tiles.DimensionBuilders.EmProp em(int);
+ method public static androidx.wear.tiles.DimensionBuilders.EmProp em(float);
method public static androidx.wear.tiles.DimensionBuilders.ExpandedDimensionProp expand();
method public static androidx.wear.tiles.DimensionBuilders.SpProp sp(@Dimension(unit=androidx.annotation.Dimension.SP) float);
method public static androidx.wear.tiles.DimensionBuilders.WrappedDimensionProp wrap();
diff --git a/wear/tiles/tiles/api/restricted_current.txt b/wear/tiles/tiles/api/restricted_current.txt
index d0a67d7..37c11ec 100644
--- a/wear/tiles/tiles/api/restricted_current.txt
+++ b/wear/tiles/tiles/api/restricted_current.txt
@@ -151,6 +151,7 @@
method public static androidx.wear.tiles.DimensionBuilders.DegreesProp degrees(float);
method public static androidx.wear.tiles.DimensionBuilders.DpProp dp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
method public static androidx.wear.tiles.DimensionBuilders.EmProp em(int);
+ method public static androidx.wear.tiles.DimensionBuilders.EmProp em(float);
method public static androidx.wear.tiles.DimensionBuilders.ExpandedDimensionProp expand();
method public static androidx.wear.tiles.DimensionBuilders.SpProp sp(@Dimension(unit=androidx.annotation.Dimension.SP) float);
method public static androidx.wear.tiles.DimensionBuilders.WrappedDimensionProp wrap();
diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/DimensionBuilders.java b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/DimensionBuilders.java
index fadeb59..3a754fc 100644
--- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/DimensionBuilders.java
+++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/DimensionBuilders.java
@@ -53,6 +53,12 @@
return new EmProp.Builder().setValue(valueEm).build();
}
+ /** Shortcut for building a {@link EmProp} using a measurement in EM. */
+ @NonNull
+ public static EmProp em(float valueEm) {
+ return new EmProp.Builder().setValue(valueEm).build();
+ }
+
/** Shortcut for building an {@link DegreesProp} using a measurement in degrees. */
@NonNull
public static DegreesProp degrees(float valueDegrees) {
diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/SysUiTileUpdateRequester.java b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/SysUiTileUpdateRequester.java
index 075b1bc..4d5cc29 100644
--- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/SysUiTileUpdateRequester.java
+++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/SysUiTileUpdateRequester.java
@@ -101,6 +101,7 @@
}
@Nullable
+ @SuppressWarnings("deprecation")
private Intent buildUpdateBindIntent() {
Intent bindIntent = new Intent(ACTION_BIND_UPDATE_REQUESTER);
bindIntent.setPackage(getSysUiPackageName());
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
index b3f0bb5..c66aed6 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
@@ -92,6 +92,7 @@
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Suppress("DEPRECATION")
open class ParserProvider {
// Open to allow testing without having to install the sample app.
open fun getParser(context: Context, watchFaceName: ComponentName): XmlResourceParser? {
diff --git a/wear/watchface/watchface-complications-data/api/current.txt b/wear/watchface/watchface-complications-data/api/current.txt
index 9ae823c..abfa218 100644
--- a/wear/watchface/watchface-complications-data/api/current.txt
+++ b/wear/watchface/watchface-complications-data/api/current.txt
@@ -2,6 +2,7 @@
package androidx.wear.watchface.complications.data {
public abstract sealed class ComplicationData {
+ method public java.time.Instant getNextChangeInstant(java.time.Instant afterInstant);
method public final android.app.PendingIntent? getTapAction();
method public final androidx.wear.watchface.complications.data.ComplicationType getType();
method public final androidx.wear.watchface.complications.data.TimeRange getValidTimeRange();
diff --git a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
index 9ae823c..abfa218 100644
--- a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
@@ -2,6 +2,7 @@
package androidx.wear.watchface.complications.data {
public abstract sealed class ComplicationData {
+ method public java.time.Instant getNextChangeInstant(java.time.Instant afterInstant);
method public final android.app.PendingIntent? getTapAction();
method public final androidx.wear.watchface.complications.data.ComplicationType getType();
method public final androidx.wear.watchface.complications.data.TimeRange getValidTimeRange();
diff --git a/wear/watchface/watchface-complications-data/api/restricted_current.txt b/wear/watchface/watchface-complications-data/api/restricted_current.txt
index bf6d73a..9c89030 100644
--- a/wear/watchface/watchface-complications-data/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data/api/restricted_current.txt
@@ -2,6 +2,7 @@
package androidx.wear.watchface.complications.data {
public abstract sealed class ComplicationData {
+ method public java.time.Instant getNextChangeInstant(java.time.Instant afterInstant);
method public final android.app.PendingIntent? getTapAction();
method public final androidx.wear.watchface.complications.data.ComplicationType getType();
method public final androidx.wear.watchface.complications.data.TimeRange getValidTimeRange();
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index 1e01a43..4866e2a 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -75,6 +75,16 @@
cachedWireComplicationData?.let {
WireComplicationDataBuilder(it)
} ?: WireComplicationDataBuilder(type.toWireComplicationType())
+
+ /**
+ * Returns the next [Instant] after [afterInstant] at which any field of the complication may
+ * change. If there's no scheduled changes then [Instant.MAX] will be returned.
+ *
+ * See [ComplicationText.getNextChangeTime]
+ *
+ * @param afterInstant The earliest [Instant] for which we're interested in changes
+ */
+ public open fun getNextChangeInstant(afterInstant: Instant): Instant = Instant.MAX
}
/**
@@ -308,6 +318,20 @@
"tapAction=$tapAction, validTimeRange=$validTimeRange)"
}
+ override fun getNextChangeInstant(afterInstant: Instant): Instant {
+ if (title != null) {
+ val titleChangeInstant = title.getNextChangeTime(afterInstant)
+ val textChangeInstant = text.getNextChangeTime(afterInstant)
+ return if (textChangeInstant.isBefore(titleChangeInstant)) {
+ textChangeInstant
+ } else {
+ titleChangeInstant
+ }
+ } else {
+ return text.getNextChangeTime(afterInstant)
+ }
+ }
+
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
@@ -469,6 +493,20 @@
"tapAction=$tapAction, validTimeRange=$validTimeRange)"
}
+ override fun getNextChangeInstant(afterInstant: Instant): Instant {
+ if (title != null) {
+ val titleChangeInstant = title.getNextChangeTime(afterInstant)
+ val textChangeInstant = text.getNextChangeTime(afterInstant)
+ return if (textChangeInstant.isBefore(titleChangeInstant)) {
+ textChangeInstant
+ } else {
+ titleChangeInstant
+ }
+ } else {
+ return text.getNextChangeTime(afterInstant)
+ }
+ }
+
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
@@ -640,6 +678,16 @@
"tapAction=$tapAction, validTimeRange=$validTimeRange)"
}
+ override fun getNextChangeInstant(afterInstant: Instant): Instant {
+ val titleChangeInstant = title?.getNextChangeTime(afterInstant) ?: Instant.MAX
+ val textChangeInstant = text?.getNextChangeTime(afterInstant) ?: Instant.MAX
+ return if (textChangeInstant.isBefore(titleChangeInstant)) {
+ textChangeInstant
+ } else {
+ titleChangeInstant
+ }
+ }
+
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
@@ -1137,6 +1185,16 @@
"validTimeRange=$validTimeRange)"
}
+ override fun getNextChangeInstant(afterInstant: Instant): Instant {
+ val titleChangeInstant = title?.getNextChangeTime(afterInstant) ?: Instant.MAX
+ val textChangeInstant = text?.getNextChangeTime(afterInstant) ?: Instant.MAX
+ return if (textChangeInstant.isBefore(titleChangeInstant)) {
+ textChangeInstant
+ } else {
+ titleChangeInstant
+ }
+ }
+
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
index 799d9c1..4c88ed9 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
@@ -502,8 +502,14 @@
override fun returnsSameText(firstInstant: Instant, secondInstant: Instant) =
delegate.returnsSameText(firstInstant.toEpochMilli(), secondInstant.toEpochMilli())
- override fun getNextChangeTime(afterInstant: Instant) =
- Instant.ofEpochMilli(delegate.getNextChangeTime(afterInstant.toEpochMilli()))
+ override fun getNextChangeTime(afterInstant: Instant): Instant {
+ val nextChangeTime = delegate.getNextChangeTime(afterInstant.toEpochMilli())
+ return if (nextChangeTime == Long.MAX_VALUE) {
+ Instant.MAX
+ } else {
+ Instant.ofEpochMilli(nextChangeTime)
+ }
+ }
override fun isAlwaysEmpty() = delegate.isAlwaysEmpty
override fun getTimeDependentText(): TimeDependentText = delegate.timeDependentText
@@ -550,8 +556,14 @@
override fun returnsSameText(firstInstant: Instant, secondInstant: Instant) =
delegate.returnsSameText(firstInstant.toEpochMilli(), secondInstant.toEpochMilli())
- override fun getNextChangeTime(afterInstant: Instant) =
- Instant.ofEpochMilli(delegate.getNextChangeTime(afterInstant.toEpochMilli()))
+ override fun getNextChangeTime(afterInstant: Instant): Instant {
+ val nextChangeTime = delegate.getNextChangeTime(afterInstant.toEpochMilli())
+ return if (nextChangeTime == Long.MAX_VALUE) {
+ Instant.MAX
+ } else {
+ Instant.ofEpochMilli(nextChangeTime)
+ }
+ }
override fun isAlwaysEmpty() = false
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
index d792341..b691df3 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
@@ -134,10 +134,8 @@
val wireText = WireComplicationText.plainText("abc")
val text = wireText.toApiComplicationText()
- assertThat(text.getTextAt(getResource(), Instant.EPOCH))
- .isEqualTo("abc")
- assertThat(text.getNextChangeTime(Instant.EPOCH))
- .isEqualTo(Instant.ofEpochMilli(Long.MAX_VALUE))
+ assertThat(text.getTextAt(getResource(), Instant.EPOCH)).isEqualTo("abc")
+ assertThat(text.getNextChangeTime(Instant.EPOCH)).isEqualTo(Instant.MAX)
assertThat(text.isAlwaysEmpty()).isFalse()
assertThat(
text.returnsSameText(
diff --git a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
index 94a2460..0c2efd9 100644
--- a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
+++ b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
@@ -2309,6 +2309,7 @@
}
@Test
+ @Suppress("DEPRECATION")
public fun watchfaceSupportsHeadlessEditing() {
val mockPackageManager = Mockito.mock(PackageManager::class.java)
@@ -2331,6 +2332,7 @@
}
@Test
+ @Suppress("DEPRECATION")
public fun watchfaceSupportsHeadlessEditing_oldApi() {
val mockPackageManager = Mockito.mock(PackageManager::class.java)
diff --git a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
index 22dd404..1fead73 100644
--- a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
+++ b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
@@ -186,6 +186,7 @@
*/
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Suppress("DEPRECATION")
@Throws(PackageManager.NameNotFoundException::class)
public fun supportsWatchFaceHeadlessEditing(
packageManager: PackageManager,
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
index 53b905d..c183430 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
@@ -438,4 +438,22 @@
.systemDataSourceFallbackDefaultType.toWireComplicationType()
)
}.toTypedArray()
+
+ /**
+ * Returns the earliest [Instant] after [afterInstant] at which any complication field in any
+ * enabled complication may change.
+ */
+ internal fun getNextChangeInstant(afterInstant: Instant): Instant {
+ var minInstant = Instant.MAX
+ for ((_, complication) in complicationSlots) {
+ if (!complication.enabled) {
+ continue
+ }
+ val instant = complication.complicationData.value.getNextChangeInstant(afterInstant)
+ if (instant.isBefore(minInstant)) {
+ minInstant = instant
+ }
+ }
+ return minInstant
+ }
}
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index 0f500b2..006be96 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -56,6 +56,7 @@
import androidx.wear.watchface.style.UserStyleSchema
import androidx.wear.watchface.style.WatchFaceLayer
import androidx.wear.watchface.utility.TraceEvent
+import java.lang.Long.min
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -801,7 +802,8 @@
// an image. However if we're animating there's no need to trigger an extra invalidation.
if (!renderer.shouldAnimate() || computeDelayTillNextFrame(
nextDrawTimeMillis,
- systemTimeProvider.getSystemTimeMillis()
+ systemTimeProvider.getSystemTimeMillis(),
+ Instant.now()
) > MIN_PERCEPTIBLE_DELAY_MILLIS
) {
watchFaceHostApi.invalidate()
@@ -976,7 +978,7 @@
internal fun onDraw() {
val startTime = getZonedDateTime()
val startInstant = startTime.toInstant()
- val startTimeMillis = startInstant.toEpochMilli()
+ val startTimeMillis = systemTimeProvider.getSystemTimeMillis()
maybeUpdateDrawMode()
complicationSlotsManager.selectComplicationDataForInstant(startInstant)
renderer.renderInternal(startTime)
@@ -984,7 +986,7 @@
if (renderer.shouldAnimate()) {
val currentTimeMillis = systemTimeProvider.getSystemTimeMillis()
- var delay = computeDelayTillNextFrame(startTimeMillis, currentTimeMillis)
+ var delay = computeDelayTillNextFrame(startTimeMillis, currentTimeMillis, Instant.now())
nextDrawTimeMillis = currentTimeMillis + delay
// We want to post our delayed task to post the choreographer frame a bit earlier than
@@ -1005,11 +1007,18 @@
renderer.renderInternal(getZonedDateTime())
}
- /** @hide */
+ /**
+ * @param startTimeMillis The SystemTime in milliseconds at which we started rendering
+ * @param currentTimeMillis The current SystemTime in milliseconds
+ * @param nowInstant The current [Instant].
+ *
+ * @hide
+ */
@UiThread
internal fun computeDelayTillNextFrame(
startTimeMillis: Long,
- currentTimeMillis: Long
+ currentTimeMillis: Long,
+ nowInstant: Instant
): Long {
// Limit update rate to conserve power when the battery is low and not charging.
val updateRateMillis =
@@ -1049,7 +1058,18 @@
nextFrameTimeMillis += (60000 - (nextFrameTimeMillis % 60000)) % 60000
}
- return nextFrameTimeMillis - currentTimeMillis
+ var delayMillis = nextFrameTimeMillis - currentTimeMillis
+
+ // Check if we need to render a frame sooner to support scheduled complication updates, e.g.
+ // the stop watch complication.
+ val nextComplicationChange = complicationSlotsManager.getNextChangeInstant(nowInstant)
+ if (nextComplicationChange != Instant.MAX) {
+ val nextComplicationChangeDelayMillis =
+ max(0, nextComplicationChange.toEpochMilli() - nowInstant.toEpochMilli())
+ delayMillis = min(delayMillis, nextComplicationChangeDelayMillis)
+ }
+
+ return delayMillis
}
/**
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 2fd05d3..35de627 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -312,6 +312,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Suppress("DEPRECATION")
public open fun getXmlWatchFaceResourceId(): Int {
return try {
packageManager.getServiceInfo(
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 11b33f0..813bdc0 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -51,10 +51,13 @@
import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
import androidx.wear.watchface.complications.SystemDataSources
import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.data.CountUpTimeReference
import androidx.wear.watchface.complications.data.EmptyComplicationData
import androidx.wear.watchface.complications.data.NoDataComplicationData
import androidx.wear.watchface.complications.data.PlainComplicationText
import androidx.wear.watchface.complications.data.ShortTextComplicationData
+import androidx.wear.watchface.complications.data.TimeDifferenceComplicationText
+import androidx.wear.watchface.complications.data.TimeDifferenceStyle
import androidx.wear.watchface.complications.data.toApiComplicationData
import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
import androidx.wear.watchface.complications.rendering.ComplicationDrawable
@@ -114,6 +117,7 @@
import java.time.ZonedDateTime
import java.util.ArrayDeque
import java.util.PriorityQueue
+import java.util.concurrent.TimeUnit
import kotlin.test.assertFailsWith
private const val INTERACTIVE_UPDATE_RATE_MS = 16L
@@ -1066,7 +1070,7 @@
UserStyleSchema(emptyList())
)
- assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
+ assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0, Instant.EPOCH)).isEqualTo(
INTERACTIVE_UPDATE_RATE_MS
)
@@ -1075,7 +1079,7 @@
context,
Intent(Intent.ACTION_BATTERY_LOW)
)
- assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
+ assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0, Instant.EPOCH)).isEqualTo(
WatchFaceImpl.MAX_LOW_POWER_INTERACTIVE_UPDATE_RATE_MS
)
@@ -1084,7 +1088,7 @@
context,
Intent(Intent.ACTION_BATTERY_OKAY)
)
- assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
+ assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0, Instant.EPOCH)).isEqualTo(
INTERACTIVE_UPDATE_RATE_MS
)
}
@@ -1097,7 +1101,7 @@
UserStyleSchema(emptyList())
)
- assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
+ assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0, Instant.EPOCH)).isEqualTo(
INTERACTIVE_UPDATE_RATE_MS
)
@@ -1106,7 +1110,7 @@
context,
Intent(Intent.ACTION_BATTERY_LOW)
)
- assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
+ assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0, Instant.EPOCH)).isEqualTo(
WatchFaceImpl.MAX_LOW_POWER_INTERACTIVE_UPDATE_RATE_MS
)
@@ -1115,7 +1119,7 @@
context,
Intent(Intent.ACTION_POWER_CONNECTED)
)
- assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
+ assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0, Instant.EPOCH)).isEqualTo(
INTERACTIVE_UPDATE_RATE_MS
)
}
@@ -1131,7 +1135,8 @@
assertThat(
watchFaceImpl.computeDelayTillNextFrame(
startTimeMillis = 0,
- currentTimeMillis = 2
+ currentTimeMillis = 2,
+ Instant.EPOCH
)
)
.isEqualTo(INTERACTIVE_UPDATE_RATE_MS - 2)
@@ -1149,7 +1154,8 @@
assertThat(
watchFaceImpl.computeDelayTillNextFrame(
startTimeMillis = 2,
- currentTimeMillis = INTERACTIVE_UPDATE_RATE_MS + 3
+ currentTimeMillis = INTERACTIVE_UPDATE_RATE_MS + 3,
+ Instant.EPOCH
)
).isEqualTo(- 1)
}
@@ -1168,7 +1174,8 @@
assertThat(
watchFaceImpl.computeDelayTillNextFrame(
startTimeMillis = 20,
- currentTimeMillis = 24
+ currentTimeMillis = 24,
+ Instant.EPOCH
)
).isEqualTo(INTERACTIVE_UPDATE_RATE_MS - 4)
}
@@ -1188,7 +1195,8 @@
assertThat(
watchFaceImpl.computeDelayTillNextFrame(
startTimeMillis = 100740,
- currentTimeMillis = 100750
+ currentTimeMillis = 100750,
+ Instant.EPOCH
)
).isEqualTo(250)
}
@@ -1208,7 +1216,8 @@
assertThat(
watchFaceImpl.computeDelayTillNextFrame(
startTimeMillis = 10000,
- currentTimeMillis = 10001
+ currentTimeMillis = 10001,
+ Instant.EPOCH
)
).isEqualTo(999)
}
@@ -1224,13 +1233,93 @@
renderer.interactiveDrawModeUpdateDelayMillis = 60000
// Simulate rendering 2s into a minute, after which we should delay till the next minute.
- watchFaceImpl.nextDrawTimeMillis = 60000 + (120)
+ watchFaceImpl.nextDrawTimeMillis = 60000 + 2000
assertThat(
watchFaceImpl.computeDelayTillNextFrame(
startTimeMillis = watchFaceImpl.nextDrawTimeMillis,
- currentTimeMillis = watchFaceImpl.nextDrawTimeMillis
+ currentTimeMillis = watchFaceImpl.nextDrawTimeMillis,
+ Instant.EPOCH
)
- ).isEqualTo(59880) // NB 59880 + 120 == 60000
+ ).isEqualTo(58000) // NB 58000 + 2000 == 60000
+ }
+
+ @Test
+ public fun computeDelayTillNextFrame_60000ms_update_with_stopwatchComplication() {
+ initEngine(
+ WatchFaceType.ANALOG,
+ listOf(leftComplication, rightComplication),
+ UserStyleSchema(emptyList())
+ )
+
+ watchFaceImpl.onComplicationSlotDataUpdate(
+ LEFT_COMPLICATION_ID,
+ ShortTextComplicationData.Builder(
+ TimeDifferenceComplicationText.Builder(
+ TimeDifferenceStyle.STOPWATCH,
+ CountUpTimeReference(Instant.parse("2022-10-30T10:15:30.001Z"))
+ ).setMinimumTimeUnit(TimeUnit.MINUTES).build(),
+ androidx.wear.watchface.complications.data.ComplicationText.EMPTY
+ ).build()
+ )
+
+ renderer.interactiveDrawModeUpdateDelayMillis = 60000
+
+ // Simulate rendering 2s into a minute of system time, normally we'd need to wait 58 seconds
+ // but the complication needs an update in 50s so our delay is shorter.
+ watchFaceImpl.nextDrawTimeMillis = 60000 + 2000
+ assertThat(
+ watchFaceImpl.computeDelayTillNextFrame(
+ startTimeMillis = watchFaceImpl.nextDrawTimeMillis,
+ currentTimeMillis = watchFaceImpl.nextDrawTimeMillis,
+ Instant.EPOCH.plusSeconds(10)
+ )
+ ).isEqualTo(50001)
+ }
+
+ @Test
+ public fun complicationSlotsManager_getNextChangeInstant() {
+ initEngine(
+ WatchFaceType.ANALOG,
+ listOf(leftComplication, rightComplication),
+ UserStyleSchema(emptyList())
+ )
+
+ // Initially neither complication has a scheduled change.
+ assertThat(complicationSlotsManager.getNextChangeInstant(Instant.EPOCH))
+ .isEqualTo(Instant.MAX)
+
+ // Sending a complication with a scheduled update alters the result of getNextChangeInstant.
+ val referenceInstant = Instant.parse("2022-10-30T10:15:30.001Z")
+ watchFaceImpl.onComplicationSlotDataUpdate(
+ LEFT_COMPLICATION_ID,
+ ShortTextComplicationData.Builder(
+ TimeDifferenceComplicationText.Builder(
+ TimeDifferenceStyle.STOPWATCH,
+ CountUpTimeReference(referenceInstant)
+ ).setMinimumTimeUnit(TimeUnit.HOURS).build(),
+ androidx.wear.watchface.complications.data.ComplicationText.EMPTY
+ ).build()
+ )
+
+ val nowInstant = Instant.EPOCH.plusSeconds(10)
+ assertThat(complicationSlotsManager.getNextChangeInstant(nowInstant))
+ .isEqualTo(Instant.EPOCH.plusSeconds(60 * 60).plusMillis(1))
+
+ // Sending another complication with an earlier scheduled update alters the result of
+ // getNextChangeInstant again.
+ watchFaceImpl.onComplicationSlotDataUpdate(
+ RIGHT_COMPLICATION_ID,
+ ShortTextComplicationData.Builder(
+ TimeDifferenceComplicationText.Builder(
+ TimeDifferenceStyle.STOPWATCH,
+ CountUpTimeReference(referenceInstant)
+ ).setMinimumTimeUnit(TimeUnit.SECONDS).build(),
+ androidx.wear.watchface.complications.data.ComplicationText.EMPTY
+ ).build()
+ )
+
+ assertThat(complicationSlotsManager.getNextChangeInstant(nowInstant))
+ .isEqualTo(Instant.EPOCH.plusSeconds(10).plusMillis(1))
}
@Test
diff --git a/wear/wear/src/main/java/androidx/wear/utils/MetadataConstants.java b/wear/wear/src/main/java/androidx/wear/utils/MetadataConstants.java
index af5400e..e8f2716 100644
--- a/wear/wear/src/main/java/androidx/wear/utils/MetadataConstants.java
+++ b/wear/wear/src/main/java/androidx/wear/utils/MetadataConstants.java
@@ -130,6 +130,7 @@
* @param context to be evaluated.
* @return Whether a given context comes from a standalone app.
*/
+ @SuppressWarnings("deprecation")
public static boolean isStandalone(Context context) {
try {
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(
@@ -150,6 +151,7 @@
* @param context to be evaluated.
* @return Whether a given context has notification bridging enabled.
*/
+ @SuppressWarnings("deprecation")
public static boolean isNotificationBridgingEnabled(Context context) {
try {
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(
@@ -173,6 +175,7 @@
* @return an integer id representing the resource id of the requested drawable, or 0 if
* no drawable was found.
*/
+ @SuppressWarnings("deprecation")
public static int getPreviewDrawableResourceId(Context context, boolean circular) {
try {
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
index 01a3a1a..5bfc430 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
@@ -384,6 +384,7 @@
* if WebView was to be loaded right now.
*/
@SuppressLint("PrivateApi")
+ @SuppressWarnings("deprecation")
private static PackageInfo getNotYetLoadedWebViewPackageInfo(Context context) {
String webviewPackageName;
try {
diff --git a/work/integration-tests/testapp/src/main/AndroidManifest.xml b/work/integration-tests/testapp/src/main/AndroidManifest.xml
index c6d608d..17ab7d6 100644
--- a/work/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/work/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -49,17 +49,20 @@
<service
android:name="androidx.work.multiprocess.RemoteWorkerService"
android:exported="false"
- android:process=":worker1" />
+ android:process=":worker1"
+ tools:ignore="MissingServiceExportedEqualsTrue" />
<service
android:name=".RemoteWorkerService2"
android:exported="false"
- android:process=":worker2" />
+ android:process=":worker2"
+ tools:ignore="MissingServiceExportedEqualsTrue" />
<service
android:name=".RemoteService"
android:exported="false"
- android:process=":remote" />
+ android:process=":remote"
+ tools:ignore="MissingServiceExportedEqualsTrue" />
</application>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest>
diff --git a/work/work-gcm/src/main/AndroidManifest.xml b/work/work-gcm/src/main/AndroidManifest.xml
index de6f154..91c1375 100644
--- a/work/work-gcm/src/main/AndroidManifest.xml
+++ b/work/work-gcm/src/main/AndroidManifest.xml
@@ -24,7 +24,8 @@
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE"
android:exported="@bool/enable_gcm_scheduler_default"
android:directBootAware="false"
- tools:targetApi="n">
+ tools:targetApi="n"
+ tools:ignore="MissingServiceExportedEqualsTrue">
<intent-filter>
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY"/>
</intent-filter>
diff --git a/work/work-multiprocess/src/main/AndroidManifest.xml b/work/work-multiprocess/src/main/AndroidManifest.xml
index 4c38d87..855611d 100644
--- a/work/work-multiprocess/src/main/AndroidManifest.xml
+++ b/work/work-multiprocess/src/main/AndroidManifest.xml
@@ -13,13 +13,16 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.work.multiprocess">
+<manifest
+ package="androidx.work.multiprocess"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools" >
<application>
<service
android:name=".RemoteWorkManagerService"
- android:exported="false" />
+ android:exported="false"
+ tools:ignore="MissingServiceExportedEqualsTrue" />
</application>
</manifest>
diff --git a/work/work-runtime/src/main/AndroidManifest.xml b/work/work-runtime/src/main/AndroidManifest.xml
index c772c5e..f8cc60a 100644
--- a/work/work-runtime/src/main/AndroidManifest.xml
+++ b/work/work-runtime/src/main/AndroidManifest.xml
@@ -37,7 +37,8 @@
android:exported="false"
android:enabled="@bool/enable_system_alarm_service_default"
android:directBootAware="false"
- tools:targetApi="n"/>
+ tools:targetApi="n"
+ tools:ignore="MissingServiceExportedEqualsTrue" />
<service
android:name="androidx.work.impl.background.systemjob.SystemJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
@@ -50,7 +51,8 @@
android:exported="false"
android:directBootAware="false"
android:enabled="@bool/enable_system_foreground_service_default"
- tools:targetApi="n"/>
+ tools:targetApi="n"
+ tools:ignore="MissingServiceExportedEqualsTrue" />
<receiver
android:name="androidx.work.impl.utils.ForceStopRunnable$BroadcastReceiver"
android:enabled="true"