Commit 0ac2feae authored by Xing Liu's avatar Xing Liu Committed by Commit Bot

Android Notification: Intercept PendingIntent for metrics recording.

This CL introduces a way to intercept all PendingIntents from
notification with broadcast receivers. In the following CL we will
add metrics recording into those broadcast receivers. This class will
eventually be used in ChromeNotificationBuilder.

Also we introduce a test that uses UIAutomator to actually click on
a notification in test instead of mocking Android notification API.
And verify the BroadcastReceiver interception.

Bug: 898269
Change-Id: I16e52f7036db4d30093517e16373547d5e1e862c
Reviewed-on: https://chromium-review.googlesource.com/c/1327803
Commit-Queue: Xing Liu <xingliu@chromium.org>
Reviewed-by: default avatarPeter Beverloo <peter@chromium.org>
Reviewed-by: default avatarDavid Trainor <dtrainor@chromium.org>
Cr-Commit-Position: refs/heads/master@{#609390}
parent 57f139f6
...@@ -1107,6 +1107,9 @@ android:value="true" /> ...@@ -1107,6 +1107,9 @@ android:value="true" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="org.chromium.chrome.browser.notifications.NotificationIntentInterceptor$Receiver"
android:exported="false"/>
<receiver android:name="org.chromium.chrome.browser.ntp.ContentSuggestionsNotifier$OpenUrlReceiver" <receiver android:name="org.chromium.chrome.browser.ntp.ContentSuggestionsNotifier$OpenUrlReceiver"
android:exported="false"/> android:exported="false"/>
<receiver android:name="org.chromium.chrome.browser.ntp.ContentSuggestionsNotifier$DeleteReceiver" <receiver android:name="org.chromium.chrome.browser.ntp.ContentSuggestionsNotifier$DeleteReceiver"
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.notifications;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.annotation.IntDef;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Random;
/**
* Class to intercept {@link PendingIntent}s from notifications, including
* {@link Notification#contentIntent}, {@link Notification.Action#actionIntent} and
* {@link Notification#deleteIntent} with broadcast receivers.
*/
public class NotificationIntentInterceptor {
private static final String TAG = "IntentInterceptor";
private static final String EXTRA_PENDING_INTENT =
"notifications.NotificationIntentInterceptor.EXTRA_PENDING_INTENT";
private static final String EXTRA_INTENT_TYPE =
"notifications.NotificationIntentInterceptor.EXTRA_INTENT_TYPE";
/**
* Enum that defines type of notification intent.
*/
@IntDef({IntentType.CONTENT_INTENT, IntentType.ACTION_INTENT, IntentType.DELETE_INTENT})
@Retention(RetentionPolicy.SOURCE)
public @interface IntentType {
int CONTENT_INTENT = 0;
int ACTION_INTENT = 1;
int DELETE_INTENT = 2;
}
/**
* Receives the event when the user taps on the notification body, notification action, or
* dismiss notification.
* {@link Notification#contentIntent}, {@link Notification#deleteIntent}
* {@link Notification.Action#actionIntent} will be delivered to this broadcast receiver.
*/
public static final class Receiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// TODO(xingliu): Record notification CTR UMA.
forwardPendingIntent(intent);
}
}
private NotificationIntentInterceptor() {}
/**
* Wraps the notification {@link PendingIntent }into another PendingIntent, to intercept clicks
* and dismiss events for metrics purpose.
* @param intentType The type of the pending intent to intercept.
* @param pendingIntent The {@link PendingIntent} of the notification that performs actual task.
*/
public static PendingIntent createInterceptPendingIntent(
@IntentType int intentType, PendingIntent pendingIntent) {
Context applicationContext = ContextUtils.getApplicationContext();
Intent intent = new Intent(applicationContext, Receiver.class);
intent.putExtra(EXTRA_PENDING_INTENT, pendingIntent);
intent.putExtra(EXTRA_INTENT_TYPE, intentType);
// TODO(xingliu): Add more extras to track notification type and action type. Use the
// combination of notification type and id as the request code.
int requestCode = new Random().nextInt(Integer.MAX_VALUE);
// This flag ensures the broadcast is delivered with foreground priority to speed up the
// broadcast delivery.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
}
return PendingIntent.getBroadcast(
applicationContext, requestCode, intent, PendingIntent.FLAG_CANCEL_CURRENT);
}
// Launches the notification's pending intent, which will perform Chrome feature related tasks.
private static void forwardPendingIntent(Intent intent) {
if (intent == null) {
Log.e(TAG, "Intent to forward is null.");
return;
}
PendingIntent pendingIntent =
(PendingIntent) (intent.getParcelableExtra(EXTRA_PENDING_INTENT));
if (pendingIntent == null) {
Log.d(TAG, "The notification's PendingIntent is null.");
return;
}
try {
pendingIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "The PendingIntent to fire is canceled.");
e.printStackTrace();
}
}
}
...@@ -967,6 +967,7 @@ chrome_java_sources = [ ...@@ -967,6 +967,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/notifications/NotificationBuilderFactory.java", "java/src/org/chromium/chrome/browser/notifications/NotificationBuilderFactory.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationCompatBuilder.java", "java/src/org/chromium/chrome/browser/notifications/NotificationCompatBuilder.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationConstants.java", "java/src/org/chromium/chrome/browser/notifications/NotificationConstants.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationIntentInterceptor.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationManagerProxy.java", "java/src/org/chromium/chrome/browser/notifications/NotificationManagerProxy.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationManagerProxyImpl.java", "java/src/org/chromium/chrome/browser/notifications/NotificationManagerProxyImpl.java",
"java/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridge.java", "java/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridge.java",
...@@ -2011,6 +2012,7 @@ chrome_test_java_sources = [ ...@@ -2011,6 +2012,7 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/multiwindow/MultiWindowUtilsTest.java", "javatests/src/org/chromium/chrome/browser/multiwindow/MultiWindowUtilsTest.java",
"javatests/src/org/chromium/chrome/browser/notifications/ChromeNotificationBuilderTest.java", "javatests/src/org/chromium/chrome/browser/notifications/ChromeNotificationBuilderTest.java",
"javatests/src/org/chromium/chrome/browser/notifications/CustomNotificationBuilderTest.java", "javatests/src/org/chromium/chrome/browser/notifications/CustomNotificationBuilderTest.java",
"javatests/src/org/chromium/chrome/browser/notifications/NotificationIntentInterceptorTest.java",
"javatests/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridgeIntentTest.java", "javatests/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridgeIntentTest.java",
"javatests/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridgeTest.java", "javatests/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridgeTest.java",
"javatests/src/org/chromium/chrome/browser/notifications/NotificationTestRule.java", "javatests/src/org/chromium/chrome/browser/notifications/NotificationTestRule.java",
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.notifications;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.Until;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.ContextUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.notifications.channels.ChannelDefinitions;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.content_public.browser.test.util.Criteria;
import org.chromium.content_public.browser.test.util.CriteriaHelper;
/**
* Test to verify intercepting notification pending intents with broadcast receiver.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class NotificationIntentInterceptorTest {
@Rule
public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
private static final String TEST_NOTIFICATION_TITLE = "Test notification title.";
private static final long WAIT_FOR_NOTIFICATION_SHOWN_TIMEOUT_MILLISECONDS = 3000;
@Before
public void setUp() throws Exception {
mActivityTestRule.startMainActivityOnBlankPage();
}
// Builds a simple notification used in tests.
private Notification buildSimpleNotification(String title) {
ChromeNotificationBuilder builder =
NotificationBuilderFactory.createChromeNotificationBuilder(
true /* preferCompat */, ChannelDefinitions.ChannelId.DOWNLOADS);
// Set content intent. UI automator may tap the notification and expand the action buttons,
// in order to reduce flakiness, don't add action button.
Context context = ContextUtils.getApplicationContext();
Intent contentIntent = new Intent(context, ChromeTabbedActivity.class);
Uri uri = Uri.parse("www.example.com");
contentIntent.setData(uri);
contentIntent.setAction(Intent.ACTION_VIEW);
int flags = PendingIntent.FLAG_ONE_SHOT;
PendingIntent contentPendingIntent =
PendingIntent.getActivity(context, 0, contentIntent, flags);
assert contentPendingIntent != null;
builder.setContentIntent(NotificationIntentInterceptor.createInterceptPendingIntent(
NotificationIntentInterceptor.IntentType.CONTENT_INTENT, contentPendingIntent));
builder.setContentTitle(title);
builder.setSmallIcon(R.drawable.offline_pin);
return builder.build();
}
/**
* Clicks the notification with UI automator. Notice the notification bar is not part of the
* app, so we have to use UI automator.
* @param text The text of notification UI.
*/
private void clickNotification(String text) {
UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.openNotification();
device.wait(
Until.hasObject(By.text(text)), WAIT_FOR_NOTIFICATION_SHOWN_TIMEOUT_MILLISECONDS);
UiObject2 textObject = device.findObject(By.text(text));
Assert.assertEquals(text, textObject.getText());
textObject.click();
}
/**
* Verifies {@link Notification#contentIntent} can be intercepted by broadcast receiver.
* Action button and dismiss have no test coverage due to difficulty in simulation with UI
* automator. On different Android version, the way to dismiss or find the action button can be
* different.
*/
@Test
@MediumTest
public void testContentIntentInterception() {
// Send notification.
NotificationManager notificationManager =
(NotificationManager) ContextUtils.getApplicationContext().getSystemService(
Context.NOTIFICATION_SERVICE);
notificationManager.notify(0, buildSimpleNotification(TEST_NOTIFICATION_TITLE));
// Click notification body.
clickNotification(TEST_NOTIFICATION_TITLE);
// Wait for another tab to load.
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
return mActivityTestRule.tabsCount(false) > 1;
}
});
notificationManager.cancelAll();
}
}
...@@ -155,7 +155,7 @@ public class PartnerDisableIncognitoModeIntegrationTest { ...@@ -155,7 +155,7 @@ public class PartnerDisableIncognitoModeIntegrationTest {
waitForParentalControlsEnabledState(true); waitForParentalControlsEnabledState(true);
CriteriaHelper.pollInstrumentationThread( CriteriaHelper.pollInstrumentationThread(
Criteria.equals(0, () -> mActivityTestRule.incognitoTabsCount())); Criteria.equals(0, () -> mActivityTestRule.tabsCount(true /* incognito */)));
} finally { } finally {
testServer.stopAndDestroyServer(); testServer.stopAndDestroyServer();
} }
......
...@@ -536,13 +536,13 @@ public class ChromeActivityTestRule<T extends ChromeActivity> extends ActivityTe ...@@ -536,13 +536,13 @@ public class ChromeActivityTestRule<T extends ChromeActivity> extends ActivityTe
} }
/** /**
* @return The number of incognito tabs currently open. * @return The number of tabs currently open.
*/ */
public int incognitoTabsCount() { public int tabsCount(boolean incognito) {
return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Integer>() { return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Integer>() {
@Override @Override
public Integer call() { public Integer call() {
return getActivity().getTabModelSelector().getModel(true).getCount(); return getActivity().getTabModelSelector().getModel(incognito).getCount();
} }
}); });
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment