blob: 71526389d092c2528f72a721ad5f978d6a940515 [file] [log] [blame]
// Copyright 2014 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.
#include "chrome/browser/notifications/notification_ui_manager_android.h"
#include <utility>
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/logging.h"
#include "base/pickle.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/notifications/notification.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"
using base::android::AttachCurrentThread;
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->WriteString(notification_data.tag);
pickle->WriteString(notification_data.icon.spec());
pickle->WriteBool(notification_data.silent);
// 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.ReadString(&notification_data->tag) ||
!iterator.ReadString(&icon_url) ||
!iterator.ReadBool(&notification_data->silent)) {
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.
static void InitializeNotificationUIManager(JNIEnv* env, jclass clazz) {
g_browser_process->notification_ui_manager();
}
// static
NotificationUIManager* NotificationUIManager::Create(PrefService* local_state) {
return new NotificationUIManagerAndroid();
}
NotificationUIManagerAndroid::NotificationUIManagerAndroid() {
java_object_.Reset(
Java_NotificationUIManager_create(
AttachCurrentThread(),
reinterpret_cast<intptr_t>(this),
base::android::GetApplicationContext()));
// TODO(peter): Synchronize notifications with the Java side.
}
NotificationUIManagerAndroid::~NotificationUIManagerAndroid() {
Java_NotificationUIManager_destroy(AttachCurrentThread(),
java_object_.obj());
}
bool NotificationUIManagerAndroid::OnNotificationClicked(
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()) {
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;
}
// Store the tag and origin of this notification so that it can later be
// closed using these details.
regenerated_notification_infos_[id] =
std::make_pair(notification_data.tag, origin.spec());
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;
}
bool NotificationUIManagerAndroid::OnNotificationClosed(
JNIEnv* env, jobject java_object, jstring notification_id) {
std::string id = ConvertJavaStringToUTF8(env, notification_id);
auto iter = profile_notifications_.find(id);
if (iter == profile_notifications_.end())
return false;
const Notification& notification = iter->second->notification();
notification.delegate()->Close(true /** by_user **/);
RemoveProfileNotification(iter->second, true /* close */);
return true;
}
void NotificationUIManagerAndroid::Add(const Notification& notification,
Profile* profile) {
// If the given notification is replacing an older one, drop its associated
// profile notification object without closing the platform notification.
// We'll use the native Android system to perform a smoother replacement.
ProfileNotification* notification_to_replace =
FindNotificationToReplace(notification, profile);
if (notification_to_replace)
RemoveProfileNotification(notification_to_replace, false /* close */);
ProfileNotification* profile_notification =
new ProfileNotification(profile, notification);
// Takes ownership of |profile_notification|.
AddProfileNotification(profile_notification);
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jstring> tag =
ConvertUTF8ToJavaString(env, notification.tag());
ScopedJavaLocalRef<jstring> id = ConvertUTF8ToJavaString(
env, profile_notification->notification().id());
ScopedJavaLocalRef<jstring> title = ConvertUTF16ToJavaString(
env, notification.title());
ScopedJavaLocalRef<jstring> body = ConvertUTF16ToJavaString(
env, notification.message());
ScopedJavaLocalRef<jstring> origin = ConvertUTF8ToJavaString(
env, notification.origin_url().GetOrigin().spec());
ScopedJavaLocalRef<jobject> icon;
SkBitmap icon_bitmap = notification.icon().AsBitmap();
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, true /* close */);
return;
}
notification_data = base::android::ToJavaByteArray(
env, static_cast<const uint8*>(pickle->data()), pickle->size());
}
Java_NotificationUIManager_displayNotification(
env,
java_object_.obj(),
tag.obj(),
id.obj(),
title.obj(),
body.obj(),
icon.obj(),
origin.obj(),
notification.silent(),
notification_data.obj());
regenerated_notification_infos_[profile_notification->notification().id()] =
std::make_pair(notification.tag(),
notification.origin_url().GetOrigin().spec());
notification.delegate()->Display();
}
bool NotificationUIManagerAndroid::Update(const Notification& notification,
Profile* profile) {
// This method is currently only called from extensions and local discovery,
// both of which are not supported on Android.
NOTIMPLEMENTED();
return false;
}
const Notification* NotificationUIManagerAndroid::FindById(
const std::string& delegate_id,
ProfileID profile_id) const {
std::string profile_notification_id =
ProfileNotification::GetProfileNotificationId(delegate_id, profile_id);
ProfileNotification* profile_notification =
FindProfileNotification(profile_notification_id);
if (!profile_notification)
return 0;
return &profile_notification->notification();
}
bool NotificationUIManagerAndroid::CancelById(const std::string& delegate_id,
ProfileID profile_id) {
std::string profile_notification_id =
ProfileNotification::GetProfileNotificationId(delegate_id, profile_id);
ProfileNotification* profile_notification =
FindProfileNotification(profile_notification_id);
if (profile_notification) {
RemoveProfileNotification(profile_notification, true /* close */);
return true;
}
// On Android, it's still possible that the notification can be closed in case
// the platform Id is known, even if no delegate exists. This happens when the
// browser process is started because of interaction with a Notification.
PlatformCloseNotification(delegate_id);
return true;
}
std::set<std::string>
NotificationUIManagerAndroid::GetAllIdsByProfileAndSourceOrigin(
Profile* profile,
const GURL& source) {
// |profile| may be invalid, so no calls must be made based on the instance.
std::set<std::string> delegate_ids;
for (auto iterator : profile_notifications_) {
ProfileNotification* profile_notification = iterator.second;
if (profile_notification->notification().origin_url() == source &&
profile_notification->profile() == profile)
delegate_ids.insert(profile_notification->notification().id());
}
return delegate_ids;
}
bool NotificationUIManagerAndroid::CancelAllBySourceOrigin(
const GURL& source_origin) {
bool removed = true;
for (auto iterator = profile_notifications_.begin();
iterator != profile_notifications_.end();) {
auto current_iterator = iterator++;
ProfileNotification* profile_notification = current_iterator->second;
if (profile_notification->notification().origin_url() == source_origin) {
RemoveProfileNotification(profile_notification, true /* close */);
removed = true;
}
}
return removed;
}
bool NotificationUIManagerAndroid::CancelAllByProfile(ProfileID profile_id) {
bool removed = true;
for (auto iterator = profile_notifications_.begin();
iterator != profile_notifications_.end();) {
auto current_iterator = iterator++;
ProfileNotification* profile_notification = current_iterator->second;
ProfileID current_profile_id =
NotificationUIManager::GetProfileID(profile_notification->profile());
if (current_profile_id == profile_id) {
RemoveProfileNotification(profile_notification, true /* close */);
removed = true;
}
}
return removed;
}
void NotificationUIManagerAndroid::CancelAll() {
for (auto iterator : profile_notifications_) {
ProfileNotification* profile_notification = iterator.second;
PlatformCloseNotification(profile_notification->notification().id());
delete profile_notification;
}
profile_notifications_.clear();
}
bool NotificationUIManagerAndroid::RegisterNotificationUIManager(JNIEnv* env) {
return RegisterNativesImpl(env);
}
void NotificationUIManagerAndroid::PlatformCloseNotification(
const std::string& notification_id) {
auto iterator = regenerated_notification_infos_.find(notification_id);
if (iterator == regenerated_notification_infos_.end())
return;
RegeneratedNotificationInfo notification_info = iterator->second;
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jstring> tag =
ConvertUTF8ToJavaString(env, notification_info.first);
ScopedJavaLocalRef<jstring> origin =
ConvertUTF8ToJavaString(env, notification_info.second);
ScopedJavaLocalRef<jstring> java_notification_id =
ConvertUTF8ToJavaString(env, notification_id);
regenerated_notification_infos_.erase(notification_id);
Java_NotificationUIManager_closeNotification(
env, java_object_.obj(), tag.obj(), java_notification_id.obj(),
origin.obj());
}
void NotificationUIManagerAndroid::AddProfileNotification(
ProfileNotification* profile_notification) {
std::string id = profile_notification->notification().id();
// Notification ids should be unique.
DCHECK(profile_notifications_.find(id) == profile_notifications_.end());
profile_notifications_[id] = profile_notification;
}
void NotificationUIManagerAndroid::RemoveProfileNotification(
ProfileNotification* profile_notification, bool close) {
std::string notification_id = profile_notification->notification().id();
if (close)
PlatformCloseNotification(notification_id);
profile_notifications_.erase(notification_id);
delete profile_notification;
}
ProfileNotification* NotificationUIManagerAndroid::FindProfileNotification(
const std::string& id) const {
auto iter = profile_notifications_.find(id);
if (iter == profile_notifications_.end())
return nullptr;
return iter->second;
}
ProfileNotification* NotificationUIManagerAndroid::FindNotificationToReplace(
const Notification& notification, Profile* profile) const {
const std::string& tag = notification.tag();
if (tag.empty())
return nullptr;
const GURL origin_url = notification.origin_url();
DCHECK(origin_url.is_valid());
for (const auto& iterator : profile_notifications_) {
ProfileNotification* profile_notification = iterator.second;
if (profile_notification->notification().tag() == tag ||
profile_notification->notification().origin_url() == origin_url ||
profile_notification->profile() == profile) {
return profile_notification;
}
}
return nullptr;
}