Merge "Convert LooperCompat to static shim" into androidx-master-dev
diff --git a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
index 1e93b2a..860515b 100644
--- a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
+++ b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
@@ -84,6 +84,12 @@
             )
         }
 
+        // NOTE: This argument is checked by ResultWriter to enable CI reports.
+        extension.defaultConfig.testInstrumentationRunnerArgument(
+            "androidx.benchmark.output.enable",
+            "true"
+        )
+
         // NOTE: .all here is a Gradle API, which will run the callback passed to it after the
         // extension variants have been resolved.
         var applied = false
@@ -91,12 +97,6 @@
             if (!applied) {
                 applied = true
                 project.tasks.named("connectedAndroidTest").configure {
-                    // NOTE: This argument is checked by ResultWriter to enable CI reports.
-                    extension.defaultConfig.testInstrumentationRunnerArgument(
-                        "androidx.benchmark.output.enable",
-                        "true"
-                    )
-
                     configureWithConnectedAndroidTest(project, it)
                 }
             }
diff --git a/browser/api/restricted_1.2.0-alpha03.txt b/browser/api/restricted_1.2.0-alpha03.txt
index 9bc0fcb..afa7eaf 100644
--- a/browser/api/restricted_1.2.0-alpha03.txt
+++ b/browser/api/restricted_1.2.0-alpha03.txt
@@ -23,6 +23,48 @@
 
 package androidx.browser.customtabs {
 
+  public class CustomTabsCallback {
+  }
+
+  public class CustomTabsClient {
+  }
+
+  public final class CustomTabsIntent {
+  }
+
+  public static final class CustomTabsIntent.Builder {
+  }
+
+
+  public abstract class CustomTabsService extends android.app.Service {
+  }
+
+
+  public final class CustomTabsSession {
+  }
+
+
+  public class CustomTabsSessionToken {
+  }
+
+
+  public abstract class PostMessageServiceConnection implements androidx.browser.customtabs.PostMessageBackend android.content.ServiceConnection {
+  }
+
+  public class TrustedWebUtils {
+  }
+
+
+
+}
+
+package androidx.browser.trusted {
+
+
+
+
+
+
 
 }
 
diff --git a/browser/api/restricted_current.txt b/browser/api/restricted_current.txt
index 9bc0fcb..afa7eaf 100644
--- a/browser/api/restricted_current.txt
+++ b/browser/api/restricted_current.txt
@@ -23,6 +23,48 @@
 
 package androidx.browser.customtabs {
 
+  public class CustomTabsCallback {
+  }
+
+  public class CustomTabsClient {
+  }
+
+  public final class CustomTabsIntent {
+  }
+
+  public static final class CustomTabsIntent.Builder {
+  }
+
+
+  public abstract class CustomTabsService extends android.app.Service {
+  }
+
+
+  public final class CustomTabsSession {
+  }
+
+
+  public class CustomTabsSessionToken {
+  }
+
+
+  public abstract class PostMessageServiceConnection implements androidx.browser.customtabs.PostMessageBackend android.content.ServiceConnection {
+  }
+
+  public class TrustedWebUtils {
+  }
+
+
+
+}
+
+package androidx.browser.trusted {
+
+
+
+
+
+
 
 }
 
diff --git a/browser/build.gradle b/browser/build.gradle
index 74657ee..6add4fe 100644
--- a/browser/build.gradle
+++ b/browser/build.gradle
@@ -23,11 +23,19 @@
     implementation(project(":concurrent-listenablefuture"))
     implementation(project(":concurrent-listenablefuture-callback"))
 
+    testImplementation(ANDROIDX_TEST_CORE)
+    testImplementation(ANDROIDX_TEST_RUNNER)
+    testImplementation(JUNIT)
+    testImplementation(ROBOLECTRIC)
+    testImplementation(MOCKITO_CORE)
+
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_CORE)
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation(ANDROIDX_TEST_RULES)
     androidTestImplementation(ESPRESSO_CORE, libs.exclude_for_espresso)
+    androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
     androidTestImplementation(project(":internal-testutils"))
 }
 
diff --git a/browser/src/androidTest/AndroidManifest.xml b/browser/src/androidTest/AndroidManifest.xml
index ac494e9..3e5c314 100644
--- a/browser/src/androidTest/AndroidManifest.xml
+++ b/browser/src/androidTest/AndroidManifest.xml
@@ -18,10 +18,94 @@
           package="android.support.customtabs.test">
 
     <application>
-        <activity android:name="androidx.browser.customtabs.TestActivity"/>
+        <activity android:name="androidx.browser.customtabs.TestActivity"
+                android:enabled="false">
+            <!-- A browsable intent filter is required for the TrustedWebActivityService. -->
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="https"
+                      android:host="www.example.com"
+                      android:pathPrefix="/notifications"/>
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
 
-        <service android:name="androidx.browser.customtabs.PostMessageService"/>
+        <service android:name="androidx.browser.customtabs.PostMessageService"
+                 android:enabled="false"/>
 
-        <service android:name="androidx.browser.customtabs.TestCustomTabsService"/>
+        <service android:name="androidx.browser.customtabs.TestCustomTabsService"
+                 android:enabled="false">
+            <intent-filter>
+                <category android:name="androidx.browser.trusted.category.TrustedWebActivities" />
+            </intent-filter>
+        </service>
+
+        <service android:name="androidx.browser.customtabs.TestCustomTabsServiceSupportsTwas"
+                 android:enabled="false">
+            <intent-filter>
+                <action android:name="android.support.customtabs.action.CustomTabsService" />
+                <category android:name="androidx.browser.trusted.category.TrustedWebActivities" />
+                <category android:name="androidx.browser.trusted.category.TrustedWebActivitySplashScreensV1" />
+            </intent-filter>
+        </service>
+
+        <service android:name="androidx.browser.customtabs.TestCustomTabsServiceNoSplashScreens"
+                 android:enabled="false">
+            <intent-filter>
+                <action android:name="android.support.customtabs.action.CustomTabsService" />
+                <category android:name="androidx.browser.trusted.category.TrustedWebActivities" />
+            </intent-filter>
+        </service>
+
+        <activity android:name="androidx.browser.trusted.TestBrowser"
+                  android:theme="@style/Theme.AppCompat.Light"
+                  android:exported="true"
+                  android:enabled="false">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+            </intent-filter>
+        </activity>
+
+        <service android:name="androidx.browser.trusted.TestTrustedWebActivityService"
+                 android:enabled="false">
+            <intent-filter>
+                <action android:name="android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </service>
+
+        <activity android:name="androidx.browser.trusted.LauncherActivity"
+                  android:label="Test Launcher Activity"
+                  android:theme="@style/Theme.AppCompat.Light"
+                  android:enabled="false">
+
+            <meta-data android:name="android.support.customtabs.trusted.DEFAULT_URL"
+                       android:value="https://www.test.com/default_url/" />
+
+            <meta-data
+                android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR"
+                android:resource="@color/status_bar_color" />
+        </activity>
+
+        <!-- For TWA splash screens -->
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="android.support.customtabs.trusted.test_fileprovider"
+            android:grantUriPermissions="true"
+            android:exported="false">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/filepaths" />
+        </provider>
     </application>
-</manifest>
+</manifest>
\ No newline at end of file
diff --git a/browser/src/androidTest/java/androidx/browser/browseractions/BrowserActionsFallbackMenuUiTest.java b/browser/src/androidTest/java/androidx/browser/browseractions/BrowserActionsFallbackMenuUiTest.java
index 1ed34b8..390df87 100644
--- a/browser/src/androidTest/java/androidx/browser/browseractions/BrowserActionsFallbackMenuUiTest.java
+++ b/browser/src/androidTest/java/androidx/browser/browseractions/BrowserActionsFallbackMenuUiTest.java
@@ -27,6 +27,7 @@
 import android.widget.TextView;
 
 import androidx.browser.R;
+import androidx.browser.customtabs.EnableComponentsTestRule;
 import androidx.browser.customtabs.TestActivity;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
@@ -34,6 +35,7 @@
 import androidx.testutils.PollingCheck;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -53,6 +55,12 @@
     @Rule
     public final ActivityTestRule<TestActivity> mActivityTestRule =
             new ActivityTestRule<>(TestActivity.class);
+
+    @Rule
+    public final EnableComponentsTestRule mEnableComponents = new EnableComponentsTestRule(
+            TestActivity.class
+    );
+
     private Context mContext;
     private List<BrowserActionItem> mMenuItems;
     private List<String> mMenuItemTitles;
@@ -110,6 +118,7 @@
      * Test whether {@link BrowserActionsFallbackMenuUi} is inflated correctly.
      */
     @Test
+    @Ignore("Test is flaky and we're removing Browser Actions.")
     public void testBrowserActionsFallbackMenuShownCorrectly() throws InterruptedException {
         final CountDownLatch signal = new CountDownLatch(1);
         final BrowserActionsFallbackMenuUi.BrowserActionsFallMenuUiListener listener =
diff --git a/browser/src/androidTest/java/androidx/browser/browseractions/BrowserActionsIntentTest.java b/browser/src/androidTest/java/androidx/browser/browseractions/BrowserActionsIntentTest.java
index 9311270..b8ace06 100644
--- a/browser/src/androidTest/java/androidx/browser/browseractions/BrowserActionsIntentTest.java
+++ b/browser/src/androidTest/java/androidx/browser/browseractions/BrowserActionsIntentTest.java
@@ -82,8 +82,8 @@
         customItems.add(customItemWithoutIcon);
 
         BrowserActionsIntent browserActionsIntent = new BrowserActionsIntent.Builder(mContext, mUri)
-                .setCustomItems(customItems)
-                .build();
+                                                            .setCustomItems(customItems)
+                                                            .build();
         Intent intent = browserActionsIntent.getIntent();
         assertTrue(intent.hasExtra(BrowserActionsIntent.EXTRA_MENU_ITEMS));
         ArrayList<Bundle> bundles =
diff --git a/browser/src/androidTest/java/androidx/browser/customtabs/EnableComponentsTestRule.java b/browser/src/androidTest/java/androidx/browser/customtabs/EnableComponentsTestRule.java
new file mode 100644
index 0000000..d8946af
--- /dev/null
+++ b/browser/src/androidTest/java/androidx/browser/customtabs/EnableComponentsTestRule.java
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+
+package androidx.browser.customtabs;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The Custom Tabs and Trusted Web Activity functionality require Activities and Services to be
+ * found in the manifest. We want to make it explicit which Activities/Services are required for
+ * each test and not let components for one test interfere with another.
+ */
+public class EnableComponentsTestRule extends TestWatcher {
+    private final List<Class> mComponents;
+
+    /**
+     * Creates this TestRule which will enable the given components and disable them after the
+     * tests.
+     */
+    public EnableComponentsTestRule(Class ... components) {
+        // TODO(peconn): Figure out some generic bounds that allows a list of Classes that are
+        // either Services or Actvities.
+        mComponents = new ArrayList(Arrays.asList(components));
+    }
+
+    @Override
+    protected void starting(Description description) {
+        setEnabled(true);
+    }
+
+    @Override
+    protected void finished(Description description) {
+        setEnabled(false);
+    }
+
+    /**
+     * Manually disables an already enabled component.
+     */
+    public void manuallyDisable(Class clazz) {
+        setComponentEnabled(clazz, false);
+    }
+
+    /**
+     * Manually enables a component. Will be disabled when test finishes.
+     */
+    public void manuallyEnable(Class clazz) {
+        setComponentEnabled(clazz, true);
+        mComponents.add(clazz);
+    }
+
+    private void setEnabled(boolean enabled) {
+        for (Class component : mComponents) {
+            setComponentEnabled(component, enabled);
+        }
+    }
+
+    private static void setComponentEnabled(Class clazz, boolean enabled) {
+        Context context = ApplicationProvider.getApplicationContext();
+        PackageManager pm = context.getPackageManager();
+        ComponentName name = new ComponentName(context, clazz);
+
+        int newState = enabled
+                ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+                : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+        int flags = PackageManager.DONT_KILL_APP;
+
+        if (pm.getComponentEnabledSetting(name) != newState) {
+            pm.setComponentEnabledSetting(name, newState, flags);
+        }
+    }
+}
diff --git a/browser/src/androidTest/java/androidx/browser/customtabs/PostMessageServiceConnectionTest.java b/browser/src/androidTest/java/androidx/browser/customtabs/PostMessageServiceConnectionTest.java
index e14f40e..e3debf3 100644
--- a/browser/src/androidTest/java/androidx/browser/customtabs/PostMessageServiceConnectionTest.java
+++ b/browser/src/androidTest/java/androidx/browser/customtabs/PostMessageServiceConnectionTest.java
@@ -18,15 +18,12 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 import android.content.Context;
-import android.content.Intent;
 
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.rule.ActivityTestRule;
-import androidx.test.rule.ServiceTestRule;
 import androidx.testutils.PollingCheck;
 
 import org.junit.Before;
@@ -34,8 +31,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.concurrent.TimeoutException;
-
 /**
  * Tests for {@link PostMessageServiceConnection} with no {@link CustomTabsService} component.
  */
@@ -43,26 +38,22 @@
 @SmallTest
 public class PostMessageServiceConnectionTest {
     @Rule
-    public final ServiceTestRule mServiceRule;
-    @Rule
-    public final ActivityTestRule<TestActivity> mActivityTestRule;
+    public final EnableComponentsTestRule mEnableComponents = new EnableComponentsTestRule(
+            PostMessageService.class
+    );
+
     private TestCustomTabsCallback mCallback;
     private Context mContext;
     private PostMessageServiceConnection mConnection;
     private boolean mServiceConnected;
 
-
-    public PostMessageServiceConnectionTest() {
-        mActivityTestRule = new ActivityTestRule<TestActivity>(TestActivity.class);
-        mServiceRule = new ServiceTestRule();
-    }
-
     @Before
     public void setup() {
         mCallback = new TestCustomTabsCallback();
-        mContext = mActivityTestRule.getActivity();
-        mConnection = new PostMessageServiceConnection(
-                new CustomTabsSessionToken(mCallback.getStub())) {
+        mContext = ApplicationProvider.getApplicationContext();
+
+        CustomTabsSessionToken token = new CustomTabsSessionToken(mCallback.getStub(), null);
+        mConnection = new PostMessageServiceConnection(token) {
             @Override
             public void onPostMessageServiceConnected() {
                 mServiceConnected = true;
@@ -73,29 +64,31 @@
                 mServiceConnected = false;
             }
         };
-        Intent intent = new Intent();
-        intent.setClassName(mContext.getPackageName(), PostMessageService.class.getName());
-        try {
-            mServiceRule.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
-        } catch (TimeoutException e) {
-            fail();
-        }
-    }
 
-    @Test
-    public void testNotifyChannelCreationAndSendMessages() {
-        PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
+        mConnection.bindSessionToPostMessageService(mContext, mContext.getPackageName());
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
             @Override
             public boolean canProceed() {
                 return mServiceConnected;
             }
         });
-        assertTrue(mServiceConnected);
+    }
+
+    @Test
+    public void testNotifyChannelCreationAndSendMessages() {
         mConnection.notifyMessageChannelReady(null);
         assertTrue(mCallback.isMessageChannelReady());
+
         mConnection.postMessage("message1", null);
         assertEquals(mCallback.getMessages().size(), 1);
+
         mConnection.postMessage("message2", null);
         assertEquals(mCallback.getMessages().size(), 2);
     }
+
+    @Test
+    public void dontUnbindTwice() throws Throwable {
+        mConnection.cleanup(mContext);
+        mConnection.cleanup(mContext);
+    }
 }
diff --git a/browser/src/androidTest/java/androidx/browser/customtabs/PostMessageTest.java b/browser/src/androidTest/java/androidx/browser/customtabs/PostMessageTest.java
index 3adef06..df6c3fc 100644
--- a/browser/src/androidTest/java/androidx/browser/customtabs/PostMessageTest.java
+++ b/browser/src/androidTest/java/androidx/browser/customtabs/PostMessageTest.java
@@ -28,7 +28,7 @@
 import android.os.Bundle;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
+import androidx.test.filters.SmallTest;
 import androidx.test.rule.ActivityTestRule;
 import androidx.test.rule.ServiceTestRule;
 import androidx.testutils.PollingCheck;
@@ -39,7 +39,7 @@
 import org.junit.runner.RunWith;
 
 import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
+
 
 /**
  * Tests for a complete loop between a browser side {@link CustomTabsService}
@@ -48,27 +48,32 @@
  * side actions.
  */
 @RunWith(AndroidJUnit4.class)
-@LargeTest
+@SmallTest
 public class PostMessageTest {
     @Rule
     public final ServiceTestRule mServiceRule;
     @Rule
     public final ActivityTestRule<TestActivity> mActivityTestRule;
+    @Rule
+    public final EnableComponentsTestRule mEnableComponents = new EnableComponentsTestRule(
+            TestActivity.class,
+            TestCustomTabsService.class,
+            PostMessageService.class
+    );
+
     private TestCustomTabsCallback mCallback;
     private Context mContext;
     private CustomTabsServiceConnection mCustomTabsServiceConnection;
     private PostMessageServiceConnection mPostMessageServiceConnection;
-    private AtomicBoolean mCustomTabsServiceConnected;
+    private boolean mCustomTabsServiceConnected;
     private boolean mPostMessageServiceConnected;
     private CustomTabsSession mSession;
 
     public PostMessageTest() {
         mActivityTestRule = new ActivityTestRule<TestActivity>(TestActivity.class);
         mServiceRule = new ServiceTestRule();
-        mCustomTabsServiceConnected = new AtomicBoolean(false);
     }
 
-
     @Before
     public void setup() {
         // Bind to PostMessageService only after CustomTabsService sends the callback to do so. This
@@ -99,17 +104,17 @@
         mCustomTabsServiceConnection = new CustomTabsServiceConnection() {
             @Override
             public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
-                mCustomTabsServiceConnected.set(true);
+                mCustomTabsServiceConnected = true;
                 mSession = client.newSession(mCallback);
             }
 
             @Override
             public void onServiceDisconnected(ComponentName componentName) {
-                mCustomTabsServiceConnected.set(false);
+                mCustomTabsServiceConnected = false;
             }
         };
         mPostMessageServiceConnection = new PostMessageServiceConnection(
-                new CustomTabsSessionToken(mCallback.getStub())) {
+                new CustomTabsSessionToken(mCallback.getStub(), null)) {
             @Override
             public void onPostMessageServiceConnected() {
                 mPostMessageServiceConnected = true;
@@ -133,16 +138,16 @@
 
     @Test
     public void testCustomTabsConnection() {
-        PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
             @Override
             public boolean canProceed() {
-                return mCustomTabsServiceConnected.get();
+                return mCustomTabsServiceConnected;
             }
         });
-        assertTrue(mCustomTabsServiceConnected.get());
+        assertTrue(mCustomTabsServiceConnected);
         assertTrue(mSession.requestPostMessageChannel(Uri.EMPTY));
         assertEquals(CustomTabsService.RESULT_SUCCESS, mSession.postMessage("", null));
-        PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
             @Override
             public boolean canProceed() {
                 return mPostMessageServiceConnected;
diff --git a/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsCallback.java b/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsCallback.java
index 6353398..7c48245 100644
--- a/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsCallback.java
+++ b/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsCallback.java
@@ -55,7 +55,7 @@
 
         @Override
         public void onRelationshipValidationResult(int relation, Uri origin, boolean result,
-                Bundle extras) throws RemoteException {
+                                                   Bundle extras) throws RemoteException {
             TestCustomTabsCallback.this.onRelationshipValidationResult(
                     relation, origin, result, extras);
         }
diff --git a/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsService.java b/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsService.java
index 071d1c2..67c3136 100644
--- a/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsService.java
+++ b/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsService.java
@@ -16,10 +16,22 @@
 
 package androidx.browser.customtabs;
 
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * A test class that simulates how a {@link CustomTabsService} would behave.
@@ -27,9 +39,24 @@
 
 public class TestCustomTabsService extends CustomTabsService {
     public static final String CALLBACK_BIND_TO_POST_MESSAGE = "BindToPostMessageService";
+    private static TestCustomTabsService sInstance;
+
+    private final CountDownLatch mFileReceivingLatch = new CountDownLatch(1);
+
     private boolean mPostMessageRequested;
     private CustomTabsSessionToken mSession;
 
+    /** Returns the instance of the Service. Returns null if it hasn't been bound yet. */
+    public static TestCustomTabsService getInstance() {
+        return sInstance;
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        sInstance = this;
+        return super.onBind(intent);
+    }
+
     @Override
     protected boolean warmup(long flags) {
         return false;
@@ -43,7 +70,7 @@
 
     @Override
     protected boolean mayLaunchUrl(CustomTabsSessionToken sessionToken,
-            Uri url, Bundle extras, List<Bundle> otherLikelyBundles) {
+                                   Uri url, Bundle extras, List<Bundle> otherLikelyBundles) {
         return false;
     }
 
@@ -74,7 +101,43 @@
 
     @Override
     protected boolean validateRelationship(CustomTabsSessionToken sessionToken,
-            @Relation int relation, Uri origin, Bundle extras) {
+                                           @Relation int relation, Uri origin, Bundle extras) {
         return false;
     }
+
+    @Override
+    protected boolean receiveFile(@NonNull CustomTabsSessionToken sessionToken, @NonNull Uri uri,
+            int purpose, @Nullable Bundle extras) {
+        boolean success = retrieveBitmap(uri);
+        if (success) {
+            mFileReceivingLatch.countDown();
+        }
+        return success;
+    }
+
+    private boolean retrieveBitmap(Uri uri) {
+        try (ParcelFileDescriptor parcelFileDescriptor =
+                     getContentResolver().openFileDescriptor(uri, "r")) {
+            if (parcelFileDescriptor == null) return false;
+            FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
+            if (fileDescriptor == null) return false;
+            Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
+            return bitmap != null;
+        } catch (IOException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Waits until a splash image file is successfully received and decoded in {@link #receiveFile}.
+     * Returns whether that happened before timeout.
+     * If already received, returns "true" immediately.
+     */
+    public boolean waitForSplashImageFile(int timeoutMillis) {
+        try {
+            return mFileReceivingLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            return false;
+        }
+    }
 }
diff --git a/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsServiceNoSplashScreens.java b/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsServiceNoSplashScreens.java
new file mode 100644
index 0000000..47586ee
--- /dev/null
+++ b/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsServiceNoSplashScreens.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+package androidx.browser.customtabs;
+
+/**
+ * A {@link TestCustomTabsService} that supports Trusted Web Activities, but not splash screens.
+ */
+public class TestCustomTabsServiceNoSplashScreens extends TestCustomTabsService {}
diff --git a/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsServiceSupportsTwas.java b/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsServiceSupportsTwas.java
new file mode 100644
index 0000000..487c7ca
--- /dev/null
+++ b/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsServiceSupportsTwas.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+package androidx.browser.customtabs;
+
+/**
+ * A {@link TestCustomTabsService} that in the manifest also supports Trusted Web Activities. This
+ * is so the two services can be turned on and off individually.
+ */
+public class TestCustomTabsServiceSupportsTwas extends TestCustomTabsService {
+
+}
diff --git a/browser/src/androidTest/java/androidx/browser/customtabs/TrustedWebUtilsTest.java b/browser/src/androidTest/java/androidx/browser/customtabs/TrustedWebUtilsTest.java
deleted file mode 100644
index 557c813..0000000
--- a/browser/src/androidTest/java/androidx/browser/customtabs/TrustedWebUtilsTest.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2018 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.browser.customtabs;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import android.content.ActivityNotFoundException;
-import android.content.Intent;
-import android.net.Uri;
-
-import androidx.core.app.BundleCompat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-import androidx.test.rule.ActivityTestRule;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Tests for TrustedWebUtils.
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class TrustedWebUtilsTest {
-    @Rule
-    public final ActivityTestRule<TestActivity> mActivityTestRule;
-
-    public TrustedWebUtilsTest() {
-        mActivityTestRule = new ActivityTestRule<>(TestActivity.class);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void testTrustedWebIntentRequiresValidSession() {
-        CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build();
-        TrustedWebUtils.launchAsTrustedWebActivity(
-                mActivityTestRule.getActivity(), customTabsIntent, Uri.EMPTY);
-    }
-
-    @Test(expected = ActivityNotFoundException.class)
-    public void testTrustedWebIntentContainsRequiredExtra() {
-        CustomTabsSession mockSession = CustomTabsSession.createMockSessionForTesting(
-                mActivityTestRule.getActivity().getComponentName());
-        CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder(mockSession).build();
-        TrustedWebUtils.launchAsTrustedWebActivity(
-                mActivityTestRule.getActivity(), customTabsIntent, Uri.EMPTY);
-        assertNotNull(BundleCompat.getBinder(
-                customTabsIntent.intent.getExtras(), CustomTabsIntent.EXTRA_SESSION));
-        assertEquals(customTabsIntent.intent.getAction(), Intent.ACTION_VIEW);
-        assertTrue(customTabsIntent.intent.hasExtra(
-                TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY));
-    }
-}
diff --git a/browser/src/androidTest/java/androidx/browser/customtabs/testutil/CustomTabConnectionRule.java b/browser/src/androidTest/java/androidx/browser/customtabs/testutil/CustomTabConnectionRule.java
new file mode 100644
index 0000000..8ded425
--- /dev/null
+++ b/browser/src/androidTest/java/androidx/browser/customtabs/testutil/CustomTabConnectionRule.java
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+package androidx.browser.customtabs.testutil;
+
+import static org.junit.Assert.fail;
+
+import android.content.ComponentName;
+import android.content.Context;
+
+import androidx.browser.customtabs.CustomTabsClient;
+import androidx.browser.customtabs.CustomTabsServiceConnection;
+import androidx.browser.customtabs.CustomTabsSession;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * TestRule that helps establishing a connection to CustomTabsService.
+ */
+public class CustomTabConnectionRule extends TestWatcher {
+
+    private final CountDownLatch mConnectionLatch = new CountDownLatch(1);
+
+    private CustomTabsSession mSession;
+
+    private Context mContext;
+
+    private CustomTabsServiceConnection mConnection = new CustomTabsServiceConnection() {
+        @Override
+        public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
+            mSession = client.newSession(null);
+            mConnectionLatch.countDown();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            mSession = null;
+        }
+    };
+
+    /**
+     * Binds the CustomTabsService, creates a {@link CustomTabsSession} and returns it.
+     */
+    public CustomTabsSession establishSessionBlocking(Context context) {
+        mContext = context;
+        if (!CustomTabsClient.bindCustomTabsService(context, context.getPackageName(),
+                mConnection)) {
+            fail("Failed to bind the service");
+            return null;
+        }
+        boolean success = false;
+        try {
+            success = mConnectionLatch.await(2, TimeUnit.SECONDS);
+        } catch (InterruptedException e) { }
+        if (!success) {
+            fail("Failed to connect to service");
+            return null;
+        }
+        return mSession;
+    }
+
+    @Override
+    protected void finished(Description description) {
+        if (mContext != null && mSession != null) {
+            try {
+                mContext.unbindService(mConnection);
+            } catch (RuntimeException e) { } // Service might be disabled at this point
+        }
+    }
+}
diff --git a/browser/src/androidTest/java/androidx/browser/customtabs/testutil/TestUtil.java b/browser/src/androidTest/java/androidx/browser/customtabs/testutil/TestUtil.java
new file mode 100644
index 0000000..40130ee
--- /dev/null
+++ b/browser/src/androidTest/java/androidx/browser/customtabs/testutil/TestUtil.java
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+package androidx.browser.customtabs.testutil;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Instrumentation;
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.browser.trusted.TestBrowser;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+
+/**
+ * Utilities for testing Custom Tabs.
+ */
+public class TestUtil {
+
+    /**
+     * Waits until {@link TestBrowser} is launched and resumed, and returns it.
+     *
+     * @param launchRunnable Runnable that should start the activity.
+     */
+    public static TestBrowser getBrowserActivityWhenLaunched(Runnable launchRunnable) {
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        Instrumentation.ActivityMonitor monitor =
+                instrumentation.addMonitor(TestBrowser.class.getName(), null, false);
+
+        launchRunnable.run();
+        TestBrowser activity =
+                (TestBrowser) instrumentation.waitForMonitorWithTimeout(monitor, 3000);
+        assertNotNull("TestBrowser wasn't launched", activity);
+
+        // ActivityMonitor is triggered in onCreate and in onResume, which can lead to races when
+        // launching several activity instances. So wait for onResume before returning.
+        boolean resumed = activity.waitForResume(3000);
+        assertTrue("TestBrowser didn't reach onResume", resumed);
+        return activity;
+    }
+
+    /**
+     * Runs the supplied Callable on the main thread, returning the result. Blocks until completes.
+     */
+    public static <T> T runOnUiThreadBlocking(Callable<T> c) {
+        FutureTask<T> task = new FutureTask<>(c);
+        new Handler(Looper.getMainLooper()).post(task);
+        try {
+            return task.get();
+        } catch (InterruptedException e) {
+            throw new RuntimeException("Interrupted waiting for callable", e);
+        } catch (ExecutionException e) {
+            throw new RuntimeException(e.getCause());
+        }
+    }
+
+
+    /**
+     * Runs the supplied Runnable on the main thread. Blocks until completes.
+     */
+    public static void runOnUiThreadBlocking(final Runnable c) {
+        runOnUiThreadBlocking(new Callable<Void>() {
+            @Override
+            public Void call() throws Exception {
+                c.run();
+                return null;
+            }
+        });
+    }
+
+}
diff --git a/browser/src/androidTest/java/androidx/browser/trusted/TestBrowser.java b/browser/src/androidTest/java/androidx/browser/trusted/TestBrowser.java
new file mode 100644
index 0000000..eefb650
--- /dev/null
+++ b/browser/src/androidTest/java/androidx/browser/trusted/TestBrowser.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+package androidx.browser.trusted;
+
+import android.os.Bundle;
+import android.os.Looper;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A fake Browser that accepts browsable Intents.
+ */
+public class TestBrowser extends AppCompatActivity {
+
+    private final CountDownLatch mResumeLatch = new CountDownLatch(1);
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mResumeLatch.countDown();
+    }
+
+    /**
+     * Waits until onResume. Returns whether has reached onResume until timeout.
+     * If already resumed, returns "true" immediately.
+     */
+    public boolean waitForResume(int timeoutMillis) {
+        assert Thread.currentThread() != Looper.getMainLooper().getThread() : "Deadlock!";
+        try {
+            return mResumeLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            return false;
+        }
+    }
+}
diff --git a/browser/src/androidTest/java/androidx/browser/trusted/TestTrustedWebActivityService.java b/browser/src/androidTest/java/androidx/browser/trusted/TestTrustedWebActivityService.java
new file mode 100644
index 0000000..d4f87fd
--- /dev/null
+++ b/browser/src/androidTest/java/androidx/browser/trusted/TestTrustedWebActivityService.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018 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.browser.trusted;
+
+import android.app.Notification;
+import android.os.Parcelable;
+
+public class TestTrustedWebActivityService extends TrustedWebActivityService {
+    public static final int SMALL_ICON_ID = 666;
+
+    @Override
+    protected boolean notifyNotificationWithChannel(String platformTag, int platformId,
+            Notification notification, String channelName) {
+        return true;
+    }
+
+    @Override
+    protected void cancelNotification(String platformTag, int platformId) {
+    }
+
+    @Override
+    protected Parcelable[] getActiveNotifications() {
+        return new Parcelable[] { null };
+    }
+
+    @Override
+    protected int getSmallIconId() {
+        return SMALL_ICON_ID;
+    }
+}
diff --git a/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityBuilderTest.java b/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityBuilderTest.java
new file mode 100644
index 0000000..c94fcc6
--- /dev/null
+++ b/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityBuilderTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.
+ */
+
+package androidx.browser.trusted;
+
+import static androidx.browser.customtabs.TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY;
+import static androidx.browser.customtabs.testutil.TestUtil.getBrowserActivityWhenLaunched;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.browser.customtabs.CustomTabsIntent;
+import androidx.browser.customtabs.CustomTabsSession;
+import androidx.browser.customtabs.CustomTabsSessionToken;
+import androidx.browser.customtabs.EnableComponentsTestRule;
+import androidx.browser.customtabs.TestActivity;
+import androidx.browser.customtabs.TestCustomTabsServiceSupportsTwas;
+import androidx.browser.customtabs.TrustedWebUtils;
+import androidx.browser.customtabs.testutil.CustomTabConnectionRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests for {@link TrustedWebActivityBuilder}.
+ */
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class TrustedWebActivityBuilderTest {
+
+    @Rule
+    public final EnableComponentsTestRule mEnableComponents = new EnableComponentsTestRule(
+            TestActivity.class,
+            TestBrowser.class,
+            TestCustomTabsServiceSupportsTwas.class
+    );
+
+    @Rule
+    public final ActivityTestRule<TestActivity> mActivityTestRule =
+            new ActivityTestRule<>(TestActivity.class, false, true);
+
+    @Rule
+    public final CustomTabConnectionRule mConnectionRule = new CustomTabConnectionRule();
+
+    private TestActivity mActivity;
+    private CustomTabsSession mSession;
+
+    @Before
+    public void setUp() {
+        mActivity = mActivityTestRule.getActivity();
+        mSession = mConnectionRule.establishSessionBlocking(mActivity);
+    }
+
+    @Test
+    public void intentIsConstructedCorrectly() {
+        Uri url = Uri.parse("https://test.com/page");
+        int statusBarColor = 0xaabbcc;
+        List<String> additionalTrustedOrigins =
+                Arrays.asList("https://m.test.com", "https://test.org");
+
+        Bundle splashScreenParams = new Bundle();
+        int splashBgColor = 0x112233;
+        splashScreenParams.putInt(
+                TrustedWebUtils.SplashScreenParamKey.BACKGROUND_COLOR, splashBgColor);
+
+        final TrustedWebActivityBuilder builder =
+                new TrustedWebActivityBuilder(mActivity, url)
+                        .setStatusBarColor(statusBarColor)
+                        .setAdditionalTrustedOrigins(additionalTrustedOrigins)
+                        .setSplashScreenParams(splashScreenParams);
+        Intent intent =
+                getBrowserActivityWhenLaunched(new Runnable() {
+                    @Override
+                    public void run() {
+                        builder.launchActivity(mSession);
+                    }
+                }).getIntent();
+
+        assertTrue(intent.getBooleanExtra(EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, false));
+        assertTrue(CustomTabsSessionToken.getSessionTokenFromIntent(intent)
+                .isAssociatedWith(mSession));
+        assertEquals(url, intent.getData());
+        assertEquals(statusBarColor, intent.getIntExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR, 0));
+        assertEquals(additionalTrustedOrigins,
+                intent.getStringArrayListExtra(TrustedWebUtils.EXTRA_ADDITIONAL_TRUSTED_ORIGINS));
+
+        Bundle splashScreenParamsReceived =
+                intent.getBundleExtra(TrustedWebUtils.EXTRA_SPLASH_SCREEN_PARAMS);
+
+        // No need to test every splash screen param: they are sent in as-is in provided Bundle.
+        assertEquals(splashBgColor, splashScreenParamsReceived.getInt(
+                TrustedWebUtils.SplashScreenParamKey.BACKGROUND_COLOR));
+    }
+}
diff --git a/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionManagerTest.java b/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionManagerTest.java
new file mode 100644
index 0000000..e9114e1
--- /dev/null
+++ b/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionManagerTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2018 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.browser.trusted;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import androidx.annotation.Nullable;
+import androidx.browser.customtabs.EnableComponentsTestRule;
+import androidx.browser.customtabs.TestActivity;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.testutils.PollingCheck;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TrustedWebActivityServiceConnectionManagerTest {
+    private static final String ORIGIN = "https://localhost:3080";
+    private static final Uri GOOD_SCOPE = Uri.parse("https://www.example.com/notifications");
+    private static final Uri BAD_SCOPE = Uri.parse("https://www.notexample.com");
+
+    private TrustedWebActivityServiceConnectionManager mManager;
+    private Context mContext;
+
+    @Rule
+    public final VerifiedProviderTestRule mVerifiedProvider = new VerifiedProviderTestRule();
+    @Rule
+    public final EnableComponentsTestRule mEnableComponents = new EnableComponentsTestRule(
+            TestTrustedWebActivityService.class,
+            TestActivity.class
+    );
+
+    @Before
+    public void setUp() {
+        mContext = ApplicationProvider.getApplicationContext();
+        mManager = new TrustedWebActivityServiceConnectionManager(mContext);
+
+        TrustedWebActivityServiceConnectionManager
+                .registerClient(mContext, ORIGIN, mContext.getPackageName());
+    }
+
+    @After
+    public void tearDown() {
+        mManager.unbindAllConnections();
+    }
+
+    @Test
+    public void testConnection() {
+        final AtomicBoolean connected = new AtomicBoolean();
+        boolean delegated = mManager.execute(GOOD_SCOPE, ORIGIN,
+                new TrustedWebActivityServiceConnectionManager.ExecutionCallback() {
+                    @Override
+                    public void onConnected(@Nullable TrustedWebActivityServiceWrapper service)
+                            throws RemoteException {
+                        assertEquals(TestTrustedWebActivityService.SMALL_ICON_ID,
+                                service.getSmallIconId());
+                        connected.set(true);
+                    }
+                });
+        assertTrue(delegated);
+
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return connected.get();
+            }
+        });
+    }
+
+
+
+    @Test
+    public void testNoService() {
+        boolean delegated = mManager.execute(BAD_SCOPE, ORIGIN,
+                new TrustedWebActivityServiceConnectionManager.ExecutionCallback() {
+                    @Override
+                    public void onConnected(@Nullable TrustedWebActivityServiceWrapper service)
+                            throws RemoteException {
+                    }
+                });
+        assertFalse(delegated);
+    }
+}
diff --git a/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceTest.java b/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceTest.java
new file mode 100644
index 0000000..8f431656
--- /dev/null
+++ b/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2018 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.browser.trusted;
+
+import static org.junit.Assert.fail;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.support.customtabs.trusted.ITrustedWebActivityService;
+
+import androidx.browser.customtabs.EnableComponentsTestRule;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.ServiceTestRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeoutException;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TrustedWebActivityServiceTest {
+    @Rule
+    public final ServiceTestRule mServiceRule;
+    @Rule
+    public final VerifiedProviderTestRule mVerifiedProvider = new VerifiedProviderTestRule();
+    @Rule
+    public final EnableComponentsTestRule mEnableComponents = new EnableComponentsTestRule(
+            TestTrustedWebActivityService.class
+    );
+
+    private Context mContext;
+    private ITrustedWebActivityService mService;
+
+    public TrustedWebActivityServiceTest() {
+        mServiceRule = new ServiceTestRule();
+    }
+
+    @Before
+    public void setup() {
+        mContext = ApplicationProvider.getApplicationContext();
+
+        Intent intent = new Intent();
+        intent.setClassName(mContext.getPackageName(),
+                TestTrustedWebActivityService.class.getName());
+        try {
+            mService = ITrustedWebActivityService.Stub.asInterface(
+                    mServiceRule.bindService(intent, mConnection, Context.BIND_AUTO_CREATE));
+        } catch (TimeoutException e) {
+            fail();
+        }
+    }
+
+    @After
+    public void tearDown() {
+        mServiceRule.unbindService();
+        TrustedWebActivityService.setVerifiedProvider(mContext, null);
+    }
+
+    // Our ServiceConnection doesn't need to do anything since the binder is returned by the
+    // ServiceRule.
+    private ServiceConnection mConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName componentName, IBinder iBinder) { }
+        @Override
+        public void onServiceDisconnected(ComponentName componentName) { }
+    };
+
+    @Test
+    public void testVerification() throws RemoteException {
+        // This only works because we're in the same process as the service, otherwise this would
+        // have to be called in the Service's process.
+        mService.getSmallIconId();
+    }
+
+    @Test(expected = SecurityException.class)
+    public void testVerificationFailure() throws RemoteException {
+        mVerifiedProvider.manuallyDisable();
+        mService.getSmallIconId();
+    }
+}
diff --git a/browser/src/androidTest/java/androidx/browser/trusted/VerifiedProviderTestRule.java b/browser/src/androidTest/java/androidx/browser/trusted/VerifiedProviderTestRule.java
new file mode 100644
index 0000000..389950f
--- /dev/null
+++ b/browser/src/androidTest/java/androidx/browser/trusted/VerifiedProviderTestRule.java
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+package androidx.browser.trusted;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+/**
+ * Sets the instrumentation context as a verified Trusted Web Activity Provider, meaning that the
+ * TrustedWebActivityService will accept calls from it.
+ */
+public class VerifiedProviderTestRule extends TestWatcher {
+    @Override
+    protected void starting(Description description) {
+        set(true);
+    }
+
+    @Override
+    protected void finished(Description description) {
+        set(false);
+    }
+
+    /**
+     * Manually disables verification, causing TrustedWebActivityService calls to throw an
+     * exception.
+     */
+    public void manuallyDisable() {
+        set(false);
+    }
+
+    private void set(boolean enabled) {
+        Context context = InstrumentationRegistry.getContext();
+        TrustedWebActivityService.setVerifiedProviderSynchronouslyForTesting(
+                context, enabled ? context.getPackageName() : null);
+    }
+}
diff --git a/browser/src/androidTest/res/drawable/splash.xml b/browser/src/androidTest/res/drawable/splash.xml
new file mode 100644
index 0000000..72f2d08
--- /dev/null
+++ b/browser/src/androidTest/res/drawable/splash.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License
+-->
+
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid
+        android:color="@color/splash_screen_color"/>
+    <size
+        android:width="10dp"
+        android:height="10dp"/>
+</shape>
\ No newline at end of file
diff --git a/browser/src/androidTest/res/values/colors.xml b/browser/src/androidTest/res/values/colors.xml
new file mode 100644
index 0000000..64c9f19d3
--- /dev/null
+++ b/browser/src/androidTest/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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
+-->
+
+<resources>
+    <color name="splash_screen_color">#FF0000FF</color>
+    <color name="status_bar_color">#FFFF0000</color>
+</resources>
\ No newline at end of file
diff --git a/browser/src/androidTest/res/xml/filepaths.xml b/browser/src/androidTest/res/xml/filepaths.xml
new file mode 100644
index 0000000..df13a9f
--- /dev/null
+++ b/browser/src/androidTest/res/xml/filepaths.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths>
+    <files-path path="twa_splash/" name="twa_splash" />
+</paths>
\ No newline at end of file
diff --git a/browser/src/main/AndroidManifest.xml b/browser/src/main/AndroidManifest.xml
index 2753de8..a6738f5 100644
--- a/browser/src/main/AndroidManifest.xml
+++ b/browser/src/main/AndroidManifest.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- Copyright 2015 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.
diff --git a/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl b/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl
index a5dd8ea..3e2c48c 100644
--- a/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl
+++ b/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl
@@ -28,4 +28,4 @@
     void onMessageChannelReady(in Bundle extras) = 3;
     void onPostMessage(String message, in Bundle extras) = 4;
     void onRelationshipValidationResult(int relation, in Uri origin, boolean result, in Bundle extras) = 5;
