Commit ef89ac8a authored by Peter E Conn's avatar Peter E Conn Committed by Commit Bot

🤝 Ask to clear data when TWA client app is cleared.

Bug: 888447
Change-Id: I3170ed456f8ccc08912e490cdfb1200789533b66
Reviewed-on: https://chromium-review.googlesource.com/c/1283040Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Reviewed-by: default avatarPeter Beverloo <peter@chromium.org>
Commit-Queue: Peter Conn <peconn@chromium.org>
Cr-Commit-Position: refs/heads/master@{#601884}
parent 4fb4d4bd
......@@ -795,15 +795,24 @@ by a child template that "extends" this file.
</service>
<!-- Components for Trusted Web Activities -->
<receiver android:name="org.chromium.chrome.browser.browserservices.ClearDataBroadcastReceiver"
<receiver android:name="org.chromium.chrome.browser.browserservices.ClientAppBroadcastReceiver"
android:exported="true">
<intent-filter>
<data android:scheme="package" />
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
<action android:name="android.intent.action.PACKAGE_DATA_CLEARED" />
</intent-filter>
{% if channel in ['default'] %}
<intent-filter>
<action android:name="org.chromium.chrome.browser.browserservices.ClientAppBroadcastReceiver.DEBUG" />
</intent-filter>
{% endif %}
</receiver>
<service android:name="org.chromium.chrome.browser.browserservices.ClearDataService"
android:exported="false">
</service>
<!-- Service for decoding images in a sandboxed process. -->
<service
android:description="@string/decoder_description"
......
// 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.browserservices;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.content.res.Resources;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.notifications.NotificationBuilderFactory;
import org.chromium.chrome.browser.notifications.channels.ChannelDefinitions;
/**
* The class responsible for showing the "Would you like to clear your data", the "You are clearing
* your data" and the "Your data has been cleared" notifications that occur when a user uninstalls
* (or clears data for) a Trusted Web Activity client app.
*
* Lifecycle: This class holds no state, create an instance or share as you like.
* Thread safety: Methods on this class can be called from any thread.
*/
public class ClearDataNotificationPublisher {
private static final String NOTIFICATION_TAG_CLEAR_DATA = "ClearDataNotification.ClearData";
/* package */ void showClearDataNotification(Context context, String appName, String url,
boolean uninstall) {
// We base the notification id on the URL so we don't have duplicate Notifications
// offering to clear the same URL.
int notificationId = url.hashCode();
Resources res = context.getResources();
String title = res.getString(uninstall ? R.string.you_have_uninstalled_app
: R.string.you_have_cleared_app, appName);
Notification notification = NotificationBuilderFactory
.createChromeNotificationBuilder(true /* preferCompat */,
ChannelDefinitions.ChannelId.BROWSER)
.setContentTitle(title)
.setContentText(res.getString(R.string.clear_related_data, url))
.addAction(R.drawable.btn_star, res.getString(R.string.clear_data_delete),
ClearDataService.getClearDataIntent(context, url, notificationId))
.addAction(R.drawable.btn_close, res.getString(R.string.close),
ClearDataService.getDismissIntent(context, notificationId))
.setSmallIcon(R.drawable.ic_chrome)
.build();
NotificationManager manager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(NOTIFICATION_TAG_CLEAR_DATA, notificationId, notification);
}
/* package */ void dismissClearDataNotification(Context context, int notificationId) {
NotificationManager manager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(NOTIFICATION_TAG_CLEAR_DATA, notificationId);
}
}
// 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.browserservices;
import android.app.IntentService;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.Nullable;
import org.chromium.base.Log;
/**
* A background service that clears browsing data.
*
* Lifecycle: This is a Service, so it will be created by the Android Framework.
* Thread safety: As an {@link IntentService}, {@link #onHandleIntent} is called on a background
* thread.
*/
public class ClearDataService extends IntentService {
private static final String TAG = "ClearDataService";
/* package */ static final String ACTION_CLEAR_DATA =
"org.chromium.chrome.browser.browserservices.ClearDataService.CLEAR_DATA";
/* package */ static final String ACTION_DISMISS =
"org.chromium.chrome.browser.browserservices.ClearDataService.DISMISS";
/* package */ static final String EXTRA_NOTIFICATION_ID =
"org.chromium.chrome.browser.browserservices.ClearDataService.NOTIFICATION_ID";
/* package */ static final String EXTRA_URL =
"org.chromium.chrome.browser.browserservices.ClearDataService.URL";
private final ClearDataNotificationPublisher mNotificationManager;
/** Constructor required by Android with default dependencies. */
public ClearDataService() {
this(new ClearDataNotificationPublisher());
}
/** Constructor with dependency injection for testing. */
public ClearDataService(ClearDataNotificationPublisher notificationManager) {
super(TAG);
mNotificationManager = notificationManager;
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
if (intent == null) return;
// ClearDataService is not exported, so as long as we don't let PendingIntents pointing to
// this class leak to other Android applications, we can trust that this code can only be
// called from Chrome (or a Notification that Chrome created).
if (!intent.hasExtra(EXTRA_NOTIFICATION_ID)) {
Log.w(TAG, "Got Intent without Notification Id");
return;
}
int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, 0);
if (ACTION_CLEAR_DATA.equals(intent.getAction())) {
String url = intent.getStringExtra(EXTRA_URL);
if (url == null) {
Log.w(TAG, "Got Clear Data Intent without URL.");
return;
}
// TODO(peconn): Clear the data!
Log.d(TAG, "Pretending to clear data for %s.", url);
mNotificationManager.dismissClearDataNotification(this, notificationId);
} else if (ACTION_DISMISS.equals(intent.getAction())) {
mNotificationManager.dismissClearDataNotification(this, notificationId);
}
}
/**
* Creates a PendingIntent to clear data for the given |url| and cancel the notification with
* the given |id|.
*/
public static PendingIntent getClearDataIntent(Context context, String url, int id) {
Intent intent = new Intent(context, ClearDataService.class);
intent.setAction(ACTION_CLEAR_DATA);
intent.putExtra(EXTRA_NOTIFICATION_ID, id);
// TODO(peconn): Consider putting this in data instead.
intent.putExtra(EXTRA_URL, url);
// See similar code in {@link getDismissIntent}.
return PendingIntent.getService(context, id, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
/**
* Creates a PendingIntent to dismiss the Notification with the given |id|.
*/
public static PendingIntent getDismissIntent(Context context, int id) {
Intent intent = new Intent(context, ClearDataService.class);
intent.setAction(ACTION_DISMISS);
// Store the notification ID in the intent itself so we can retrieve it later.
intent.putExtra(EXTRA_NOTIFICATION_ID, id);
// Also use the notification ID as the request code so we can have multiple pending intents
// existing at the same time for different applications/urls.
return PendingIntent.getService(context, id, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
}
......@@ -9,6 +9,7 @@ import android.content.Context;
import android.content.Intent;
import org.chromium.base.Log;
import org.chromium.chrome.browser.ChromeVersionInfo;
import java.util.Arrays;
import java.util.HashSet;
......@@ -20,55 +21,89 @@ import java.util.Set;
* corresponding to that app.
*
* Trusted Web Activities are registered to an origin (eg https://www.example.com), however because
* cookies can be scoped more loosely, at 'domain and registry'/TLD+1 level (eg *.example.com) [1],
* we need to clear data at that level. This unfortunately can lead to too much data getting cleared
* - for example if the https://maps.google.com TWA is cleared, you'll loose cookies for
* https://mail.google.com too.
* cookies can be scoped more loosely, at eTLD+1 (or domain) level (eg *.example.com) [1], we need
* to clear data at that level. This unfortunately can lead to too much data getting cleared - for
* example if the https://maps.google.com TWA is cleared, you'll loose cookies for
* https://mail.google.com too (since they both share the google.com domain).
*
* We find this acceptable for two reasons:
* - The alternative is *not* clearing some related data - eg a TWA linked to
* https://peconn.github.io sets a cookie with Domain=github.io. The TWA is uninstalled and
* https://maps.google.com sets a cookie with Domain=google.com. The TWA is uninstalled and
* reinstalled and it can access the cookie it stored before.
* - We ask the user before clearing the data and while doing so display the scope of data we're
* going to wipe.
*
* [1] https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Scope_of_cookies
*
* Lifecycle: The lifecycle of this class is managed by Android.
* Thread safety: {@link #onReceive} will be called on the UI thread.
*/
public class ClearDataBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "ClearDataBroadRec";
public class ClientAppBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "ClientAppBroadRec";
/**
* An Action that will trigger clearing data on local builds only, for development. The adb
* command to trigger is:
* adb shell am broadcast \
* -n com.google.android.apps.chrome/\
* org.chromium.chrome.browser.browserservices.ClientAppBroadcastReceiver \
* -a org.chromium.chrome.browser.browserservices.ClientAppBroadcastReceiver.DEBUG \
* --ei android.intent.extra.UID 23
*
* But replace 23 with the uid of a Trusted Web Activity Client app.
*/
private static final String ACTION_DEBUG =
"org.chromium.chrome.browser.browserservices.ClientAppBroadcastReceiver.DEBUG";
private static final Set<String> BROADCASTS = new HashSet<>(Arrays.asList(
Intent.ACTION_PACKAGE_DATA_CLEARED,
Intent.ACTION_PACKAGE_FULLY_REMOVED
));
private final ClearDataNotificationPublisher mNotificationPublisher;
private final ClientAppDataRegister mRegister;
/** Constructor with default dependencies for Android. */
public ClientAppBroadcastReceiver() {
this(new ClearDataNotificationPublisher(), new ClientAppDataRegister());
}
/** Constructor to allow dependency injection in tests. */
public ClientAppBroadcastReceiver(ClearDataNotificationPublisher notificationPublisher,
ClientAppDataRegister register) {
mNotificationPublisher = notificationPublisher;
mRegister = register;
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) return;
// Since we only care about ACTION_PACKAGE_DATA_CLEARED and and ACTION_PACKAGE_FULLY_REMOVED
// which are protected Intents, we can assume that anything that gets past here will be a
// legitimate Intent sent by the system.
if (!BROADCASTS.contains(intent.getAction())) return;
boolean debug = ChromeVersionInfo.isLocalBuild() && ACTION_DEBUG.equals(intent.getAction());
if (!debug && !BROADCASTS.contains(intent.getAction())) return;
int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
if (uid == -1) return;
ClientAppDataRegister register;
try (BrowserServicesMetrics.TimingMetric unused =
BrowserServicesMetrics.getClientAppDataLoadTimingContext()) {
register = new ClientAppDataRegister();
// The ClientAppDataRegister (because it uses Preferences) is loaded lazily, so to time
// opening the file we must include the first read as well.
if (!register.chromeHoldsDataForPackage(uid)) return;
if (!mRegister.chromeHoldsDataForPackage(uid)) {
Log.d(TAG, "Chrome holds no data for package.");
return;
}
}
String appName = register.getAppNameForRegisteredUid(uid);
Set<String> domainAndRegistries = register.getDomainAndRegistriesForRegisteredUid(uid);
String appName = mRegister.getAppNameForRegisteredUid(uid);
Set<String> origins = mRegister.getDomainsForRegisteredUid(uid);
for (String domainAndRegistry : domainAndRegistries) {
String action = Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(intent.getAction())
? "been uninstalled" : "had its data cleared";
Log.d(TAG, "%s has just %s, it was linked to %s.", appName, action, domainAndRegistry);
for (String origin : origins) {
boolean uninstalled = Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(intent.getAction());
mNotificationPublisher.showClearDataNotification(context, appName, origin, uninstalled);
}
}
}
......@@ -19,7 +19,7 @@ import java.util.Set;
* {@link ClientAppDataRegister}. It performs three main duties:
* - Holds a cache to deduplicate requests (for performance not correctness).
* - Transforming the package name into a uid and app label.
* - Transforming the origin into a domain and registry (requires native).
* - Transforming the origin into a domain (requires native).
*
* Lifecycle: There should be a 1-1 relationship between this class and
* {@link TrustedWebActivityUi}. Having more instances won't effect correctness, but will limit the
......@@ -37,14 +37,14 @@ public class ClientAppDataRecorder {
/**
* Cache so we don't send the same request multiple times. {@link #register} is called on each
* navigation and each call to {@link ClientAppDataRegister#registerPackageForDomainAndRegistry}
* navigation and each call to {@link ClientAppDataRegister#registerPackageForDomain}
* modifies SharedPreferences, so we need to cut down on the number of calls.
*/
private final Set<String> mCache = new HashSet<>();
/** Class to allow mocking native calls in unit tests. */
/* package */ static class UrlTransformer {
/* package */ String getDomainAndRegistry(Origin origin) {
/* package */ String getDomain(Origin origin) {
return UrlUtilities.getDomainAndRegistry(
origin.toString(), true /* includePrivateRegistries */);
}
......@@ -64,9 +64,9 @@ public class ClientAppDataRecorder {
}
/**
* Calls {@link ClientAppDataRegister#registerPackageForDomainAndRegistry}, looking up the uid
* and app name for the |packageName|, extracting the domain and registry from the origin and
* deduplicating multiple requests with the same parameters.
* Calls {@link ClientAppDataRegister#registerPackageForDomain}, looking up the uid
* and app name for the |packageName|, extracting the domain from the origin and deduplicating
* multiple requests with the same parameters.
*/
/* package */ void register(String packageName, Origin origin) {
if (mCache.contains(combine(packageName, origin))) return;
......@@ -82,11 +82,10 @@ public class ClientAppDataRecorder {
return;
}
String domainAndRegistry = mUrlTransformer.getDomainAndRegistry(origin);
String domain = mUrlTransformer.getDomain(origin);
Log.d(TAG, "Registering %d (%s) for %s", ai.uid, appLabel, domainAndRegistry);
mClientAppDataRegister.registerPackageForDomainAndRegistry(
ai.uid, appLabel, domainAndRegistry);
Log.d(TAG, "Registering %d (%s) for %s", ai.uid, appLabel, domain);
mClientAppDataRegister.registerPackageForDomain(ai.uid, appLabel, domain);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Couldn't find name for client package %s", packageName);
}
......
......@@ -40,10 +40,9 @@ public class ClientAppDataRegister {
/**
* Saves to Preferences that the app with |uid| has the application name |appName| and when it
* is removed or cleared, we should consider doing the same with Chrome data relevant to
* |domainAndRegistry|.
* |domain|.
*/
/* package */ void registerPackageForDomainAndRegistry(
int uid, String appName, String domainAndRegistry) {
/* package */ void registerPackageForDomain(int uid, String appName, String domain) {
// Store the UID in the main Chrome Preferences.
Set<String> uids = getUids();
uids.add(String.valueOf(uid));
......@@ -52,12 +51,11 @@ public class ClientAppDataRegister {
// Store the package name for the UID.
mPreferences.edit().putString(createAppNameKey(uid), appName).apply();
// Store the domainAndRegistry for the UID.
String key = createDomainAndRegistriesKey(uid);
Set<String> domainAndRegistries =
new HashSet<>(mPreferences.getStringSet(key, Collections.emptySet()));
domainAndRegistries.add(domainAndRegistry);
mPreferences.edit().putStringSet(key, domainAndRegistries).apply();
// Store the domain for the UID.
String key = createDomainKey(uid);
Set<String> domains = new HashSet<>(mPreferences.getStringSet(key, Collections.emptySet()));
domains.add(domain);
mPreferences.edit().putStringSet(key, domains).apply();
}
private void setUids(Set<String> uids) {
......@@ -74,7 +72,7 @@ public class ClientAppDataRegister {
setUids(uids);
mPreferences.edit().putString(createAppNameKey(uid), null).apply();
mPreferences.edit().putStringSet(createDomainAndRegistriesKey(uid), null).apply();
mPreferences.edit().putStringSet(createDomainKey(uid), null).apply();
}
/* package */ boolean chromeHoldsDataForPackage(int uid) {
......@@ -89,11 +87,11 @@ public class ClientAppDataRegister {
}
/**
* Gets all the 'domain and registry's that have been registered for the uid.
* Gets all the domains that have been registered for the uid.
* Do not modify the set returned by this method.
*/
/* package */ Set<String> getDomainAndRegistriesForRegisteredUid(int uid) {
return mPreferences.getStringSet(createDomainAndRegistriesKey(uid), Collections.emptySet());
/* package */ Set<String> getDomainsForRegisteredUid(int uid) {
return mPreferences.getStringSet(createDomainKey(uid), Collections.emptySet());
}
/**
......@@ -105,10 +103,10 @@ public class ClientAppDataRegister {
}
/**
* Creates the Preferences key to access the set of 'domain and registry's for an app.
* Creates the Preferences key to access the set of domains for an app.
* If you modify this you'll have to migrate old data.
*/
private static String createDomainAndRegistriesKey(int uid) {
return uid + ".domainAndRegistry";
private static String createDomainKey(int uid) {
return uid + ".domain";
}
}
......@@ -3582,6 +3582,15 @@ However, you aren’t invisible. Going private doesn’t hide your browsing from
<message name="IDS_TWA_RUNNING_IN_CHROME" desc="Message on a snackbar indicating that the current Activity may use Chrome data (the rest of the app may not be)." translateable="false">
Running in Chrome
</message>
<message name="IDS_YOU_HAVE_UNINSTALLED_APP" desc="Notification title saying the user has uninstalled the given app." translateable="false">
You have uninstalled <ph name="APP_NAME">%1$s<ex>YouTube</ex></ph>.
</message>
<message name="IDS_YOU_HAVE_CLEARED_APP" desc="Notification title saying the user has cleared data for the given app." translateable="false">
You have cleared data for <ph name="APP_NAME">%1$s<ex>YouTube</ex></ph>.
</message>
<message name="IDS_CLEAR_RELATED_DATA" desc="Notification text asking if the user wants to clear data for the given url as they have just uninstalled/cleared an app linked to that URL.">
Would you like to clear data for <ph name="URL">%1$s<ex>youtube.com</ex></ph>?
</message>
<message name="IDS_WEBAPP_TAP_TO_COPY_URL" desc="Message on the notification that indicates that taping it will copy a Web App's URL into the clipboard.">
Tap to copy the URL for this app
......
......@@ -162,7 +162,9 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/browserservices/BrowserSessionContentHandler.java",
"java/src/org/chromium/chrome/browser/browserservices/BrowserSessionContentUtils.java",
"java/src/org/chromium/chrome/browser/browserservices/BrowserSessionDataProvider.java",
"java/src/org/chromium/chrome/browser/browserservices/ClearDataBroadcastReceiver.java",
"java/src/org/chromium/chrome/browser/browserservices/ClearDataNotificationPublisher.java",
"java/src/org/chromium/chrome/browser/browserservices/ClearDataService.java",
"java/src/org/chromium/chrome/browser/browserservices/ClientAppBroadcastReceiver.java",
"java/src/org/chromium/chrome/browser/browserservices/ClientAppDataRecorder.java",
"java/src/org/chromium/chrome/browser/browserservices/ClientAppDataRegister.java",
"java/src/org/chromium/chrome/browser/browserservices/Origin.java",
......@@ -2214,6 +2216,9 @@ chrome_junit_test_java_sources = [
"junit/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetControllerTest.java",
"junit/src/org/chromium/chrome/browser/background_task_scheduler/NativeBackgroundTaskTest.java",
"junit/src/org/chromium/chrome/browser/browseractions/BrowserActionsIntentTest.java",
"junit/src/org/chromium/chrome/browser/browserservices/ClearDataNotificationPublisherTest.java",
"junit/src/org/chromium/chrome/browser/browserservices/ClearDataServiceTest.java",
"junit/src/org/chromium/chrome/browser/browserservices/ClientAppBroadcastReceiverTest.java",
"junit/src/org/chromium/chrome/browser/browserservices/ClientAppDataRecorderTest.java",
"junit/src/org/chromium/chrome/browser/browserservices/ClientAppDataRegisterTest.java",
"junit/src/org/chromium/chrome/browser/browserservices/OriginTest.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.browserservices;
import android.app.NotificationManager;
import android.content.Context;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowNotificationManager;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
/**
* Tests for {@link ClearDataNotificationPublisher}.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE,
shadows={ ShadowNotificationManager.class })
public class ClearDataNotificationPublisherTest {
private ClearDataNotificationPublisher mClearDataNotificationPublisher;
private Context mContext;
private ShadowNotificationManager mShadowNotificationManager;
@Before
public void setUp() {
mClearDataNotificationPublisher = new ClearDataNotificationPublisher();
mContext = RuntimeEnvironment.application;
mShadowNotificationManager = Shadows.shadowOf((NotificationManager.from(mContext)));
}
@Test
@Feature("TrustedWebActivities")
public void showNotification() {
mClearDataNotificationPublisher.showClearDataNotification(
mContext, "App", "https://www.example.com/", true);
Assert.assertEquals(1, mShadowNotificationManager.getAllNotifications().size());
}
@Test
@Feature("TrustedWebActivities")
public void singleNotificationPerOrigin() {
mClearDataNotificationPublisher.showClearDataNotification(
mContext, "App", "https://www.website.com/", true);
mClearDataNotificationPublisher.showClearDataNotification(
mContext, "App 2", "https://www.website.com/", true);
Assert.assertEquals(1, mShadowNotificationManager.getAllNotifications().size());
mClearDataNotificationPublisher.showClearDataNotification(
mContext, "App", "https://www.otherwebsite.com/", true);
Assert.assertEquals(2, mShadowNotificationManager.getAllNotifications().size());
}
}
// 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.browserservices;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowPendingIntent;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
/**
* Tests for {@link ClearDataService}.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE,
shadows={ ShadowPendingIntent.class })
public class ClearDataServiceTest {
@Mock public ClearDataNotificationPublisher mNotificationManager;
private ClearDataService mService;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mService = new ClearDataService(mNotificationManager);
}
@Test
@Feature("TrustedWebActivities")
public void dismissNotification() {
int notificationId = 12345;
ShadowPendingIntent intent = Shadows.shadowOf(
ClearDataService.getDismissIntent(RuntimeEnvironment.application, notificationId));
mService.onHandleIntent(intent.getSavedIntent());
verify(mNotificationManager).dismissClearDataNotification(any(), eq(notificationId));
}
}
// 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.browserservices;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.content.Intent;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* Tests for {@link ClientAppBroadcastReceiver}.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ClientAppBroadcastReceiverTest {
@Mock public Context mContext;
@Mock public ClientAppDataRegister mDataRegister;
@Mock public ClearDataNotificationPublisher mNotificationManager;
private ClientAppBroadcastReceiver mReceiver;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mReceiver = new ClientAppBroadcastReceiver(mNotificationManager, mDataRegister);
mContext = RuntimeEnvironment.application;
}
private Intent createMockIntent(int id, String action) {
Intent intent = new Intent();
intent.putExtra(Intent.EXTRA_UID, id);
intent.setAction(action);
return intent;
}
private void addToRegister(int id, String appName, Set<String> domainAndRegistries) {
doReturn(true).when(mDataRegister).chromeHoldsDataForPackage(eq(id));
doReturn(appName).when(mDataRegister).getAppNameForRegisteredUid(eq(id));
doReturn(domainAndRegistries)
.when(mDataRegister)
.getDomainsForRegisteredUid(eq(id));
}
/** Makes sure we don't show a notification if we don't have any data for the app. */
@Test
@Feature("TrustedWebActivities")
public void chromeHoldsNoData() {
mReceiver.onReceive(mContext, createMockIntent(12, Intent.ACTION_PACKAGE_FULLY_REMOVED));
verify(mNotificationManager, never())
.showClearDataNotification(any(), anyString(), anyString(), anyBoolean());
}
/** Tests the basic flow. */
@Test
@Feature("TrustedWebActivities")
public void chromeHoldsData() {
int id = 23;
String appName = "App Name";
String domain = "example.com";
Set<String> domains = new HashSet<>(Arrays.asList(domain));
addToRegister(id, appName, domains);
mReceiver.onReceive(mContext, createMockIntent(id, Intent.ACTION_PACKAGE_FULLY_REMOVED));
verify(mNotificationManager)
.showClearDataNotification(any(), eq(appName), eq(domain), eq(true));
}
/** Tests we deal with multiple domains well. */
@Test
@Feature("TrustedWebActivities")
public void multipleDomains() {
int id = 45;
String appName = "App Name 2";
String domain1 = "example.com";
String domain2 = "example2.com";
Set<String> domains = new HashSet<>(Arrays.asList(domain1, domain2));
addToRegister(id, appName, domains);
mReceiver.onReceive(mContext, createMockIntent(id, Intent.ACTION_PACKAGE_FULLY_REMOVED));
verify(mNotificationManager).showClearDataNotification(
any(), eq(appName), eq(domain1), eq(true));
verify(mNotificationManager).showClearDataNotification(
any(), eq(appName), eq(domain2), eq(true));
}
/** Tests we differentiate between app uninstalled and data cleared. */
@Test
@Feature("TrustedWebActivities")
public void onDataClear() {
int id = 23;
String appName = "App Name";
String domain = "example.com";
Set<String> domains = new HashSet<>(Arrays.asList(domain));
addToRegister(id, appName, domains);
mReceiver.onReceive(mContext, createMockIntent(id, Intent.ACTION_PACKAGE_DATA_CLEARED));
verify(mNotificationManager)
.showClearDataNotification(any(), eq(appName), eq(domain), eq(false));
}
}
......@@ -46,7 +46,7 @@ public class ClientAppDataRecorderTest {
private final ClientAppDataRecorder.UrlTransformer mUrlTransformer =
new ClientAppDataRecorder.UrlTransformer() {
@Override
String getDomainAndRegistry(Origin origin) {
String getDomain(Origin origin) {
return transform(origin);
}
};
......@@ -80,7 +80,7 @@ public class ClientAppDataRecorderTest {
@Feature("TrustedWebActivities")
public void testRegister() {
mRecorder.register(APP_PACKAGE, ORIGIN);
verify(mRegister).registerPackageForDomainAndRegistry(APP_UID, APP_NAME, transform(ORIGIN));
verify(mRegister).registerPackageForDomain(APP_UID, APP_NAME, transform(ORIGIN));
}
@Test
......@@ -88,7 +88,7 @@ public class ClientAppDataRecorderTest {
public void testDeduplicate() {
mRecorder.register(APP_PACKAGE, ORIGIN);
mRecorder.register(APP_PACKAGE, ORIGIN);
verify(mRegister).registerPackageForDomainAndRegistry(APP_UID, APP_NAME, transform(ORIGIN));
verify(mRegister).registerPackageForDomain(APP_UID, APP_NAME, transform(ORIGIN));
}
@Test
......@@ -96,8 +96,8 @@ public class ClientAppDataRecorderTest {
public void testDifferentOrigins() {
mRecorder.register(APP_PACKAGE, ORIGIN);
mRecorder.register(APP_PACKAGE, OTHER_ORIGIN);
verify(mRegister).registerPackageForDomainAndRegistry(APP_UID, APP_NAME, transform(ORIGIN));
verify(mRegister).registerPackageForDomainAndRegistry(
verify(mRegister).registerPackageForDomain(APP_UID, APP_NAME, transform(ORIGIN));
verify(mRegister).registerPackageForDomain(
APP_UID, APP_NAME, transform(OTHER_ORIGIN));
}
......@@ -107,6 +107,6 @@ public class ClientAppDataRecorderTest {
mRecorder.register(MISSING_PACKAGE, ORIGIN);
// Implicitly checking we don't throw.
verify(mRegister, times(0))
.registerPackageForDomainAndRegistry(anyInt(), anyString(), any());
.registerPackageForDomain(anyInt(), anyString(), any());
}
}
......@@ -23,8 +23,8 @@ import java.util.Set;
public class ClientAppDataRegisterTest {
private static final int UID = 23;
private static final String APP_NAME = "Example App";
private static final String DOMAIN_AND_REGISTRY = "example.com";
private static final String OTHER_DOMAIN_AND_REGISTRY = "otherexample.com";
private static final String DOMAIN = "example.com";
private static final String OTHER_DOMAIN = "otherexample.com";
private ClientAppDataRegister mRegister;
......@@ -36,7 +36,7 @@ public class ClientAppDataRegisterTest {
@Test
@Feature("TrustedWebActivities")
public void registration() {
mRegister.registerPackageForDomainAndRegistry(UID, APP_NAME, DOMAIN_AND_REGISTRY);
mRegister.registerPackageForDomain(UID, APP_NAME, DOMAIN);
Assert.assertTrue(mRegister.chromeHoldsDataForPackage(UID));
Assert.assertEquals(APP_NAME, mRegister.getAppNameForRegisteredUid(UID));
......@@ -45,7 +45,7 @@ public class ClientAppDataRegisterTest {
@Test
@Feature("TrustedWebActivities")
public void deregistration() {
mRegister.registerPackageForDomainAndRegistry(UID, APP_NAME, DOMAIN_AND_REGISTRY);
mRegister.registerPackageForDomain(UID, APP_NAME, DOMAIN);
mRegister.removePackage(UID);
Assert.assertFalse(mRegister.chromeHoldsDataForPackage(UID));
......@@ -55,30 +55,30 @@ public class ClientAppDataRegisterTest {
@Test
@Feature("TrustedWebActivities")
public void getOrigins() {
mRegister.registerPackageForDomainAndRegistry(UID, APP_NAME, DOMAIN_AND_REGISTRY);
mRegister.registerPackageForDomainAndRegistry(UID, APP_NAME, OTHER_DOMAIN_AND_REGISTRY);
mRegister.registerPackageForDomain(UID, APP_NAME, DOMAIN);
mRegister.registerPackageForDomain(UID, APP_NAME, OTHER_DOMAIN);
Set<String> origins = mRegister.getDomainAndRegistriesForRegisteredUid(UID);
Set<String> origins = mRegister.getDomainsForRegisteredUid(UID);
Assert.assertEquals(2, origins.size());
Assert.assertTrue(origins.contains(DOMAIN_AND_REGISTRY));
Assert.assertTrue(origins.contains(OTHER_DOMAIN_AND_REGISTRY));
Assert.assertTrue(origins.contains(DOMAIN));
Assert.assertTrue(origins.contains(OTHER_DOMAIN));
}
@Test
@Feature("TrustedWebActivities")
public void clearOrigins() {
mRegister.registerPackageForDomainAndRegistry(UID, APP_NAME, DOMAIN_AND_REGISTRY);
mRegister.registerPackageForDomainAndRegistry(UID, APP_NAME, OTHER_DOMAIN_AND_REGISTRY);
mRegister.registerPackageForDomain(UID, APP_NAME, DOMAIN);
mRegister.registerPackageForDomain(UID, APP_NAME, OTHER_DOMAIN);
mRegister.removePackage(UID);
Set<String> origins = mRegister.getDomainAndRegistriesForRegisteredUid(UID);
Set<String> origins = mRegister.getDomainsForRegisteredUid(UID);
Assert.assertTrue(origins.isEmpty());
}
@Test
@Feature("TrustedWebActivities")
public void getAppName() {
mRegister.registerPackageForDomainAndRegistry(UID, APP_NAME, DOMAIN_AND_REGISTRY);
mRegister.registerPackageForDomain(UID, APP_NAME, DOMAIN);
Assert.assertEquals(APP_NAME, mRegister.getAppNameForRegisteredUid(UID));
}
}
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