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
*/