-}
\ No newline at end of file
+}
diff --git a/browser/src/main/aidl/android/support/customtabs/ICustomTabsService.aidl b/browser/src/main/aidl/android/support/customtabs/ICustomTabsService.aidl
index c89d9db..52e72bf 100644
--- a/browser/src/main/aidl/android/support/customtabs/ICustomTabsService.aidl
+++ b/browser/src/main/aidl/android/support/customtabs/ICustomTabsService.aidl
@@ -30,11 +30,14 @@
 interface ICustomTabsService {
     boolean warmup(long flags) = 1;
     boolean newSession(in ICustomTabsCallback callback) = 2;
+    boolean newSessionWithExtras(in ICustomTabsCallback callback, in Bundle extras) = 9;
     boolean mayLaunchUrl(in ICustomTabsCallback callback, in Uri url,
             in Bundle extras, in List<Bundle> otherLikelyBundles) = 3;
     Bundle extraCommand(String commandName, in Bundle args) = 4;
     boolean updateVisuals(in ICustomTabsCallback callback, in Bundle bundle) = 5;
     boolean requestPostMessageChannel(in ICustomTabsCallback callback, in Uri postMessageOrigin) = 6;
+    boolean requestPostMessageChannelWithExtras(in ICustomTabsCallback callback, in Uri postMessageOrigin, in Bundle extras) = 10;
     int postMessage(in ICustomTabsCallback callback, String message, in Bundle extras) = 7;
     boolean validateRelationship(in ICustomTabsCallback callback, int relation, in Uri origin, in Bundle extras) = 8;
-}
\ No newline at end of file
+    boolean receiveFile(in ICustomTabsCallback callback, in Uri uri, int purpose, in Bundle extras) = 11;
+}
diff --git a/browser/src/main/aidl/android/support/customtabs/IPostMessageService.aidl b/browser/src/main/aidl/android/support/customtabs/IPostMessageService.aidl
index 666fe83c..2c8a605 100644
--- a/browser/src/main/aidl/android/support/customtabs/IPostMessageService.aidl
+++ b/browser/src/main/aidl/android/support/customtabs/IPostMessageService.aidl
@@ -27,4 +27,4 @@
 interface IPostMessageService {
     void onMessageChannelReady(in ICustomTabsCallback callback, in Bundle extras) = 1;
     void onPostMessage(in ICustomTabsCallback callback, String message, in Bundle extras) = 2;
-}
\ No newline at end of file
+}
diff --git a/browser/src/main/aidl/android/support/customtabs/trusted/ITrustedWebActivityService.aidl b/browser/src/main/aidl/android/support/customtabs/trusted/ITrustedWebActivityService.aidl
new file mode 100644
index 0000000..ff465c4
--- /dev/null
+++ b/browser/src/main/aidl/android/support/customtabs/trusted/ITrustedWebActivityService.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.customtabs.trusted;
+
+/**
+ * Interface to a TrustedWebActivityService.
+ * @hide
+ */
+interface ITrustedWebActivityService {
+    Bundle areNotificationsEnabled(in Bundle args) = 5;
+    Bundle notifyNotificationWithChannel(in Bundle args) = 1;
+    void cancelNotification(in Bundle args) = 2;
+    Bundle getActiveNotifications() = 4;
+    int getSmallIconId() = 3;
+    Bundle getSmallIconBitmap() = 6;
+}
diff --git a/browser/src/main/java/androidx/browser/customtabs/CustomTabsCallback.java b/browser/src/main/java/androidx/browser/customtabs/CustomTabsCallback.java
index 2daff4e..f201a0b 100644
--- a/browser/src/main/java/androidx/browser/customtabs/CustomTabsCallback.java
+++ b/browser/src/main/java/androidx/browser/customtabs/CustomTabsCallback.java
@@ -19,6 +19,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 
+import androidx.annotation.RestrictTo;
 import androidx.browser.customtabs.CustomTabsService.Relation;
 
 /**
@@ -58,6 +59,15 @@
     public static final int TAB_HIDDEN = 6;
 
     /**
+     * Key for the extra included in {@link #onRelationshipValidationResult} {@code extras}
+     * containing whether the verification was performed while the device was online. This may be
+     * missing in cases verification was short cut.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static final String ONLINE_EXTRAS_KEY = "online";
+
+    /**
      * To be called when a navigation event happens.
      *
      * @param navigationEvent The code corresponding to the navigation event.
diff --git a/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java b/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
index a7161ce..e1fa8c9 100644
--- a/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
+++ b/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
@@ -16,8 +16,7 @@
 
 package androidx.browser.customtabs;
 
-import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
-
+import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -36,7 +35,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-import androidx.browser.customtabs.CustomTabsService.Relation;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -48,12 +46,14 @@
 public class CustomTabsClient {
     private final ICustomTabsService mService;
     private final ComponentName mServiceComponentName;
+    private final Context mApplicationContext;
 
-    /** @hide */
-    @RestrictTo(LIBRARY_GROUP_PREFIX)
-    CustomTabsClient(ICustomTabsService service, ComponentName componentName) {
+    /**@hide*/
+    CustomTabsClient(ICustomTabsService service, ComponentName componentName,
+            Context applicationContext) {
         mService = service;
         mServiceComponentName = componentName;
+        mApplicationContext = applicationContext;
     }
 
     /**
@@ -70,6 +70,7 @@
      */
     public static boolean bindCustomTabsService(Context context,
             String packageName, CustomTabsServiceConnection connection) {
+        connection.setApplicationContext(context.getApplicationContext());
         Intent intent = new Intent(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
         if (!TextUtils.isEmpty(packageName)) intent.setPackage(packageName);
         return context.bindService(intent, connection,
@@ -78,8 +79,6 @@
 
     /**
      * Returns the preferred package to use for Custom Tabs, preferring the default VIEW handler.
-     *
-     * see getPackageName(Context, List<String>, boolean)
      */
     public static String getPackageName(Context context, @Nullable List<String> packages) {
         return getPackageName(context, packages, false);
@@ -178,6 +177,12 @@
         }
     }
 
+    private static PendingIntent createSessionId(Context context, int sessionId) {
+        // Create a {@link PendingIntent} with empty Action to prevent using it other than
+        // a session identifier.
+        return PendingIntent.getActivity(context, sessionId, new Intent(), 0);
+    }
+
     /**
      * Creates a new session through an ICustomTabsService with the optional callback. This session
      * can be used to associate any related communication through the service with an intent and
@@ -188,9 +193,75 @@
      * @return The session object that was created as a result of the transaction. The client can
      *         use this to relay session specific calls.
      *         Null on error.
+     * TODO(peconn): Mark as @Nullable, prompting API change.
      */
     public CustomTabsSession newSession(final CustomTabsCallback callback) {
-        ICustomTabsCallback.Stub wrapper = new ICustomTabsCallback.Stub() {
+        return newSessionInternal(callback, null);
+    }
+
+    /**
+     * Creates a new session or updates a callback for the existing session
+     * through an ICustomTabsService. This session can be used to associate any related
+     * communication through the service with an intent and then later with a Custom Tab.
+     * The client can then send later service calls or intents to through same
+     * session-intent-Custom Tab association.
+     * @param callback The callback through which the client will receive updates about the created
+     *                 session. Can be null. All the callbacks will be received on the UI thread.
+     * @param id The session id. If the session with the specified id already exists,
+     *           updates callback.
+     * @return The session object that was created as a result of the transaction. The client can
+     *         use this to relay session specific calls.
+     *         Null on error.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public @Nullable CustomTabsSession newSession(final CustomTabsCallback callback, int id) {
+        return newSessionInternal(callback, createSessionId(mApplicationContext, id));
+    }
+
+    /**
+     * Creates a new pending session with an optional callback. This session can be converted to
+     * a standard session using {@link #attachSession} after connection.
+     *
+     * {@see PendingSession}
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static CustomTabsSession.PendingSession newPendingSession(
+            Context context, final CustomTabsCallback callback, int id) {
+        PendingIntent sessionId = createSessionId(context, id);
+
+        return new CustomTabsSession.PendingSession(callback, sessionId);
+    }
+
+    private @Nullable CustomTabsSession newSessionInternal(final CustomTabsCallback callback,
+                @Nullable PendingIntent sessionId) {
+        ICustomTabsCallback.Stub wrapper = createCallbackWrapper(callback);
+        Bundle extras = new Bundle();
+        if (sessionId != null) extras.putParcelable(CustomTabsIntent.EXTRA_SESSION_ID, sessionId);
+        try {
+            if (!mService.newSessionWithExtras(wrapper, extras)) return null;
+        } catch (RemoteException e) {
+            return null;
+        }
+        return new CustomTabsSession(mService, wrapper, mServiceComponentName, sessionId);
+    }
+
+    /**
+     * Can be used as a channel between the Custom Tabs client and the provider to do something that
+     * is not part of the API yet.
+     */
+    public Bundle extraCommand(String commandName, Bundle args) {
+        try {
+            return mService.extraCommand(commandName, args);
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    private ICustomTabsCallback.Stub createCallbackWrapper(final CustomTabsCallback callback) {
+        return new ICustomTabsCallback.Stub() {
             private Handler mHandler = new Handler(Looper.getMainLooper());
 
             @Override
@@ -242,8 +313,8 @@
 
             @Override
             public void onRelationshipValidationResult(
-                    final @Relation int relation, final Uri requestedOrigin, final boolean result,
-                    final @Nullable Bundle extras) throws RemoteException {
+                    final @CustomTabsService.Relation int relation, final Uri requestedOrigin,
+                    final boolean result, final @Nullable Bundle extras) throws RemoteException {
                 if (callback == null) return;
                 mHandler.post(new Runnable() {
                     @Override
@@ -254,20 +325,16 @@
                 });
             }
         };
-
-        try {
-            if (!mService.newSession(wrapper)) return null;
-        } catch (RemoteException e) {
-            return null;
-        }
-        return new CustomTabsSession(mService, wrapper, mServiceComponentName);
     }
 
-    public Bundle extraCommand(String commandName, Bundle args) {
-        try {
-            return mService.extraCommand(commandName, args);
-        } catch (RemoteException e) {
-            return null;
-        }
+    /**
+     * Associate {@link CustomTabsSession.PendingSession} with the service
+     * and turn it into a {@link CustomTabsSession}.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public CustomTabsSession attachSession(CustomTabsSession.PendingSession session) {
+        return newSessionInternal(session.getCallback(), session.getId());
     }
 }
diff --git a/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java b/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
index 45e2e80..0cb25e5 100644
--- a/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
+++ b/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
@@ -24,6 +24,7 @@
 import android.graphics.Color;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.util.SparseArray;
 import android.view.View;
 import android.widget.RemoteViews;
@@ -72,6 +73,15 @@
     public static final String EXTRA_SESSION = "android.support.customtabs.extra.SESSION";
 
     /**
+     * Extra used to match the session ID. This is PendingIntent which is created with
+     * {@link CustomTabsClient#createSessionId}.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static final String EXTRA_SESSION_ID = "android.support.customtabs.extra.SESSION_ID";
+
+    /**
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -346,7 +356,19 @@
          * {@link CustomTabsSession}.
          */
         public Builder() {
-            this(null);
+            initialize(null, null);
+        }
+
+        /**
+         * Creates a {@link CustomTabsIntent.Builder} object associated with a given
+         * {@link CustomTabsSession.PendingSession}.
+         *
+         * {@see Builder(CustomTabsSession)}
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        public Builder(@Nullable CustomTabsSession.PendingSession session) {
+            initialize(null, session.getId());
         }
 
         /**
@@ -359,10 +381,21 @@
          * @param session The session to associate this Builder with.
          */
         public Builder(@Nullable CustomTabsSession session) {
-            if (session != null) mIntent.setPackage(session.getComponentName().getPackageName());
+            if (session != null) {
+                mIntent.setPackage(session.getComponentName().getPackageName());
+                initialize(session.getBinder(), session.getId());
+            } else {
+                initialize(null, null);
+            }
+        }
+
+        private void initialize(@Nullable IBinder session, @Nullable PendingIntent sessionId) {
             Bundle bundle = new Bundle();
-            BundleCompat.putBinder(
-                    bundle, EXTRA_SESSION, session == null ? null : session.getBinder());
+            BundleCompat.putBinder(bundle, EXTRA_SESSION, session);
+            if (sessionId != null) {
+                bundle.putParcelable(EXTRA_SESSION_ID, sessionId);
+            }
+
             mIntent.putExtras(bundle);
         }
 
diff --git a/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java b/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
index fca989c..d271667 100644
--- a/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
+++ b/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
@@ -16,6 +16,7 @@
 
 package androidx.browser.customtabs;
 
+import android.app.PendingIntent;
 import android.app.Service;
 import android.content.Intent;
 import android.net.Uri;
@@ -27,6 +28,9 @@
 import android.support.customtabs.ICustomTabsService;
 
 import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
 import androidx.collection.ArrayMap;
 
 import java.lang.annotation.Retention;
@@ -56,6 +60,16 @@
             "androidx.browser.trusted.category.NavBarColorCustomization";
 
     /**
+     * An Intent filter category to signify that the Custom Tabs provider supports Trusted Web
+     * Activities.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static final String TRUSTED_WEB_ACTIVITY_CATEGORY =
+            "androidx.browser.trusted.category.TrustedWebActivities";
+
+    /**
      * For {@link CustomTabsService#mayLaunchUrl} calls that wants to specify more than one url,
      * this key can be used with {@link Bundle#putParcelable(String, android.os.Parcelable)}
      * to insert a new url to each bundle inside list of bundles.
@@ -105,7 +119,27 @@
      */
     public static final int RELATION_HANDLE_ALL_URLS = 2;
 
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
+
+    /**
+     * Enumerates the possible purposes of files received in {@link #receiveFile}.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({FILE_PURPOSE_TWA_SPLASH_IMAGE})
+    public @interface FilePurpose {
+    }
+
+    /**
+     * File is a splash image to be shown on top of a Trusted Web Activity while the web contents
+     * are loading.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static final int FILE_PURPOSE_TWA_SPLASH_IMAGE = 1;
+
     final Map<IBinder, DeathRecipient> mDeathRecipientMap = new ArrayMap<>();
 
     private ICustomTabsService.Stub mBinder = new ICustomTabsService.Stub() {
@@ -117,7 +151,17 @@
 
         @Override
         public boolean newSession(ICustomTabsCallback callback) {
-            final CustomTabsSessionToken sessionToken = new CustomTabsSessionToken(callback);
+            return newSessionInternal(callback, null);
+        }
+
+        @Override
+        public boolean newSessionWithExtras(ICustomTabsCallback callback, Bundle extras) {
+            return newSessionInternal(callback, getSessionIdFromBundle(extras));
+        }
+
+        private boolean newSessionInternal(ICustomTabsCallback callback, PendingIntent sessionId) {
+            final CustomTabsSessionToken sessionToken =
+                    new CustomTabsSessionToken(callback, sessionId);
             try {
                 DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
                     @Override
@@ -139,7 +183,8 @@
         public boolean mayLaunchUrl(ICustomTabsCallback callback, Uri url,
                 Bundle extras, List<Bundle> otherLikelyBundles) {
             return CustomTabsService.this.mayLaunchUrl(
-                    new CustomTabsSessionToken(callback), url, extras, otherLikelyBundles);
+                    new CustomTabsSessionToken(callback, getSessionIdFromBundle(extras)),
+                    url, extras, otherLikelyBundles);
         }
 
         @Override
@@ -150,27 +195,53 @@
         @Override
         public boolean updateVisuals(ICustomTabsCallback callback, Bundle bundle) {
             return CustomTabsService.this.updateVisuals(
-                    new CustomTabsSessionToken(callback), bundle);
+                    new CustomTabsSessionToken(callback, getSessionIdFromBundle(bundle)), bundle);
         }
 
         @Override
         public boolean requestPostMessageChannel(ICustomTabsCallback callback,
                 Uri postMessageOrigin) {
             return CustomTabsService.this.requestPostMessageChannel(
-                    new CustomTabsSessionToken(callback), postMessageOrigin);
+                    new CustomTabsSessionToken(callback, null), postMessageOrigin);
+        }
+
+        @Override
+        public boolean requestPostMessageChannelWithExtras(ICustomTabsCallback callback,
+                Uri postMessageOrigin, Bundle extras) {
+            return CustomTabsService.this.requestPostMessageChannel(
+                    new CustomTabsSessionToken(callback, getSessionIdFromBundle(extras)),
+                    postMessageOrigin);
         }
 
         @Override
         public int postMessage(ICustomTabsCallback callback, String message, Bundle extras) {
             return CustomTabsService.this.postMessage(
-                    new CustomTabsSessionToken(callback), message, extras);
+                    new CustomTabsSessionToken(callback, getSessionIdFromBundle(extras)),
+                    message, extras);
         }
 
         @Override
         public boolean validateRelationship(
                 ICustomTabsCallback callback, @Relation int relation, Uri origin, Bundle extras) {
             return CustomTabsService.this.validateRelationship(
-                    new CustomTabsSessionToken(callback), relation, origin, extras);
+                    new CustomTabsSessionToken(callback, getSessionIdFromBundle(extras)),
+                    relation, origin, extras);
+        }
+
+        @Override
+        public boolean receiveFile(ICustomTabsCallback callback, @NonNull Uri uri,
+                @FilePurpose int purpose, @Nullable Bundle extras) {
+            return CustomTabsService.this.receiveFile(
+                    new CustomTabsSessionToken(callback, getSessionIdFromBundle(extras)),
+                    uri, purpose, extras);
+        }
+
+        private @Nullable PendingIntent getSessionIdFromBundle(@Nullable Bundle bundle) {
+            if (bundle == null) return null;
+
+            PendingIntent sessionId = bundle.getParcelable(CustomTabsIntent.EXTRA_SESSION_ID);
+            bundle.remove(CustomTabsIntent.EXTRA_SESSION_ID);
+            return sessionId;
         }
     };
 
@@ -270,8 +341,7 @@
      *                     with the same structure in {@link CustomTabsIntent.Builder}.
      * @return Whether the operation was successful.
      */
-    protected abstract boolean updateVisuals(CustomTabsSessionToken sessionToken,
-            Bundle bundle);
+    protected abstract boolean updateVisuals(CustomTabsSessionToken sessionToken, Bundle bundle);
 
     /**
      * Sends a request to create a two way postMessage channel between the client and the browser
@@ -320,6 +390,28 @@
      * @return true if the request has been submitted successfully.
      */
     protected abstract boolean validateRelationship(
-            CustomTabsSessionToken sessionToken, @Relation int relation, Uri origin,
-            Bundle extras);
+            CustomTabsSessionToken sessionToken, @Relation int relation, Uri origin, Bundle extras);
+
+
+    /**
+     * Receive a file from client by given Uri, e.g. in order to display a large bitmap in a Custom
+     * Tab.
+     *
+     * Prior to calling this method, the client grants a read permission to the target
+     * Custom Tabs provider via {@link android.content.Context#grantUriPermission}.
+     *
+     * The file is read and processed (where applicable) synchronously.
+     *
+     * @param sessionToken The unique identifier for the session.
+     * @param uri {@link Uri} of the file.
+     * @param purpose Purpose of transferring this file, one of the constants enumerated in
+     *                {@code CustomTabsService#FilePurpose}.
+     * @param extras Reserved for future use.
+     * @return {@code true} if the file was received successfully.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    protected abstract boolean receiveFile(@NonNull CustomTabsSessionToken sessionToken,
+            @NonNull Uri uri, @FilePurpose int purpose, @Nullable Bundle extras);
 }
diff --git a/browser/src/main/java/androidx/browser/customtabs/CustomTabsServiceConnection.java b/browser/src/main/java/androidx/browser/customtabs/CustomTabsServiceConnection.java
index 6333213..b0b3340 100644
--- a/browser/src/main/java/androidx/browser/customtabs/CustomTabsServiceConnection.java
+++ b/browser/src/main/java/androidx/browser/customtabs/CustomTabsServiceConnection.java
@@ -17,21 +17,37 @@
 package androidx.browser.customtabs;
 
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.ServiceConnection;
 import android.os.IBinder;
 import android.support.customtabs.ICustomTabsService;
 
+import androidx.annotation.RestrictTo;
+
 /**
  * Abstract {@link ServiceConnection} to use while binding to a {@link CustomTabsService}. Any
  * client implementing this is responsible for handling changes related with the lifetime of the
  * connection like rebinding on disconnect.
  */
 public abstract class CustomTabsServiceConnection implements ServiceConnection {
+    private Context mApplicationContext;
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    /* package */ void setApplicationContext(Context context) {
+        mApplicationContext = context;
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    /* package */ Context getApplicationContext() {
+        return mApplicationContext;
+    }
 
     @Override
     public final void onServiceConnected(ComponentName name, IBinder service) {
         onCustomTabsServiceConnected(name, new CustomTabsClient(
-                ICustomTabsService.Stub.asInterface(service), name) {
+                ICustomTabsService.Stub.asInterface(service), name, mApplicationContext) {
         });
     }
 
diff --git a/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java b/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
index 99540f5..e90cd1f 100644
--- a/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
+++ b/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
@@ -30,6 +30,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.browser.customtabs.CustomTabsService.Relation;
 import androidx.browser.customtabs.CustomTabsService.Result;
@@ -48,6 +49,16 @@
     private final ComponentName mComponentName;
 
     /**
+     * The session ID is represented by {@link PendingIntent}. Other apps cannot
+     * forge {@link PendingIntent}. The {@link PendingIntent#equals(Object)} method
+     * considers two {@link PendingIntent} objects equal if their action, data, type,
+     * class and category are the same (even across a process being killed).
+     *
+     * {@see Intent#filterEquals()}
+     */
+    private final PendingIntent mId;
+
+    /**
      * Provides browsers a way to generate a mock {@link CustomTabsSession} for testing
      * purposes.
      *
@@ -59,14 +70,16 @@
     public static CustomTabsSession createMockSessionForTesting(
             @NonNull ComponentName componentName) {
         return new CustomTabsSession(
-                null, new CustomTabsSessionToken.MockCallback(), componentName);
+                null, new CustomTabsSessionToken.MockCallback(), componentName, null);
     }
 
     /* package */ CustomTabsSession(
-            ICustomTabsService service, ICustomTabsCallback callback, ComponentName componentName) {
+            ICustomTabsService service, ICustomTabsCallback callback, ComponentName componentName,
+            @Nullable PendingIntent sessionId) {
         mService = service;
         mCallback = callback;
         mComponentName = componentName;
+        mId = sessionId;
     }
 
     /**
@@ -86,6 +99,7 @@
      * @return                   true for success.
      */
     public boolean mayLaunchUrl(Uri url, Bundle extras, List<Bundle> otherLikelyBundles) {
+        addIdToBundle(extras);
         try {
             return mService.mayLaunchUrl(mCallback, url, extras, otherLikelyBundles);
         } catch (RemoteException e) {
@@ -109,6 +123,7 @@
 
         Bundle metaBundle = new Bundle();
         metaBundle.putBundle(CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE, bundle);
+        addIdToBundle(bundle);
         try {
             return mService.updateVisuals(mCallback, metaBundle);
         } catch (RemoteException e) {
@@ -131,6 +146,7 @@
         bundle.putParcelable(CustomTabsIntent.EXTRA_REMOTEVIEWS, remoteViews);
         bundle.putIntArray(CustomTabsIntent.EXTRA_REMOTEVIEWS_VIEW_IDS, clickableIDs);
         bundle.putParcelable(CustomTabsIntent.EXTRA_REMOTEVIEWS_PENDINGINTENT, pendingIntent);
+        addIdToBundle(bundle);
         try {
             return mService.updateVisuals(mCallback, bundle);
         } catch (RemoteException e) {
@@ -157,6 +173,7 @@
 
         Bundle metaBundle = new Bundle();
         metaBundle.putBundle(CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE, bundle);
+        addIdToBundle(metaBundle);
         try {
             return mService.updateVisuals(mCallback, metaBundle);
         } catch (RemoteException e) {
@@ -174,9 +191,11 @@
      *         asynchronous.
      */
     public boolean requestPostMessageChannel(Uri postMessageOrigin) {
+        Bundle extras = new Bundle();
+        addIdToBundle(extras);
         try {
-            return mService.requestPostMessageChannel(
-                    mCallback, postMessageOrigin);
+            return mService.requestPostMessageChannelWithExtras(
+                    mCallback, postMessageOrigin, extras);
         } catch (RemoteException e) {
             return false;
         }
@@ -196,6 +215,7 @@
      */
     @Result
     public int postMessage(String message, Bundle extras) {
+        addIdToBundle(extras);
         synchronized (mLock) {
             try {
                 return mService.postMessage(mCallback, message, extras);
@@ -231,6 +251,10 @@
                 || relation > CustomTabsService.RELATION_HANDLE_ALL_URLS) {
             return false;
         }
+        if (extras == null) {
+            extras = new Bundle();
+        }
+        addIdToBundle(extras);
         try {
             return mService.validateRelationship(mCallback, relation, origin, extras);
         } catch (RemoteException e) {
@@ -238,6 +262,42 @@
         }
     }
 
+    /**
+     * Passes an URI of a file, e.g. in order to pass a large bitmap to be displayed in the
+     * Custom Tabs provider.
+     *
+     * Prior to calling this method, the client needs to grant a read permission to the target
+     * Custom Tabs provider via {@link android.content.Context#grantUriPermission}.
+     *
+     * The file is read and processed (where applicable) synchronously, therefore it's recommended
+     * to call this method on a background thread.
+     *
+     * @param uri {@link Uri} of the file.
+     * @param purpose Purpose of transferring this file, one of the constants enumerated in
+     *                {@code CustomTabsService#FilePurpose}.
+     * @param extras Reserved for future use.
+     * @return {@code true} if the file was received successfully.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public boolean receiveFile(@NonNull Uri uri, @CustomTabsService.FilePurpose int purpose,
+            @Nullable Bundle extras) {
+        if (extras == null) {
+            extras = new Bundle();
+        }
+        addIdToBundle(extras);
+        try {
+            return mService.receiveFile(mCallback, uri, purpose, extras);
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    private void addIdToBundle(Bundle bundle) {
+        if (mId != null) bundle.putParcelable(CustomTabsIntent.EXTRA_SESSION_ID, mId);
+    }
+
     /* package */ IBinder getBinder() {
         return mCallback.asBinder();
     }
@@ -245,4 +305,36 @@
     /* package */ ComponentName getComponentName() {
         return mComponentName;
     }
+
+    /* package */ PendingIntent getId() {
+        return mId;
+    }
+
+    /**
+     * A class to be used instead of {@link CustomTabsSession} before we are connected
+     * {@link CustomTabsService}.
+     *
+     * Use {@link CustomTabsClient#attachSession(PendingSession)} to get {@link CustomTabsSession}.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static class PendingSession {
+        private final CustomTabsCallback mCallback;
+        private final PendingIntent mId;
+
+        /* package */ PendingSession(
+                CustomTabsCallback callback, PendingIntent sessionId) {
+            mCallback = callback;
+            mId = sessionId;
+        }
+
+        /* package */ PendingIntent getId() {
+            return mId;
+        }
+
+        /* package */ CustomTabsCallback getCallback() {
+            return mCallback;
+        }
+    }
 }
diff --git a/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java b/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java
index b666adf..3a50815 100644
--- a/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java
+++ b/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java
@@ -16,6 +16,7 @@
 
 package androidx.browser.customtabs;
 
+import android.app.PendingIntent;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
@@ -25,7 +26,8 @@
 import android.util.Log;
 
 import androidx.annotation.NonNull;
-import androidx.browser.customtabs.CustomTabsService.Relation;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
 import androidx.core.app.BundleCompat;
 
 /**
@@ -35,8 +37,17 @@
 public class CustomTabsSessionToken {
     private static final String TAG = "CustomTabsSessionToken";
 
+    /**
+     * Both {@link #mCallbackBinder} and {@link #mSessionId} are used as session ID.
+     * At least one of the ID should be not null. If {@link #mSessionId} is null,
+     * the session will be invalidated as soon as the client goes away.
+     * Otherwise the browser will attempt to keep the session parameters,
+     * but it might drop them to reclaim resources
+     */
     @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final ICustomTabsCallback mCallbackBinder;
+    @Nullable final ICustomTabsCallback mCallbackBinder;
+    @Nullable private final PendingIntent mSessionId;
+
     private final CustomTabsCallback mCallback;
 
     /* package */ static class MockCallback extends ICustomTabsCallback.Stub {
@@ -53,8 +64,8 @@
         public void onPostMessage(String message, Bundle extras) {}
 
         @Override
-        public void onRelationshipValidationResult(@Relation int relation, Uri requestedOrigin,
-                boolean result, Bundle extras) {}
+        public void onRelationshipValidationResult(@CustomTabsService.Relation int relation,
+                Uri requestedOrigin, boolean result, Bundle extras) {}
 
         @Override
         public IBinder asBinder() {
@@ -68,12 +79,16 @@
      * @param intent The intent to generate the token from. This has to include an extra for
      *               {@link CustomTabsIntent#EXTRA_SESSION}.
      * @return The token that was generated.
+     *
+     * TODO(peconn): Mark @Nullable with an API change.
      */
     public static CustomTabsSessionToken getSessionTokenFromIntent(Intent intent) {
         Bundle b = intent.getExtras();
+        if (b == null) return null;
         IBinder binder = BundleCompat.getBinder(b, CustomTabsIntent.EXTRA_SESSION);
-        if (binder == null) return null;
-        return new CustomTabsSessionToken(ICustomTabsCallback.Stub.asInterface(binder));
+        PendingIntent sessionId = intent.getParcelableExtra(CustomTabsIntent.EXTRA_SESSION_ID);
+        if (binder == null && sessionId == null) return null;
+        return new CustomTabsSessionToken(ICustomTabsCallback.Stub.asInterface(binder), sessionId);
     }
 
     /**
@@ -84,13 +99,15 @@
      */
     @NonNull
     public static CustomTabsSessionToken createMockSessionTokenForTesting() {
-        return new CustomTabsSessionToken(new MockCallback());
+        return new CustomTabsSessionToken(new MockCallback(), null);
     }
 
-    CustomTabsSessionToken(ICustomTabsCallback callbackBinder) {
+    CustomTabsSessionToken(@Nullable ICustomTabsCallback callbackBinder,
+            @Nullable PendingIntent sessionId) {
         mCallbackBinder = callbackBinder;
-        mCallback = new CustomTabsCallback() {
+        mSessionId = sessionId;
 
+        mCallback = callbackBinder == null ? null : new CustomTabsCallback() {
             @Override
             public void onNavigationEvent(int navigationEvent, Bundle extras) {
                 try {
@@ -128,8 +145,8 @@
             }
 
             @Override
-            public void onRelationshipValidationResult(@Relation int relation, Uri origin,
-                    boolean result, Bundle extras) {
+            public void onRelationshipValidationResult(@CustomTabsService.Relation int relation,
+                    Uri origin, boolean result, Bundle extras) {
                 try {
                     mCallbackBinder.onRelationshipValidationResult(
                             relation, origin, result, extras);
@@ -145,16 +162,41 @@
         return mCallbackBinder.asBinder();
     }
 
+    PendingIntent getId() {
+        return mSessionId;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public boolean hasCallback() {
+        return mCallbackBinder != null;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public boolean hasId() {
+        return mSessionId != null;
+    }
+
     @Override
     public int hashCode() {
+        if (mSessionId != null) return mSessionId.hashCode();
+
         return getCallbackBinder().hashCode();
     }
 
     @Override
     public boolean equals(Object o) {
         if (!(o instanceof CustomTabsSessionToken)) return false;
-        CustomTabsSessionToken token = (CustomTabsSessionToken) o;
-        return token.getCallbackBinder().equals(mCallbackBinder.asBinder());
+        CustomTabsSessionToken other = (CustomTabsSessionToken) o;
+        if (mSessionId != null && other.getId() != null) return mSessionId.equals(other.getId());
+
+        return other.getCallbackBinder() != null
+                && other.getCallbackBinder().equals(mCallbackBinder.asBinder());
     }
 
     /**
diff --git a/browser/src/main/java/androidx/browser/customtabs/PostMessageBackend.java b/browser/src/main/java/androidx/browser/customtabs/PostMessageBackend.java
new file mode 100644
index 0000000..cb6ebb5
--- /dev/null
+++ b/browser/src/main/java/androidx/browser/customtabs/PostMessageBackend.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018 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.browser.customtabs;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Abstracts a receiver of postMessage events. For example, this could be a service connection like
+ * {@link PostMessageServiceConnection} or it could be a local client.
+ *
+ * <p>This will always be backed by a class on the provider side rather than the client side.
+ * However, in the case of {@link PostMessageServiceConnection}, it will defer to the client by
+ * making remote calls.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface PostMessageBackend {
+
+    /**
+     * Posts a message to the client.
+     * @param message The String message to post.
+     * @param extras Unused.
+     * @return Whether the postMessage was sent successfully.
+     */
+    boolean onPostMessage(String message, Bundle extras);
+
+    /**
+     * Notifies the client that the postMessage channel is ready to be used.
+     * @param extras Unused.
+     * @return Whether the notification was sent successfully.
+     */
+    boolean onNotifyMessageChannelReady(Bundle extras);
+
+    /**
+     * Notifies the client that the channel has been disconnected.
+     * @param appContext The application context.
+     */
+    void onDisconnectChannel(Context appContext);
+}
diff --git a/browser/src/main/java/androidx/browser/customtabs/PostMessageServiceConnection.java b/browser/src/main/java/androidx/browser/customtabs/PostMessageServiceConnection.java
index a5b7c56..e14e3fd 100644
--- a/browser/src/main/java/androidx/browser/customtabs/PostMessageServiceConnection.java
+++ b/browser/src/main/java/androidx/browser/customtabs/PostMessageServiceConnection.java
@@ -25,21 +25,44 @@
 import android.os.RemoteException;
 import android.support.customtabs.ICustomTabsCallback;
 import android.support.customtabs.IPostMessageService;
+import android.util.Log;
+
+import androidx.annotation.RestrictTo;
 
 /**
  * A {@link ServiceConnection} for Custom Tabs providers to use while connecting to a
  * {@link PostMessageService} on the client side.
+ *
+ * TODO(peconn): Make this not abstract with API change.
  */
-public abstract class PostMessageServiceConnection implements ServiceConnection {
+public abstract class PostMessageServiceConnection
+        implements PostMessageBackend, ServiceConnection {
+    private static final String TAG = "PostMessageServConn";
+
     private final Object mLock = new Object();
     private final ICustomTabsCallback mSessionBinder;
     private IPostMessageService mService;
+    private String mPackageName;
+    // Indicates that a message channel has been opened. We're ready to post messages once this is
+    // true and we've connected to the {@link PostMessageService}.
+    private boolean mMessageChannelCreated;
 
     public PostMessageServiceConnection(CustomTabsSessionToken session) {
         mSessionBinder = ICustomTabsCallback.Stub.asInterface(session.getCallbackBinder());
     }
 
     /**
+     * Sets the package name unique to the session.
+     * @param packageName The package name for the client app for the owning session.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public void setPackageName(String packageName) {
+        mPackageName = packageName;
+    }
+
+    /**
      * Binds the browser side to the client app through the given {@link PostMessageService} name.
      * After this, this {@link PostMessageServiceConnection} can be used for sending postMessage
      * related communication back to the client.
@@ -50,7 +73,27 @@
     public boolean bindSessionToPostMessageService(Context context, String packageName) {
         Intent intent = new Intent();
         intent.setClassName(packageName, PostMessageService.class.getName());
-        return context.bindService(intent, this, Context.BIND_AUTO_CREATE);
+        boolean success = context.bindService(intent, this, Context.BIND_AUTO_CREATE);
+        if (!success) {
+            Log.w(TAG, "Could not bind to PostMessageService in client.");
+        }
+        return success;
+    }
+
+    /**
+     * See
+     * {@link PostMessageServiceConnection#bindSessionToPostMessageService(Context, String)}.
+     * Attempts to bind with the package name set during initialization.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public boolean bindSessionToPostMessageService(Context appContext) {
+        return bindSessionToPostMessageService(appContext, mPackageName);
+    }
+
+    private boolean isBoundToService() {
+        return mService != null;
     }
 
     /**
@@ -58,7 +101,10 @@
      * @param context The context to be unbound from.
      */
     public void unbindFromContext(Context context) {
-        context.unbindService(this);
+        if (isBoundToService()) {
+            context.unbindService(this);
+            mService = null;
+        }
     }
 
     @Override
@@ -74,6 +120,27 @@
     }
 
     /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Override
+    public final boolean onNotifyMessageChannelReady(Bundle extras) {
+        return notifyMessageChannelReady(extras);
+    }
+
+    /**
+     * Records that the message channel has been created and notifies the client. This method
+     * should be called when the browser binds to the client side {@link PostMessageService} and
+     * also readies a connection to the web frame.
+     * @param extras Unused.
+     * @return Whether the notification was sent successfully.
+     */
+    public final boolean notifyMessageChannelReady(Bundle extras) {
+        mMessageChannelCreated = true;
+        return notifyMessageChannelReadyInternal(extras);
+    }
+
+    /**
      * Notifies the client that the postMessage channel requested with
      * {@link CustomTabsService#requestPostMessageChannel(
      * CustomTabsSessionToken, android.net.Uri)} is ready. This method should be
@@ -83,8 +150,8 @@
      * @param extras Reserved for future use.
      * @return Whether the notification was sent to the remote successfully.
      */
-    public final boolean notifyMessageChannelReady(Bundle extras) {
-        if (mService == null) return false;
+    private boolean notifyMessageChannelReadyInternal(Bundle extras) {
+        if (!isBoundToService()) return false;
         synchronized (mLock) {
             try {
                 mService.onMessageChannelReady(mSessionBinder, extras);
@@ -96,6 +163,15 @@
     }
 
     /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Override
+    public final boolean onPostMessage(String message, Bundle extras) {
+        return postMessage(message, extras);
+    }
+
+    /**
      * Posts a message to the client. This should be called when a tab controlled by related
      * {@link CustomTabsSession} has sent a postMessage. If postMessage() is called from a single
      * thread, then the messages will be posted in the same order.
@@ -105,7 +181,7 @@
      * @return Whether the postMessage was sent to the remote successfully.
      */
     public final boolean postMessage(String message, Bundle extras) {
-        if (mService == null) return false;
+        if (!isBoundToService()) return false;
         synchronized (mLock) {
             try {
                 mService.onPostMessage(mSessionBinder, message, extras);
@@ -117,12 +193,34 @@
     }
 
     /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Override
+    public void onDisconnectChannel(Context appContext) {
+        unbindFromContext(appContext);
+    }
+
+    /**
      * Called when the {@link PostMessageService} connection is established.
      */
-    public void onPostMessageServiceConnected() {}
+    public void onPostMessageServiceConnected() {
+        if (mMessageChannelCreated) notifyMessageChannelReadyInternal(null);
+    }
 
     /**
      * Called when the connection is lost with the {@link PostMessageService}.
      */
     public void onPostMessageServiceDisconnected() {}
+
+    /**
+     * Cleans up any dependencies that this handler might have.
+     * @param context Context to use for unbinding if necessary.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public void cleanup(Context context) {
+        if (isBoundToService()) unbindFromContext(context);
+    }
 }
diff --git a/browser/src/main/java/androidx/browser/customtabs/TrustedWebUtils.java b/browser/src/main/java/androidx/browser/customtabs/TrustedWebUtils.java
index 5b5b2ab..d3690e43 100644
--- a/browser/src/main/java/androidx/browser/customtabs/TrustedWebUtils.java
+++ b/browser/src/main/java/androidx/browser/customtabs/TrustedWebUtils.java
@@ -16,12 +16,26 @@
 
 package androidx.browser.customtabs;
 
+import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
+
+import android.app.PendingIntent;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.Matrix;
 import android.net.Uri;
 import android.os.Bundle;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.StringDef;
 import androidx.core.app.BundleCompat;
+import androidx.core.content.FileProvider;
+
+import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /**
  * Class for utilities and convenience calls for opening a qualifying web page as a
@@ -45,19 +59,202 @@
  *  for sending details of the verification results.
  */
 public class TrustedWebUtils {
-
     /**
      * Boolean extra that triggers a {@link CustomTabsIntent} launch to be in a fullscreen UI with
      * no browser controls.
-     *
-     * @see TrustedWebUtils#launchAsTrustedWebActivity(Context, CustomTabsIntent, Uri).
      */
     public static final String EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY =
             "android.support.customtabs.extra.LAUNCH_AS_TRUSTED_WEB_ACTIVITY";
 
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static final String EXTRA_ADDITIONAL_TRUSTED_ORIGINS =
+            "android.support.customtabs.extra.ADDITIONAL_TRUSTED_ORIGINS";
+
+    /**
+     * @see #launchBrowserSiteSettings
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static final String ACTION_MANAGE_TRUSTED_WEB_ACTIVITY_DATA =
+            "android.support.customtabs.action.ACTION_MANAGE_TRUSTED_WEB_ACTIVITY_DATA";
+
+    /**
+     * Extra that stores the {@link Bundle} of splash screen parameters, see
+     * {@link SplashScreenParamKey}.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static final String EXTRA_SPLASH_SCREEN_PARAMS =
+            "androidx.browser.trusted.EXTRA_SPLASH_SCREEN_PARAMS";
+
+
+    /**
+     * The keys of the entries in the {@link Bundle} passed in {@link #EXTRA_SPLASH_SCREEN_PARAMS}.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public interface SplashScreenParamKey {
+        /**
+         * The version of splash screens to use.
+         * The value must be one of {@link SplashScreenVersion}.
+         */
+        String VERSION = "androidx.browser.trusted.KEY_SPLASH_SCREEN_VERSION";
+
+        /**
+         * The background color of the splash screen.
+         * The value must be an integer representing the color in RGB (alpha channel is ignored if
+         * provided). The default is white.
+         */
+        String BACKGROUND_COLOR =
+                "androidx.browser.trusted.trusted.KEY_SPLASH_SCREEN_BACKGROUND_COLOR";
+
+        /**
+         * The {@link android.widget.ImageView.ScaleType} to apply to the image on the splash
+         * screen.
+         * The value must be an integer - the ordinal of the ScaleType.
+         * The default is {@link android.widget.ImageView.ScaleType#CENTER}.
+         */
+        String SCALE_TYPE = "androidx.browser.trusted.KEY_SPLASH_SCREEN_SCALE_TYPE";
+
+        /**
+         * The transformation matrix to apply to the image on the splash screen. See
+         * {@link android.widget.ImageView#setImageMatrix}. Only needs to be provided if the scale
+         * type is {@link android.widget.ImageView.ScaleType#MATRIX}.
+         * The value must be an array of 9 floats or null. This array can be retrieved from
+         * {@link Matrix#getValues)}. The default is null.
+         */
+        String IMAGE_TRANSFORMATION_MATRIX =
+                "androidx.browser.trusted.KEY_SPLASH_SCREEN_TRANSFORMATION_MATRIX";
+
+        /**
+         * The duration of fade out animation in milliseconds to be played when removing splash
+         * screen.
+         * The value must be provided as an int. The default is 0 (no animation).
+         */
+        String FADE_OUT_DURATION_MS =
+                "androidx.browser.trusted.KEY_SPLASH_SCREEN_FADE_OUT_DURATION";
+    }
+
+
+
+
+    /**
+     * These constants are the categories the providers add to the intent filter of
+     * CustomTabService implementation to declare the support of a particular version of splash
+     * screens. The are also passed by the client as the value for the key
+     * {@link SplashScreenParamKey#VERSION} when launching a Trusted Web Activity.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @StringDef({SplashScreenVersion.V1})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SplashScreenVersion {
+        /**
+         * The splash screen is transferred via {@link CustomTabsSession#receiveFile},
+         * and then used by Trusted Web Activity when it is launched.
+         *
+         * The passed image is shown in a full-screen ImageView.
+         * The following parameters are supported:
+         * - {@link SplashScreenParamKey#BACKGROUND_COLOR},
+         * - {@link SplashScreenParamKey#SCALE_TYPE},
+         * - {@link SplashScreenParamKey#IMAGE_TRANSFORMATION_MATRIX}
+         * - {@link SplashScreenParamKey#FADE_OUT_DURATION_MS}.
+         */
+        String V1 = "androidx.browser.trusted.category.TrustedWebActivitySplashScreensV1";
+    }
+
     private TrustedWebUtils() {}
 
     /**
+     * Open the site settings for given url in the web browser. The url must belong to the origin
+     * associated with the calling application via the Digital Asset Links. Prior to calling, one
+     * must establish a connection to {@link CustomTabsService} and create a
+     * {@link CustomTabsSession}.
+     *
+     * It is also required to do {@link CustomTabsClient#warmup} and
+     * {@link CustomTabsSession#validateRelationship} before calling this method.
+     *
+     * @param context {@link Context} to use while launching site-settings activity.
+     * @param session The {@link CustomTabsSession} used to verify the origin.
+     * @param uri The {@link Uri} for which site-settings are to be shown.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static void launchBrowserSiteSettings(Context context, CustomTabsSession session,
+            Uri uri) {
+        Intent intent = new Intent(TrustedWebUtils.ACTION_MANAGE_TRUSTED_WEB_ACTIVITY_DATA);
+        intent.setPackage(session.getComponentName().getPackageName());
+        intent.setData(uri);
+
+        Bundle bundle = new Bundle();
+        BundleCompat.putBinder(bundle, CustomTabsIntent.EXTRA_SESSION, session.getBinder());
+        intent.putExtras(bundle);
+        PendingIntent id = session.getId();
+        if (id != null) {
+            intent.putExtra(CustomTabsIntent.EXTRA_SESSION_ID, id);
+        }
+        context.startActivity(intent);
+    }
+
+    /**
+     * Returns whether the splash screens feature is supported by the given package.
+     * Note: you can call this method prior to connecting to a {@link CustomTabsService}. This way,
+     * if true is returned, the splash screen can be shown as soon as possible.
+     *
+     * TODO(pshmakov): make TwaProviderPicker gather supported features, including splash screens,
+     * to avoid extra PackageManager queries.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static boolean splashScreensAreSupported(Context context, String packageName,
+            @SplashScreenVersion String version) {
+        Intent serviceIntent = new Intent()
+                .setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION)
+                .setPackage(packageName);
+        ResolveInfo resolveInfo = context.getPackageManager()
+                .resolveService(serviceIntent, PackageManager.GET_RESOLVED_FILTER);
+        if (resolveInfo == null || resolveInfo.filter == null) return false;
+        return resolveInfo.filter.hasCategory(version);
+    }
+
+    /**
+     * Transfers the splash image to a Custom Tabs provider. The reading and decoding of the image
+     * happens synchronously, so it's recommended to call this method on a worker thread.
+     *
+     *
+     * @param context {@link Context} to use.
+     * @param file {@link File} with the image.
+     * @param fileProviderAuthority authority of {@link FileProvider} used to generate an URI for
+     *                              the file.
+     * @param packageName Package name of Custom Tabs provider.
+     * @param session {@link CustomTabsSession} established with the Custom Tabs provider.
+     * @return True if the image was received and processed successfully.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static boolean transferSplashImage(Context context, File file,
+            String fileProviderAuthority, String packageName, CustomTabsSession session) {
+        // TODO(peconn): Return this comment to the javadoc once TWABuilder is not hidden.
+        // This method should be called prior to {@link TrustedWebActivityBuilder#launchActivity}.
+        // Pass additional parameters, such as background color, using
+        // {@link TrustedWebActivityBuilder#setSplashScreenParams(Bundle)}.
+
+        Uri uri = FileProvider.getUriForFile(context, fileProviderAuthority, file);
+        context.grantUriPermission(packageName, uri, FLAG_GRANT_READ_URI_PERMISSION);
+        return session.receiveFile(uri, CustomTabsService.FILE_PURPOSE_TWA_SPLASH_IMAGE, null);
+    }
+
+    /**
      * Launch the given {@link CustomTabsIntent} as a Trusted Web Activity. The given
      * {@link CustomTabsIntent} should have a valid {@link CustomTabsSession} associated with it
      * during construction. Once the Trusted Web Activity is launched, browser side implementations
@@ -69,6 +266,8 @@
      *                         Trusted Web Activity. Note that all customizations in the given
      *                         associated with browser toolbar controls will be ignored.
      * @param uri The web page to launch as Trusted Web Activity.
+     *
+     * TODO(peconn): Deprecate with API change.
      */
     public static void launchAsTrustedWebActivity(@NonNull Context context,
             @NonNull CustomTabsIntent customTabsIntent, @NonNull Uri uri) {
diff --git a/browser/src/main/java/androidx/browser/trusted/NotificationApiHelperForM.java b/browser/src/main/java/androidx/browser/trusted/NotificationApiHelperForM.java
new file mode 100644
index 0000000..d7762b5
--- /dev/null
+++ b/browser/src/main/java/androidx/browser/trusted/NotificationApiHelperForM.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+package androidx.browser.trusted;
+
+import android.app.NotificationManager;
+import android.os.Build;
+import android.os.Parcelable;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ *
+ * Utility class to use new APIs that were added in M (API level 23). These need to exist in a
+ * separate class so that Android framework can successfully verify classes without
+ * encountering the new APIs.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class NotificationApiHelperForM {
+
+    /**
+     * Returns the active notifications as an array of Parcelables. Since StatusBarNotification was
+     * added in API 18, returning the result as StatusBarNotification[] would prevent classes from
+     * being verified on earlier Jellybean builds.
+     */
+    @RequiresApi(Build.VERSION_CODES.M)
+    static Parcelable[] getActiveNotifications(NotificationManager manager) {
+        return manager.getActiveNotifications();
+    }
+
+    private NotificationApiHelperForM() {}
+}
diff --git a/browser/src/main/java/androidx/browser/trusted/NotificationApiHelperForO.java b/browser/src/main/java/androidx/browser/trusted/NotificationApiHelperForO.java
new file mode 100644
index 0000000..760d69c
--- /dev/null
+++ b/browser/src/main/java/androidx/browser/trusted/NotificationApiHelperForO.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+package androidx.browser.trusted;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ *
+ * Utility class to use new APIs that were added in O (API level 25). These need to exist in a
+ * separate class so that Android framework can successfully verify classes without
+ * encountering the new APIs.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class NotificationApiHelperForO {
+    @RequiresApi(Build.VERSION_CODES.O)
+    static boolean isChannelEnabled(NotificationManager manager, String channelId) {
+        NotificationChannel channel = manager.getNotificationChannel(channelId);
+
+        return channel == null || channel.getImportance() != NotificationManager.IMPORTANCE_NONE;
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    static Notification copyNotificationOntoChannel(Context context, NotificationManager manager,
+            Notification notification, String channelId, String channelName) {
+        // Create the notification channel, (no-op if already created).
+        manager.createNotificationChannel(new NotificationChannel(channelId,
+                channelName, NotificationManager.IMPORTANCE_DEFAULT));
+
+        // Check that the channel is enabled.
+        if (manager.getNotificationChannel(channelId).getImportance()
+                == NotificationManager.IMPORTANCE_NONE) {
+            return null;
+        }
+
+        // Set our notification to have that channel.
+        Notification.Builder builder = Notification.Builder.recoverBuilder(context, notification);
+        builder.setChannelId(channelId);
+        return builder.build();
+    }
+
+    private NotificationApiHelperForO() {}
+}
diff --git a/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityBuilder.java b/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityBuilder.java
new file mode 100644
index 0000000..1b42e41
--- /dev/null
+++ b/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityBuilder.java
@@ -0,0 +1,155 @@
+/*
+ * 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.
+ */
+
+package androidx.browser.trusted;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.browser.customtabs.CustomTabsIntent;
+import androidx.browser.customtabs.CustomTabsSession;
+import androidx.browser.customtabs.TrustedWebUtils;
+import androidx.core.content.ContextCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Constructs and launches an intent to start a Trusted Web Activity.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class TrustedWebActivityBuilder {
+    private final Context mContext;
+    private final Uri mUri;
+
+    @Nullable
+    private Integer mStatusBarColor;
+
+    @Nullable
+    private List<String> mAdditionalTrustedOrigins;
+
+    @Nullable
+    private Bundle mSplashScreenParams;
+
+    /**
+     * Creates a Builder given the required parameters.
+     * @param context {@link Context} to use.
+     * @param uri The web page to launch as Trusted Web Activity.
+     */
+    public TrustedWebActivityBuilder(Context context, Uri uri) {
+        mContext = context;
+        mUri = uri;
+    }
+
+    /**
+     * Sets the status bar color to be seen while the Trusted Web Activity is running.
+     */
+    public TrustedWebActivityBuilder setStatusBarColor(int color) {
+        mStatusBarColor = color;
+        return this;
+    }
+
+    /**
+     * Sets a list of additional trusted origins that the user may navigate or be redirected to
+     * from the starting uri.
+     *
+     * For example, if the user starts at https://www.example.com/page1 and is redirected to
+     * https://m.example.com/page2, and both origins are associated with the calling application
+     * via the Digital Asset Links, then pass "https://www.example.com/page1" as uri and
+     * Arrays.asList("https://m.example.com") as additionalTrustedOrigins.
+     *
+     * Alternatively, use {@link CustomTabsSession#validateRelationship} to validate additional
+     * origins asynchronously, but that would delay launching the Trusted Web Activity.
+     */
+    public TrustedWebActivityBuilder setAdditionalTrustedOrigins(List<String> origins) {
+        mAdditionalTrustedOrigins = origins;
+        return this;
+    }
+
+    /**
+     * Sets the parameters of a splash screen shown while the web page is loading, such as
+     * background color. See {@link TrustedWebUtils.SplashScreenParamKey} for a list of supported
+     * parameters.
+     *
+     * To provide the image for the splash screen, use {@link TrustedWebUtils#transferSplashImage},
+     * prior to calling {@link #launchActivity} on the builder.
+     *
+     * It is recommended to also show the same splash screen in the app as soon as possible,
+     * prior to establishing a CustomTabConnection. The Trusted Web Activity provider should
+     * ensure seamless transition of the splash screen from the app onto the top of webpage
+     * being loaded.
+     *
+     * The splash screen will be removed on the first paint of the page, or when the page load
+     * fails.
+     */
+    public TrustedWebActivityBuilder setSplashScreenParams(Bundle splashScreenParams) {
+        mSplashScreenParams = splashScreenParams;
+        return this;
+    }
+
+    /**
+     * Launches a Trusted Web Activity. Once it is launched, browser side implementations may
+     * have their own fallback behavior (e.g. showing the page in a custom tab UI with toolbar).
+     *
+     * @param session The {@link CustomTabsSession} to use for launching a Trusted Web Activity.
+     */
+    public void launchActivity(CustomTabsSession session) {
+        if (session == null) {
+            throw new NullPointerException("CustomTabsSession is required for launching a TWA");
+        }
+
+        CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(session);
+        if (mStatusBarColor != null) {
+            // Toolbar color applies also to the status bar.
+            intentBuilder.setToolbarColor(mStatusBarColor);
+        }
+
+        Intent intent = intentBuilder.build().intent;
+        intent.setData(mUri);
+        intent.putExtra(TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true);
+        if (mAdditionalTrustedOrigins != null) {
+            intent.putExtra(TrustedWebUtils.EXTRA_ADDITIONAL_TRUSTED_ORIGINS,
+                    new ArrayList<>(mAdditionalTrustedOrigins));
+        }
+
+        if (mSplashScreenParams != null) {
+            intent.putExtra(TrustedWebUtils.EXTRA_SPLASH_SCREEN_PARAMS, mSplashScreenParams);
+        }
+        ContextCompat.startActivity(mContext, intent, null);
+    }
+
+    /**
+     * Returns the {@link Uri} to be launched with this Builder.
+     */
+    public Uri getUrl() {
+        return mUri;
+    }
+
+    /**
+     * Returns the color set via {@link #setStatusBarColor(int)} or null if not set.
+     */
+    @Nullable
+    public Integer getStatusBarColor() {
+        return mStatusBarColor;
+    }
+
+}
diff --git a/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityService.java b/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityService.java
new file mode 100644
index 0000000..efa1e34
--- /dev/null
+++ b/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityService.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright 2018 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.browser.trusted;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.graphics.BitmapFactory;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcelable;
+import android.os.StrictMode;
+import android.support.customtabs.trusted.ITrustedWebActivityService;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.browser.trusted.TrustedWebActivityServiceWrapper.ActiveNotificationsArgs;
+import androidx.browser.trusted.TrustedWebActivityServiceWrapper.CancelNotificationArgs;
+import androidx.browser.trusted.TrustedWebActivityServiceWrapper.NotificationsEnabledArgs;
+import androidx.browser.trusted.TrustedWebActivityServiceWrapper.NotifyNotificationArgs;
+import androidx.browser.trusted.TrustedWebActivityServiceWrapper.ResultArgs;
+import androidx.core.app.NotificationManagerCompat;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * The TrustedWebActivityService lives in a client app and serves requests from a Trusted Web
+ * Activity provider. At present it only serves requests to display notifications.
+ * <p>
+ * When the provider receives a notification from a scope that is associated with a Trusted Web
+ * Activity client app, it will attempt to connect to a TrustedWebActivityService and forward calls.
+ * This allows the client app to display the notifications itself, meaning it is attributable to the
+ * client app and is managed by notification permissions of the client app, not the provider.
+ * <p>
+ * TrustedWebActivityService is usable as it is, by adding the following to your AndroidManifest:
+ *
+ * <pre>
+ * <service
+ *     android:name="android.support.customtabs.trusted.TrustedWebActivityService"
+ *     android:enabled="true"
+ *     android:exported="true">
+ *
+ *     <meta-data android:name="android.support.customtabs.trusted.SMALL_ICON"
+ *         android:resource="@drawable/ic_notification_icon" />
+ *
+ *     <intent-filter>
+ *         <action android:name="android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE"/>
+ *         <category android:name="android.intent.category.DEFAULT"/>
+ *     </intent-filter>
+ * </service>
+ * </pre>
+ *
+ * The SMALL_ICON resource should point to a drawable to be used for the notification's small icon.
+ * <p>
+ * Alternatively for greater customization, TrustedWebActivityService can be extended and
+ * {@link #onCreate}, {@link #getSmallIconId}, {@link #notifyNotificationWithChannel} and
+ * {@link #cancelNotification} can be overridden. In this case the manifest entry should be updated
+ * to point to the extending class.
+ * <p>
+ * As this is an AIDL Service, calls to {@link #getSmallIconId},
+ * {@link #notifyNotificationWithChannel} and {@link #cancelNotification} can occur on different
+ * Binder threads, so overriding implementations need to be thread-safe.
+ * <p>
+ * For security, the TrustedWebActivityService will check that whatever connects to it is the
+ * Trusted Web Activity provider that it was previously verified with. For testing,
+ * {@link #setVerifiedProviderSynchronouslyForTesting} can be used to to allow connections from the
+ * given package.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class TrustedWebActivityService extends Service {
+    /** An Intent Action used by the provider to find the TrustedWebActivityService or subclass. */
+    public static final String INTENT_ACTION =
+            "android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE";
+    /** The Android Manifest meta-data name to specify a small icon id to use. */
+    public static final String SMALL_ICON_META_DATA_NAME =
+            "android.support.customtabs.trusted.SMALL_ICON";
+
+    /** Used as a return value of {@link #getSmallIconId} when the icon is not provided. */
+    public static final int NO_ID = -1;
+
+    private static final String PREFS_FILE = "TrustedWebActivityVerifiedProvider";
+    private static final String PREFS_VERIFIED_PROVIDER = "Provider";
+
+    static final String KEY_SMALL_ICON_BITMAP =
+            "android.support.customtabs.trusted.SMALL_ICON_BITMAP";
+
+    private NotificationManager mNotificationManager;
+
+    public int mVerifiedUid = -1;
+
+    private final ITrustedWebActivityService.Stub mBinder =
+            new ITrustedWebActivityService.Stub() {
+        @Override
+        public Bundle areNotificationsEnabled(Bundle bundle) {
+            checkCaller();
+
+            NotificationsEnabledArgs args = NotificationsEnabledArgs.fromBundle(bundle);
+            boolean result =
+                    TrustedWebActivityService.this.areNotificationsEnabled(args.channelName);
+
+            return new ResultArgs(result).toBundle();
+        }
+
+        @Override
+        public Bundle notifyNotificationWithChannel(Bundle bundle) {
+            checkCaller();
+
+            NotifyNotificationArgs args = NotifyNotificationArgs.fromBundle(bundle);
+
+            boolean success = TrustedWebActivityService.this.notifyNotificationWithChannel(
+                    args.platformTag, args.platformId, args.notification, args.channelName);
+
+            return new ResultArgs(success).toBundle();
+        }
+
+        @Override
+        public void cancelNotification(Bundle bundle) {
+            checkCaller();
+
+            CancelNotificationArgs args = CancelNotificationArgs.fromBundle(bundle);
+
+            TrustedWebActivityService.this.cancelNotification(args.platformTag, args.platformId);
+        }
+
+        @Override
+        public Bundle getActiveNotifications() {
+            checkCaller();
+
+            return new ActiveNotificationsArgs(
+                    TrustedWebActivityService.this.getActiveNotifications()).toBundle();
+        }
+
+        @Override
+        public int getSmallIconId() {
+            checkCaller();
+
+            return TrustedWebActivityService.this.getSmallIconId();
+        }
+
+        @Override
+        public Bundle getSmallIconBitmap() {
+            checkCaller();
+
+            return TrustedWebActivityService.this.getSmallIconBitmap();
+        }
+
+        private void checkCaller() {
+            if (mVerifiedUid == -1) {
+                String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
+                // We need to read Preferences. This should only be called on the Binder thread
+                // which is designed to handle long running, blocking tasks, so disk I/O should be
+                // OK.
+                StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads();
+                try {
+                    String verifiedPackage = getPreferences(TrustedWebActivityService.this)
+                            .getString(PREFS_VERIFIED_PROVIDER, null);
+
+                    if (Arrays.asList(packages).contains(verifiedPackage)) {
+                        mVerifiedUid = getCallingUid();
+
+                        return;
+                    }
+                } finally {
+                    StrictMode.setThreadPolicy(policy);
+                }
+            }
+
+            if (mVerifiedUid == getCallingUid()) return;
+
+            throw new SecurityException("Caller is not verified as Trusted Web Activity provider.");
+        }
+    };
+
+    /**
+     * Called by the system when the service is first created. Do not call this method directly.
+     * Overrides must call {@code super.onCreate()}.
+     */
+    @Override
+    @CallSuper
+    public void onCreate() {
+        super.onCreate();
+        mNotificationManager =
+                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+    }
+
+    /**
+     * Checks whether notifications are enabled.
+     * @param channelName The name of the notification channel to be used on Android O+.
+     * @return Whether notifications are enabled.
+     */
+    protected boolean areNotificationsEnabled(String channelName) {
+        ensureOnCreateCalled();
+
+        if (!NotificationManagerCompat.from(this).areNotificationsEnabled()) return false;
+
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return true;
+
+        return NotificationApiHelperForO.isChannelEnabled(mNotificationManager,
+                channelNameToId(channelName));
+    }
+
+    /**
+     * Displays a notification.
+     * @param platformTag The notification tag, see
+     *                    {@link NotificationManager#notify(String, int, Notification)}.
+     * @param platformId The notification id, see
+     *                   {@link NotificationManager#notify(String, int, Notification)}.
+     * @param notification The notification to be displayed, constructed by the provider.
+     * @param channelName The name of the notification channel that the notification should be
+     *                    displayed on. This method gets or creates a channel from the name and
+     *                    modifies the notification to use that channel.
+     * @return Whether the notification was successfully displayed (the channel/app may be blocked
+     *         by the user).
+     */
+    protected boolean notifyNotificationWithChannel(String platformTag, int platformId,
+            Notification notification, String channelName) {
+        ensureOnCreateCalled();
+
+        if (!NotificationManagerCompat.from(this).areNotificationsEnabled()) return false;
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            String channelId = channelNameToId(channelName);
+            notification = NotificationApiHelperForO.copyNotificationOntoChannel(this,
+                    mNotificationManager, notification, channelId, channelName);
+
+            if (!NotificationApiHelperForO.isChannelEnabled(mNotificationManager, channelId)) {
+                return false;
+            }
+        }
+
+        mNotificationManager.notify(platformTag, platformId, notification);
+        return true;
+    }
+
+    /**
+     * Cancels a notification.
+     * @param platformTag The notification tag, see
+     *                    {@link NotificationManager#cancel(String, int)}.
+     * @param platformId The notification id, see
+     *                   {@link NotificationManager#cancel(String, int)}.
+     */
+    protected void cancelNotification(String platformTag, int platformId) {
+        ensureOnCreateCalled();
+        mNotificationManager.cancel(platformTag, platformId);
+    }
+
+    /**
+     * Returns a list of active notifications, essentially calling
+     * NotificationManager#getActiveNotifications. The default implementation does not work on
+     * pre-Android M.
+     * @return An array of StatusBarNotifications as Parcelables.
+     */
+
+    protected Parcelable[] getActiveNotifications() {
+        ensureOnCreateCalled();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return NotificationApiHelperForM.getActiveNotifications(mNotificationManager);
+        }
+        throw new IllegalStateException("getActiveNotifications cannot be called pre-M.");
+    }
+
+    Bundle getSmallIconBitmap() {
+        int id = getSmallIconId();
+        Bundle bundle = new Bundle();
+        if (id == NO_ID) {
+            return bundle;
+        }
+        bundle.putParcelable(KEY_SMALL_ICON_BITMAP,
+                BitmapFactory.decodeResource(getResources(), id));
+        return bundle;
+    }
+
+    /**
+     * Returns the Android resource id of a drawable to be used for the small icon of the
+     * notification. This is called by the provider as it is constructing the notification, so a
+     * complete notification can be passed to the client.
+     *
+     * Default behaviour looks for meta-data with the name {@link #SMALL_ICON_META_DATA_NAME} in
+     * service section of the manifest.
+     * @return A resource id for the small icon, or {@link #NO_ID} if not found.
+     */
+    protected int getSmallIconId() {
+        try {
+            ServiceInfo info = getPackageManager().getServiceInfo(
+                    new ComponentName(this, getClass()), PackageManager.GET_META_DATA);
+
+            if (info.metaData == null) return NO_ID;
+
+            return info.metaData.getInt(SMALL_ICON_META_DATA_NAME, NO_ID);
+        } catch (PackageManager.NameNotFoundException e) {
+            // Will only happen if the package provided (the one we are running in) is not
+            // installed - so should never happen.
+            return NO_ID;
+        }
+    }
+
+    @Override
+    public final IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    @Override
+    public final boolean onUnbind(Intent intent) {
+        mVerifiedUid = -1;
+
+        return super.onUnbind(intent);
+    }
+
+    /**
+     * Should *not* be called on UI Thread, as accessing Preferences may hit disk.
+     */
+    static SharedPreferences getPreferences(Context context) {
+        return context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
+    }
+
+    /**
+     * Sets (asynchronously) the package that this service will accept connections from.
+     * @param context A context to be used to access SharedPreferences.
+     * @param provider The package of the provider to accept connections from or null to clear.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static final void setVerifiedProvider(final Context context, @Nullable String provider) {
+        final String providerEmptyChecked =
+                (provider == null || provider.isEmpty()) ? null : provider;
+
+        // Perform on a background thread as accessing Preferences may cause disk access.
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... voids) {
+                SharedPreferences.Editor editor = getPreferences(context).edit();
+                editor.putString(PREFS_VERIFIED_PROVIDER, providerEmptyChecked);
+                editor.apply();
+                return null;
+            }
+        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+
+    /**
+     * See {@link #setVerifiedProvider}, the main difference being that this approach sets the
+     * provider synchronously, so may trigger a disk read.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static final void setVerifiedProviderSynchronouslyForTesting(Context context,
+            @Nullable String provider) {
+        String providerEmptyChecked = (provider == null || provider.isEmpty()) ? null : provider;
+
+        StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads();
+        try {
+            SharedPreferences.Editor editor = getPreferences(context).edit();
+            editor.putString(PREFS_VERIFIED_PROVIDER, providerEmptyChecked);
+            editor.apply();
+        } finally {
+            StrictMode.setThreadPolicy(policy);
+        }
+    }
+
+    private static String channelNameToId(String name) {
+        return name.toLowerCase(Locale.ROOT).replace(' ', '_') + "_channel_id";
+    }
+
+    private void ensureOnCreateCalled() {
+        if (mNotificationManager != null) return;
+        throw new IllegalStateException("TrustedWebActivityService has not been properly "
+                + "initialized. Did onCreate() call super.onCreate()?");
+    }
+}
diff --git a/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionManager.java b/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionManager.java
new file mode 100644
index 0000000..d13c9dc
--- /dev/null
+++ b/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionManager.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright 2018 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.browser.trusted;
+
+import android.annotation.SuppressLint;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.StrictMode;
+import android.os.TransactionTooLargeException;
+import android.support.customtabs.trusted.ITrustedWebActivityService;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A TrustedWebActivityServiceConnectionManager will be used by a Trusted Web Activity provider and
+ * takes care of connecting to and communicating with {@link TrustedWebActivityService}s.
+ * <p>
+ * Trusted Web Activity client apps are registered with the {@link #registerClient}, associating a
+ * package with an origin. There may be multiple packages associated with a single origin.
+ * Note, the origins are essentially keys to a map of origin to package name - while they
+ * semantically are web origins, they aren't used that way.
+ * <p>
+ * To interact with a {@link TrustedWebActivityService}, call {@link #execute}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class TrustedWebActivityServiceConnectionManager {
+    private static final String TAG = "TWAConnectionManager";
+    private static final String PREFS_FILE = "TrustedWebActivityVerifiedPackages";
+
+    /**
+     * A callback to be executed once a connection to a {@link TrustedWebActivityService} is open.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public interface ExecutionCallback {
+        /**
+         * Is run when a connection is open.
+         * See {@link #execute} for more information.
+         * @param service A {@link TrustedWebActivityServiceWrapper} wrapping the connected
+         *                {@link TrustedWebActivityService}.
+         *                It may be null if the connection failed.
+         * @throws RemoteException May be thrown by {@link TrustedWebActivityServiceWrapper}'s
+         *                         methods.
+         *                         If the user does not want to catch them, they will be caught
+         *                         gracefully by {@link #execute}.
+         */
+        void onConnected(@Nullable TrustedWebActivityServiceWrapper service) throws RemoteException;
+    }
+
+    /** The callback used internally that will wrap an ExecutionCallback. */
+    private interface WrappedCallback {
+        void onConnected(@Nullable TrustedWebActivityServiceWrapper service);
+    }
+
+    /**
+     * Holds a connection to a TrustedWebActivityService.
+     * It should only be used on the UI Thread.
+     */
+    private class Connection implements ServiceConnection {
+        private TrustedWebActivityServiceWrapper mService;
+        private List<WrappedCallback> mCallbacks = new LinkedList<>();
+        private final Uri mScope;
+
+        Connection(Uri scope) {
+            mScope = scope;
+        }
+
+        public TrustedWebActivityServiceWrapper getService() {
+            return mService;
+        }
+
+        /** This method will be called on the UI Thread by the Android Framework. */
+        @Override
+        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+            mService = new TrustedWebActivityServiceWrapper(
+                    ITrustedWebActivityService.Stub.asInterface(iBinder), componentName);
+            for (WrappedCallback callback : mCallbacks) {
+                callback.onConnected(mService);
+            }
+            mCallbacks.clear();
+        }
+
+        /** This method will be called on the UI Thread by the Android Framework. */
+        @Override
+        public void onServiceDisconnected(ComponentName componentName) {
+            mService = null;
+            mConnections.remove(mScope);
+        }
+
+        public void addCallback(WrappedCallback callback) {
+            if (mService == null) {
+                mCallbacks.add(callback);
+            } else {
+                callback.onConnected(mService);
+            }
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    final Context mContext;
+
+    /** Map from ServiceWorker scope to Connection. */
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    Map<Uri, Connection> mConnections = new HashMap<>();
+
+    private static AtomicReference<SharedPreferences> sSharedPreferences = new AtomicReference<>();
+
+    /**
+     * Gets the verified packages for the given origin. |origin| may be null, in which case this
+     * method call will just trigger caching the Preferences.
+     *
+     * This is safe to be called on any thread, however it may hit disk.
+     *
+     * @param context A Context to be used for accessing SharedPreferences.
+     * @param origin The origin that was previously used with {@link #registerClient}.
+     * @return A set of package names. This set is safe to be modified.
+     */
+    public static Set<String> getVerifiedPackages(Context context, String origin) {
+        // Loading preferences is on the critical path for this class - we need to synchronously
+        // inform the client whether or not an notification can be handled by a TWA.
+        // I considered loading the preferences into a cache on a background thread when this class
+        // was created, but ultimately if that load hadn't completed by the time {@link #execute} or
+        // {@link #registerClient} were called, we'd still need to block for it to complete.
+        // Therefore we attempt to asynchronously load the preferences in the constructor, but if
+        // they aren't loaded by the time they are needed, we disable StrictMode and read them on
+        // the main thread.
+        StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads();
+
+        try {
+            if (sSharedPreferences.get() == null) {
+                sSharedPreferences.compareAndSet(null,
+                        context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE));
+            }
+
+            return origin == null ? null :
+                    new HashSet<>(sSharedPreferences.get().getStringSet(origin,
+                            Collections.<String>emptySet()));
+        } finally {
+            StrictMode.setThreadPolicy(policy);
+        }
+    }
+
+    /**
+     * Creates a TrustedWebActivityServiceConnectionManager.
+     * @param context A Context used for accessing SharedPreferences.
+     */
+    public TrustedWebActivityServiceConnectionManager(Context context) {
+        mContext = context.getApplicationContext();
+
+        // Asynchronously try to load (and therefore cache) the preferences.
+        AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
+            @Override
+            public void run() {
+                getVerifiedPackages(mContext, null);
+            }
+        });
+    }
+
+    private static WrappedCallback wrapCallback(final ExecutionCallback callback) {
+        return new WrappedCallback() {
+            @Override
+            public void onConnected(@Nullable final TrustedWebActivityServiceWrapper service) {
+                AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            callback.onConnected(service);
+                        } catch (TransactionTooLargeException e) {
+                            Log.w(TAG,
+                                    "TransactionTooLargeException from TrustedWebActivityService, "
+                                            + "possibly due to large size of small icon.", e);
+                        } catch (RemoteException | RuntimeException e) {
+                            Log.w(TAG,
+                                    "Exception while trying to use TrustedWebActivityService.", e);
+                        }
+                    }
+                });
+            }
+        };
+    }
+
+    /**
+     * Connects to the appropriate {@link TrustedWebActivityService} or uses an existing connection
+     * if available and runs code once connected.
+     * <p>
+     * To find a Service to connect to, this method attempts to resolve an
+     * {@link Intent#ACTION_VIEW} Intent with the {@code scope} as data. The first of the resolved
+     * packages that registered (through {@link #registerClient}) to {@code origin} will be chosen.
+     * Finally, an Intent with the action {@link TrustedWebActivityService#INTENT_ACTION} will be
+     * used to find the Service.
+     * <p>
+     * This method should be called on the UI thread.
+     *
+     * @param scope The scope used in an Intent to find packages that may have a
+     *              {@link TrustedWebActivityService}.
+     * @param origin An origin that the {@link TrustedWebActivityService} package must be registered
+     *               to.
+     * @param callback A {@link ExecutionCallback} that will be run with a connection.
+     *                 It will be run on a background thread from the ThreadPool as most methods
+     *                 from {@link TrustedWebActivityServiceWrapper} require this.
+     *                 Any {@link RemoteException} or {@link RuntimeException} exceptions thrown by
+     *                 the callback will be swallowed.
+     *                 This is to allow users to deal with exceptions thrown by
+     *                 {@link TrustedWebActivityServiceWrapper} if they wish, but to fail
+     *                 gracefully if they don't.
+     * @return Whether a {@link TrustedWebActivityService} was found.
+     */
+    @SuppressLint("StaticFieldLeak")
+    public boolean execute(final Uri scope, String origin, final ExecutionCallback callback) {
+        final WrappedCallback wrappedCallback = wrapCallback(callback);
+
+        // If we have an existing connection, use it.
+        Connection connection = mConnections.get(scope);
+        if (connection != null) {
+            connection.addCallback(wrappedCallback);
+            return true;
+        }
+
+        // Check that this is a notification we want to handle.
+        final Intent bindServiceIntent = createServiceIntent(mContext, scope, origin, true);
+        if (bindServiceIntent == null) return false;
+
+        final Connection newConnection = new Connection(scope);
+        newConnection.addCallback(wrappedCallback);
+
+        // Create a new connection.
+        new AsyncTask<Void, Void, Connection>() {
+            @Override
+            protected Connection doInBackground(Void... voids) {
+                try {
+                    // We can pass newConnection to bindService here on a background thread because
+                    // bindService assures us it will use newConnection on the UI thread.
+                    if (mContext.bindService(bindServiceIntent, newConnection,
+                            Context.BIND_AUTO_CREATE)) {
+                        return newConnection;
+                    }
+
+                    mContext.unbindService(newConnection);
+                    return null;
+                } catch (SecurityException e) {
+                    Log.w(TAG, "SecurityException while binding.", e);
+                    return null;
+                }
+            }
+
+            @Override
+            protected void onPostExecute(Connection newConnection) {
+                if (newConnection == null) {
+                    wrappedCallback.onConnected(null);
+                } else {
+                    mConnections.put(scope, newConnection);
+                }
+            }
+        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+
+        return true;
+    }
+
+    /**
+     * Checks if a TrustedWebActivityService exists to handle requests for the given scope and
+     * origin. The value will be the same as that returned from {@link #execute} so calling that
+     * and checking the return may be more convenient.
+     *
+     * This method should be called on the UI thread.
+     *
+     * @param scope The scope used in an Intent to find packages that may have a
+     *              {@link TrustedWebActivityService}.
+     * @param origin An origin that the {@link TrustedWebActivityService} package must be registered
+     *               to.
+     * @return Whether a {@link TrustedWebActivityService} was found.
+     */
+    public boolean serviceExistsForScope(Uri scope, String origin) {
+        // If we have an existing connection, we can deal with the scope.
+        if (mConnections.get(scope) != null) return true;
+
+        return createServiceIntent(mContext, scope, origin, false) != null;
+    }
+
+    /**
+     * Unbinds all open connections to Trusted Web Activity clients.
+     */
+    void unbindAllConnections() {
+        for (Connection connection : mConnections.values()) {
+            mContext.unbindService(connection);
+        }
+        mConnections.clear();
+    }
+
+    /**
+
+     * Creates an Intent to launch the Service for the given scope and verified origin. Will
+     * return null if there is no applicable Service.
+     */
+    private @Nullable Intent createServiceIntent(Context appContext, Uri scope, String origin,
+            boolean shouldLog) {
+        Set<String> possiblePackages = getVerifiedPackages(appContext, origin);
+
+        if (possiblePackages == null || possiblePackages.size() == 0) {
+            return null;
+        }
+
+        // Get a list of installed packages that would match the scope.
+        Intent scopeResolutionIntent = new Intent();
+        scopeResolutionIntent.setData(scope);
+        scopeResolutionIntent.setAction(Intent.ACTION_VIEW);
+        // TODO(peconn): Do we want MATCH_ALL here.
+        // TODO(peconn): Do we need a category here?
+        List<ResolveInfo> candidateActivities = appContext.getPackageManager()
+                .queryIntentActivities(scopeResolutionIntent, PackageManager.MATCH_DEFAULT_ONLY);
+
+        // Choose the first of the installed packages that is verified.
+        String resolvedPackage = null;
+        for (ResolveInfo info : candidateActivities) {
+            String packageName = info.activityInfo.packageName;
+
+            if (possiblePackages.contains(packageName)) {
+                resolvedPackage = packageName;
+                break;
+            }
+        }
+
+        if (resolvedPackage == null) {
+            if (shouldLog) Log.w(TAG, "No TWA candidates for " + origin + " have been registered.");
+            return null;
+        }
+
+        // Find the TrustedWebActivityService within that package.
+        Intent serviceResolutionIntent = new Intent();
+        serviceResolutionIntent.setPackage(resolvedPackage);
+        serviceResolutionIntent.setAction(TrustedWebActivityService.INTENT_ACTION);
+        ResolveInfo info = appContext.getPackageManager().resolveService(serviceResolutionIntent,
+                PackageManager.MATCH_ALL);
+
+        if (info == null) {
+            if (shouldLog) Log.w(TAG, "Could not find TWAService for " + resolvedPackage);
+            return null;
+        }
+
+        if (shouldLog) {
+            Log.i(TAG, "Found " + info.serviceInfo.name + " to handle request for " + origin);
+        }
+        Intent finalIntent = new Intent();
+        finalIntent.setComponent(new ComponentName(resolvedPackage, info.serviceInfo.name));
+        return finalIntent;
+    }
+
+    /**
+     * Registers (and persists) a package to be used for an origin. This information is persisted
+     * in SharedPreferences. Although this method can be called on any thread, it may read
+     * SharedPreferences and hit the disk, so call it on a background thread if possible.
+     * @param context A Context to access SharedPreferences.
+     * @param origin The origin for which the package is relevant.
+     * @param clientPackage The packages to register.
+     */
+    public static void registerClient(Context context, String origin, String clientPackage) {
+        Set<String> possiblePackages = getVerifiedPackages(context, origin);
+        possiblePackages.add(clientPackage);
+
+        // sSharedPreferences won't be null after a call to getVerifiedPackages.
+        SharedPreferences.Editor editor = sSharedPreferences.get().edit();
+        editor.putStringSet(origin, possiblePackages);
+        editor.apply();
+    }
+
+    // TODO(peconn): Do we want to be able to unregister a client? To wipe all clients?
+}
diff --git a/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityServiceWrapper.java b/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityServiceWrapper.java
new file mode 100644
index 0000000..57cb37c
--- /dev/null
+++ b/browser/src/main/java/androidx/browser/trusted/TrustedWebActivityServiceWrapper.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2018 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.browser.trusted;
+
+import android.app.Notification;
+import android.content.ComponentName;
+import android.graphics.Bitmap;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.support.customtabs.trusted.ITrustedWebActivityService;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ * TrustedWebActivityServiceWrapper is used by a Trusted Web Activity provider app to wrap calls to
+ * the {@link TrustedWebActivityService} in the client app.
+ * All of these calls except {@link #getComponentName()} forward over IPC
+ * to corresponding calls on {@link TrustedWebActivityService}, eg {@link #getSmallIconId()}
+ * forwards to {@link TrustedWebActivityService#getSmallIconId()}.
+ * <p>
+ * These IPC calls are synchronous, though the {@link TrustedWebActivityService} method may hit the
+ * disk. Therefore it is recommended to call them on a background thread (without StrictMode).
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class TrustedWebActivityServiceWrapper {
+    // Inputs.
+    private static final String KEY_PLATFORM_TAG =
+            "android.support.customtabs.trusted.PLATFORM_TAG";
+    private static final String KEY_PLATFORM_ID =
+            "android.support.customtabs.trusted.PLATFORM_ID";
+    private static final String KEY_NOTIFICATION =
+            "android.support.customtabs.trusted.NOTIFICATION";
+    private static final String KEY_CHANNEL_NAME =
+            "android.support.customtabs.trusted.CHANNEL_NAME";
+    private static final String KEY_ACTIVE_NOTIFICATIONS =
+            "android.support.customtabs.trusted.ACTIVE_NOTIFICATIONS";
+
+    // Outputs.
+    private static final String KEY_NOTIFICATION_SUCCESS =
+            "android.support.customtabs.trusted.NOTIFICATION_SUCCESS";
+
+    private final ITrustedWebActivityService mService;
+    private final ComponentName mComponentName;
+
+    TrustedWebActivityServiceWrapper(ITrustedWebActivityService service,
+            ComponentName componentName) {
+        mService = service;
+        mComponentName = componentName;
+    }
+
+    /**
+     * Checks whether notifications are enabled.
+     * @param channelName The name of the channel to check enabled status. Only used on Android O+.
+     * @return Whether notifications or the notification channel is blocked for the client app.
+     * @throws RemoteException If the Service dies while responding to the request.
+     */
+    public boolean areNotificationsEnabled(String channelName) throws RemoteException {
+        Bundle args = new NotificationsEnabledArgs(channelName).toBundle();
+        return ResultArgs.fromBundle(mService.areNotificationsEnabled(args)).success;
+    }
+
+    /**
+     * Requests a notification be shown.
+     * @param platformTag The tag to identify the notification.
+     * @param platformId The id to identify the notification.
+     * @param notification The notification.
+     * @param channel The name of the channel in the Trusted Web Activity client app to display the
+     *                notification on.
+     * @return Whether notifications or the notification channel are blocked for the client app.
+     * @throws RemoteException If the Service dies while responding to the request.
+     * @throws SecurityException If verification with the TrustedWebActivityService fails.
+     */
+    public boolean notify(String platformTag, int platformId, Notification notification,
+            String channel) throws RemoteException, SecurityException {
+        Bundle args = new NotifyNotificationArgs(platformTag, platformId, notification, channel)
+                .toBundle();
+        return ResultArgs.fromBundle(mService.notifyNotificationWithChannel(args)).success;
+    }
+
+    /**
+     * Requests a notification be cancelled.
+     * @param platformTag The tag to identify the notification.
+     * @param platformId The id to identify the notification.
+     * @throws RemoteException If the Service dies while responding to the request.
+     * @throws SecurityException If verification with the TrustedWebActivityService fails.
+     */
+    public void cancel(String platformTag, int platformId)
+            throws RemoteException, SecurityException {
+        Bundle args = new CancelNotificationArgs(platformTag, platformId).toBundle();
+        mService.cancelNotification(args);
+    }
+
+    /**
+     * Gets the notifications shown by the Trusted Web Activity client. Can only be called on
+     * Android M and above.
+     * @return An StatusBarNotification[] as a Parcelable[]. This is so this code can compile for
+     *         Jellybean (even if it must not be called for pre-Marshmallow).
+     * @throws RemoteException If the Service dies while responding to the request.
+     * @throws SecurityException If verification with the TrustedWebActivityService fails.
+     * @throws IllegalStateException If called on Android pre-M.
+     *
+     * TODO(peconn): Figure out want to handle the return type before making this public.
+     */
+    @RequiresApi(Build.VERSION_CODES.M)
+    public Parcelable[] getActiveNotifications()
+            throws RemoteException, SecurityException, IllegalStateException {
+        Bundle notifications = mService.getActiveNotifications();
+        return ActiveNotificationsArgs.fromBundle(notifications).notifications;
+    }
+
+    /**
+     * Requests an Android resource id to be used for the notification small icon.
+     * @return An Android resource id for the notification small icon. -1 if non found.
+     * @throws RemoteException If the Service dies while responding to the request.
+     * @throws SecurityException If verification with the TrustedWebActivityService fails.
+     */
+    public int getSmallIconId() throws RemoteException, SecurityException {
+        return mService.getSmallIconId();
+    }
+
+    /**
+     * Requests a bitmap of a small icon to be used for the notification
+     * small icon. The bitmap is decoded on the side of Trusted Web Activity client using
+     * the resource id from {@link TrustedWebActivityService#getSmallIconId}.
+     * @return {@link SmallIconData} with both an id and a bitmap
+     * @throws RemoteException If the Service dies while responding to the request.
+     * @throws SecurityException If verification with the TrustedWebActivityService fails.
+     */
+    @Nullable
+    public Bitmap getSmallIconBitmap() throws RemoteException, SecurityException {
+        return mService.getSmallIconBitmap()
+                .getParcelable(TrustedWebActivityService.KEY_SMALL_ICON_BITMAP);
+    }
+
+    /**
+     * Gets the {@link ComponentName} of the connected Trusted Web Activity client app.
+     * @return The Trusted Web Activity client app component name.
+     */
+    public ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    static class NotifyNotificationArgs {
+        public final String platformTag;
+        public final int platformId;
+        public final Notification notification;
+        public final String channelName;
+
+        NotifyNotificationArgs(String platformTag, int platformId,
+                Notification notification, String channelName) {
+            this.platformTag = platformTag;
+            this.platformId = platformId;
+            this.notification = notification;
+            this.channelName = channelName;
+        }
+
+        public static NotifyNotificationArgs fromBundle(Bundle bundle) {
+            ensureBundleContains(bundle, KEY_PLATFORM_TAG);
+            ensureBundleContains(bundle, KEY_PLATFORM_ID);
+            ensureBundleContains(bundle, KEY_NOTIFICATION);
+            ensureBundleContains(bundle, KEY_CHANNEL_NAME);
+
+            return new NotifyNotificationArgs(bundle.getString(KEY_PLATFORM_TAG),
+                    bundle.getInt(KEY_PLATFORM_ID),
+                    (Notification) bundle.getParcelable(KEY_NOTIFICATION),
+                    bundle.getString(KEY_CHANNEL_NAME));
+        }
+
+        public Bundle toBundle() {
+            Bundle args = new Bundle();
+            args.putString(KEY_PLATFORM_TAG, platformTag);
+            args.putInt(KEY_PLATFORM_ID, platformId);
+            args.putParcelable(KEY_NOTIFICATION, notification);
+            args.putString(KEY_CHANNEL_NAME, channelName);
+            return args;
+        }
+    }
+
+    static class CancelNotificationArgs {
+        public final String platformTag;
+        public final int platformId;
+
+        CancelNotificationArgs(String platformTag, int platformId) {
+            this.platformTag = platformTag;
+            this.platformId = platformId;
+        }
+
+        public static CancelNotificationArgs fromBundle(Bundle bundle) {
+            ensureBundleContains(bundle, KEY_PLATFORM_TAG);
+            ensureBundleContains(bundle, KEY_PLATFORM_ID);
+
+            return new CancelNotificationArgs(bundle.getString(KEY_PLATFORM_TAG),
+                    bundle.getInt(KEY_PLATFORM_ID));
+        }
+
+        public Bundle toBundle() {
+            Bundle args = new Bundle();
+            args.putString(KEY_PLATFORM_TAG, platformTag);
+            args.putInt(KEY_PLATFORM_ID, platformId);
+            return args;
+        }
+    }
+
+    static class ResultArgs {
+        public final boolean success;
+
+        ResultArgs(boolean success) {
+            this.success = success;
+        }
+
+        public static ResultArgs fromBundle(Bundle bundle) {
+            ensureBundleContains(bundle, KEY_NOTIFICATION_SUCCESS);
+            return new ResultArgs(bundle.getBoolean(KEY_NOTIFICATION_SUCCESS));
+        }
+
+        public Bundle toBundle() {
+            Bundle args = new Bundle();
+            args.putBoolean(KEY_NOTIFICATION_SUCCESS, success);
+            return args;
+        }
+    }
+
+    static class ActiveNotificationsArgs {
+        public final Parcelable[] notifications;
+
+        ActiveNotificationsArgs(Parcelable[] notifications) {
+            this.notifications = notifications;
+        }
+
+        public static ActiveNotificationsArgs fromBundle(Bundle bundle) {
+            ensureBundleContains(bundle, KEY_ACTIVE_NOTIFICATIONS);
+            return new ActiveNotificationsArgs(bundle.getParcelableArray(KEY_ACTIVE_NOTIFICATIONS));
+        }
+
+        public Bundle toBundle() {
+            Bundle args = new Bundle();
+            args.putParcelableArray(KEY_ACTIVE_NOTIFICATIONS, notifications);
+            return args;
+        }
+    }
+
+    static class NotificationsEnabledArgs {
+        public final String channelName;
+
+        NotificationsEnabledArgs(String channelName) {
+            this.channelName = channelName;
+        }
+
+        public static NotificationsEnabledArgs fromBundle(Bundle bundle) {
+            ensureBundleContains(bundle, KEY_CHANNEL_NAME);
+            return new NotificationsEnabledArgs(bundle.getString(KEY_CHANNEL_NAME));
+        }
+
+        public Bundle toBundle() {
+            Bundle args = new Bundle();
+            args.putString(KEY_CHANNEL_NAME, channelName);
+            return args;
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    static void ensureBundleContains(Bundle args, String key) {
+        if (args.containsKey(key)) return;
+        throw new IllegalArgumentException("Bundle must contain " + key);
+    }
+}
diff --git a/browser/src/main/res/layout/browser_actions_context_menu_page.xml b/browser/src/main/res/layout/browser_actions_context_menu_page.xml
index 696f2ff..1436087 100644
--- a/browser/src/main/res/layout/browser_actions_context_menu_page.xml
+++ b/browser/src/main/res/layout/browser_actions_context_menu_page.xml
@@ -50,4 +50,4 @@
         android:paddingBottom="8dp"
         android:dividerHeight="0dp"
         android:divider="@null" />
-</androidx.browser.browseractions.BrowserActionsFallbackMenuView>
+</androidx.browser.browseractions.BrowserActionsFallbackMenuView>
\ No newline at end of file
diff --git a/browser/src/main/res/layout/browser_actions_context_menu_row.xml b/browser/src/main/res/layout/browser_actions_context_menu_row.xml
index b08d8a8..5460503 100644
--- a/browser/src/main/res/layout/browser_actions_context_menu_row.xml
+++ b/browser/src/main/res/layout/browser_actions_context_menu_row.xml
@@ -33,11 +33,11 @@
         android:textSize="15sp"
         android:textColor="@color/browser_actions_text_color" />
     <ImageView
-            android:id="@+id/browser_actions_menu_item_icon"
-            android:layout_width="20dp"
-            android:layout_height="match_parent"
-            android:paddingTop="8dp"
-            android:paddingBottom="8dp"
-            android:scaleType="centerInside"
-            android:contentDescription="@null" />
+        android:id="@+id/browser_actions_menu_item_icon"
+        android:layout_width="20dp"
+        android:layout_height="match_parent"
+        android:paddingTop="8dp"
+        android:paddingBottom="8dp"
+        android:scaleType="centerInside"
+        android:contentDescription="@null" />
 </LinearLayout>
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java
index e4d6e5a..5c5feda 100644
--- a/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java
@@ -22,18 +22,11 @@
 
 import android.Manifest;
 import android.content.Context;
-import android.graphics.SurfaceTexture;
-import android.hardware.camera2.CameraDevice;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
-import android.util.Size;
-import android.view.Surface;
 
-import androidx.camera.camera2.impl.Camera2CameraFactory;
-import androidx.camera.camera2.impl.util.SemaphoreReleasingCamera2Callbacks;
-import androidx.camera.core.CameraFactory;
-import androidx.camera.core.CameraRepository;
+import androidx.camera.core.AppConfig;
 import androidx.camera.core.CameraX;
 import androidx.camera.core.CameraX.LensFacing;
 import androidx.camera.core.ImageAnalysis;
@@ -41,11 +34,8 @@
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCaptureConfig;
 import androidx.camera.core.ImageProxy;
-import androidx.camera.core.ImmediateSurface;
 import androidx.camera.core.Preview;
 import androidx.camera.core.PreviewConfig;
-import androidx.camera.core.SessionConfig;
-import androidx.camera.core.UseCaseGroup;
 import androidx.camera.testing.CameraUtil;
 import androidx.camera.testing.fakes.FakeLifecycleOwner;
 import androidx.lifecycle.MutableLiveData;
@@ -61,7 +51,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.Map;
 import java.util.concurrent.Semaphore;
 
 /**
@@ -80,13 +69,10 @@
     private FakeLifecycleOwner mLifecycle;
     private HandlerThread mHandlerThread;
     private Handler mMainThreadHandler;
-    private CallbackAttachingImageCapture mImageCapture;
+    private ImageCapture mImageCapture;
     private ImageAnalysis mImageAnalysis;
     private Preview mPreview;
     private ImageAnalysis.Analyzer mImageAnalyzer;
-    private CameraRepository mCameraRepository;
-    private CameraFactory mCameraFactory;
-    private UseCaseGroup mUseCaseGroup;
 
     private Observer<Long> createCountIncrementingObserver() {
         return new Observer<Long>() {
@@ -100,8 +86,12 @@
     @Before
     public void setUp() {
         assumeTrue(CameraUtil.deviceHasCamera());
+
         Context context = ApplicationProvider.getApplicationContext();
-        CameraX.init(context, Camera2AppConfig.create(context));
+        AppConfig config = Camera2AppConfig.create(context);
+
+        CameraX.init(context, config);
+
         mLifecycle = new FakeLifecycleOwner();
         mHandlerThread = new HandlerThread("ErrorHandlerThread");
         mHandlerThread.start();
@@ -127,22 +117,13 @@
      * Test Combination: Preview + ImageCapture
      */
     @Test
-    public void previewCombinesImageCapture() throws InterruptedException {
+    public void previewCombinesImageCapture() {
         initPreview();
         initImageCapture();
 
-        mUseCaseGroup.addUseCase(mImageCapture);
-        mUseCaseGroup.addUseCase(mPreview);
-
         CameraX.bindToLifecycle(mLifecycle, mPreview, mImageCapture);
         mLifecycle.startAndResume();
 
-        mImageCapture.doNotifyActive();
-        mCameraRepository.onGroupActive(mUseCaseGroup);
-
-        // Wait for the CameraCaptureSession.onConfigured callback.
-        mImageCapture.mSessionStateCallback.waitForOnConfigured(1);
-
         assertThat(mLifecycle.getObserverCount()).isEqualTo(2);
         assertThat(CameraX.isBound(mPreview)).isTrue();
         assertThat(CameraX.isBound(mImageCapture)).isTrue();
@@ -163,12 +144,12 @@
 
                 mAnalysisResult.observe(mLifecycle,
                         createCountIncrementingObserver());
-
-                CameraX.bindToLifecycle(mLifecycle, mPreview, mImageAnalysis);
-                mLifecycle.startAndResume();
             }
         });
 
+        CameraX.bindToLifecycle(mLifecycle, mPreview, mImageAnalysis);
+        mLifecycle.startAndResume();
+
         // Wait for 10 frames to be analyzed.
         mSemaphore.acquire(10);
 
@@ -185,13 +166,6 @@
         initImageAnalysis();
         initImageCapture();
 
-        mUseCaseGroup.addUseCase(mImageCapture);
-        mUseCaseGroup.addUseCase(mImageAnalysis);
-        mUseCaseGroup.addUseCase(mPreview);
-
-        mImageCapture.doNotifyActive();
-        mCameraRepository.onGroupActive(mUseCaseGroup);
-
         mMainThreadHandler.post(new Runnable() {
             @Override
             public void run() {
@@ -208,13 +182,7 @@
         // Wait for 10 frames to be analyzed.
         mSemaphore.acquire(10);
 
-        // Wait for the CameraCaptureSession.onConfigured callback.
-        try {
-            mImageCapture.mSessionStateCallback.waitForOnConfigured(1);
-        } catch (InterruptedException e) {
-            e.printStackTrace();
-        }
-
+        assertThat(mLifecycle.getObserverCount()).isEqualTo(3);
         assertThat(CameraX.isBound(mPreview)).isTrue();
         assertThat(CameraX.isBound(mImageAnalysis)).isTrue();
         assertThat(CameraX.isBound(mImageCapture)).isTrue();
@@ -238,20 +206,10 @@
     }
 
     private void initImageCapture() {
-        mCameraRepository = new CameraRepository();
-        mCameraFactory = new Camera2CameraFactory(ApplicationProvider.getApplicationContext());
-        mCameraRepository.init(mCameraFactory);
-        mUseCaseGroup = new UseCaseGroup();
-
         ImageCaptureConfig imageCaptureConfig =
                 new ImageCaptureConfig.Builder().setLensFacing(LensFacing.BACK).build();
-        String cameraId = getCameraIdForLensFacingUnchecked(imageCaptureConfig.getLensFacing());
-        mImageCapture = new CallbackAttachingImageCapture(imageCaptureConfig, cameraId);
 
-        mImageCapture.addStateChangeListener(
-                mCameraRepository.getCamera(
-                        getCameraIdForLensFacingUnchecked(
-                                imageCaptureConfig.getLensFacing())));
+        mImageCapture = new ImageCapture(imageCaptureConfig);
     }
 
     private void initPreview() {
@@ -263,44 +221,4 @@
 
         mPreview = new Preview(previewConfig);
     }
-
-    private String getCameraIdForLensFacingUnchecked(LensFacing lensFacing) {
-        try {
-            return mCameraFactory.cameraIdForLensFacing(lensFacing);
-        } catch (Exception e) {
-            throw new IllegalArgumentException(
-                    "Unable to attach to camera with LensFacing " + lensFacing, e);
-        }
-    }
-
-    /** A use case which attaches to a camera with various callbacks. */
-    private static class CallbackAttachingImageCapture extends ImageCapture {
-        private final SemaphoreReleasingCamera2Callbacks.SessionStateCallback
-                mSessionStateCallback =
-                new SemaphoreReleasingCamera2Callbacks.SessionStateCallback();
-        private final SurfaceTexture mSurfaceTexture = new SurfaceTexture(0);
-
-        CallbackAttachingImageCapture(ImageCaptureConfig config, String cameraId) {
-            super(config);
-            // Use most supported resolution for different supported hardware level devices,
-            // especially for legacy devices.
-            mSurfaceTexture.setDefaultBufferSize(640, 480);
-            SessionConfig.Builder builder = new SessionConfig.Builder();
-            builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
-            builder.addSurface(new ImmediateSurface(new Surface(mSurfaceTexture)));
-            builder.addSessionStateCallback(mSessionStateCallback);
-
-            attachToCamera(cameraId, builder.build());
-        }
-
-        @Override
-        protected Map<String, Size> onSuggestedResolutionUpdated(
-                Map<String, Size> suggestedResolutionMap) {
-            return suggestedResolutionMap;
-        }
-
-        void doNotifyActive() {
-            super.notifyActive();
-        }
-    }
 }
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2ImplCameraXTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2ImplCameraXTest.java
index dd32354..85febf0 100644
--- a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2ImplCameraXTest.java
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2ImplCameraXTest.java
@@ -279,19 +279,20 @@
     public void bind_unbind_loopWithOutAnalyzer() {
         ImageAnalysisConfig.Builder builder =
                 new ImageAnalysisConfig.Builder().setLensFacing(DEFAULT_LENS_FACING);
-        new Camera2Config.Extender(builder).setDeviceStateCallback(mMockStateCallback);
         mLifecycle.startAndResume();
 
         for (int i = 0; i < 2; i++) {
+            CameraDevice.StateCallback callback = Mockito.mock(CameraDevice.StateCallback.class);
+            new Camera2Config.Extender(builder).setDeviceStateCallback(callback);
             ImageAnalysisConfig config = builder.build();
             ImageAnalysis useCase = new ImageAnalysis(config);
             CameraX.bindToLifecycle(mLifecycle, useCase);
 
-            verify(mMockStateCallback, timeout(5000)).onOpened(any(CameraDevice.class));
+            verify(callback, timeout(5000)).onOpened(any(CameraDevice.class));
 
             CameraX.unbind(useCase);
 
-            verify(mMockStateCallback, timeout(3000)).onClosed(any(CameraDevice.class));
+            verify(callback, timeout(3000)).onClosed(any(CameraDevice.class));
         }
     }
 
@@ -299,20 +300,21 @@
     public void bind_unbind_loopWithAnalyzer() {
         ImageAnalysisConfig.Builder builder =
                 new ImageAnalysisConfig.Builder().setLensFacing(DEFAULT_LENS_FACING);
-        new Camera2Config.Extender(builder).setDeviceStateCallback(mMockStateCallback);
         mLifecycle.startAndResume();
 
         for (int i = 0; i < 2; i++) {
+            CameraDevice.StateCallback callback = Mockito.mock(CameraDevice.StateCallback.class);
+            new Camera2Config.Extender(builder).setDeviceStateCallback(callback);
             ImageAnalysisConfig config = builder.build();
             ImageAnalysis useCase = new ImageAnalysis(config);
             CameraX.bindToLifecycle(mLifecycle, useCase);
             useCase.setAnalyzer(mImageAnalyzer);
 
-            verify(mMockStateCallback, timeout(5000)).onOpened(any(CameraDevice.class));
+            verify(callback, timeout(5000)).onOpened(any(CameraDevice.class));
 
             CameraX.unbind(useCase);
 
-            verify(mMockStateCallback, timeout(3000)).onClosed(any(CameraDevice.class));
+            verify(callback, timeout(3000)).onClosed(any(CameraDevice.class));
         }
     }
 
diff --git a/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/ExtensionVersionImpl.java b/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/ExtensionVersionImpl.java
new file mode 100644
index 0000000..feaa53a
--- /dev/null
+++ b/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/ExtensionVersionImpl.java
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+package androidx.camera.extensions.impl;
+
+/**
+ * Stub implementation for the extension version check.
+ *
+ * <p>This class should be implemented by OEM and deployed to the target devices.
+ */
+public class ExtensionVersionImpl {
+    public ExtensionVersionImpl() {
+    }
+
+    /**
+     * Provide the current CameraX extension library version to vendor library and vendor would
+     * need to return the supported version for this device. If the returned version is not
+     * supported by CameraX library, the Preview and ImageCapture would not be able to enable the
+     * specific effects provided by the vendor.
+     *
+     * <p>CameraX library provides the Semantic Versioning string in a form of
+     * MAJOR.MINOR.PATCH-description
+     * We will increment the
+     * MAJOR version when make incompatible API changes,
+     * MINOR version when add functionality in a backwards-compatible manner, and
+     * PATCH version when make backwards-compatible bug fixes. And the description can be ignored.
+     *
+     * <p>Vendor library should provide MAJOR.MINOR.PATCH to CameraX. The MAJOR and MINOR
+     * version is used to map to the version of CameraX that it supports, and CameraX extension
+     * would only available when MAJOR version is matched with CameraX current version. The PATCH
+     * version does not indicate compatibility. The patch version should be incremented whenever
+     * the vendor library makes bug fixes or updates to the algorithm.
+     *
+     * @param version the version of CameraX library formatted as MAJOR.MINOR.PATCH-description.
+     * @return the version that vendor supported in this device. The MAJOR.MINOR.PATCH format
+     * should be used.
+     */
+    public String checkApiVersion(String version) {
+        throw new RuntimeException("Stub, replace with implementation.");
+    }
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
index d3abde1..b59a25e 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
@@ -24,12 +24,17 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Intent;
 
 import androidx.camera.core.FlashMode;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.Preview;
 import androidx.camera.integration.core.idlingresource.ElapsedTimeIdlingResource;
 import androidx.camera.integration.core.idlingresource.WaitForViewToShow;
+import androidx.camera.testing.CameraUtil;
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.espresso.Espresso;
 import androidx.test.espresso.IdlingRegistry;
 import androidx.test.espresso.IdlingResource;
@@ -42,6 +47,8 @@
 import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.Until;
 
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -53,14 +60,18 @@
 
     private static final int LAUNCH_TIMEOUT_MS = 5000;
     private static final int IDLE_TIMEOUT_MS = 1000;
+    private static final String BASIC_SAMPLE_PACKAGE = "androidx.camera.integration.core";
 
     private final UiDevice mDevice =
             UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
     private final String mLauncherPackageName = mDevice.getLauncherPackageName();
+    private final Intent mIntent = ApplicationProvider.getApplicationContext().getPackageManager()
+            .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
 
     @Rule
     public ActivityTestRule<CameraXActivity> mActivityRule =
-            new ActivityTestRule<>(CameraXActivity.class);
+            new ActivityTestRule<>(CameraXActivity.class, true,
+                    false);
 
     @Rule
     public GrantPermissionRule mCameraPermissionRule =
@@ -78,6 +89,19 @@
         IdlingRegistry.getInstance().unregister(idlingResource);
     }
 
+    @Before
+    public void setUp() {
+        assumeTrue(CameraUtil.deviceHasCamera());
+        // Launch Activity
+        mActivityRule.launchActivity(mIntent);
+    }
+
+    @After
+    public void tearDown() {
+        pressBackAndReturnHome();
+        mActivityRule.finishActivity();
+    }
+
     @Test
     public void testFlashToggleButton() {
         waitFor(new WaitForViewToShow(R.id.flash_toggle));
@@ -162,5 +186,11 @@
         mDevice.wait(Until.hasObject(By.pkg(mLauncherPackageName).depth(0)), LAUNCH_TIMEOUT_MS);
     }
 
+    private void pressBackAndReturnHome() {
+        mDevice.pressBack();
+
+        // Returns to Home to restart next test.
+        mDevice.pressHome();
+    }
 }
 
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ExtensionVersionImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ExtensionVersionImpl.java
new file mode 100644
index 0000000..1640d0c
--- /dev/null
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ExtensionVersionImpl.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+package androidx.camera.extensions.impl;
+
+/**
+ * Implementation for extension version check.
+ *
+ * <p>This class should be implemented by OEM and deployed to the target devices. 3P developers
+ * don't need to implement this, unless this is used for related testing usage.
+ */
+public class ExtensionVersionImpl {
+    public ExtensionVersionImpl() {
+    }
+
+    /**
+     * Provide the current CameraX extension library version to vendor library and vendor would
+     * need to return the supported version for this device. If the returned version is not
+     * supported by CameraX library, the Preview and ImageCapture would not be able to enable the
+     * specific effects provided by the vendor.
+     *
+     * <p>CameraX library provides the Semantic Versioning string in a form of
+     * MAJOR.MINOR.PATCH-description
+     * We will increment the
+     * MAJOR version when make incompatible API changes,
+     * MINOR version when add functionality in a backwards-compatible manner, and
+     * PATCH version when make backwards-compatible bug fixes. And the description can be ignored.
+     *
+     * <p>Vendor library should provide MAJOR.MINOR.PATCH to CameraX. The MAJOR and MINOR
+     * version is used to map to the version of CameraX that it supports, and CameraX extension
+     * would only available when MAJOR version is matched with CameraX current version. The PATCH
+     * version does not indicate compatibility. The patch version should be incremented whenever
+     * the vendor library makes bug fixes or updates to the algorithm.
+     *
+     * @param version the version of CameraX library formatted as MAJOR.MINOR.PATCH-description.
+     * @return the version that vendor supported in this device. The MAJOR.MINOR.PATCH format
+     * should be used.
+     */
+    public String checkApiVersion(String version) {
+        return "1.0.0-alpha02";
+    }
+}
diff --git a/car/core/api/1.0.0-alpha8.txt b/car/core/api/1.0.0-alpha8.txt
index a2f8065..d66bb93 100644
--- a/car/core/api/1.0.0-alpha8.txt
+++ b/car/core/api/1.0.0-alpha8.txt
@@ -46,6 +46,35 @@
     method public CharSequence getTitle();
   }
 
+  public final class CarMultipleChoiceDialog extends android.app.Dialog {
+  }
+
+  public static final class CarMultipleChoiceDialog.Builder {
+    ctor public CarMultipleChoiceDialog.Builder(android.content.Context);
+    method public android.app.Dialog create();
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setBody(@StringRes int);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setBody(CharSequence?);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setCancelable(boolean);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setItems(java.util.List<androidx.car.app.CarMultipleChoiceDialog.Item!>);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setNegativeButton(@StringRes int);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setNegativeButton(CharSequence);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setOnCancelListener(android.content.DialogInterface.OnCancelListener?);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setOnDismissListener(android.content.DialogInterface.OnDismissListener?);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setPositiveButton(@StringRes int, androidx.car.app.CarMultipleChoiceDialog.OnMultiChoiceClickListener);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setPositiveButton(CharSequence, androidx.car.app.CarMultipleChoiceDialog.OnMultiChoiceClickListener);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setTitle(@StringRes int);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setTitle(CharSequence?);
+  }
+
+  public static class CarMultipleChoiceDialog.Item {
+    ctor public CarMultipleChoiceDialog.Item(CharSequence, boolean);
+    ctor public CarMultipleChoiceDialog.Item(CharSequence, CharSequence?, boolean);
+  }
+
+  public static interface CarMultipleChoiceDialog.OnMultiChoiceClickListener {
+    method public void onClick(android.content.DialogInterface, boolean[]);
+  }
+
   public final class CarSingleChoiceDialog extends android.app.Dialog {
   }
 
diff --git a/car/core/api/current.txt b/car/core/api/current.txt
index a2f8065..d66bb93 100644
--- a/car/core/api/current.txt
+++ b/car/core/api/current.txt
@@ -46,6 +46,35 @@
     method public CharSequence getTitle();
   }
 
+  public final class CarMultipleChoiceDialog extends android.app.Dialog {
+  }
+
+  public static final class CarMultipleChoiceDialog.Builder {
+    ctor public CarMultipleChoiceDialog.Builder(android.content.Context);
+    method public android.app.Dialog create();
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setBody(@StringRes int);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setBody(CharSequence?);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setCancelable(boolean);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setItems(java.util.List<androidx.car.app.CarMultipleChoiceDialog.Item!>);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setNegativeButton(@StringRes int);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setNegativeButton(CharSequence);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setOnCancelListener(android.content.DialogInterface.OnCancelListener?);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setOnDismissListener(android.content.DialogInterface.OnDismissListener?);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setPositiveButton(@StringRes int, androidx.car.app.CarMultipleChoiceDialog.OnMultiChoiceClickListener);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setPositiveButton(CharSequence, androidx.car.app.CarMultipleChoiceDialog.OnMultiChoiceClickListener);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setTitle(@StringRes int);
+    method public androidx.car.app.CarMultipleChoiceDialog.Builder setTitle(CharSequence?);
+  }
+
+  public static class CarMultipleChoiceDialog.Item {
+    ctor public CarMultipleChoiceDialog.Item(CharSequence, boolean);
+    ctor public CarMultipleChoiceDialog.Item(CharSequence, CharSequence?, boolean);
+  }
+
+  public static interface CarMultipleChoiceDialog.OnMultiChoiceClickListener {
+    method public void onClick(android.content.DialogInterface, boolean[]);
+  }
+
   public final class CarSingleChoiceDialog extends android.app.Dialog {
   }
 
diff --git a/car/core/res/layout/car_selction_dialog.xml b/car/core/res/layout/car_selection_dialog.xml
similarity index 100%
rename from car/core/res/layout/car_selction_dialog.xml
rename to car/core/res/layout/car_selection_dialog.xml
diff --git a/car/core/res/values/themes.xml b/car/core/res/values/themes.xml
index 8d2fdbd..cfe998a 100644
--- a/car/core/res/values/themes.xml
+++ b/car/core/res/values/themes.xml
@@ -37,6 +37,7 @@
         <item name="android:colorPrimaryDark">@android:color/black</item>
         <item name="android:editTextColor">@color/car_body1_light</item>
         <item name="android:editTextStyle">@style/Widget.Car.EditText</item>
+        <item name="textInputStyle">@style/Widget.Design.TextInputLayout</item>
         <item name="android:listChoiceIndicatorMultiple">@drawable/car_checkbox</item>
         <item name="android:listChoiceIndicatorSingle">@drawable/car_radio_button</item>
         <item name="android:listPreferredItemHeightSmall">@dimen/car_double_line_list_item_height
@@ -259,7 +260,7 @@
     <!-- Framework and AppCompat Dialog Themes -->
     <!-- ===================================== -->
 
-    <!-- Styles for framework and the Appcompat AlertDialog. This style will automatically
+    <!-- Styles for framework and the MaterialComponents AlertDialog. This style will automatically
          change the background color of the dialog based on the day/night mode. -->
     <style name="Theme.Car.Dialog.Alert" parent="Theme.MaterialComponents.Dialog.Alert">
         <item name="android:background">@color/car_card</item>
@@ -285,6 +286,7 @@
         <item name="buttonBarNegativeButtonStyle">@style/Widget.Car.Button.Borderless.Colored</item>
         <item name="buttonBarPositiveButtonStyle">@style/Widget.Car.Button.Borderless.Colored</item>
         <item name="listDividerAlertDialog">@drawable/car_preference_divider</item>
+        <item name="textInputStyle">@style/Widget.Design.TextInputLayout</item>
     </style>
 
     <!-- Style for framework and the Appcompat AlertDialog that is fixed to have a light colored
diff --git a/car/core/src/main/java/androidx/car/app/CarMultipleChoiceDialog.java b/car/core/src/main/java/androidx/car/app/CarMultipleChoiceDialog.java
new file mode 100644
index 0000000..41f2734
--- /dev/null
+++ b/car/core/src/main/java/androidx/car/app/CarMultipleChoiceDialog.java
@@ -0,0 +1,519 @@
+/*
+ * 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.
+ */
+
+package androidx.car.app;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.car.R;
+import androidx.car.util.DropShadowScrollListener;
+import androidx.car.widget.CheckBoxListItem;
+import androidx.car.widget.ListItem;
+import androidx.car.widget.ListItemAdapter;
+import androidx.car.widget.ListItemProvider;
+import androidx.car.widget.PagedListView;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A subclass of {@link Dialog} that is tailored for the car environment. This dialog can display a
+ * title, body text, a fixed list of multiple choice items and up to two buttons -- a positive and
+ * negative button. Multiple choice items use a checkbox to indicate selection.
+ *
+ * <p>Note that this dialog cannot be created with an empty list or without a positive button.
+ */
+public final class CarMultipleChoiceDialog extends Dialog {
+    private final CharSequence mTitle;
+    private final CharSequence mBodyText;
+    private final CharSequence mPositiveButtonText;
+    private final CharSequence mNegativeButtonText;
+    private TextView mTitleView;
+    private TextView mBodyTextView;
+
+    private boolean[] mCheckedItems;
+
+    private ListItemAdapter mAdapter;
+
+    private PagedListView mList;
+
+    @Nullable
+    private final OnMultiChoiceClickListener mOnClickListener;
+
+    /** Flag for if a touch on the scrim of the dialog will dismiss it. */
+    private boolean mDismissOnTouchOutside;
+
+    CarMultipleChoiceDialog(Context context, Builder builder) {
+        super(context, CarDialogUtil.getDialogTheme(context));
+
+        mTitle = builder.mTitle;
+        mBodyText = builder.mSubtitle;
+        mOnClickListener = builder.mOnClickListener;
+        mPositiveButtonText = builder.mPositiveButtonText;
+        mNegativeButtonText = builder.mNegativeButtonText;
+
+        mCheckedItems = new boolean[builder.mItems.size()];
+
+        initializeWithItems(builder.mItems);
+    }
+
+    @Override
+    public void setTitle(CharSequence title) {
+        // Ideally this method should be private; the dialog should only be modifiable through the
+        // Builder. Unfortunately, this method is defined with the Dialog itself and is public.
+        // So, throw an error if this method is ever called.
+        throw new UnsupportedOperationException("Title should only be set from the Builder");
+    }
+
+    /**
+     * @see Dialog#setCanceledOnTouchOutside(boolean)
+     */
+    @Override
+    public void setCanceledOnTouchOutside(boolean cancel) {
+        super.setCanceledOnTouchOutside(cancel);
+        // Need to override this method to save the value of cancel.
+        mDismissOnTouchOutside = cancel;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Window window = getWindow();
+        window.setContentView(R.layout.car_selection_dialog);
+
+        // Ensure that the dialog takes up the entire window. This is needed because the scrollbar
+        // needs to be drawn off the dialog.
+        WindowManager.LayoutParams layoutParams = window.getAttributes();
+        layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
+        layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
+        window.setAttributes(layoutParams);
+
+        // The container for this dialog takes up the entire screen. As a result, need to manually
+        // listen for clicks and dismiss the dialog when necessary.
+        window.findViewById(R.id.container).setOnClickListener(v -> handleTouchOutside());
+
+        initializeTitle();
+        initializeBodyText();
+        initializeList();
+        initializeButtons();
+
+        // Need to set this elevation listener last because the text and list need to be
+        // initialized first.
+        initializeTextElevationListener();
+    }
+
+    private void initializeButtons() {
+        boolean isButtonPresent = false;
+        Window window = getWindow();
+        Button positiveButtonView = window.findViewById(R.id.positive_button);
+        if (!TextUtils.isEmpty(mPositiveButtonText)) {
+            isButtonPresent = true;
+            positiveButtonView.setText(mPositiveButtonText);
+            positiveButtonView.setOnClickListener(v -> {
+                if (mOnClickListener != null) {
+                    mOnClickListener.onClick(this,
+                            Arrays.copyOf(mCheckedItems, mCheckedItems.length));
+                }
+                dismiss();
+            });
+        } else {
+            positiveButtonView.setVisibility(View.GONE);
+        }
+
+        Button negativeButtonView = window.findViewById(R.id.negative_button);
+        if (!TextUtils.isEmpty(mNegativeButtonText)) {
+            isButtonPresent = true;
+            negativeButtonView.setText(mNegativeButtonText);
+            negativeButtonView.setOnClickListener(v -> dismiss());
+        } else {
+            negativeButtonView.setVisibility(View.GONE);
+        }
+
+        if (!isButtonPresent) {
+            window.findViewById(R.id.button_panel).setVisibility(View.GONE);
+        }
+    }
+
+    private void initializeTitle() {
+        mTitleView = getWindow().findViewById(R.id.title);
+        mTitleView.setText(mTitle);
+        mTitleView.setVisibility(!TextUtils.isEmpty(mTitle) ? View.VISIBLE : View.GONE);
+    }
+
+    private void initializeBodyText() {
+        mBodyTextView = getWindow().findViewById(R.id.bodyText);
+        mBodyTextView.setText(mBodyText);
+        mBodyTextView.setVisibility(!TextUtils.isEmpty(mBodyText) ? View.VISIBLE : View.GONE);
+    }
+
+    private void initializeTextElevationListener() {
+        if (mTitleView.getVisibility() != View.GONE) {
+            mList.addOnScrollListener(new DropShadowScrollListener(mTitleView));
+        } else if (mBodyTextView.getVisibility() != View.GONE) {
+            mList.addOnScrollListener(new DropShadowScrollListener(mBodyTextView));
+        }
+    }
+
+    private void initializeList() {
+        mList = getWindow().findViewById(R.id.list);
+        mList.setMaxPages(PagedListView.UNLIMITED_PAGES);
+        mList.setAdapter(mAdapter);
+        mList.setDividerVisibilityManager(mAdapter);
+
+        CarDialogUtil.setUpDialogList(mList, getWindow().findViewById(R.id.scrollbar));
+    }
+
+    /**
+     * Handles if a touch has been detected outside of the dialog. If
+     * {@link #mDismissOnTouchOutside} has been set, then the dialog will be dismissed.
+     */
+    private void handleTouchOutside() {
+        if (mDismissOnTouchOutside) {
+            dismiss();
+        }
+    }
+
+    /**
+     * Initializes {@link #mAdapter} to display the items in the given array by utilizing
+     * {@link CheckBoxListItem}.
+     */
+    private void initializeWithItems(List<Item> items) {
+        List<ListItem> listItems = new ArrayList<>();
+
+        for (int i = 0; i < items.size(); i++) {
+            listItems.add(createItem(/* selectionItem= */ items.get(i), /* position= */ i));
+        }
+
+        mAdapter = new ListItemAdapter(getContext(), new ListItemProvider.ListProvider(listItems));
+    }
+
+    /**
+     * Creates the {@link CheckBoxListItem} that represents an item in the {@code
+     * CarMultipleChoiceDialog}.
+     *
+     * @param {@link   Item} to display as a {@link CheckBoxListItem}.
+     * @param position The position of the item in the list.
+     */
+    private CheckBoxListItem createItem(Item selectionItem, int position) {
+        CheckBoxListItem item = new CheckBoxListItem(getContext());
+        item.setTitle(selectionItem.mTitle);
+        item.setBody(selectionItem.mBody);
+        item.setShowCompoundButtonDivider(false);
+        item.addViewBinder(vh -> {
+            vh.getCompoundButton().setChecked(selectionItem.mIsChecked);
+            vh.getCompoundButton().setOnCheckedChangeListener(
+                    (buttonView, isChecked) -> {
+                        mCheckedItems[position] = isChecked;
+                    });
+        });
+
+        mCheckedItems[position] = selectionItem.mIsChecked;
+        return item;
+    }
+
+    /**
+     * A struct that holds data for a multiple choice item. A multiple choice item is a
+     * combination of the item title and optional body text.
+     */
+    public static class Item {
+
+        final CharSequence mTitle;
+        final CharSequence mBody;
+        final boolean mIsChecked;
+
+        /**
+         * Creates a Item.
+         *
+         * @param title   The title of the item. This value must be non-empty.
+         * @param checked Whether the item is selected.
+         */
+        public Item(@NonNull CharSequence title, boolean checked) {
+            this(title,  /* body= */ null, checked);
+        }
+
+        /**
+         * Creates a Item.
+         *
+         * @param title   The title of the item. This value must be non-empty.
+         * @param body    The secondary body text of the item.
+         * @param checked Whether the item is selected.
+         */
+        public Item(@NonNull CharSequence title, @Nullable CharSequence body, boolean checked) {
+            if (TextUtils.isEmpty(title)) {
+                throw new IllegalArgumentException("Title cannot be empty.");
+            }
+
+            mTitle = title;
+            mBody = body;
+            mIsChecked = checked;
+        }
+    }
+
+    /**
+     * Interface used to allow the creator the dialog to run some code when the selection of items
+     * is confirmed with the positive button of {@link CarMultipleChoiceDialog}.
+     */
+    public interface OnMultiChoiceClickListener {
+        /**
+         * This method will be invoked when the positive button of the dialog is clicked.
+         *
+         * @param dialog       the dialog where the selection was made
+         * @param checkedItems specifies which items are checked.
+         */
+        void onClick(@NonNull DialogInterface dialog, @NonNull boolean[] checkedItems);
+    }
+
+    /**
+     * Builder class that can be used to create a {@link CarMultipleChoiceDialog} by configuring
+     * the options for the list and behavior of the dialog.
+     */
+    public static final class Builder {
+        private final Context mContext;
+
+        CharSequence mTitle;
+        CharSequence mSubtitle;
+        List<Item> mItems;
+        OnMultiChoiceClickListener mOnClickListener;
+
+        CharSequence mPositiveButtonText;
+        CharSequence mNegativeButtonText;
+
+        private boolean mCancelable = true;
+        private OnCancelListener mOnCancelListener;
+        private OnDismissListener mOnDismissListener;
+
+        /**
+         * Creates a new instance of the {@code Builder}.
+         *
+         * @param context The {@code Context} that the dialog is to be created in.
+         */
+        public Builder(@NonNull Context context) {
+            mContext = context;
+        }
+
+        /**
+         * Sets the title of the dialog to be the given string resource.
+         *
+         * @param titleId The resource id of the string to be used as the title.
+         *                Text style will be retained.
+         * @return This {@link Builder} object to allow for chaining of calls.
+         */
+        @NonNull
+        public Builder setTitle(@StringRes int titleId) {
+            mTitle = mContext.getText(titleId);
+            return this;
+        }
+
+        /**
+         * Sets the title of the dialog for be the given string.
+         *
+         * @param title The string to be used as the title.
+         * @return This {@link Builder} object to allow for chaining of calls.
+         */
+        @NonNull
+        public Builder setTitle(@Nullable CharSequence title) {
+            mTitle = title;
+            return this;
+        }
+
+        /**
+         * Sets the body text of the dialog to be the given string resource.
+         *
+         * @param bodyTextId The resource id of the string to be used as the body.
+         *                   Text style will be retained.
+         * @return This {@link Builder} object to allow for chaining of calls.
+         */
+        @NonNull
+        public Builder setBody(@StringRes int bodyTextId) {
+            mSubtitle = mContext.getText(bodyTextId);
+            return this;
+        }
+
+        /**
+         * Sets the bodyText of the dialog for be the given string.
+         *
+         * @param bodyText The string to be used as the body.
+         * @return This {@link Builder} object to allow for chaining of calls.
+         */
+        @NonNull
+        public Builder setBody(@Nullable CharSequence bodyText) {
+            mSubtitle = bodyText;
+            return this;
+        }
+
+        /**
+         * Sets the items that should appear in the list.
+         *
+         * <p>The provided list of items cannot be {@code null} or empty. Passing an empty list
+         * to this method will throw can exception.
+         *
+         * @param items The items that will appear in the list.
+         * @return This {@link Builder} object to allow for chaining of calls.
+         */
+        @NonNull
+        public Builder setItems(@NonNull List<Item> items) {
+            if (items.size() == 0) {
+                throw new IllegalArgumentException("Provided list of items cannot be empty.");
+            }
+
+            mItems = items;
+            return this;
+        }
+
+        /**
+         * Configure the dialog to include a positive button.
+         *
+         * @param textId          The resource id of the text to display in the positive button.
+         * @param onClickListener The listener that will be notified of item selection.
+         * @return This {@link Builder} object to allow for chaining of calls to set methods.
+         */
+        @NonNull
+        public Builder setPositiveButton(@StringRes int textId,
+                @NonNull OnMultiChoiceClickListener onClickListener) {
+            setPositiveButton(mContext.getText(textId), onClickListener);
+            return this;
+        }
+
+        /**
+         * Configure the dialog to include a positive button.
+         *
+         * @param text            The text to display in the positive button.
+         * @param onClickListener The listener that will be notified of a selection.
+         * @return This {@link Builder} object to allow for chaining of calls to set methods.
+         */
+        @NonNull
+        public Builder setPositiveButton(@NonNull CharSequence text,
+                @NonNull OnMultiChoiceClickListener onClickListener) {
+            mPositiveButtonText = text;
+            mOnClickListener = onClickListener;
+            return this;
+        }
+
+        /**
+         * Configure the dialog to include a negative button.
+         *
+         * @param textId The resource id of the text to display in the negative button.
+         * @return This {@link Builder} object to allow for chaining of calls to set methods.
+         */
+        @NonNull
+        public Builder setNegativeButton(@StringRes int textId) {
+            mNegativeButtonText = mContext.getText(textId);
+            return this;
+        }
+
+        /**
+         * Configure the dialog to include a negative button.
+         *
+         * @param text The text to display in the negative button.
+         * @return This {@link Builder} object to allow for chaining of calls to set methods.
+         */
+        @NonNull
+        public Builder setNegativeButton(@NonNull CharSequence text) {
+            mNegativeButtonText = text;
+            return this;
+        }
+
+        /**
+         * Sets whether the dialog is cancelable or not. Default is {@code true}.
+         *
+         * @return This {@link Builder} object to allow for chaining of calls.
+         */
+        @NonNull
+        public Builder setCancelable(boolean cancelable) {
+            mCancelable = cancelable;
+            return this;
+        }
+
+        /**
+         * Sets the callback that will be called if the dialog is canceled.
+         *
+         * <p>Even in a cancelable dialog, the dialog may be dismissed for reasons other than
+         * being canceled or one of the supplied choices being selected.
+         * If you are interested in listening for all cases where the dialog is dismissed
+         * and not just when it is canceled, see {@link #setOnDismissListener(OnDismissListener)}.
+         *
+         * @param onCancelListener The listener to be invoked when this dialog is canceled.
+         * @return This {@link Builder} object to allow for chaining of calls.
+         * @see #setCancelable(boolean)
+         * @see #setOnDismissListener(OnDismissListener)
+         */
+        @NonNull
+        public Builder setOnCancelListener(@Nullable OnCancelListener onCancelListener) {
+            mOnCancelListener = onCancelListener;
+            return this;
+        }
+
+        /**
+         * Sets the callback that will be called when the dialog is dismissed for any reason.
+         *
+         * @return This {@link Builder} object to allow for chaining of calls.
+         */
+        @NonNull
+        public Builder setOnDismissListener(@Nullable OnDismissListener onDismissListener) {
+            mOnDismissListener = onDismissListener;
+            return this;
+        }
+
+        /**
+         * Creates a {@link CarMultipleChoiceDialog}, which is returned as a {@link Dialog}, with
+         * the arguments supplied to this {@link Builder}.
+         *
+         * <p>If {@link #setItems(List)} is never called, then calling this method
+         * will throw an exception.
+         *
+         * <p>Calling this method does not display the dialog. Utilize this dialog within a
+         * {@link androidx.fragment.app.DialogFragment} to show the dialog.
+         */
+        @NonNull
+        public Dialog create() {
+            // Check that the dialog was created with a list of items.
+            if (mItems == null || mItems.size() == 0) {
+                throw new IllegalStateException(
+                        "CarMultipleChoiceDialog must be created with a non-empty list.");
+            }
+
+            if (TextUtils.isEmpty(mPositiveButtonText)) {
+                throw new IllegalStateException(
+                        "CarMultipleChoiceDialog cannot be created without a positive button.");
+            }
+
+            CarMultipleChoiceDialog dialog = new CarMultipleChoiceDialog(mContext,
+                    /* builder= */ this);
+
+            dialog.setCancelable(mCancelable);
+            dialog.setCanceledOnTouchOutside(mCancelable);
+            dialog.setOnCancelListener(mOnCancelListener);
+            dialog.setOnDismissListener(mOnDismissListener);
+
+            return dialog;
+        }
+    }
+}
diff --git a/car/core/src/main/java/androidx/car/app/CarSingleChoiceDialog.java b/car/core/src/main/java/androidx/car/app/CarSingleChoiceDialog.java
index ea91bd1..ac4e043 100644
--- a/car/core/src/main/java/androidx/car/app/CarSingleChoiceDialog.java
+++ b/car/core/src/main/java/androidx/car/app/CarSingleChoiceDialog.java
@@ -109,7 +109,7 @@
         super.onCreate(savedInstanceState);
 
         Window window = getWindow();
-        window.setContentView(R.layout.car_selction_dialog);
+        window.setContentView(R.layout.car_selection_dialog);
 
         // Ensure that the dialog takes up the entire window. This is needed because the scrollbar
         // needs to be drawn off the dialog.
diff --git a/jetifier/jetifier/migration.config b/jetifier/jetifier/migration.config
index 62f6add..452adad 100644
--- a/jetifier/jetifier/migration.config
+++ b/jetifier/jetifier/migration.config
@@ -702,6 +702,10 @@
       "to": "ignore"
     },
     {
+      "from": "androidx/browser/trusted/(.*)",
+      "to": "ignore"
+    },
+    {
       "from": "androidx/browser/R(.*)",
       "to": "ignore"
     },
diff --git a/lifecycle/livedata/ktx/api/2.2.0-alpha02.txt b/lifecycle/livedata/ktx/api/2.2.0-alpha02.txt
index 689fb6b..9dc2bee 100644
--- a/lifecycle/livedata/ktx/api/2.2.0-alpha02.txt
+++ b/lifecycle/livedata/ktx/api/2.2.0-alpha02.txt
@@ -14,8 +14,8 @@
   public interface LiveDataScope<T> {
     method public suspend Object! emit(T? value, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public suspend Object emitSource(androidx.lifecycle.LiveData<T> source, kotlin.coroutines.Continuation<? super kotlinx.coroutines.DisposableHandle> p);
-    method public T? getInitialValue();
-    property public abstract T? initialValue;
+    method public T? getLatestValue();
+    property public abstract T? latestValue;
   }
 
   public final class TransformationsKt {
diff --git a/lifecycle/livedata/ktx/api/current.txt b/lifecycle/livedata/ktx/api/current.txt
index 689fb6b..9dc2bee 100644
--- a/lifecycle/livedata/ktx/api/current.txt
+++ b/lifecycle/livedata/ktx/api/current.txt
@@ -14,8 +14,8 @@
   public interface LiveDataScope<T> {
     method public suspend Object! emit(T? value, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public suspend Object emitSource(androidx.lifecycle.LiveData<T> source, kotlin.coroutines.Continuation<? super kotlinx.coroutines.DisposableHandle> p);
-    method public T? getInitialValue();
-    property public abstract T? initialValue;
+    method public T? getLatestValue();
+    property public abstract T? latestValue;
   }
 
   public final class TransformationsKt {
diff --git a/lifecycle/livedata/ktx/src/main/java/androidx/lifecycle/CoroutineLiveData.kt b/lifecycle/livedata/ktx/src/main/java/androidx/lifecycle/CoroutineLiveData.kt
index 905e846..f6e7ea8 100644
--- a/lifecycle/livedata/ktx/src/main/java/androidx/lifecycle/CoroutineLiveData.kt
+++ b/lifecycle/livedata/ktx/src/main/java/androidx/lifecycle/CoroutineLiveData.kt
@@ -64,22 +64,25 @@
     suspend fun emitSource(source: LiveData<T>): DisposableHandle
 
     /**
-     * Denotes the value of the [LiveData] when this block is started.
+     * References the current value of the [LiveData].
      *
-     * If it is the first time block is running, [initialValue] will be `null`. You can use this
+     * If the block never `emit`ed a value, [latestValue] will be `null`. You can use this
      * value to check what was then latest value `emit`ed by your `block` before it got cancelled.
      *
-     * Note that if the block called [emitSource], then `initialValue` will be last value
+     * Note that if the block called [emitSource], then `latestValue` will be last value
      * dispatched by the `source` [LiveData].
      */
-    val initialValue: T?
+    val latestValue: T?
 }
 
 internal class LiveDataScopeImpl<T>(
     internal var target: CoroutineLiveData<T>,
-    context: CoroutineContext,
-    override val initialValue: T? = target.value
+    context: CoroutineContext
 ) : LiveDataScope<T> {
+
+    override val latestValue: T?
+        get() = target.value
+
     // use `liveData` provided context + main dispatcher to communicate with the target
     // LiveData. This gives us main thread safety as well as cancellation cooperation
     private val coroutineContext = context + Dispatchers.Main
@@ -231,7 +234,7 @@
  *
  * After a cancellation, if the [LiveData] becomes active again, the [block] will be re-executed
  * from the beginning. If you would like to continue the operations based on where it was stopped
- * last, you can use the [LiveDataScope.initialValue] function to get the last
+ * last, you can use the [LiveDataScope.latestValue] function to get the last
  * [LiveDataScope.emit]ed value.
 
  * If the [block] completes successfully *or* is cancelled due to reasons other than [LiveData]
diff --git a/lifecycle/livedata/ktx/src/main/java/androidx/lifecycle/CoroutineLiveDataApi26.kt b/lifecycle/livedata/ktx/src/main/java/androidx/lifecycle/CoroutineLiveDataApi26.kt
index 6c890fd7..57b7756 100644
--- a/lifecycle/livedata/ktx/src/main/java/androidx/lifecycle/CoroutineLiveDataApi26.kt
+++ b/lifecycle/livedata/ktx/src/main/java/androidx/lifecycle/CoroutineLiveDataApi26.kt
@@ -38,7 +38,7 @@
  *
  * After a cancellation, if the [LiveData] becomes active again, the [block] will be re-executed
  * from the beginning. If you would like to continue the operations based on where it was stopped
- * last, you can use the [LiveDataScope.initialValue] function to get the last
+ * last, you can use the [LiveDataScope.latestValue] function to get the last
  * [LiveDataScope.emit]ed value.
 
  * If the [block] completes successfully *or* is cancelled due to reasons other than [LiveData]
diff --git a/lifecycle/livedata/ktx/src/test/java/androidx/lifecycle/BuildLiveDataTest.kt b/lifecycle/livedata/ktx/src/test/java/androidx/lifecycle/BuildLiveDataTest.kt
index aa71bcf..2f53b3b 100644
--- a/lifecycle/livedata/ktx/src/test/java/androidx/lifecycle/BuildLiveDataTest.kt
+++ b/lifecycle/livedata/ktx/src/test/java/androidx/lifecycle/BuildLiveDataTest.kt
@@ -212,41 +212,41 @@
     }
 
     @Test
-    fun readInitialValue() {
-        val initial = AtomicReference<Int?>()
+    fun readLatestValue() {
+        val latest = AtomicReference<Int?>()
         val ld = liveData<Int>(testContext) {
-            initial.set(initialValue)
+            latest.set(latestValue)
         }
         runOnMain {
             ld.value = 3
         }
         ld.addObserver()
         triggerAllActions()
-        assertThat(initial.get()).isEqualTo(3)
+        assertThat(latest.get()).isEqualTo(3)
     }
 
     @Test
-    fun readInitialValue_ignoreYielded() {
-        val initial = AtomicReference<Int?>()
+    fun readLatestValue_readWithinBlock() {
+        val latest = AtomicReference<Int?>()
         val ld = liveData<Int>(testContext) {
             emit(5)
-            initial.set(initialValue)
+            latest.set(latestValue)
         }
         ld.addObserver()
         triggerAllActions()
-        assertThat(initial.get()).isNull()
+        assertThat(latest.get()).isEqualTo(5)
     }
 
     @Test
-    fun readInitialValue_keepYieldedFromBefore() {
-        val initial = AtomicReference<Int?>()
+    fun readLatestValue_keepYieldedFromBefore() {
+        val latest = AtomicReference<Int?>()
         val ld = liveData<Int>(testContext, 10) {
-            if (initialValue == null) {
+            if (latestValue == null) {
                 emit(5)
                 delay(500000) // wait for cancellation
             }
 
-            initial.set(initialValue)
+            latest.set(latestValue)
         }
         ld.addObserver().apply {
             triggerAllActions()
@@ -256,10 +256,10 @@
         triggerAllActions()
         // wait for it to be cancelled
         advanceTimeBy(10)
-        assertThat(initial.get()).isNull()
+        assertThat(latest.get()).isNull()
         ld.addObserver()
         triggerAllActions()
-        assertThat(initial.get()).isEqualTo(5)
+        assertThat(latest.get()).isEqualTo(5)
     }
 
     @Test
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaController.java b/media2/session/src/main/java/androidx/media2/session/MediaController.java
index 40f44dc..1f37d9d 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaController.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaController.java
@@ -1764,6 +1764,8 @@
         /**
          * Called when the player's current item is changed. It's also called after
          * {@link #setPlaylist} or {@link #setMediaItem}.
+         * Also called when {@link MediaItem#setMetadata(MediaMetadata)} is called on the current
+         * media item.
          * <p>
          * When it's called, you should invalidate previous playback information and wait for later
          * callbacks. Also, current, previous, and next media item indices may need to be updated.
@@ -1779,6 +1781,8 @@
         /**
          * Called when a playlist is changed. It's also called after {@link #setPlaylist} or
          * {@link #setMediaItem}.
+         * Also called when {@link MediaItem#setMetadata(MediaMetadata)} is called on a media item
+         * that is contained in the current playlist.
          * <p>
          * When it's called, current, previous, and next media item indices may need to be updated.
          *
diff --git a/navigation/fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.java b/navigation/fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.java
index b417f7a..3505e37 100644
--- a/navigation/fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.java
+++ b/navigation/fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.java
@@ -127,6 +127,9 @@
     @Override
     @Nullable
     public Bundle onSaveState() {
+        if (mDialogCount == 0) {
+            return null;
+        }
         Bundle b = new Bundle();
         b.putInt(KEY_DIALOG_COUNT, mDialogCount);
         return b;
diff --git a/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java b/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java
index 90882eb..ad55ba1 100644
--- a/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java
+++ b/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java
@@ -325,13 +325,15 @@
             @Nullable Bundle savedInstanceState) {
         super.onInflate(context, attrs, savedInstanceState);
 
-        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
-        final int graphId = a.getResourceId(R.styleable.NavHostFragment_navGraph, 0);
-        final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
-
+        final TypedArray navHost = context.obtainStyledAttributes(attrs, R.styleable.NavHost);
+        final int graphId = navHost.getResourceId(R.styleable.NavHost_navGraph, 0);
         if (graphId != 0) {
             mGraphId = graphId;
         }
+        navHost.recycle();
+
+        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
+        final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
         if (defaultHost) {
             mDefaultNavHost = true;
         }
diff --git a/navigation/fragment/src/main/res-public/values/public_attrs.xml b/navigation/fragment/src/main/res-public/values/public_attrs.xml
index 8570072..1b5d9cc 100644
--- a/navigation/fragment/src/main/res-public/values/public_attrs.xml
+++ b/navigation/fragment/src/main/res-public/values/public_attrs.xml
@@ -18,5 +18,4 @@
 <!-- Definitions of attributes to be exposed as public -->
 <resources>
     <public type="attr" name="defaultNavHost"/>
-    <public type="attr" name="navGraph"/>
 </resources>
diff --git a/navigation/fragment/src/main/res/values/attrs.xml b/navigation/fragment/src/main/res/values/attrs.xml
index 3ed78f8..a199f31 100644
--- a/navigation/fragment/src/main/res/values/attrs.xml
+++ b/navigation/fragment/src/main/res/values/attrs.xml
@@ -23,7 +23,6 @@
     </declare-styleable>
 
     <declare-styleable name="NavHostFragment">
-        <attr name="navGraph" format="reference" />
         <attr name="defaultNavHost" format="boolean" />
     </declare-styleable>
 </resources>
diff --git a/navigation/runtime/src/main/res-public/values/public_attrs.xml b/navigation/runtime/src/main/res-public/values/public_attrs.xml
index 6aa2525..2a3e7d8e 100644
--- a/navigation/runtime/src/main/res-public/values/public_attrs.xml
+++ b/navigation/runtime/src/main/res-public/values/public_attrs.xml
@@ -21,4 +21,5 @@
     <public type="attr" name="data"/>
     <public type="attr" name="dataPattern"/>
     <public type="attr" name="graph"/>
+    <public type="attr" name="navGraph"/>
 </resources>
diff --git a/navigation/runtime/src/main/res/values/attrs.xml b/navigation/runtime/src/main/res/values/attrs.xml
index 59945eb..b15e4ec 100644
--- a/navigation/runtime/src/main/res/values/attrs.xml
+++ b/navigation/runtime/src/main/res/values/attrs.xml
@@ -20,6 +20,10 @@
         <attr name="targetPackage" format="string" />
     </declare-styleable>
 
+    <declare-styleable name="NavHost">
+        <attr name="navGraph" format="reference" />
+    </declare-styleable>
+
     <declare-styleable name="NavInclude">
         <attr name="graph" format="reference" />
     </declare-styleable>
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt
index 724dc3b..fd44206 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt
@@ -64,6 +64,7 @@
                 inTransaction = inTransaction,
                 queryResultBinder = resultBinder
         )
+        // TODO: Lift this restriction, to allow for INSERT, UPDATE and DELETE raw statements.
         context.checker.check(rawQueryMethod.returnsValue, executableElement,
                 ProcessorErrors.RAW_QUERY_BAD_RETURN_TYPE)
         return rawQueryMethod
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/RawQueryMethod.kt b/room/compiler/src/main/kotlin/androidx/room/vo/RawQueryMethod.kt
index 027bd45..0ffe998 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/RawQueryMethod.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/RawQueryMethod.kt
@@ -17,6 +17,7 @@
 package androidx.room.vo
 
 import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.KotlinTypeNames
 import androidx.room.ext.SupportDbTypeNames
 import androidx.room.ext.typeName
 import androidx.room.solver.query.result.QueryResultBinder
@@ -29,20 +30,22 @@
  * It is self sufficient and must have all generics etc resolved once created.
  */
 data class RawQueryMethod(
-        val element: ExecutableElement,
-        val name: String,
-        val returnType: TypeMirror,
-        val inTransaction: Boolean,
-        val observedTableNames: Set<String>,
-        val runtimeQueryParam: RuntimeQueryParameter?,
-        val queryResultBinder: QueryResultBinder) {
+    val element: ExecutableElement,
+    val name: String,
+    val returnType: TypeMirror,
+    val inTransaction: Boolean,
+    val observedTableNames: Set<String>,
+    val runtimeQueryParam: RuntimeQueryParameter?,
+    val queryResultBinder: QueryResultBinder
+) {
     val returnsValue by lazy {
-        returnType.typeName() != TypeName.VOID
+        returnType.typeName() != TypeName.VOID && returnType.typeName() != KotlinTypeNames.UNIT
     }
 
     data class RuntimeQueryParameter(
-            val paramName: String,
-            val type: TypeName) {
+        val paramName: String,
+        val type: TypeName
+    ) {
         fun isString() = CommonTypeNames.STRING == type
         fun isSupportQuery() = SupportDbTypeNames.QUERY == type
     }
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
index d6d6269..2b82d06 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
@@ -31,6 +31,7 @@
 import androidx.room.testing.TestInvocation
 import androidx.room.testing.TestProcessor
 import androidx.room.vo.RawQueryMethod
+import androidx.sqlite.db.SupportSQLiteQuery
 import com.google.auto.common.MoreElements
 import com.google.auto.common.MoreTypes
 import com.google.common.truth.Truth
@@ -43,6 +44,8 @@
 import org.hamcrest.CoreMatchers.`is`
 import org.hamcrest.MatcherAssert.assertThat
 import org.junit.Test
+import simpleRun
+import javax.lang.model.util.ElementFilter
 
 class RawQueryMethodProcessorTest {
     @Test
@@ -185,6 +188,27 @@
         )
     }
 
+    interface RawQuerySuspendUnitDao {
+        @RawQuery
+        suspend fun foo(query: SupportSQLiteQuery)
+    }
+
+    @Test
+    fun suspendUnit() {
+        simpleRun { invocation ->
+            val daoElement =
+                invocation.typeElement(RawQuerySuspendUnitDao::class.java.canonicalName)
+            val daoFunctionElement = ElementFilter.methodsIn(daoElement.enclosedElements).first()
+            RawQueryMethodProcessor(
+                baseContext = invocation.context,
+                containing = MoreTypes.asDeclared(daoElement.asType()),
+                executableElement = daoFunctionElement
+            ).process()
+        }.failsToCompile().withErrorContaining(
+            ProcessorErrors.RAW_QUERY_BAD_RETURN_TYPE
+        )
+    }
+
     @Test
     fun noArgs() {
         singleQueryMethod(
@@ -270,14 +294,14 @@
     }
 
     private fun singleQueryMethod(
-            vararg input: String,
-            handler: (RawQueryMethod, TestInvocation) -> Unit
+        vararg input: String,
+        handler: (RawQueryMethod, TestInvocation) -> Unit
     ): CompileTester {
         return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
                 .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
-                        DAO_PREFIX
-                                + input.joinToString("\n")
-                                + DAO_SUFFIX
+                        DAO_PREFIX +
+                                input.joinToString("\n") +
+                                DAO_SUFFIX
                 ), COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER,
                         COMMON.DATA_SOURCE_FACTORY, COMMON.POSITIONAL_DATA_SOURCE,
                         COMMON.NOT_AN_ENTITY))
diff --git a/samples/SupportCarDemos/src/main/AndroidManifest.xml b/samples/SupportCarDemos/src/main/AndroidManifest.xml
index 495ccc2..1300c61 100644
--- a/samples/SupportCarDemos/src/main/AndroidManifest.xml
+++ b/samples/SupportCarDemos/src/main/AndroidManifest.xml
@@ -252,13 +252,23 @@
         </activity>
 
         <activity android:name=".CarSingleChoiceDialogDemoActivity"
-            android:label="CarSingleSelectionDialog Demo"
+            android:label="CarSingleChoiceDialog Demo"
             android:parentActivityName=".SupportCarDemoActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.SAMPLE_CODE"/>
             </intent-filter>
         </activity>
+
+        <activity android:name=".CarMultipleChoiceDialogDemoActivity"
+            android:label="CarMultipleChoiceDialog Demo"
+            android:parentActivityName=".SupportCarDemoActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.SAMPLE_CODE"/>
+            </intent-filter>
+        </activity>
+
         <activity android:name=".CheckBoxListItemActivity"
             android:label="CheckBoxListItem Demo"
             android:parentActivityName=".SupportCarDemoActivity">
diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarMultipleChoiceDialogDemoActivity.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarMultipleChoiceDialogDemoActivity.java
new file mode 100644
index 0000000..adee9cf
--- /dev/null
+++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarMultipleChoiceDialogDemoActivity.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+
+package com.example.androidx.car;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import androidx.car.app.CarMultipleChoiceDialog;
+import androidx.car.widget.CarToolbar;
+import androidx.fragment.app.FragmentActivity;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * An activity to demo a CarMultipleChoiceDialog. Clicking the create button will display a dialog
+ * with a list of multiple-choice list items.
+ */
+public class CarMultipleChoiceDialogDemoActivity extends FragmentActivity {
+
+    private static final int NUM_OF_ITEMS_WITH_TITLE = 5;
+    private static final int NUM_OF_ITEMS_WITH_TITLE_AND_BODY = 5;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.selection_dialog_activity);
+
+        CarToolbar toolbar = findViewById(R.id.car_toolbar);
+        toolbar.setTitle(R.string.multiple_choice_dialog_title);
+        toolbar.setNavigationIconOnClickListener(v -> onNavigateUp());
+
+        findViewById(R.id.create_dialog).setOnClickListener(v -> createAndShowDialog());
+    }
+
+    private void createAndShowDialog() {
+        Dialog test = new CarMultipleChoiceDialog.Builder(this)
+                .setTitle("Sample multiple choice dialog")
+                .setBody("You can put any generic body text here.")
+                .setItems(createSelectionItems())
+                .setPositiveButton("Okay",
+                        (dialog, checkedItems) -> Toast.makeText(
+                                CarMultipleChoiceDialogDemoActivity.this,
+                                String.format(Locale.getDefault(), "Checked items: %s",
+                                        Arrays.toString(checkedItems)), Toast.LENGTH_SHORT).show())
+                .setNegativeButton("Cancel")
+                .create();
+        test.setCanceledOnTouchOutside(true);
+        test.show();
+    }
+
+    private List<CarMultipleChoiceDialog.Item> createSelectionItems() {
+        List<CarMultipleChoiceDialog.Item> items = new ArrayList<>();
+
+        CarMultipleChoiceDialog.Item item;
+
+        for (int i = 0; i < NUM_OF_ITEMS_WITH_TITLE; i++) {
+            item = new CarMultipleChoiceDialog.Item(
+                    String.format(Locale.getDefault(), "Item with index: %d", i), false);
+            items.add(item);
+        }
+
+        for (int i = 0; i < NUM_OF_ITEMS_WITH_TITLE_AND_BODY; i++) {
+            item = new CarMultipleChoiceDialog.Item(
+                    String.format(Locale.getDefault(), "Item with index: %d", i), "With body text",
+                    false);
+            items.add(item);
+        }
+
+        return items;
+    }
+}
diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarSingleChoiceDialogDemoActivity.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarSingleChoiceDialogDemoActivity.java
index a99e325..b540b14 100644
--- a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarSingleChoiceDialogDemoActivity.java
+++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarSingleChoiceDialogDemoActivity.java
@@ -39,7 +39,7 @@
         setContentView(R.layout.selection_dialog_activity);
 
         CarToolbar toolbar = findViewById(R.id.car_toolbar);
-        toolbar.setTitle(R.string.single_selection_dialog_title);
+        toolbar.setTitle(R.string.single_choice_dialog_title);
         toolbar.setNavigationIconOnClickListener(v -> onNavigateUp());
 
         findViewById(R.id.create_dialog).setOnClickListener(v -> createAndShowDialog());
@@ -47,7 +47,7 @@
 
     private void createAndShowDialog() {
         Dialog test = new CarSingleChoiceDialog.Builder(this)
-                .setTitle("Sample single selection dialog")
+                .setTitle("Sample single choice dialog")
                 .setBody("You can put any generic body text here.")
                 .setItems(createSelectionItems(), 0)
                 .setPositiveButton("Okay",
diff --git a/samples/SupportCarDemos/src/main/res/values/strings.xml b/samples/SupportCarDemos/src/main/res/values/strings.xml
index fd26a43..d70ef46 100644
--- a/samples/SupportCarDemos/src/main/res/values/strings.xml
+++ b/samples/SupportCarDemos/src/main/res/values/strings.xml
@@ -27,7 +27,9 @@
     <string name="checkbox_list_item_title">CheckBoxListItem</string>
     <string name="sub_header_list_item_title">SubheaderListItem</string>
     <string name="alpha_jump_title">Alpha Jump Demo</string>
-    <string name="single_selection_dialog_title">CarSingleChoiceDialog Demo</string>
+    <string name="single_choice_dialog_title">CarSingleChoiceDialog Demo</string>
+    <string name="multiple_choice_dialog_title">CarMultipleChoiceChoiceDialog Demo</string>
+
 
     <string name="long_text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</string>
     <string name="super_long_text">
diff --git a/ui/platform/api/1.0.0-alpha01.txt b/ui/platform/api/1.0.0-alpha01.txt
index c18d568..765675c 100644
--- a/ui/platform/api/1.0.0-alpha01.txt
+++ b/ui/platform/api/1.0.0-alpha01.txt
@@ -332,6 +332,10 @@
     ctor public EditableUtilKt();
   }
 
+  public final class RecordingInputConnectionKt {
+    ctor public RecordingInputConnectionKt();
+  }
+
 }
 
 package androidx.ui.core.pointerinput {
diff --git a/ui/platform/api/current.txt b/ui/platform/api/current.txt
index c18d568..765675c 100644
--- a/ui/platform/api/current.txt
+++ b/ui/platform/api/current.txt
@@ -332,6 +332,10 @@
     ctor public EditableUtilKt();
   }
 
+  public final class RecordingInputConnectionKt {
+    ctor public RecordingInputConnectionKt();
+  }
+
 }
 
 package androidx.ui.core.pointerinput {
diff --git a/ui/platform/src/androidTest/java/androidx/ui/core/input/RecordingInputConnectionTest.kt b/ui/platform/src/androidTest/java/androidx/ui/core/input/RecordingInputConnectionTest.kt
new file mode 100644
index 0000000..42f9ad9
--- /dev/null
+++ b/ui/platform/src/androidTest/java/androidx/ui/core/input/RecordingInputConnectionTest.kt
@@ -0,0 +1,167 @@
+/*
+ * 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.
+ */
+
+package androidx.ui.core.input
+
+import androidx.test.filters.SmallTest
+import androidx.ui.core.TextRange
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.argumentCaptor
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.never
+import com.nhaarman.mockitokotlin2.times
+import com.nhaarman.mockitokotlin2.verify
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class RecordingInputConnectionTest {
+
+    private lateinit var ic: RecordingInputConnection
+    private lateinit var listener: InputEventListener
+
+    @Before
+    fun setup() {
+        listener = mock()
+        ic = RecordingInputConnection(listener)
+    }
+
+    @Test
+    fun getTextBeforeAndAfterCursorTest() {
+        assertEquals("", ic.getTextBeforeCursor(100, 0))
+        assertEquals("", ic.getTextAfterCursor(100, 0))
+
+        // Set "Hello, World", and place the cursor at the beginning of the text.
+        ic.inputState = InputState(
+            text = "Hello, World",
+            selection = TextRange(0, 0))
+
+        assertEquals("", ic.getTextBeforeCursor(100, 0))
+        assertEquals("Hello, World", ic.getTextAfterCursor(100, 0))
+
+        // Set "Hello, World", and place the cursor between "H" and "e".
+        ic.inputState = InputState(
+            text = "Hello, World",
+            selection = TextRange(1, 1))
+
+        assertEquals("H", ic.getTextBeforeCursor(100, 0))
+        assertEquals("ello, World", ic.getTextAfterCursor(100, 0))
+
+        // Set "Hello, World", and place the cursor at the end of the text.
+        ic.inputState = InputState(
+            text = "Hello, World",
+            selection = TextRange(12, 12))
+
+        assertEquals("Hello, World", ic.getTextBeforeCursor(100, 0))
+        assertEquals("", ic.getTextAfterCursor(100, 0))
+    }
+
+    @Test
+    fun getTextBeforeAndAfterCursorTest_maxCharTest() {
+        // Set "Hello, World", and place the cursor at the beginning of the text.
+        ic.inputState = InputState(
+            text = "Hello, World",
+            selection = TextRange(0, 0))
+
+        assertEquals("", ic.getTextBeforeCursor(5, 0))
+        assertEquals("Hello", ic.getTextAfterCursor(5, 0))
+
+        // Set "Hello, World", and place the cursor between "H" and "e".
+        ic.inputState = InputState(
+            text = "Hello, World",
+            selection = TextRange(1, 1))
+
+        assertEquals("H", ic.getTextBeforeCursor(5, 0))
+        assertEquals("ello,", ic.getTextAfterCursor(5, 0))
+
+        // Set "Hello, World", and place the cursor at the end of the text.
+        ic.inputState = InputState(
+            text = "Hello, World",
+            selection = TextRange(12, 12))
+
+        assertEquals("World", ic.getTextBeforeCursor(5, 0))
+        assertEquals("", ic.getTextAfterCursor(5, 0))
+    }
+
+    @Test
+    fun getSelectedTextTest() {
+        // Set "Hello, World", and place the cursor at the beginning of the text.
+        ic.inputState = InputState(
+            text = "Hello, World",
+            selection = TextRange(0, 0))
+
+        assertEquals("", ic.getSelectedText(0))
+
+        // Set "Hello, World", and place the cursor between "H" and "e".
+        ic.inputState = InputState(
+            text = "Hello, World",
+            selection = TextRange(0, 1))
+
+        assertEquals("H", ic.getSelectedText(0))
+
+        // Set "Hello, World", and place the cursor at the end of the text.
+        ic.inputState = InputState(
+            text = "Hello, World",
+            selection = TextRange(0, 12))
+
+        assertEquals("Hello, World", ic.getSelectedText(0))
+    }
+
+    @Test
+    fun commitTextTest() {
+        val captor = argumentCaptor<List<EditOperation>>()
+
+        ic.inputState = InputState(text = "", selection = TextRange(0, 0))
+
+        // Inserting "Hello, " into the empty text field.
+        assertTrue(ic.commitText("Hello, ", 1))
+
+        verify(listener, times(1)).onEditOperations(captor.capture())
+        val editOps = captor.lastValue
+        assertEquals(1, editOps.size)
+        assertEquals(CommitTextEditOp("Hello, ", 1), editOps[0])
+    }
+
+    @Test
+    fun commitTextTest_batchSession() {
+        val captor = argumentCaptor<List<EditOperation>>()
+
+        ic.inputState = InputState(text = "", selection = TextRange(0, 0))
+
+        // IME set text "Hello, World." with two commitText API within the single batch session.
+        // Do not callback to listener during batch session.
+        ic.beginBatchEdit()
+
+        assertTrue(ic.commitText("Hello, ", 1))
+        verify(listener, never()).onEditOperations(any())
+
+        assertTrue(ic.commitText("World.", 1))
+        verify(listener, never()).onEditOperations(any())
+
+        ic.endBatchEdit()
+
+        verify(listener, times(1)).onEditOperations(captor.capture())
+        val editOps = captor.lastValue
+        assertEquals(2, editOps.size)
+        assertEquals(CommitTextEditOp("Hello, ", 1), editOps[0])
+        assertEquals(CommitTextEditOp("World.", 1), editOps[1])
+    }
+}
\ No newline at end of file
diff --git a/ui/platform/src/main/java/androidx/ui/core/input/EditOperation.kt b/ui/platform/src/main/java/androidx/ui/core/input/EditOperation.kt
new file mode 100644
index 0000000..bbd1044
--- /dev/null
+++ b/ui/platform/src/main/java/androidx/ui/core/input/EditOperation.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+package androidx.ui.core.input
+
+/**
+ * An enum class for the type of edit operations.
+ */
+internal enum class OpType {
+    COMMIT_TEXT,
+    // TODO(nona): Introduce other API callback, setComposingRange, etc.
+}
+
+/**
+ * A base class of all EditOperations
+ *
+ * An EditOperation is a representation of platform IME API call. For example, in Android,
+ * InputConnection#commitText API call is translated to CommitTextEditOp object.
+ */
+internal open class EditOperation(val type: OpType)
+
+/**
+ * An edit opration represent commitText callback from InputMethod.
+ * @see https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#commitText(java.lang.CharSequence,%20int)
+ */
+internal data class CommitTextEditOp(
+    /**
+     * The text to commit. We ignore any styles in the original API.
+     */
+    val text: String,
+
+    /**
+     * The cursor position after inserted text.
+     * See original commitText API docs for more details.
+     */
+    val newCursorPostion: Int
+) : EditOperation(OpType.COMMIT_TEXT)
diff --git a/ui/platform/src/main/java/androidx/ui/core/input/InputEventListener.kt b/ui/platform/src/main/java/androidx/ui/core/input/InputEventListener.kt
new file mode 100644
index 0000000..b4b7c24
--- /dev/null
+++ b/ui/platform/src/main/java/androidx/ui/core/input/InputEventListener.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+package androidx.ui.core.input
+
+/**
+ * An interface of listening IME events.
+ */
+internal interface InputEventListener {
+    /**
+     * Called when IME sends some input events.
+     *
+     * @param editOps The list of edit operations.
+     */
+    fun onEditOperations(editOps: List<EditOperation>)
+
+    // TODO(nona): add more input event callbacks, editor action etc.
+}
\ No newline at end of file
diff --git a/ui/platform/src/main/java/androidx/ui/core/input/InputState.kt b/ui/platform/src/main/java/androidx/ui/core/input/InputState.kt
new file mode 100644
index 0000000..e1c3c55
--- /dev/null
+++ b/ui/platform/src/main/java/androidx/ui/core/input/InputState.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+package androidx.ui.core.input
+
+import androidx.ui.core.TextRange
+import androidx.ui.core.substring
+
+/**
+ * Stores an input state for IME
+ *
+ * IME can request editor state with calling getTextBeforeCursor, getSelectedText, etc.
+ * This class stores a snapshot of the input state of the edit buffer and provide utility functions
+ * for answering these information retrieval requests.
+ */
+internal data class InputState(
+    /**
+     * A text visible to IME
+     */
+    val text: String,
+
+    /**
+     * A selection range visible to IME.
+     * The selection range must be valid range in the given text.
+     */
+    val selection: TextRange,
+
+    /**
+     * A composition range visible to IME.
+     * If null, there is no composition range.
+     * If non-null, the composition range must be valid range in the given text.
+     */
+    val composition: TextRange? = null
+) {
+
+    /**
+     * Helper function for getting text before selection range.
+     */
+    fun getTextBeforeSelection(maxChars: Int): String =
+        text.substring(Math.max(0, selection.start - maxChars), selection.start)
+
+    /**
+     * Helper function for getting text after selection range.
+     */
+    fun getTextAfterSelection(maxChars: Int): String =
+        text.substring(selection.end, Math.min(selection.end + maxChars, text.length))
+
+    /**
+     * Helper function for getting text currently selected.
+     */
+    fun getSelectedText(): String = text.substring(selection)
+}
diff --git a/ui/platform/src/main/java/androidx/ui/core/input/RecordingInputConnection.kt b/ui/platform/src/main/java/androidx/ui/core/input/RecordingInputConnection.kt
new file mode 100644
index 0000000..6921510
--- /dev/null
+++ b/ui/platform/src/main/java/androidx/ui/core/input/RecordingInputConnection.kt
@@ -0,0 +1,235 @@
+/*
+ * 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.
+ */
+
+package androidx.ui.core.input
+
+import android.os.Bundle
+import android.os.Handler
+import android.text.TextUtils
+import android.util.Log
+import android.view.KeyEvent
+import android.view.inputmethod.CompletionInfo
+import android.view.inputmethod.CorrectionInfo
+import android.view.inputmethod.ExtractedText
+import android.view.inputmethod.ExtractedTextRequest
+import android.view.inputmethod.InputConnection
+import android.view.inputmethod.InputContentInfo
+import androidx.ui.core.TextRange
+
+private val DEBUG = false
+private val TAG = "RecordingIC"
+
+internal class RecordingInputConnection(
+    /**
+     * An input event listener.
+     */
+    val eventListener: InputEventListener
+) : InputConnection {
+
+    // The depth of the batch session. 0 means no session.
+    private var batchDepth: Int = 0
+
+    // The input state.
+    var inputState: InputState = InputState("", TextRange(0, 0), null)
+        set(value) {
+            if (DEBUG) { Log.d(TAG, "New InputState has set: $inputState") }
+            field = value
+        }
+
+    // The recoding editing ops.
+    private val editOps = mutableListOf<EditOperation>()
+
+    // /////////////////////////////////////////////////////////////////////////////////////////////
+    // Callbacks for text editing session
+    // /////////////////////////////////////////////////////////////////////////////////////////////
+
+    override fun beginBatchEdit(): Boolean {
+        if (DEBUG) { Log.d(TAG, "beginBatchEdit()") }
+        batchDepth++
+        return true
+    }
+
+    override fun endBatchEdit(): Boolean {
+        if (DEBUG) { Log.d(TAG, "endBatchEdit()") }
+        batchDepth--
+        if (batchDepth == 0) {
+            eventListener.onEditOperations(editOps.toList())
+            editOps.clear()
+        }
+        return batchDepth > 0
+    }
+
+    override fun closeConnection() {
+        if (DEBUG) { Log.d(TAG, "closeConnection()") }
+        TODO("not implemented")
+    }
+
+    // /////////////////////////////////////////////////////////////////////////////////////////////
+    // Callbacks for text editing
+    // /////////////////////////////////////////////////////////////////////////////////////////////
+
+    override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean {
+        if (DEBUG) { Log.d(TAG, "commitText($text, $newCursorPosition)") }
+        beginBatchEdit()
+        try {
+            editOps.add(CommitTextEditOp(text.toString(), newCursorPosition))
+        } finally {
+            endBatchEdit()
+        }
+        return true
+    }
+
+    override fun setComposingRegion(start: Int, end: Int): Boolean {
+        if (DEBUG) { Log.d(TAG, "setComposingRegion($start, $end)") }
+        TODO("not implemented")
+    }
+
+    override fun setComposingText(text: CharSequence?, newCursorPosition: Int): Boolean {
+        if (DEBUG) { Log.d(TAG, "setComposingText($text, $newCursorPosition)") }
+        TODO("not implemented")
+    }
+
+    override fun deleteSurroundingTextInCodePoints(beforeLength: Int, afterLength: Int): Boolean {
+        if (DEBUG) { Log.d(TAG, "deleteSurroundingTextInCodePoints($beforeLength, $afterLength)") }
+        TODO("not implemented")
+    }
+
+    override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
+        if (DEBUG) { Log.d(TAG, "deleteSurroundingText($beforeLength, $afterLength)") }
+        TODO("not implemented")
+    }
+
+    override fun setSelection(start: Int, end: Int): Boolean {
+        if (DEBUG) { Log.d(TAG, "setSelection($start, $end)") }
+        TODO("not implemented")
+    }
+
+    override fun finishComposingText(): Boolean {
+        if (DEBUG) { Log.d(TAG, "finishComposingText()") }
+        TODO("not implemented")
+    }
+
+    override fun sendKeyEvent(event: KeyEvent?): Boolean {
+        if (DEBUG) { Log.d(TAG, "sendKeyEvent($event)") }
+        TODO("not implemented")
+    }
+
+    // /////////////////////////////////////////////////////////////////////////////////////////////
+    // Callbacks for retrieving editing buffers
+    // /////////////////////////////////////////////////////////////////////////////////////////////
+
+    override fun getTextBeforeCursor(maxChars: Int, flags: Int): CharSequence {
+        if (DEBUG) { Log.d(TAG, "getTextBeforeCursor($maxChars, $flags)") }
+        return inputState.getTextBeforeSelection(maxChars)
+    }
+
+    override fun getTextAfterCursor(maxChars: Int, flags: Int): CharSequence {
+        if (DEBUG) { Log.d(TAG, "getTextAfterCursor($maxChars, $flags)") }
+        return inputState.getTextAfterSelection(maxChars)
+    }
+
+    override fun getSelectedText(flags: Int): CharSequence {
+        if (DEBUG) { Log.d(TAG, "getSelectedText($flags)") }
+        return inputState.getSelectedText()
+    }
+
+    override fun requestCursorUpdates(cursorUpdateMode: Int): Boolean {
+        if (DEBUG) { Log.d(TAG, "requestCursorUpdates($cursorUpdateMode)") }
+        TODO("not implemented")
+    }
+
+    override fun getExtractedText(request: ExtractedTextRequest?, flags: Int): ExtractedText {
+        if (DEBUG) { Log.d(TAG, "getExtractedText($request, $flags)") }
+        TODO("not implemented")
+    }
+
+    // /////////////////////////////////////////////////////////////////////////////////////////////
+    // Editor action and Key events.
+    // /////////////////////////////////////////////////////////////////////////////////////////////
+
+    override fun performContextMenuAction(id: Int): Boolean {
+        if (DEBUG) { Log.d(TAG, "performContextMenuAction($id)") }
+        TODO("not implemented")
+    }
+
+    override fun performEditorAction(editorAction: Int): Boolean {
+        if (DEBUG) { Log.d(TAG, "performEditorAction($editorAction)") }
+        TODO("not implemented")
+    }
+
+    // /////////////////////////////////////////////////////////////////////////////////////////////
+    // Unsupported callbacks
+    // /////////////////////////////////////////////////////////////////////////////////////////////
+
+    override fun commitCompletion(text: CompletionInfo?): Boolean {
+        if (DEBUG) { Log.d(TAG, "commitCompletion(${text?.text})") }
+        // We don't support this callback.
+        // The API documents says this should return if the input connection is no longer valid, but
+        // The Chromium implementation already returning false, so assuming it is safe to return
+        // false if not supported.
+        // see https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnection.java
+        return false
+    }
+
+    override fun commitCorrection(correctionInfo: CorrectionInfo?): Boolean {
+        if (DEBUG) { Log.d(TAG, "commitCorrection($correctionInfo)") }
+        // We don't support this callback.
+        // The API documents says this should return if the input connection is no longer valid, but
+        // The Chromium implementation already returning false, so assuming it is safe to return
+        // false if not supported.
+        // see https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnection.java
+        return false
+    }
+
+    override fun getHandler(): Handler? {
+        if (DEBUG) { Log.d(TAG, "getHandler()") }
+        return null // Returns null means using default Handler
+    }
+
+    override fun clearMetaKeyStates(states: Int): Boolean {
+        if (DEBUG) { Log.d(TAG, "clearMetaKeyStates($states)") }
+        // We don't support this callback.
+        // The API documents says this should return if the input connection is no longer valid, but
+        // The Chromium implementation already returning false, so assuming it is safe to return
+        // false if not supported.
+        // see https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnection.java
+        return false
+    }
+
+    override fun reportFullscreenMode(enabled: Boolean): Boolean {
+        if (DEBUG) { Log.d(TAG, "reportFullscreenMode($enabled)") }
+        return false // This value is ignored according to the API docs.
+    }
+
+    override fun getCursorCapsMode(reqModes: Int): Int {
+        if (DEBUG) { Log.d(TAG, "getCursorCapsMode($reqModes)") }
+        return TextUtils.getCapsMode(inputState.text, inputState.selection.start, reqModes)
+    }
+
+    override fun performPrivateCommand(action: String?, data: Bundle?): Boolean {
+        if (DEBUG) { Log.d(TAG, "performPrivateCommand($action, $data)") }
+        return true // API doc says we should return true even if we didn't understand the command.
+    }
+
+    override fun commitContent(
+        inputContentInfo: InputContentInfo,
+        flags: Int,
+        opts: Bundle?
+    ): Boolean {
+        if (DEBUG) { Log.d(TAG, "commitContent($inputContentInfo, $flags, $opts)") }
+        return false // We don't accept any contents.
+    }
+}
\ No newline at end of file
diff --git a/ui/text/src/androidTest/java/androidx/ui/engine/text/ParagraphIntegrationTest.kt b/ui/text/src/androidTest/java/androidx/ui/engine/text/ParagraphIntegrationTest.kt
index 94ffb0e..9b7fa98 100644
--- a/ui/text/src/androidTest/java/androidx/ui/engine/text/ParagraphIntegrationTest.kt
+++ b/ui/text/src/androidTest/java/androidx/ui/engine/text/ParagraphIntegrationTest.kt
@@ -401,6 +401,74 @@
     }
 
     @Test
+    fun getBoundingBoxForTextPosition_ltr_singleLine() {
+        val text = "abc"
+        val fontSize = 50.0f
+        val paragraph = simpleParagraph(text = text, fontSize = fontSize)
+
+        paragraph.layout(ParagraphConstraints(width = text.length * fontSize))
+        // test positions that are 0, 1, 2 ... which maps to chars 0, 1, 2 ...
+        for (i in 0..text.length - 1) {
+            val textPosition = TextPosition(i, TextAffinity.upstream)
+            val box = paragraph.getBoundingBoxForTextPosition(textPosition)
+            assertThat(box.left, equalTo(i * fontSize))
+            assertThat(box.right, equalTo((i + 1) * fontSize))
+            assertThat(box.top, equalTo(0f))
+            assertThat(box.bottom, equalTo(fontSize))
+        }
+    }
+
+    @Test
+    fun getBoundingBoxForTextPosition_ltr_multiLines() {
+        val firstLine = "abc"
+        val secondLine = "def"
+        val text = firstLine + secondLine
+        val fontSize = 50.0f
+        val paragraph = simpleParagraph(text = text, fontSize = fontSize)
+
+        paragraph.layout(ParagraphConstraints(width = firstLine.length * fontSize))
+
+        // test positions are 3, 4, 5 and always on the second line
+        // which maps to chars 3, 4, 5
+        for (i in 0..secondLine.length - 1) {
+            val textPosition = TextPosition(i + firstLine.length, TextAffinity.upstream)
+            val box = paragraph.getBoundingBoxForTextPosition(textPosition)
+            assertThat(box.left, equalTo(i * fontSize))
+            assertThat(box.right, equalTo((i + 1) * fontSize))
+            assertThat(box.top, equalTo(fontSize))
+            assertThat(box.bottom, equalTo((2f + 1 / 5f) * fontSize))
+        }
+    }
+
+    @Test
+    fun getBoundingBoxForTextPosition_ltr_textPosition_negative() {
+        val text = "abc"
+        val fontSize = 50.0f
+        val paragraph = simpleParagraph(text = text, fontSize = fontSize)
+
+        paragraph.layout(ParagraphConstraints(width = text.length * fontSize))
+
+        val textPosition = TextPosition(-1, TextAffinity.upstream)
+        val box = paragraph.getBoundingBoxForTextPosition(textPosition)
+        assertThat(box.left, equalTo(0f))
+        assertThat(box.right, equalTo(0f))
+        assertThat(box.top, equalTo(0f))
+        assertThat(box.bottom, equalTo(fontSize))
+    }
+
+    @Test(expected = java.lang.IndexOutOfBoundsException::class)
+    fun getBoundingBoxForTextPosition_ltr_textPosition_larger_than_length_throw_exception() {
+        val text = "abc"
+        val fontSize = 50.0f
+        val paragraph = simpleParagraph(text = text, fontSize = fontSize)
+
+        paragraph.layout(ParagraphConstraints(width = text.length * fontSize))
+
+        val textPosition = TextPosition(text.length + 1, TextAffinity.upstream)
+        paragraph.getBoundingBoxForTextPosition(textPosition)
+    }
+
+    @Test
     fun locale_withCJK_shouldNotDrawSame() {
         val text = "\u82B1"
         val fontSize = 10.0f
diff --git a/ui/text/src/main/java/androidx/ui/engine/text/Paragraph.kt b/ui/text/src/main/java/androidx/ui/engine/text/Paragraph.kt
index efbcf8b..446dd9f 100644
--- a/ui/text/src/main/java/androidx/ui/engine/text/Paragraph.kt
+++ b/ui/text/src/main/java/androidx/ui/engine/text/Paragraph.kt
@@ -16,6 +16,7 @@
 package androidx.ui.engine.text
 
 import androidx.ui.engine.geometry.Offset
+import androidx.ui.engine.geometry.Rect
 import androidx.ui.engine.text.platform.ParagraphAndroid
 import androidx.ui.painting.Canvas
 import androidx.ui.painting.Path
@@ -168,6 +169,14 @@
     }
 
     /**
+     * Returns the bounding box as Rect of the character for given TextPosition. Rect includes the
+     * top, bottom, left and right of a character.
+     */
+    internal fun getBoundingBoxForTextPosition(textPosition: TextPosition): Rect {
+        return paragraphImpl.getBoundingBoxForTextPosition(textPosition)
+    }
+
+    /**
      * Returns the TextRange of the word at the given offset. Characters not
      * part of a word, such as spaces, symbols, and punctuation, have word breaks
      * on both sides. In such cases, this method will return TextRange(offset, offset+1).
diff --git a/ui/text/src/main/java/androidx/ui/engine/text/platform/ParagraphAndroid.kt b/ui/text/src/main/java/androidx/ui/engine/text/platform/ParagraphAndroid.kt
index 50214bb..b5751c2b0 100644
--- a/ui/text/src/main/java/androidx/ui/engine/text/platform/ParagraphAndroid.kt
+++ b/ui/text/src/main/java/androidx/ui/engine/text/platform/ParagraphAndroid.kt
@@ -55,6 +55,7 @@
 import androidx.text.style.WordSpacingSpan
 import androidx.ui.core.px
 import androidx.ui.engine.geometry.Offset
+import androidx.ui.engine.geometry.Rect
 import androidx.ui.engine.text.FontStyle
 import androidx.ui.engine.text.FontSynthesis
 import androidx.ui.engine.text.FontWeight
@@ -222,6 +223,22 @@
         return Pair(Offset(horizontal, top), Offset(horizontal, bottom))
     }
 
+    /**
+     * Returns the bounding box as Rect of the character for given TextPosition. Rect includes the
+     * top, bottom, left and right of a character.
+     */
+    // TODO:(qqd) Implement RTL case.
+    fun getBoundingBoxForTextPosition(textPosition: TextPosition): Rect {
+        val left = ensureLayout.getPrimaryHorizontal(textPosition.offset)
+        val right = ensureLayout.getPrimaryHorizontal(textPosition.offset + 1)
+
+        val line = ensureLayout.getLineForOffset(textPosition.offset)
+        val top = ensureLayout.getLineTop(line)
+        val bottom = ensureLayout.getLineBottom(line)
+
+        return Rect(top = top, bottom = bottom, left = left, right = right)
+    }
+
     fun getPathForRange(start: Int, end: Int): Path {
         val path = android.graphics.Path()
         ensureLayout.getSelectionPath(start, end, path)
diff --git a/ui/text/src/main/java/androidx/ui/painting/TextPainter.kt b/ui/text/src/main/java/androidx/ui/painting/TextPainter.kt
index a79daf6..5887e6d 100644
--- a/ui/text/src/main/java/androidx/ui/painting/TextPainter.kt
+++ b/ui/text/src/main/java/androidx/ui/painting/TextPainter.kt
@@ -17,6 +17,7 @@
 package androidx.ui.painting
 
 import androidx.ui.engine.geometry.Offset
+import androidx.ui.engine.geometry.Rect
 import androidx.ui.engine.geometry.Size
 import androidx.ui.engine.text.Paragraph
 import androidx.ui.engine.text.ParagraphBuilder
@@ -379,6 +380,17 @@
     }
 
     /**
+     * Returns the bounding box as Rect of the character for given TextPosition. Rect includes the
+     * top, bottom, left and right of a character.
+     *
+     * Valid only after [layout] has been called.
+     */
+    internal fun getBoundingBoxForTextPosition(textPosition: TextPosition): Rect {
+        assert(!needsLayout)
+        return paragraph!!.getBoundingBoxForTextPosition(textPosition)
+    }
+
+    /**
      * Returns the text range of the word at the given offset. Characters not part of a word, such
      * as spaces, symbols, and punctuation, have word breaks on both sides. In such cases, this
      * method will return a text range that contains the given text position.
diff --git a/ui/text/src/main/java/androidx/ui/rendering/paragraph/RenderParagraph.kt b/ui/text/src/main/java/androidx/ui/rendering/paragraph/RenderParagraph.kt
index b27f3d3..7bdf471 100644
--- a/ui/text/src/main/java/androidx/ui/rendering/paragraph/RenderParagraph.kt
+++ b/ui/text/src/main/java/androidx/ui/rendering/paragraph/RenderParagraph.kt
@@ -23,6 +23,7 @@
 import androidx.ui.core.px
 import androidx.ui.core.round
 import androidx.ui.engine.geometry.Offset
+import androidx.ui.engine.geometry.Rect
 import androidx.ui.engine.geometry.Size
 import androidx.ui.engine.text.TextAlign
 import androidx.ui.engine.text.TextDirection
@@ -396,6 +397,17 @@
     }
 
     /**
+     * Returns the bounding box as Rect of the character for given TextPosition. Rect includes the
+     * top, bottom, left and right of a character.
+     *
+     * Valid only after [layout] has been called.
+     */
+    internal fun getBoundingBoxForTextPosition(textPosition: TextPosition): Rect {
+        layoutTextWithConstraints(constraints!!)
+        return textPainter.getBoundingBoxForTextPosition(textPosition)
+    }
+
+    /**
      * Returns the text range of the word at the given offset. Characters not part of a word, such
      * as spaces, symbols, and punctuation, have word breaks on both sides. In such cases, this
      * method will return a text range that contains the given text position.
diff --git a/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java b/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
index 994b88e..f956bdb 100644
--- a/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
+++ b/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
@@ -83,7 +83,7 @@
     }
 
     @Override
-    public @NonNull List<Scheduler> createSchedulers(Context context) {
+    public @NonNull List<Scheduler> createSchedulers(Context context, TaskExecutor taskExecutor) {
         mScheduler = new TestScheduler(context);
         return Collections.singletonList((Scheduler) mScheduler);
     }
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
index 3f56c07..434354e 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
@@ -30,7 +30,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.arch.core.executor.ArchTaskExecutor;
-import androidx.arch.core.executor.TaskExecutor;
 import androidx.lifecycle.Observer;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -44,6 +43,7 @@
 import androidx.work.impl.background.greedy.GreedyScheduler;
 import androidx.work.impl.model.WorkSpec;
 import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 import androidx.work.worker.RandomSleepTestWorker;
 
 import org.junit.After;
@@ -84,7 +84,7 @@
 
     @Before
     public void setUp() {
-        ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
+        ArchTaskExecutor.getInstance().setDelegate(new androidx.arch.core.executor.TaskExecutor() {
             @Override
             public void executeOnDiskIO(@NonNull Runnable runnable) {
                 runnable.run();
@@ -109,10 +109,13 @@
                 .setExecutor(executor)
                 .setMaxSchedulerLimit(TEST_SCHEDULER_LIMIT)
                 .build();
+        TaskExecutor taskExecutor = new InstantWorkTaskExecutor();
         mWorkManagerImplSpy = spy(
-                new WorkManagerImpl(context, configuration, new InstantWorkTaskExecutor(), true));
+                new WorkManagerImpl(context, configuration, taskExecutor, true));
 
-        TrackingScheduler trackingScheduler = new TrackingScheduler(context, mWorkManagerImplSpy);
+        TrackingScheduler trackingScheduler =
+                new TrackingScheduler(context, taskExecutor, mWorkManagerImplSpy);
+
         Processor processor = new Processor(context,
                 configuration,
                 mWorkManagerImplSpy.getWorkTaskExecutor(),
@@ -183,8 +186,10 @@
 
         private Set<String> mScheduledWorkSpecIds;
 
-        TrackingScheduler(Context context, WorkManagerImpl workManagerImpl) {
-            super(context, workManagerImpl);
+        TrackingScheduler(Context context,
+                TaskExecutor taskExecutor,
+                WorkManagerImpl workManagerImpl) {
+            super(context, taskExecutor, workManagerImpl);
             mScheduledWorkSpecIds = new HashSet<>();
         }
 
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java
index 1466513..32d6c33 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java
@@ -33,6 +33,7 @@
 import androidx.work.impl.WorkManagerImpl;
 import androidx.work.impl.constraints.WorkConstraintsTracker;
 import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 import androidx.work.worker.TestWorker;
 
 import org.junit.Before;
@@ -56,10 +57,12 @@
 
     @Before
     public void setUp() {
+        TaskExecutor taskExecutor = mock(TaskExecutor.class);
         mWorkManagerImpl = mock(WorkManagerImpl.class);
         mMockProcessor = mock(Processor.class);
         mMockWorkConstraintsTracker = mock(WorkConstraintsTracker.class);
         when(mWorkManagerImpl.getProcessor()).thenReturn(mMockProcessor);
+        when(mWorkManagerImpl.getWorkTaskExecutor()).thenReturn(taskExecutor);
         mGreedyScheduler = new GreedyScheduler(mWorkManagerImpl, mMockWorkConstraintsTracker);
     }
 
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
index 70f95f3..620307f 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
@@ -132,7 +132,7 @@
         mProcessor = new Processor(
                 mContext,
                 mConfiguration,
-                new InstantWorkTaskExecutor(),
+                instantTaskExecutor,
                 mDatabase,
                 Collections.singletonList(mScheduler));
         mSpyProcessor = spy(mProcessor);
@@ -142,11 +142,11 @@
         mDispatcher.setCompletedListener(mCompletedListener);
         mSpyDispatcher = spy(mDispatcher);
 
-        mBatteryChargingTracker = spy(new BatteryChargingTracker(mContext));
-        mBatteryNotLowTracker = spy(new BatteryNotLowTracker(mContext));
+        mBatteryChargingTracker = spy(new BatteryChargingTracker(mContext, instantTaskExecutor));
+        mBatteryNotLowTracker = spy(new BatteryNotLowTracker(mContext, instantTaskExecutor));
         // Requires API 24+ types.
         mNetworkStateTracker = mock(NetworkStateTracker.class);
-        mStorageNotLowTracker = spy(new StorageNotLowTracker(mContext));
+        mStorageNotLowTracker = spy(new StorageNotLowTracker(mContext, instantTaskExecutor));
         mTracker = mock(Trackers.class);
 
         when(mTracker.getBatteryChargingTracker()).thenReturn(mBatteryChargingTracker);
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryChargingTrackerTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryChargingTrackerTest.java
index 5203451..c59a654 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryChargingTrackerTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryChargingTrackerTest.java
@@ -41,6 +41,7 @@
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 import androidx.work.impl.constraints.ConstraintListener;
+import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -57,8 +58,7 @@
     public void setUp() {
         mMockContext = mock(Context.class);
         when(mMockContext.getApplicationContext()).thenReturn(mMockContext);
-
-        mTracker = new BatteryChargingTracker(mMockContext);
+        mTracker = new BatteryChargingTracker(mMockContext, new InstantWorkTaskExecutor());
         mListener = mock(ConstraintListener.class);
     }
 
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryNotLowTrackerTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryNotLowTrackerTest.java
index cb4a6db..83b5b59 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryNotLowTrackerTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryNotLowTrackerTest.java
@@ -36,6 +36,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.work.impl.constraints.ConstraintListener;
+import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -61,7 +62,7 @@
         mMockContext = mock(Context.class);
         when(mMockContext.getApplicationContext()).thenReturn(mMockContext);
 
-        mTracker = new BatteryNotLowTracker(mMockContext);
+        mTracker = new BatteryNotLowTracker(mMockContext, new InstantWorkTaskExecutor());
         mListener = mock(ConstraintListener.class);
     }
 
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/ConstraintTrackerTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/ConstraintTrackerTest.java
index a93c5d4..fc04e38 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/ConstraintTrackerTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/ConstraintTrackerTest.java
@@ -27,6 +27,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.work.impl.constraints.ConstraintListener;
+import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 import org.junit.After;
 import org.junit.Before;
@@ -43,8 +45,9 @@
     @Before
     public void setUp() {
         mMockContext = mock(Context.class);
+        TaskExecutor taskExecutor = new InstantWorkTaskExecutor();
         when(mMockContext.getApplicationContext()).thenReturn(mMockContext);
-        mTracker = new TestConstraintTracker(mMockContext);
+        mTracker = new TestConstraintTracker(mMockContext, taskExecutor);
     }
 
     @After
@@ -178,8 +181,8 @@
         int mStopTrackingCount;
         Boolean mInitialState = null;
 
-        TestConstraintTracker(Context context) {
-            super(context);
+        TestConstraintTracker(Context context, TaskExecutor taskExecutor) {
+            super(context, taskExecutor);
         }
 
         @Override
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/NetworkStateTrackerTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/NetworkStateTrackerTest.java
index 7907368..d43733b 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/NetworkStateTrackerTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/NetworkStateTrackerTest.java
@@ -32,6 +32,7 @@
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 import androidx.work.impl.constraints.NetworkState;
+import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -54,7 +55,7 @@
         when(mMockContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE)))
                 .thenReturn(mMockConnectivityManager);
 
-        mTracker = new NetworkStateTracker(mMockContext);
+        mTracker = new NetworkStateTracker(mMockContext, new InstantWorkTaskExecutor());
     }
 
     @Test
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/StorageNotLowTrackerTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/StorageNotLowTrackerTest.java
index 13d66d9..59efc3f 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/StorageNotLowTrackerTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/constraints/trackers/StorageNotLowTrackerTest.java
@@ -34,6 +34,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.work.impl.constraints.ConstraintListener;
+import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -51,7 +52,7 @@
         mMockContext = mock(Context.class);
         when(mMockContext.getApplicationContext()).thenReturn(mMockContext);
 
-        mTracker = new StorageNotLowTracker(mMockContext);
+        mTracker = new StorageNotLowTracker(mMockContext, new InstantWorkTaskExecutor());
         mListener = mock(ConstraintListener.class);
     }
 
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
index 3224b6b..2ebd002 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
@@ -103,13 +103,14 @@
         mWorkManagerImpl = mock(WorkManagerImpl.class);
         mScheduler = mock(Scheduler.class);
         when(mWorkManagerImpl.getWorkDatabase()).thenReturn(mDatabase);
+        when(mWorkManagerImpl.getWorkTaskExecutor()).thenReturn(mWorkTaskExecutor);
         when(mWorkManagerImpl.getConfiguration()).thenReturn(mConfiguration);
 
-        mBatteryChargingTracker = spy(new BatteryChargingTracker(mContext));
-        mBatteryNotLowTracker = spy(new BatteryNotLowTracker(mContext));
+        mBatteryChargingTracker = spy(new BatteryChargingTracker(mContext, mWorkTaskExecutor));
+        mBatteryNotLowTracker = spy(new BatteryNotLowTracker(mContext, mWorkTaskExecutor));
         // Requires API 24+ types.
         mNetworkStateTracker = mock(NetworkStateTracker.class);
-        mStorageNotLowTracker = spy(new StorageNotLowTracker(mContext));
+        mStorageNotLowTracker = spy(new StorageNotLowTracker(mContext, mWorkTaskExecutor));
         mTracker = mock(Trackers.class);
 
         when(mTracker.getBatteryChargingTracker()).thenReturn(mBatteryChargingTracker);
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
index db25f4b..e5e7b21 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -227,7 +227,7 @@
         WorkDatabase database = WorkDatabase.create(
                 applicationContext, configuration.getTaskExecutor(), useTestDatabase);
         Logger.setLogger(new Logger.LogcatLogger(configuration.getMinimumLoggingLevel()));
-        List<Scheduler> schedulers = createSchedulers(applicationContext);
+        List<Scheduler> schedulers = createSchedulers(applicationContext, workTaskExecutor);
         Processor processor = new Processor(
                 context,
                 configuration,
@@ -657,9 +657,11 @@
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public @NonNull List<Scheduler> createSchedulers(Context context) {
+    public @NonNull List<Scheduler> createSchedulers(Context context, TaskExecutor taskExecutor) {
         return Arrays.asList(
                 Schedulers.createBestAvailableBackgroundScheduler(context, this),
-                new GreedyScheduler(context, this));
+                // Specify the task executor directly here as this happens before internalInit.
+                // GreedyScheduler creates ConstraintTrackers and controllers eagerly.
+                new GreedyScheduler(context, taskExecutor, this));
     }
 }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java b/work/workmanager/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
index 79aff8e..c2d0e4f 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
@@ -31,6 +31,7 @@
 import androidx.work.impl.constraints.WorkConstraintsCallback;
 import androidx.work.impl.constraints.WorkConstraintsTracker;
 import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -53,9 +54,12 @@
     private boolean mRegisteredExecutionListener;
     private final Object mLock;
 
-    public GreedyScheduler(Context context, WorkManagerImpl workManagerImpl) {
+    public GreedyScheduler(Context context,
+            TaskExecutor taskExecutor,
+            WorkManagerImpl workManagerImpl) {
+
         mWorkManagerImpl = workManagerImpl;
-        mWorkConstraintsTracker = new WorkConstraintsTracker(context, this);
+        mWorkConstraintsTracker = new WorkConstraintsTracker(context, taskExecutor, this);
         mLock = new Object();
     }
 
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java
index ef12d58..8b843e2 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java
@@ -25,6 +25,7 @@
 import androidx.work.Logger;
 import androidx.work.impl.constraints.WorkConstraintsTracker;
 import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -53,7 +54,8 @@
         mContext = context;
         mStartId = startId;
         mDispatcher = dispatcher;
-        mWorkConstraintsTracker = new WorkConstraintsTracker(mContext, null);
+        TaskExecutor taskExecutor = mDispatcher.getTaskExecutor();
+        mWorkConstraintsTracker = new WorkConstraintsTracker(mContext, taskExecutor, null);
     }
 
     @WorkerThread
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/DelayMetCommandHandler.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/DelayMetCommandHandler.java
index 5558754..8fcfd26 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/DelayMetCommandHandler.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/DelayMetCommandHandler.java
@@ -32,6 +32,7 @@
 import androidx.work.impl.constraints.WorkConstraintsTracker;
 import androidx.work.impl.model.WorkSpec;
 import androidx.work.impl.utils.WakeLocks;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 import java.util.Collections;
 import java.util.List;
@@ -101,7 +102,8 @@
         mStartId = startId;
         mDispatcher = dispatcher;
         mWorkSpecId = workSpecId;
-        mWorkConstraintsTracker = new WorkConstraintsTracker(mContext, this);
+        TaskExecutor taskExecutor = dispatcher.getTaskExecutor();
+        mWorkConstraintsTracker = new WorkConstraintsTracker(mContext, taskExecutor, this);
         mHasConstraints = false;
         mCurrentState = STATE_INITIAL;
         mLock = new Object();
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java
index 849feb7..548da36 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java
@@ -33,6 +33,7 @@
 import androidx.work.impl.Processor;
 import androidx.work.impl.WorkManagerImpl;
 import androidx.work.impl.utils.WakeLocks;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -55,6 +56,7 @@
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     final Context mContext;
+    private final TaskExecutor mTaskExecutor;
     private final WorkTimer mWorkTimer;
     private final Processor mProcessor;
     private final WorkManagerImpl mWorkManager;
@@ -83,6 +85,7 @@
         mWorkTimer = new WorkTimer();
         mWorkManager = workManager != null ? workManager : WorkManagerImpl.getInstance(context);
         mProcessor = processor != null ? processor : mWorkManager.getProcessor();
+        mTaskExecutor = mWorkManager.getWorkTaskExecutor();
         mProcessor.addExecutionListener(this);
         // a list of pending intents which need to be processed
         mIntents = new ArrayList<>();
@@ -180,6 +183,10 @@
         return mWorkManager;
     }
 
+    TaskExecutor getTaskExecutor() {
+        return mTaskExecutor;
+    }
+
     void postOnMainThread(@NonNull Runnable runnable) {
         mMainHandler.post(runnable);
     }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/ConstraintListener.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/ConstraintListener.java
index c4616a2..f466ce8 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/ConstraintListener.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/ConstraintListener.java
@@ -15,6 +15,7 @@
  */
 package androidx.work.impl.constraints;
 
+import androidx.annotation.MainThread;
 import androidx.annotation.Nullable;
 
 /**
@@ -29,5 +30,6 @@
      * Called when the value of a constraint has changed.
      * @param newValue the new value of the constraint
      */
+    @MainThread
     void onConstraintChanged(@Nullable T newValue);
 }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/WorkConstraintsTracker.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/WorkConstraintsTracker.java
index 8e6dfa6..612428a 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/WorkConstraintsTracker.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/WorkConstraintsTracker.java
@@ -31,6 +31,7 @@
 import androidx.work.impl.constraints.controllers.NetworkUnmeteredController;
 import androidx.work.impl.constraints.controllers.StorageNotLowController;
 import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -54,22 +55,27 @@
     private final Object mLock;
 
     /**
-     * @param context  The application {@link Context}
-     * @param callback The callback is only necessary when you need {@link WorkConstraintsTracker}
-     *                 to notify you about changes in constraints for the list of  {@link
-     *                 WorkSpec}'s that it is tracking.
+     * @param context      The application {@link Context}
+     * @param taskExecutor The {@link TaskExecutor} being used by WorkManager.
+     * @param callback     The callback is only necessary when you need
+     *                     {@link WorkConstraintsTracker} to notify you about changes in
+     *                     constraints for the list of {@link WorkSpec}'s that it is tracking.
      */
-    public WorkConstraintsTracker(Context context, @Nullable WorkConstraintsCallback callback) {
+    public WorkConstraintsTracker(
+            @NonNull Context context,
+            @NonNull TaskExecutor taskExecutor,
+            @Nullable WorkConstraintsCallback callback) {
+
         Context appContext = context.getApplicationContext();
         mCallback = callback;
         mConstraintControllers = new ConstraintController[] {
-                new BatteryChargingController(appContext),
-                new BatteryNotLowController(appContext),
-                new StorageNotLowController(appContext),
-                new NetworkConnectedController(appContext),
-                new NetworkUnmeteredController(appContext),
-                new NetworkNotRoamingController(appContext),
-                new NetworkMeteredController(appContext)
+                new BatteryChargingController(appContext, taskExecutor),
+                new BatteryNotLowController(appContext, taskExecutor),
+                new StorageNotLowController(appContext, taskExecutor),
+                new NetworkConnectedController(appContext, taskExecutor),
+                new NetworkUnmeteredController(appContext, taskExecutor),
+                new NetworkNotRoamingController(appContext, taskExecutor),
+                new NetworkMeteredController(appContext, taskExecutor)
         };
         mLock = new Object();
     }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/BatteryChargingController.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/BatteryChargingController.java
index ce8ebf5..1edcb6d 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/BatteryChargingController.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/BatteryChargingController.java
@@ -20,14 +20,15 @@
 import androidx.annotation.NonNull;
 import androidx.work.impl.constraints.trackers.Trackers;
 import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 /**
  * A {@link ConstraintController} for battery charging events.
  */
 
 public class BatteryChargingController extends ConstraintController<Boolean> {
-    public BatteryChargingController(Context context) {
-        super(Trackers.getInstance(context).getBatteryChargingTracker());
+    public BatteryChargingController(Context context, TaskExecutor taskExecutor) {
+        super(Trackers.getInstance(context, taskExecutor).getBatteryChargingTracker());
     }
 
     @Override
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/BatteryNotLowController.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/BatteryNotLowController.java
index 07e5149..f7667c1 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/BatteryNotLowController.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/BatteryNotLowController.java
@@ -20,14 +20,15 @@
 import androidx.annotation.NonNull;
 import androidx.work.impl.constraints.trackers.Trackers;
 import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 /**
  * A {@link ConstraintController} for battery not low events.
  */
 
 public class BatteryNotLowController extends ConstraintController<Boolean> {
-    public BatteryNotLowController(Context context) {
-        super(Trackers.getInstance(context).getBatteryNotLowTracker());
+    public BatteryNotLowController(Context context, TaskExecutor taskExecutor) {
+        super(Trackers.getInstance(context, taskExecutor).getBatteryNotLowTracker());
     }
 
     @Override
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkConnectedController.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkConnectedController.java
index 9514d44..2384e2a 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkConnectedController.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkConnectedController.java
@@ -25,6 +25,7 @@
 import androidx.work.impl.constraints.NetworkState;
 import androidx.work.impl.constraints.trackers.Trackers;
 import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 /**
  * A {@link ConstraintController} for monitoring that any usable network connection is available.
@@ -36,8 +37,8 @@
  */
 
 public class NetworkConnectedController extends ConstraintController<NetworkState> {
-    public NetworkConnectedController(Context context) {
-        super(Trackers.getInstance(context).getNetworkStateTracker());
+    public NetworkConnectedController(Context context, TaskExecutor taskExecutor) {
+        super(Trackers.getInstance(context, taskExecutor).getNetworkStateTracker());
     }
 
     @Override
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkMeteredController.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkMeteredController.java
index 2c323a0..f011152 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkMeteredController.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkMeteredController.java
@@ -26,6 +26,7 @@
 import androidx.work.impl.constraints.NetworkState;
 import androidx.work.impl.constraints.trackers.Trackers;
 import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 /**
  * A {@link ConstraintController} for monitoring that the network connection is metered.
@@ -34,8 +35,8 @@
 public class NetworkMeteredController extends ConstraintController<NetworkState> {
     private static final String TAG = Logger.tagWithPrefix("NetworkMeteredCtrlr");
 
-    public NetworkMeteredController(Context context) {
-        super(Trackers.getInstance(context).getNetworkStateTracker());
+    public NetworkMeteredController(Context context, TaskExecutor taskExecutor) {
+        super(Trackers.getInstance(context, taskExecutor).getNetworkStateTracker());
     }
 
     @Override
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkNotRoamingController.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkNotRoamingController.java
index 890980b..c02a3ed 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkNotRoamingController.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkNotRoamingController.java
@@ -26,6 +26,7 @@
 import androidx.work.impl.constraints.NetworkState;
 import androidx.work.impl.constraints.trackers.Trackers;
 import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 /**
  * A {@link ConstraintController} for monitoring that the network connection is not roaming.
@@ -34,8 +35,8 @@
 public class NetworkNotRoamingController extends ConstraintController<NetworkState> {
     private static final String TAG = Logger.tagWithPrefix("NetworkNotRoamingCtrlr");
 
-    public NetworkNotRoamingController(Context context) {
-        super(Trackers.getInstance(context).getNetworkStateTracker());
+    public NetworkNotRoamingController(Context context, TaskExecutor taskExecutor) {
+        super(Trackers.getInstance(context, taskExecutor).getNetworkStateTracker());
     }
 
     @Override
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java
index 085af4d..799a112 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java
@@ -24,14 +24,17 @@
 import androidx.work.impl.constraints.NetworkState;
 import androidx.work.impl.constraints.trackers.Trackers;
 import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 /**
  * A {@link ConstraintController} for monitoring that the network connection is unmetered.
  */
 
 public class NetworkUnmeteredController extends ConstraintController<NetworkState> {
-    public NetworkUnmeteredController(Context context) {
-        super(Trackers.getInstance(context).getNetworkStateTracker());
+    public NetworkUnmeteredController(
+            @NonNull Context context,
+            @NonNull TaskExecutor taskExecutor) {
+        super(Trackers.getInstance(context, taskExecutor).getNetworkStateTracker());
     }
 
     @Override
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/StorageNotLowController.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/StorageNotLowController.java
index 6c1d40a..b593023 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/StorageNotLowController.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/StorageNotLowController.java
@@ -20,14 +20,15 @@
 import androidx.annotation.NonNull;
 import androidx.work.impl.constraints.trackers.Trackers;
 import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 /**
  * A {@link ConstraintController} for storage not low events.
  */
 
 public class StorageNotLowController extends ConstraintController<Boolean> {
-    public StorageNotLowController(Context context) {
-        super(Trackers.getInstance(context).getStorageNotLowTracker());
+    public StorageNotLowController(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
+        super(Trackers.getInstance(context, taskExecutor).getStorageNotLowTracker());
     }
 
     @Override
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.java
index 6241a14..1e26fc1 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.java
@@ -24,6 +24,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.work.Logger;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 /**
  * Tracks whether or not the device's battery is charging.
@@ -37,9 +38,10 @@
     /**
      * Create an instance of {@link BatteryChargingTracker}.
      * @param context The application {@link Context}
+     * @param taskExecutor The internal {@link TaskExecutor} being used by WorkManager.
      */
-    public BatteryChargingTracker(Context context) {
-        super(context);
+    public BatteryChargingTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
+        super(context, taskExecutor);
     }
 
     @Override
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.java
index 0a74b0f..17ae379 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.java
@@ -23,6 +23,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.work.Logger;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 /**
  * Tracks whether or not the device's battery level is low.
@@ -46,9 +47,10 @@
     /**
      * Create an instance of {@link BatteryNotLowTracker}.
      * @param context The application {@link Context}
+     * @param taskExecutor The internal {@link TaskExecutor} being used by WorkManager.
      */
-    public BatteryNotLowTracker(Context context) {
-        super(context);
+    public BatteryNotLowTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
+        super(context, taskExecutor);
     }
 
     /**
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.java
index 3ddc44a..2ae5dfa 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.java
@@ -24,6 +24,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.work.Logger;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 /**
  * A {@link ConstraintTracker} with a {@link BroadcastReceiver} for monitoring constraint changes.
@@ -44,8 +45,10 @@
         }
     };
 
-    public BroadcastReceiverConstraintTracker(Context context) {
-        super(context);
+    public BroadcastReceiverConstraintTracker(
+            @NonNull Context context,
+            @NonNull TaskExecutor taskExecutor) {
+        super(context, taskExecutor);
     }
 
     /**
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java
index 9dc2f07..6d164d2 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java
@@ -17,9 +17,11 @@
 
 import android.content.Context;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.work.Logger;
 import androidx.work.impl.constraints.ConstraintListener;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 import java.util.ArrayList;
 import java.util.LinkedHashSet;
@@ -37,13 +39,18 @@
 
     private static final String TAG = Logger.tagWithPrefix("ConstraintTracker");
 
+    protected final TaskExecutor mTaskExecutor;
     protected final Context mAppContext;
+
     private final Object mLock = new Object();
     private final Set<ConstraintListener<T>> mListeners = new LinkedHashSet<>();
-    private T mCurrentState;
 
-    ConstraintTracker(Context context) {
+    // Synthetic access
+    T mCurrentState;
+
+    ConstraintTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
         mAppContext = context.getApplicationContext();
+        mTaskExecutor = taskExecutor;
     }
 
     /**
@@ -95,13 +102,19 @@
             }
             mCurrentState = newState;
 
-            // onConstraintChanged may lead to calls to addListener or removeListener.  This can
-            // potentially result in a modification to the set while it is being iterated over, so
-            // we handle this by creating a copy and using that for iteration.
-            List<ConstraintListener<T>> listenersList = new ArrayList<>(mListeners);
-            for (ConstraintListener<T> listener : listenersList) {
-                listener.onConstraintChanged(mCurrentState);
-            }
+            // onConstraintChanged may lead to calls to addListener or removeListener.
+            // This can potentially result in a modification to the set while it is being
+            // iterated over, so we handle this by creating a copy and using that for
+            // iteration.
+            final List<ConstraintListener<T>> listenersList = new ArrayList<>(mListeners);
+            mTaskExecutor.getMainThreadExecutor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    for (ConstraintListener<T> listener : listenersList) {
+                        listener.onConstraintChanged(mCurrentState);
+                    }
+                }
+            });
         }
     }
 
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.java
index 920a9ac..66e1bec 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.java
@@ -26,11 +26,13 @@
 import android.net.NetworkInfo;
 import android.os.Build;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.core.net.ConnectivityManagerCompat;
 import androidx.work.Logger;
 import androidx.work.impl.constraints.NetworkState;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 /**
  * A {@link ConstraintTracker} for monitoring network state.
@@ -60,9 +62,10 @@
     /**
      * Create an instance of {@link NetworkStateTracker}
      * @param context the application {@link Context}
+     * @param taskExecutor The internal {@link TaskExecutor} being used by WorkManager.
      */
-    public NetworkStateTracker(Context context) {
-        super(context);
+    public NetworkStateTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
+        super(context, taskExecutor);
         mConnectivityManager =
                 (ConnectivityManager) mAppContext.getSystemService(Context.CONNECTIVITY_SERVICE);
         if (isNetworkCallbackSupported()) {
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.java
index e484bc6..cf97361 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.java
@@ -22,6 +22,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.work.Logger;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 /**
  * Tracks whether or not the device's storage is low.
@@ -35,9 +36,10 @@
     /**
      * Create an instance of {@link StorageNotLowTracker}.
      * @param context The application {@link Context}
+     * @param taskExecutor The internal {@link TaskExecutor} being used by WorkManager.
      */
-    public StorageNotLowTracker(Context context) {
-        super(context);
+    public StorageNotLowTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
+        super(context, taskExecutor);
     }
 
     @Override
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/Trackers.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/Trackers.java
index e4e7af8..4e0a5ac 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/Trackers.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/trackers/Trackers.java
@@ -20,6 +20,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 /**
  * A singleton class to hold an instance of each {@link ConstraintTracker}.
@@ -36,9 +37,10 @@
      * @param context The initializing context (we only use the application context)
      * @return The singleton instance of {@link Trackers}.
      */
-    public static synchronized Trackers getInstance(Context context) {
+    @NonNull
+    public static synchronized Trackers getInstance(Context context, TaskExecutor taskExecutor) {
         if (sInstance == null) {
-            sInstance = new Trackers(context);
+            sInstance = new Trackers(context, taskExecutor);
         }
         return sInstance;
     }
@@ -56,12 +58,12 @@
     private NetworkStateTracker mNetworkStateTracker;
     private StorageNotLowTracker mStorageNotLowTracker;
 
-    private Trackers(Context context) {
+    private Trackers(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
         Context appContext = context.getApplicationContext();
-        mBatteryChargingTracker = new BatteryChargingTracker(appContext);
-        mBatteryNotLowTracker = new BatteryNotLowTracker(appContext);
-        mNetworkStateTracker = new NetworkStateTracker(appContext);
-        mStorageNotLowTracker = new StorageNotLowTracker(appContext);
+        mBatteryChargingTracker = new BatteryChargingTracker(appContext, taskExecutor);
+        mBatteryNotLowTracker = new BatteryNotLowTracker(appContext, taskExecutor);
+        mNetworkStateTracker = new NetworkStateTracker(appContext, taskExecutor);
+        mStorageNotLowTracker = new StorageNotLowTracker(appContext, taskExecutor);
     }
 
     /**
@@ -69,6 +71,7 @@
      *
      * @return The tracker used to track battery charging status
      */
+    @NonNull
     public BatteryChargingTracker getBatteryChargingTracker() {
         return mBatteryChargingTracker;
     }
@@ -78,6 +81,7 @@
      *
      * @return The tracker used to track if the battery is okay or low
      */
+    @NonNull
     public BatteryNotLowTracker getBatteryNotLowTracker() {
         return mBatteryNotLowTracker;
     }
@@ -87,6 +91,7 @@
      *
      * @return The tracker used to track state of the network
      */
+    @NonNull
     public NetworkStateTracker getNetworkStateTracker() {
         return mNetworkStateTracker;
     }
@@ -96,6 +101,7 @@
      *
      * @return The tracker used to track if device storage is okay or low.
      */
+    @NonNull
     public StorageNotLowTracker getStorageNotLowTracker() {
         return mStorageNotLowTracker;
     }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/workers/ConstraintTrackingWorker.java b/work/workmanager/src/main/java/androidx/work/impl/workers/ConstraintTrackingWorker.java
index ab3bc26..f98fd96 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/workers/ConstraintTrackingWorker.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/workers/ConstraintTrackingWorker.java
@@ -33,6 +33,7 @@
 import androidx.work.impl.constraints.WorkConstraintsTracker;
 import androidx.work.impl.model.WorkSpec;
 import androidx.work.impl.utils.futures.SettableFuture;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -116,7 +117,7 @@
             return;
         }
         WorkConstraintsTracker workConstraintsTracker =
-                new WorkConstraintsTracker(getApplicationContext(), this);
+                new WorkConstraintsTracker(getApplicationContext(), getTaskExecutor(), this);
 
         // Start tracking
         workConstraintsTracker.replace(Collections.singletonList(workSpec));
@@ -187,11 +188,23 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @VisibleForTesting
+    @NonNull
     public WorkDatabase getWorkDatabase() {
         return WorkManagerImpl.getInstance(getApplicationContext()).getWorkDatabase();
     }
 
     /**
+     * @return The instance of {@link TaskExecutor}.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @VisibleForTesting
+    @NonNull
+    public TaskExecutor getTaskExecutor() {
+        return WorkManagerImpl.getInstance(getApplicationContext()).getWorkTaskExecutor();
+    }
+
+    /**
      * @return The {@link Worker} used for delegated work
      * @hide
      */