Commit 69231d50 authored by peter's avatar peter Committed by Commit bot

Store serialized notification data as an Intent extra on Android.

When a displayed notification outlives the browser process, we need to
be able to wake up the browser and re-create all required objects and
data in order to fire the "notificationclick" event on the Service
Worker associated with a notification.

This patch serializes the notification data in a pickle, and passes
it as an opaque byte array to the Java side, where it's stored in the
Android intent. When the intent fires, we pass it back to C++ and
unserialize the data there.

BUG=90795, 432527

Review URL: https://codereview.chromium.org/839533004

Cr-Commit-Position: refs/heads/master@{#310770}
parent fadb3654
......@@ -15,6 +15,7 @@ public class NotificationConstants {
"org.chromium.chrome.browser.notifications.CLOSE_NOTIFICATION";
public static final String EXTRA_NOTIFICATION_ID = "notification_id";
public static final String EXTRA_NOTIFICATION_DATA = "notification_data";
/**
* Unique identifier for a single sync notification. Since the notification ID is reused,
......
......@@ -125,19 +125,30 @@ public class NotificationUIManager {
String notificationId = intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_ID);
if (NotificationConstants.ACTION_CLICK_NOTIFICATION.equals(intent.getAction())) {
return sInstance.onNotificationClicked(notificationId);
} else if (NotificationConstants.ACTION_CLOSE_NOTIFICATION.equals(intent.getAction())) {
if (!intent.hasExtra(NotificationConstants.EXTRA_NOTIFICATION_DATA)) {
Log.e(TAG, "No notification data has been provided.");
return false;
}
byte[] notificationData =
intent.getByteArrayExtra(NotificationConstants.EXTRA_NOTIFICATION_DATA);
return sInstance.onNotificationClicked(notificationId, notificationData);
}
if (NotificationConstants.ACTION_CLOSE_NOTIFICATION.equals(intent.getAction())) {
return sInstance.onNotificationClosed(notificationId);
} else {
Log.e(TAG, "Unrecognized Notification action: " + intent.getAction());
return false;
}
Log.e(TAG, "Unrecognized Notification action: " + intent.getAction());
return false;
}
private PendingIntent getPendingIntent(String notificationId, int platformId, String action) {
private PendingIntent getPendingIntent(String action, String notificationId, int platformId,
byte[] notificationData) {
Intent intent = new Intent(action);
intent.setClass(mAppContext, NotificationService.Receiver.class);
intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_ID, notificationId);
intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_DATA, notificationData);
return PendingIntent.getBroadcast(mAppContext, platformId, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
......@@ -153,11 +164,12 @@ public class NotificationUIManager {
* @param icon Icon to be displayed in the notification. When this isn't a valid Bitmap, a
* default icon will be generated instead.
* @param origin Full text of the origin, including the protocol, owning this notification.
* @param notificationData Serialized data associated with the notification.
* @return The id using which the notification can be identified.
*/
@CalledByNative
private int displayNotification(String notificationId, String title, String body, Bitmap icon,
String origin) {
String origin, byte[] notificationData) {
if (icon == null || icon.getWidth() == 0) {
icon = getIconGenerator().generateIconForUrl(origin);
}
......@@ -169,11 +181,11 @@ public class NotificationUIManager {
.setLargeIcon(icon)
.setSmallIcon(R.drawable.notification_badge)
.setContentIntent(getPendingIntent(
notificationId, mLastNotificationId,
NotificationConstants.ACTION_CLICK_NOTIFICATION))
NotificationConstants.ACTION_CLICK_NOTIFICATION,
notificationId, mLastNotificationId, notificationData))
.setDeleteIntent(getPendingIntent(
notificationId, mLastNotificationId,
NotificationConstants.ACTION_CLOSE_NOTIFICATION))
NotificationConstants.ACTION_CLOSE_NOTIFICATION,
notificationId, mLastNotificationId, notificationData))
.setSubText(origin);
if (sObserver != null) {
......@@ -219,8 +231,9 @@ public class NotificationUIManager {
mNotificationManager.cancel(platformId);
}
private boolean onNotificationClicked(String notificationId) {
return nativeOnNotificationClicked(mNativeNotificationManager, notificationId);
private boolean onNotificationClicked(String notificationId, byte[] notificationData) {
return nativeOnNotificationClicked(
mNativeNotificationManager, notificationId, notificationData);
}
private boolean onNotificationClosed(String notificationId) {
......@@ -230,7 +243,8 @@ public class NotificationUIManager {
private static native void nativeInitializeNotificationUIManager();
private native boolean nativeOnNotificationClicked(
long nativeNotificationUIManagerAndroid, String notificationId);
long nativeNotificationUIManagerAndroid, String notificationId,
byte[] notificationData);
private native boolean nativeOnNotificationClosed(
long nativeNotificationUIManagerAndroid, String notificationId);
}
......@@ -4,11 +4,18 @@
#include "chrome/browser/notifications/notification_ui_manager_android.h"
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/logging.h"
#include "base/pickle.h"
#include "base/strings/string16.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/notifications/persistent_notification_delegate.h"
#include "chrome/browser/notifications/platform_notification_service_impl.h"
#include "chrome/browser/notifications/profile_notification.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "content/public/common/persistent_notification_status.h"
#include "content/public/common/platform_notification_data.h"
#include "jni/NotificationUIManager_jni.h"
#include "ui/gfx/android/java_bitmap.h"
#include "ui/gfx/image/image.h"
......@@ -18,6 +25,102 @@ using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertUTF16ToJavaString;
using base::android::ConvertUTF8ToJavaString;
namespace {
// The maximum size of the serialized pickle that carries a notification's meta
// information. Notifications carrying more data will be silently dropped - with
// an error being written to the log.
const int kMaximumSerializedNotificationSizeBytes = 1024 * 1024;
// Persistent notifications are likely to outlive the browser process they were
// created by on Android. In order to be able to re-surface the notification
// when the user interacts with them, all relevant notification data needs to
// be serialized with the notification itself.
//
// In the near future, as more features get added to Chrome's notification
// implementation, this will be done by storing the persistent notification data
// in a database. However, to support launching Chrome for Android from a
// notification event until that exists, serialize the data in the Intent.
//
// TODO(peter): Move towards storing notification data in a database rather than
// as a serialized Intent extra.
scoped_ptr<Pickle> SerializePersistentNotification(
const content::PlatformNotificationData& notification_data,
const GURL& origin,
int64 service_worker_registration_id) {
scoped_ptr<Pickle> pickle(new Pickle);
// content::PlatformNotificationData
pickle->WriteString16(notification_data.title);
pickle->WriteInt(static_cast<int>(notification_data.direction));
pickle->WriteString(notification_data.lang);
pickle->WriteString16(notification_data.body);
pickle->WriteString16(notification_data.tag);
pickle->WriteString(notification_data.icon.spec());
// The origin which is displaying the notification.
pickle->WriteString(origin.spec());
// The Service Worker registration this notification is associated with.
pickle->WriteInt64(service_worker_registration_id);
if (pickle->size() > kMaximumSerializedNotificationSizeBytes)
return nullptr;
return pickle.Pass();
}
bool UnserializePersistentNotification(
const Pickle& pickle,
content::PlatformNotificationData* notification_data,
GURL* origin,
int64* service_worker_registration_id) {
DCHECK(notification_data && origin && service_worker_registration_id);
PickleIterator iterator(pickle);
std::string icon_url, origin_url;
int direction_value;
// Unpack content::PlatformNotificationData.
if (!iterator.ReadString16(&notification_data->title) ||
!iterator.ReadInt(&direction_value) ||
!iterator.ReadString(&notification_data->lang) ||
!iterator.ReadString16(&notification_data->body) ||
!iterator.ReadString16(&notification_data->tag) ||
!iterator.ReadString(&icon_url)) {
return false;
}
notification_data->direction =
static_cast<content::PlatformNotificationData::NotificationDirection>(
direction_value);
notification_data->icon = GURL(icon_url);
// Unpack the origin which displayed this notification.
if (!iterator.ReadString(&origin_url))
return false;
*origin = GURL(origin_url);
// Unpack the Service Worker registration id.
if (!iterator.ReadInt64(service_worker_registration_id))
return false;
return true;
}
// Called when the "notificationclick" event in the Service Worker has finished
// executing for a notification that was created in a previous instance of the
// browser.
void OnEventDispatchComplete(content::PersistentNotificationStatus status) {
// TODO(peter): Add UMA statistics based on |status|.
// TODO(peter): Decide if we want to forcefully shut down the browser process
// if we're confident it was created for delivering this event.
}
} // namespace
// Called by the Java side when a notification event has been received, but the
// NotificationUIManager has not been initialized yet. Enforce initialization of
// the class.
......@@ -46,18 +149,55 @@ NotificationUIManagerAndroid::~NotificationUIManagerAndroid() {
}
bool NotificationUIManagerAndroid::OnNotificationClicked(
JNIEnv* env, jobject java_object, jstring notification_id) {
JNIEnv* env,
jobject java_object,
jstring notification_id,
jbyteArray serialized_notification_data) {
std::string id = ConvertJavaStringToUTF8(env, notification_id);
auto iter = profile_notifications_.find(id);
if (iter == profile_notifications_.end()) {
// TODO(peter): Handle the "click" event for notifications which have
// outlived the browser process that created them.
if (iter != profile_notifications_.end()) {
const Notification& notification = iter->second->notification();
notification.delegate()->Click();
return true;
}
// If the Notification were not found, it may be a persistent notification
// that outlived the Chrome browser process. In this case, try to
// unserialize the notification's serialized data and trigger the click
// event manually.
std::vector<uint8> bytes;
base::android::JavaByteArrayToByteVector(env, serialized_notification_data,
&bytes);
if (!bytes.size())
return false;
content::PlatformNotificationData notification_data;
GURL origin;
int64 service_worker_registration_id;
Pickle pickle(reinterpret_cast<const char*>(&bytes[0]), bytes.size());
if (!UnserializePersistentNotification(pickle, &notification_data, &origin,
&service_worker_registration_id)) {
return false;
}
const Notification& notification = iter->second->notification();
notification.delegate()->Click();
PlatformNotificationServiceImpl* service =
PlatformNotificationServiceImpl::GetInstance();
// TODO(peter): Rather than assuming that the last used profile is the
// appropriate one for this notification, the used profile should be
// stored as part of the notification's data. See https://crbug.com/437574.
service->OnPersistentNotificationClick(
ProfileManager::GetLastUsedProfile(),
service_worker_registration_id,
id,
origin,
notification_data,
base::Bind(&OnEventDispatchComplete));
return true;
}
......@@ -102,9 +242,29 @@ void NotificationUIManagerAndroid::Add(const Notification& notification,
if (!icon_bitmap.isNull())
icon = gfx::ConvertToJavaBitmap(&icon_bitmap);
ScopedJavaLocalRef<jbyteArray> notification_data;
if (true /** is_persistent_notification **/) {
PersistentNotificationDelegate* delegate =
static_cast<PersistentNotificationDelegate*>(notification.delegate());
scoped_ptr<Pickle> pickle = SerializePersistentNotification(
delegate->notification_data(),
notification.origin_url(),
delegate->service_worker_registration_id());
if (!pickle) {
LOG(ERROR) <<
"Unable to serialize the notification, payload too large (max 1MB).";
RemoveProfileNotification(profile_notification);
return;
}
notification_data = base::android::ToJavaByteArray(
env, static_cast<const uint8*>(pickle->data()), pickle->size());
}
int platform_id = Java_NotificationUIManager_displayNotification(
env, java_object_.obj(), id.obj(), title.obj(), body.obj(), icon.obj(),
origin.obj());
origin.obj(), notification_data.obj());
std::string notification_id = profile_notification->notification().id();
platform_notifications_[notification_id] = platform_id;
......
......@@ -25,7 +25,8 @@ class NotificationUIManagerAndroid : public NotificationUIManager {
// Called by the Java implementation when a notification has been clicked on.
bool OnNotificationClicked(JNIEnv* env,
jobject java_object,
jstring notification_id);
jstring notification_id,
jbyteArray serialized_notification_data);
// Called by the Java implementation when a notification has been closed.
bool OnNotificationClosed(JNIEnv* env,
......
......@@ -26,6 +26,14 @@ class PersistentNotificationDelegate : public NotificationDelegate {
const GURL& origin,
const content::PlatformNotificationData& notification_data);
int64 service_worker_registration_id() const {
return service_worker_registration_id_;
}
const content::PlatformNotificationData& notification_data() const {
return notification_data_;
}
// NotificationDelegate implementation.
void Display() override;
void Close(bool by_user) override;
......
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