Commit 911bf513 authored by Richard Knoll's avatar Richard Knoll Committed by Commit Bot

Implement a notification suspender that stores / shows notifications.

This implements notification suspension if a site is paused. The
SuspensionTracker notifies the new NotificationSuspender of changes. The
NotificationPlatformBridge depends on the NotificationSuspender to check
if a notification is allowed to be displayed.

Bug: 949475
Change-Id: Id66bfd1479a7e77f34f6322d4432c5103d17d9ca
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1628753
Commit-Queue: Richard Knoll <knollr@chromium.org>
Reviewed-by: default avatarPeter Beverloo <peter@chromium.org>
Reviewed-by: default avatarDavid Trainor <dtrainor@chromium.org>
Reviewed-by: default avatarPatrick Noland <pnoland@chromium.org>
Cr-Commit-Position: refs/heads/master@{#666393}
parent 12e78f45
...@@ -2606,6 +2606,7 @@ generate_jni("chrome_jni_headers") { ...@@ -2606,6 +2606,7 @@ generate_jni("chrome_jni_headers") {
"java/src/org/chromium/chrome/browser/tabmodel/TabModelObserverJniBridge.java", "java/src/org/chromium/chrome/browser/tabmodel/TabModelObserverJniBridge.java",
"java/src/org/chromium/chrome/browser/toolbar/LocationBarModel.java", "java/src/org/chromium/chrome/browser/toolbar/LocationBarModel.java",
"java/src/org/chromium/chrome/browser/translate/TranslateBridge.java", "java/src/org/chromium/chrome/browser/translate/TranslateBridge.java",
"java/src/org/chromium/chrome/browser/usage_stats/NotificationSuspender.java",
"java/src/org/chromium/chrome/browser/usage_stats/UsageStatsBridge.java", "java/src/org/chromium/chrome/browser/usage_stats/UsageStatsBridge.java",
"java/src/org/chromium/chrome/browser/util/ChromeContextUtil.java", "java/src/org/chromium/chrome/browser/util/ChromeContextUtil.java",
"java/src/org/chromium/chrome/browser/util/FeatureUtilities.java", "java/src/org/chromium/chrome/browser/util/FeatureUtilities.java",
......
...@@ -1626,6 +1626,7 @@ chrome_java_sources = [ ...@@ -1626,6 +1626,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/upgrade/PackageReplacedBroadcastReceiver.java", "java/src/org/chromium/chrome/browser/upgrade/PackageReplacedBroadcastReceiver.java",
"java/src/org/chromium/chrome/browser/usage_stats/DigitalWellbeingClient.java", "java/src/org/chromium/chrome/browser/usage_stats/DigitalWellbeingClient.java",
"java/src/org/chromium/chrome/browser/usage_stats/EventTracker.java", "java/src/org/chromium/chrome/browser/usage_stats/EventTracker.java",
"java/src/org/chromium/chrome/browser/usage_stats/NotificationSuspender.java",
"java/src/org/chromium/chrome/browser/usage_stats/PageViewObserver.java", "java/src/org/chromium/chrome/browser/usage_stats/PageViewObserver.java",
"java/src/org/chromium/chrome/browser/usage_stats/SuspendedTab.java", "java/src/org/chromium/chrome/browser/usage_stats/SuspendedTab.java",
"java/src/org/chromium/chrome/browser/usage_stats/SuspensionTracker.java", "java/src/org/chromium/chrome/browser/usage_stats/SuspensionTracker.java",
......
...@@ -258,6 +258,7 @@ public abstract class ChromeFeatureList { ...@@ -258,6 +258,7 @@ public abstract class ChromeFeatureList {
public static final String MODAL_PERMISSION_DIALOG_VIEW = "ModalPermissionDialogView"; public static final String MODAL_PERMISSION_DIALOG_VIEW = "ModalPermissionDialogView";
public static final String NEW_PHOTO_PICKER = "NewPhotoPicker"; public static final String NEW_PHOTO_PICKER = "NewPhotoPicker";
public static final String NETWORK_SERVICE = "NetworkService"; public static final String NETWORK_SERVICE = "NetworkService";
public static final String NOTIFICATION_SUSPENDER = "NotificationSuspender";
public static final String NO_CREDIT_CARD_ABORT = "NoCreditCardAbort"; public static final String NO_CREDIT_CARD_ABORT = "NoCreditCardAbort";
public static final String NTP_ARTICLE_SUGGESTIONS = "NTPArticleSuggestions"; public static final String NTP_ARTICLE_SUGGESTIONS = "NTPArticleSuggestions";
public static final String NTP_BUTTON = "NTPButton"; public static final String NTP_BUTTON = "NTPButton";
......
...@@ -36,7 +36,6 @@ import android.view.WindowManager; ...@@ -36,7 +36,6 @@ import android.view.WindowManager;
import org.chromium.base.ActivityState; import org.chromium.base.ActivityState;
import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ApplicationStatus; import org.chromium.base.ApplicationStatus;
import org.chromium.base.BuildInfo;
import org.chromium.base.CommandLine; import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils; import org.chromium.base.ContextUtils;
import org.chromium.base.Log; import org.chromium.base.Log;
...@@ -832,8 +831,7 @@ public class ChromeTabbedActivity ...@@ -832,8 +831,7 @@ public class ChromeTabbedActivity
FeedProcessScopeFactory.getFeedAppLifecycle(); FeedProcessScopeFactory.getFeedAppLifecycle();
} }
if (BuildInfo.isAtLeastQ() if (UsageStatsService.isEnabled()) {
&& ChromeFeatureList.isEnabled(ChromeFeatureList.USAGE_STATS)) {
UsageStatsService.getInstance().createPageViewObserver(mTabModelSelectorImpl, this); UsageStatsService.getInstance().createPageViewObserver(mTabModelSelectorImpl, this);
} }
} }
......
...@@ -41,6 +41,7 @@ import org.chromium.chrome.browser.preferences.PreferencesLauncher; ...@@ -41,6 +41,7 @@ import org.chromium.chrome.browser.preferences.PreferencesLauncher;
import org.chromium.chrome.browser.preferences.website.SingleCategoryPreferences; import org.chromium.chrome.browser.preferences.website.SingleCategoryPreferences;
import org.chromium.chrome.browser.preferences.website.SingleWebsitePreferences; import org.chromium.chrome.browser.preferences.website.SingleWebsitePreferences;
import org.chromium.chrome.browser.preferences.website.SiteSettingsCategory; import org.chromium.chrome.browser.preferences.website.SiteSettingsCategory;
import org.chromium.chrome.browser.usage_stats.NotificationSuspender;
import org.chromium.chrome.browser.webapps.ChromeWebApkHost; import org.chromium.chrome.browser.webapps.ChromeWebApkHost;
import org.chromium.chrome.browser.webapps.WebApkServiceClient; import org.chromium.chrome.browser.webapps.WebApkServiceClient;
import org.chromium.components.url_formatter.UrlFormatter; import org.chromium.components.url_formatter.UrlFormatter;
...@@ -62,7 +63,7 @@ public class NotificationPlatformBridge { ...@@ -62,7 +63,7 @@ public class NotificationPlatformBridge {
// We always use the same integer id when showing and closing notifications. The notification // We always use the same integer id when showing and closing notifications. The notification
// tag is always set, which is a safe and sufficient way of identifying a notification, so the // tag is always set, which is a safe and sufficient way of identifying a notification, so the
// integer id is not needed anymore except it must not vary in an uncontrolled way. // integer id is not needed anymore except it must not vary in an uncontrolled way.
@VisibleForTesting static final int PLATFORM_ID = -1; public static final int PLATFORM_ID = -1;
// We always use the same request code for pending intents. We use other ways to force // We always use the same request code for pending intents. We use other ways to force
// uniqueness of pending intents when necessary. // uniqueness of pending intents when necessary.
...@@ -383,8 +384,7 @@ public class NotificationPlatformBridge { ...@@ -383,8 +384,7 @@ public class NotificationPlatformBridge {
* tag, or if the notification tag didn't match the expected format. * tag, or if the notification tag didn't match the expected format.
*/ */
@Nullable @Nullable
@VisibleForTesting public static String getOriginFromNotificationTag(@Nullable String tag) {
static String getOriginFromNotificationTag(@Nullable String tag) {
if (tag == null if (tag == null
|| !tag.startsWith(NotificationConstants.PERSISTENT_NOTIFICATION_TAG_PREFIX || !tag.startsWith(NotificationConstants.PERSISTENT_NOTIFICATION_TAG_PREFIX
+ NotificationConstants.NOTIFICATION_TAG_SEPARATOR)) + NotificationConstants.NOTIFICATION_TAG_SEPARATOR))
...@@ -538,11 +538,16 @@ public class NotificationPlatformBridge { ...@@ -538,11 +538,16 @@ public class NotificationPlatformBridge {
ChromeNotification notification = ChromeNotification notification =
buildNotification(notificationBuilder, notificationId, origin, actions, image); buildNotification(notificationBuilder, notificationId, origin, actions, image);
// Display notification as Chrome.
mNotificationManager.notify(notification); // Store notification if its origin is suspended.
NotificationUmaTracker.getInstance().onNotificationShown( NotificationSuspender.maybeSuspendNotification(notification).then((suspended) -> {
NotificationUmaTracker.SystemNotificationType.SITES, if (suspended) return;
notification.getNotification()); // Display notification as Chrome.
mNotificationManager.notify(notification);
NotificationUmaTracker.getInstance().onNotificationShown(
NotificationUmaTracker.SystemNotificationType.SITES,
notification.getNotification());
});
} }
private NotificationBuilderBase prepareNotificationBuilder(String notificationId, String origin, private NotificationBuilderBase prepareNotificationBuilder(String notificationId, String origin,
......
// Copyright 2019 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.usage_stats;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.webkit.URLUtil;
import org.chromium.base.CollectionUtil;
import org.chromium.base.ContextUtils;
import org.chromium.base.Promise;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.notifications.ChromeNotification;
import org.chromium.chrome.browser.notifications.NotificationMetadata;
import org.chromium.chrome.browser.notifications.NotificationPlatformBridge;
import org.chromium.chrome.browser.notifications.NotificationUmaTracker;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.content_public.browser.BrowserStartupController;
import org.chromium.content_public.browser.BrowserStartupController.StartupCallback;
import java.util.ArrayList;
import java.util.List;
/**
* Class that suspends and revives notifications for suspended websites. All calls must be made on
* the UI thread.
*/
@JNINamespace("usage_stats")
public class NotificationSuspender {
private final Profile mProfile;
private final Context mContext;
private final NotificationManager mNotificationManager;
private static boolean isEnabled() {
return UsageStatsService.isEnabled()
&& ChromeFeatureList.isEnabled(ChromeFeatureList.NOTIFICATION_SUSPENDER);
}
/**
* Suspends the given notification if it originates from a suspended domain.
* @param notification The notification to suspend.
* @return A {@link Promise} that resolves to whether the given notification got suspended.
*/
public static Promise<Boolean> maybeSuspendNotification(ChromeNotification notification) {
// No need to initialize UsageStatsService if it is disabled.
if (!isEnabled()) return Promise.fulfilled(false);
return waitForChromeStartup()
.then((Void v) -> UsageStatsService.getInstance().getAllSuspendedWebsitesAsync())
.then((List<String> fqdns) -> {
if (!fqdns.contains(getValidFqdnOrEmptyString(notification))) return false;
UsageStatsService.getInstance()
.getNotificationSuspender()
.storeNotificationResources(CollectionUtil.newArrayList(notification));
return true;
});
}
public NotificationSuspender(Profile profile) {
mProfile = profile;
mContext = ContextUtils.getApplicationContext();
mNotificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
}
public void setWebsitesSuspended(List<String> fqdns, boolean suspended) {
if (fqdns.isEmpty() || !isEnabled()) return;
if (suspended) {
storeNotificationResources(getActiveNotificationsForFqdns(fqdns));
} else {
unsuspendWebsites(fqdns);
}
}
private void storeNotificationResources(List<ChromeNotification> notifications) {
if (notifications.isEmpty()) return;
String[] ids = new String[notifications.size()];
String[] origins = new String[notifications.size()];
Bitmap[] resources = new Bitmap[notifications.size() * 3];
for (int i = 0; i < notifications.size(); ++i) {
Notification notification = notifications.get(i).getNotification();
String tag = notifications.get(i).getMetadata().tag;
ids[i] = tag;
origins[i] = NotificationPlatformBridge.getOriginFromNotificationTag(tag);
resources[i * 3 + 0] = getNotificationIcon(notification);
resources[i * 3 + 1] = getNotificationBadge(notification);
resources[i * 3 + 2] = getNotificationImage(notification);
mNotificationManager.cancel(tag, NotificationPlatformBridge.PLATFORM_ID);
}
NotificationSuspenderJni.get().storeNotificationResources(
mProfile, ids, origins, resources);
}
private void unsuspendWebsites(List<String> fqdns) {
if (fqdns.isEmpty()) return;
// Handle both http and https schemes as native expects origins.
String[] origins = new String[fqdns.size() * 2];
for (int i = 0; i < fqdns.size(); ++i) {
origins[i * 2 + 0] = "http://" + fqdns.get(i);
origins[i * 2 + 1] = "https://" + fqdns.get(i);
}
NotificationSuspenderJni.get().reDisplayNotifications(mProfile, origins);
}
@TargetApi(Build.VERSION_CODES.M)
private List<ChromeNotification> getActiveNotificationsForFqdns(List<String> fqdns) {
List<ChromeNotification> notifications = new ArrayList<>();
for (StatusBarNotification notification : mNotificationManager.getActiveNotifications()) {
if (notification.getId() != NotificationPlatformBridge.PLATFORM_ID) continue;
String tag = notification.getTag();
String origin = NotificationPlatformBridge.getOriginFromNotificationTag(tag);
if (!URLUtil.isHttpUrl(origin) && !URLUtil.isHttpsUrl(origin)) continue;
if (!fqdns.contains(Uri.parse(origin).getHost())) continue;
NotificationMetadata metadata =
new NotificationMetadata(NotificationUmaTracker.SystemNotificationType.SITES,
tag, NotificationPlatformBridge.PLATFORM_ID);
notifications.add(new ChromeNotification(notification.getNotification(), metadata));
}
return notifications;
}
@TargetApi(Build.VERSION_CODES.P)
private Bitmap getBitmapFromIcon(Icon icon) {
if (icon == null || icon.getType() != Icon.TYPE_BITMAP) return null;
return ((BitmapDrawable) icon.loadDrawable(mContext)).getBitmap();
}
@TargetApi(Build.VERSION_CODES.M)
private Bitmap getNotificationIcon(Notification notification) {
return getBitmapFromIcon(notification.getLargeIcon());
}
@TargetApi(Build.VERSION_CODES.M)
private Bitmap getNotificationBadge(Notification notification) {
return getBitmapFromIcon(notification.getSmallIcon());
}
private Bitmap getNotificationImage(Notification notification) {
return (Bitmap) notification.extras.get(Notification.EXTRA_PICTURE);
}
private static String getValidFqdnOrEmptyString(ChromeNotification notification) {
String tag = notification.getMetadata().tag;
String origin = NotificationPlatformBridge.getOriginFromNotificationTag(tag);
if (TextUtils.isEmpty(origin)) return "";
String host = Uri.parse(origin).getHost();
return host == null ? "" : host;
}
private static Promise<Void> waitForChromeStartup() {
BrowserStartupController browserStartup =
BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER);
if (browserStartup.isStartupSuccessfullyCompleted()) return Promise.fulfilled(null);
Promise<Void> promise = new Promise<>();
browserStartup.addStartupCompletedObserver(new StartupCallback() {
@Override
public void onSuccess() {
promise.fulfill(null);
}
@Override
public void onFailure() {
promise.reject(null);
}
});
return promise;
}
@NativeMethods
interface Natives {
// Stores the given |resources| to be displayed later again. Note that |resources| is
// expected to have 3 entries (icon, badge, image in that order) for each notification id in
// |notificationIds|. If a notification does not have a particular resource, pass null
// instead. |origins| must be the same size as |notificationIds|.
void storeNotificationResources(
Profile profile, String[] notificationIds, String[] origins, Bitmap[] resources);
// Displays all suspended notifications for the given |origins|.
void reDisplayNotifications(Profile profile, String[] origins);
}
}
...@@ -14,12 +14,14 @@ import java.util.List; ...@@ -14,12 +14,14 @@ import java.util.List;
* Class that tracks which sites are currently suspended. * Class that tracks which sites are currently suspended.
*/ */
public class SuspensionTracker { public class SuspensionTracker {
private UsageStatsBridge mBridge; private final UsageStatsBridge mBridge;
private Promise<List<String>> mRootPromise; private final NotificationSuspender mNotificationSuspender;
private final Promise<List<String>> mRootPromise;
private Promise<Void> mWritePromise; private Promise<Void> mWritePromise;
public SuspensionTracker(UsageStatsBridge bridge) { public SuspensionTracker(UsageStatsBridge bridge, NotificationSuspender notificationSuspender) {
mBridge = bridge; mBridge = bridge;
mNotificationSuspender = notificationSuspender;
mRootPromise = new Promise<>(); mRootPromise = new Promise<>();
mBridge.getAllSuspensions((result) -> { mRootPromise.fulfill(result); }); mBridge.getAllSuspensions((result) -> { mRootPromise.fulfill(result); });
mWritePromise = Promise.fulfilled(null); mWritePromise = Promise.fulfilled(null);
...@@ -55,7 +57,7 @@ public class SuspensionTracker { ...@@ -55,7 +57,7 @@ public class SuspensionTracker {
} else { } else {
result.removeAll(fqdns); result.removeAll(fqdns);
} }
mNotificationSuspender.setWebsitesSuspended(fqdns, suspended);
newWritePromise.fulfill(null); newWritePromise.fulfill(null);
} else { } else {
newWritePromise.reject(); newWritePromise.reject();
......
...@@ -6,6 +6,7 @@ package org.chromium.chrome.browser.usage_stats; ...@@ -6,6 +6,7 @@ package org.chromium.chrome.browser.usage_stats;
import android.app.Activity; import android.app.Activity;
import org.chromium.base.BuildInfo;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.base.Promise; import org.chromium.base.Promise;
import org.chromium.base.ThreadUtils; import org.chromium.base.ThreadUtils;
...@@ -31,6 +32,7 @@ public class UsageStatsService { ...@@ -31,6 +32,7 @@ public class UsageStatsService {
private static UsageStatsService sInstance; private static UsageStatsService sInstance;
private EventTracker mEventTracker; private EventTracker mEventTracker;
private NotificationSuspender mNotificationSuspender;
private SuspensionTracker mSuspensionTracker; private SuspensionTracker mSuspensionTracker;
private TokenTracker mTokenTracker; private TokenTracker mTokenTracker;
private UsageStatsBridge mBridge; private UsageStatsBridge mBridge;
...@@ -42,8 +44,14 @@ public class UsageStatsService { ...@@ -42,8 +44,14 @@ public class UsageStatsService {
private DigitalWellbeingClient mClient; private DigitalWellbeingClient mClient;
private boolean mOptInState; private boolean mOptInState;
/** Returns if the UsageStatsService is enabled on this device */
public static boolean isEnabled() {
return BuildInfo.isAtLeastQ() && ChromeFeatureList.isEnabled(ChromeFeatureList.USAGE_STATS);
}
/** Get the global instance of UsageStatsService */ /** Get the global instance of UsageStatsService */
public static UsageStatsService getInstance() { public static UsageStatsService getInstance() {
assert isEnabled();
if (sInstance == null) { if (sInstance == null) {
sInstance = new UsageStatsService(); sInstance = new UsageStatsService();
} }
...@@ -56,7 +64,8 @@ public class UsageStatsService { ...@@ -56,7 +64,8 @@ public class UsageStatsService {
Profile profile = Profile.getLastUsedProfile().getOriginalProfile(); Profile profile = Profile.getLastUsedProfile().getOriginalProfile();
mBridge = new UsageStatsBridge(profile, this); mBridge = new UsageStatsBridge(profile, this);
mEventTracker = new EventTracker(mBridge); mEventTracker = new EventTracker(mBridge);
mSuspensionTracker = new SuspensionTracker(mBridge); mNotificationSuspender = new NotificationSuspender(profile);
mSuspensionTracker = new SuspensionTracker(mBridge, mNotificationSuspender);
mTokenTracker = new TokenTracker(mBridge); mTokenTracker = new TokenTracker(mBridge);
mPageViewObservers = new ArrayList<>(); mPageViewObservers = new ArrayList<>();
...@@ -67,6 +76,10 @@ public class UsageStatsService { ...@@ -67,6 +76,10 @@ public class UsageStatsService {
mClient = AppHooks.get().createDigitalWellbeingClient(); mClient = AppHooks.get().createDigitalWellbeingClient();
} }
/* package */ NotificationSuspender getNotificationSuspender() {
return mNotificationSuspender;
}
/** /**
* Create a {@link PageViewObserver} for the given tab model selector and activity. * Create a {@link PageViewObserver} for the given tab model selector and activity.
* @param tabModelSelector The tab model selector that should be used to get the current tab * @param tabModelSelector The tab model selector that should be used to get the current tab
......
...@@ -2585,6 +2585,7 @@ jumbo_split_static_library("browser") { ...@@ -2585,6 +2585,7 @@ jumbo_split_static_library("browser") {
"android/trusted_cdn.cc", "android/trusted_cdn.cc",
"android/trusted_cdn.h", "android/trusted_cdn.h",
"android/url_utilities.cc", "android/url_utilities.cc",
"android/usage_stats/notification_suspender.cc",
"android/usage_stats/usage_stats_bridge.cc", "android/usage_stats/usage_stats_bridge.cc",
"android/usage_stats/usage_stats_bridge.h", "android/usage_stats/usage_stats_bridge.h",
"android/usage_stats/usage_stats_database.cc", "android/usage_stats/usage_stats_database.cc",
......
...@@ -145,6 +145,7 @@ const base::Feature* kFeaturesExposedToJava[] = { ...@@ -145,6 +145,7 @@ const base::Feature* kFeaturesExposedToJava[] = {
&kIntentBlockExternalFormRedirectsNoGesture, &kIntentBlockExternalFormRedirectsNoGesture,
&kJellyBeanSupported, &kJellyBeanSupported,
&kNewPhotoPicker, &kNewPhotoPicker,
&kNotificationSuspender,
&kNoCreditCardAbort, &kNoCreditCardAbort,
&kNTPButton, &kNTPButton,
&kNTPLaunchAfterInactivity, &kNTPLaunchAfterInactivity,
...@@ -452,6 +453,11 @@ const base::Feature kSearchEnginePromoNewDevice{ ...@@ -452,6 +453,11 @@ const base::Feature kSearchEnginePromoNewDevice{
const base::Feature kNewPhotoPicker{"NewPhotoPicker", const base::Feature kNewPhotoPicker{"NewPhotoPicker",
base::FEATURE_ENABLED_BY_DEFAULT}; base::FEATURE_ENABLED_BY_DEFAULT};
// TODO(knollr): This is a temporary kill switch, it can be removed once we feel
// okay about leaving it on.
const base::Feature kNotificationSuspender{"NotificationSuspender",
base::FEATURE_ENABLED_BY_DEFAULT};
const base::Feature kNoCreditCardAbort{"NoCreditCardAbort", const base::Feature kNoCreditCardAbort{"NoCreditCardAbort",
base::FEATURE_ENABLED_BY_DEFAULT}; base::FEATURE_ENABLED_BY_DEFAULT};
......
...@@ -83,6 +83,7 @@ extern const base::Feature kIntentBlockExternalFormRedirectsNoGesture; ...@@ -83,6 +83,7 @@ extern const base::Feature kIntentBlockExternalFormRedirectsNoGesture;
extern const base::Feature kJellyBeanSupported; extern const base::Feature kJellyBeanSupported;
extern const base::Feature kLanguagesPreference; extern const base::Feature kLanguagesPreference;
extern const base::Feature kNewPhotoPicker; extern const base::Feature kNewPhotoPicker;
extern const base::Feature kNotificationSuspender;
extern const base::Feature kNoCreditCardAbort; extern const base::Feature kNoCreditCardAbort;
extern const base::Feature kNTPButton; extern const base::Feature kNTPButton;
extern const base::Feature kNTPLaunchAfterInactivity; extern const base::Feature kNTPLaunchAfterInactivity;
......
// Copyright 2019 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 <vector>
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/logging.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_android.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/platform_notification_context.h"
#include "content/public/browser/storage_partition.h"
#include "jni/NotificationSuspender_jni.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/android/java_bitmap.h"
#include "ui/gfx/image/image.h"
using base::android::AttachCurrentThread;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;
using content::BrowserContext;
using content::NotificationResourceData;
using content::PlatformNotificationContext;
namespace {
SkBitmap ExtractImage(JNIEnv* env,
const JavaParamRef<jobjectArray>& j_resources,
int index) {
ScopedJavaLocalRef<jobject> j_image(
env, env->GetObjectArrayElement(j_resources, index));
return j_image.is_null()
? SkBitmap()
: CreateSkBitmapFromJavaBitmap(gfx::JavaBitmap(j_image));
}
std::vector<blink::NotificationResources> ParseResources(
JNIEnv* env,
const JavaParamRef<jobjectArray>& j_resources) {
// Resources is an array of bitmaps with the following order:
// [icon, badge, image, icon, badge, image, ...]
int resource_count = env->GetArrayLength(j_resources);
DCHECK(resource_count % 3 == 0);
std::vector<blink::NotificationResources> resources;
for (int i = 0; i < resource_count; i += 3) {
blink::NotificationResources res;
res.notification_icon = ExtractImage(env, j_resources, i + 0);
res.badge = ExtractImage(env, j_resources, i + 1);
res.image = ExtractImage(env, j_resources, i + 2);
resources.emplace_back(std::move(res));
}
return resources;
}
PlatformNotificationContext* GetContext(Profile* profile, const GURL& origin) {
auto* partition = BrowserContext::GetStoragePartitionForSite(profile, origin);
auto* context = partition->GetPlatformNotificationContext();
DCHECK(context);
return context;
}
} // namespace
namespace usage_stats {
// Stores the given |j_resources| to be displayed later again. Note that
// |j_resources| is expected to have 3 entries (icon, badge, image in that
// order) for each notification id in |j_notification_ids|. If a notification
// does not have a particular resource, pass null instead. |j_origins| must be
// the same size as |j_notification_ids|.
static void JNI_NotificationSuspender_StoreNotificationResources(
JNIEnv* env,
const JavaParamRef<jobject>& j_profile,
const JavaParamRef<jobjectArray>& j_notification_ids,
const JavaParamRef<jobjectArray>& j_origins,
const JavaParamRef<jobjectArray>& j_resources) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile);
DCHECK(profile);
std::vector<std::string> id_strings;
AppendJavaStringArrayToStringVector(env, j_notification_ids, &id_strings);
std::vector<std::string> origin_strings;
AppendJavaStringArrayToStringVector(env, j_origins, &origin_strings);
std::vector<blink::NotificationResources> resources =
ParseResources(env, j_resources);
DCHECK(id_strings.size() == origin_strings.size());
DCHECK(id_strings.size() == resources.size());
// Group resources by context.
std::map<PlatformNotificationContext*, std::vector<NotificationResourceData>>
resources_by_context;
for (size_t i = 0; i < id_strings.size(); ++i) {
GURL origin(std::move(origin_strings[i]));
if (!origin.is_valid() || !origin.SchemeIsHTTPOrHTTPS())
continue;
resources_by_context[GetContext(profile, origin)].emplace_back(
NotificationResourceData{std::move(id_strings[i]), std::move(origin),
std::move(resources[i])});
}
// Store resources in each context.
for (auto& entry : resources_by_context) {
entry.first->WriteNotificationResources(std::move(entry.second),
base::DoNothing());
}
}
// ReDisplays all notifications with stored resources for all |j_origins|.
static void JNI_NotificationSuspender_ReDisplayNotifications(
JNIEnv* env,
const JavaParamRef<jobject>& j_profile,
const JavaParamRef<jobjectArray>& j_origins) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile);
DCHECK(profile);
std::vector<std::string> origin_strings;
AppendJavaStringArrayToStringVector(env, j_origins, &origin_strings);
// Group origins by context.
std::map<PlatformNotificationContext*, std::vector<GURL>> origins_by_context;
for (std::string& origin_string : origin_strings) {
GURL origin(std::move(origin_string));
if (!origin.is_valid() || !origin.SchemeIsHTTPOrHTTPS())
continue;
origins_by_context[GetContext(profile, origin)].emplace_back(
std::move(origin));
}
// ReDisplay notifications from each context.
for (auto& entry : origins_by_context) {
entry.first->ReDisplayNotifications(std::move(entry.second),
base::DoNothing());
}
}
} // namespace usage_stats
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