Use flags to indicate reason for Uri changes.

As part of MediaProvider becoming a Mainline module, some partners
need to understand more details about what triggered a Uri change
notification.  This helps listening apps avoid making expensive and
race-condition-prone calls back into MediaProvider; typically when
they're only interested in insert and delete operations.  This change
uses the existing "flags" argument to communicate the reason.

This change adds overloads to ContentObserver for listening apps to
receive these flags.  In addition, we add overloads that deliver a
clustered set of multiple Uris together in a single Binder transaction
to improve overall efficiency.  (This matches well with the existing
CR.notifyChange() API that we added earlier this year, since they
both work with Iterable<Uri>.)

Tests to verify that we only collapse Uris together when all other
method arguments are identical.

Bug: 147778404, 144464323
Test: atest CtsDatabaseTestCases
Test: atest CtsContentTestCases:android.content.cts.ContentResolverTest
Test: atest FrameworksServicesTests:com.android.server.content.ObserverNodeTest
Test: atest --test-mapping packages/providers/MediaProvider
Change-Id: I0bbd8a8b4a898ab6f891d085de0ecb4d68cbe302
This commit is contained in:
Jeff Sharkey
2020-03-05 09:56:41 -07:00
parent 5dbabb62a6
commit d70325359e
7 changed files with 358 additions and 143 deletions

View File

@@ -9921,8 +9921,11 @@ package android.content {
field public static final String EXTRA_REFRESH_SUPPORTED = "android.content.extra.REFRESH_SUPPORTED";
field public static final String EXTRA_SIZE = "android.content.extra.SIZE";
field public static final String EXTRA_TOTAL_COUNT = "android.content.extra.TOTAL_COUNT";
field public static final int NOTIFY_DELETE = 16; // 0x10
field public static final int NOTIFY_INSERT = 4; // 0x4
field public static final int NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS = 2; // 0x2
field public static final int NOTIFY_SYNC_TO_NETWORK = 1; // 0x1
field public static final int NOTIFY_UPDATE = 8; // 0x8
field public static final String QUERY_ARG_GROUP_COLUMNS = "android:query-arg-group-columns";
field public static final String QUERY_ARG_LIMIT = "android:query-arg-limit";
field public static final String QUERY_ARG_OFFSET = "android:query-arg-offset";
@@ -12965,9 +12968,13 @@ package android.database {
ctor public ContentObserver(android.os.Handler);
method public boolean deliverSelfNotifications();
method @Deprecated public final void dispatchChange(boolean);
method public final void dispatchChange(boolean, android.net.Uri);
method public final void dispatchChange(boolean, @Nullable android.net.Uri);
method public final void dispatchChange(boolean, @Nullable android.net.Uri, int);
method public final void dispatchChange(boolean, @NonNull Iterable<android.net.Uri>, int);
method public void onChange(boolean);
method public void onChange(boolean, android.net.Uri);
method public void onChange(boolean, @Nullable android.net.Uri);
method public void onChange(boolean, @Nullable android.net.Uri, int);
method public void onChange(boolean, @NonNull Iterable<android.net.Uri>, int);
}
public interface CrossProcessCursor extends android.database.Cursor {

View File

@@ -629,7 +629,10 @@ public abstract class ContentResolver implements ContentInterface {
/** @hide */
@IntDef(flag = true, prefix = { "NOTIFY_" }, value = {
NOTIFY_SYNC_TO_NETWORK,
NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS
NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS,
NOTIFY_INSERT,
NOTIFY_UPDATE,
NOTIFY_DELETE
})
@Retention(RetentionPolicy.SOURCE)
public @interface NotifyFlags {}
@@ -650,6 +653,36 @@ public abstract class ContentResolver implements ContentInterface {
*/
public static final int NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS = 1<<1;
/**
* Flag for {@link #notifyChange(Uri, ContentObserver, int)}: typically set
* by a {@link ContentProvider} to indicate that this notification is the
* result of an {@link ContentProvider#insert} call.
* <p>
* Sending these detailed flags are optional, but providers are strongly
* recommended to send them.
*/
public static final int NOTIFY_INSERT = 1 << 2;
/**
* Flag for {@link #notifyChange(Uri, ContentObserver, int)}: typically set
* by a {@link ContentProvider} to indicate that this notification is the
* result of an {@link ContentProvider#update} call.
* <p>
* Sending these detailed flags are optional, but providers are strongly
* recommended to send them.
*/
public static final int NOTIFY_UPDATE = 1 << 3;
/**
* Flag for {@link #notifyChange(Uri, ContentObserver, int)}: typically set
* by a {@link ContentProvider} to indicate that this notification is the
* result of a {@link ContentProvider#delete} call.
* <p>
* Sending these detailed flags are optional, but providers are strongly
* recommended to send them.
*/
public static final int NOTIFY_DELETE = 1 << 4;
/**
* No exception, throttled by app standby normally.
* @hide

View File

@@ -16,11 +16,17 @@
package android.database;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentResolver.NotifyFlags;
import android.net.Uri;
import android.os.Handler;
import android.os.UserHandle;
import java.util.Arrays;
/**
* Receives call backs for changes to content.
* Must be implemented by objects which are added to a {@link ContentObservable}.
@@ -101,12 +107,10 @@ public abstract class ContentObserver {
* This method is called when a content change occurs.
* Includes the changed content Uri when available.
* <p>
* Subclasses should override this method to handle content changes.
* To ensure correct operation on older versions of the framework that
* did not provide a Uri argument, applications should also implement
* the {@link #onChange(boolean)} overload of this method whenever they
* implement the {@link #onChange(boolean, Uri)} overload.
* </p><p>
* Subclasses should override this method to handle content changes. To
* ensure correct operation on older versions of the framework that did not
* provide richer arguments, applications should implement all overloads.
* <p>
* Example implementation:
* <pre><code>
* // Implement the onChange(boolean) method to delegate the change notification to
@@ -126,38 +130,63 @@ public abstract class ContentObserver {
* </p>
*
* @param selfChange True if this is a self-change notification.
* @param uri The Uri of the changed content, or null if unknown.
* @param uri The Uri of the changed content.
*/
public void onChange(boolean selfChange, Uri uri) {
public void onChange(boolean selfChange, @Nullable Uri uri) {
onChange(selfChange);
}
/**
* Dispatches a change notification to the observer. Includes the changed
* content Uri when available and also the user whose content changed.
* This method is called when a content change occurs. Includes the changed
* content Uri when available.
* <p>
* Subclasses should override this method to handle content changes. To
* ensure correct operation on older versions of the framework that did not
* provide richer arguments, applications should implement all overloads.
*
* @param selfChange True if this is a self-change notification.
* @param uri The Uri of the changed content, or null if unknown.
* @param userId The user whose content changed. Can be either a specific
* user or {@link UserHandle#USER_ALL}.
*
* @hide
* @param uri The Uri of the changed content.
* @param flags Flags indicating details about this change.
*/
public void onChange(boolean selfChange, Uri uri, int userId) {
public void onChange(boolean selfChange, @Nullable Uri uri, @NotifyFlags int flags) {
onChange(selfChange, uri);
}
/**
* This method is called when a content change occurs. Includes the changed
* content Uris when available.
* <p>
* Subclasses should override this method to handle content changes. To
* ensure correct operation on older versions of the framework that did not
* provide richer arguments, applications should implement all overloads.
*
* @param selfChange True if this is a self-change notification.
* @param uris The Uris of the changed content.
* @param flags Flags indicating details about this change.
*/
public void onChange(boolean selfChange, @NonNull Iterable<Uri> uris, @NotifyFlags int flags) {
for (Uri uri : uris) {
onChange(selfChange, uri, flags);
}
}
/** @hide */
public void onChange(boolean selfChange, @NonNull Iterable<Uri> uris, @NotifyFlags int flags,
@UserIdInt int userId) {
onChange(selfChange, uris, flags);
}
/**
* Dispatches a change notification to the observer.
* <p>
* If a {@link Handler} was supplied to the {@link ContentObserver} constructor,
* then a call to the {@link #onChange} method is posted to the handler's message queue.
* Otherwise, the {@link #onChange} method is invoked immediately on this thread.
* </p>
* If a {@link Handler} was supplied to the {@link ContentObserver}
* constructor, then a call to the {@link #onChange} method is posted to the
* handler's message queue. Otherwise, the {@link #onChange} method is
* invoked immediately on this thread.
*
* @param selfChange True if this is a self-change notification.
*
* @deprecated Use {@link #dispatchChange(boolean, Uri)} instead.
* @deprecated Callers should migrate towards using a richer overload that
* provides more details about the change, such as
* {@link #dispatchChange(boolean, Iterable, int)}.
*/
@Deprecated
public final void dispatchChange(boolean selfChange) {
@@ -165,57 +194,66 @@ public abstract class ContentObserver {
}
/**
* Dispatches a change notification to the observer.
* Includes the changed content Uri when available.
* Dispatches a change notification to the observer. Includes the changed
* content Uri when available.
* <p>
* If a {@link Handler} was supplied to the {@link ContentObserver} constructor,
* then a call to the {@link #onChange} method is posted to the handler's message queue.
* Otherwise, the {@link #onChange} method is invoked immediately on this thread.
* </p>
* If a {@link Handler} was supplied to the {@link ContentObserver}
* constructor, then a call to the {@link #onChange} method is posted to the
* handler's message queue. Otherwise, the {@link #onChange} method is
* invoked immediately on this thread.
*
* @param selfChange True if this is a self-change notification.
* @param uri The Uri of the changed content, or null if unknown.
* @param uri The Uri of the changed content.
*/
public final void dispatchChange(boolean selfChange, Uri uri) {
dispatchChange(selfChange, uri, UserHandle.getCallingUserId());
public final void dispatchChange(boolean selfChange, @Nullable Uri uri) {
dispatchChange(selfChange, Arrays.asList(uri), 0, UserHandle.getCallingUserId());
}
/**
* Dispatches a change notification to the observer. Includes the changed
* content Uri when available and also the user whose content changed.
* content Uri when available.
* <p>
* If a {@link Handler} was supplied to the {@link ContentObserver} constructor,
* then a call to the {@link #onChange} method is posted to the handler's message queue.
* Otherwise, the {@link #onChange} method is invoked immediately on this thread.
* </p>
* If a {@link Handler} was supplied to the {@link ContentObserver}
* constructor, then a call to the {@link #onChange} method is posted to the
* handler's message queue. Otherwise, the {@link #onChange} method is
* invoked immediately on this thread.
*
* @param selfChange True if this is a self-change notification.
* @param uri The Uri of the changed content, or null if unknown.
* @param userId The user whose content changed.
* @param uri The Uri of the changed content.
* @param flags Flags indicating details about this change.
*/
private void dispatchChange(boolean selfChange, Uri uri, int userId) {
if (mHandler == null) {
onChange(selfChange, uri, userId);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri, userId));
}
public final void dispatchChange(boolean selfChange, @Nullable Uri uri,
@NotifyFlags int flags) {
dispatchChange(selfChange, Arrays.asList(uri), flags, UserHandle.getCallingUserId());
}
/**
* Dispatches a change notification to the observer. Includes the changed
* content Uris when available.
* <p>
* If a {@link Handler} was supplied to the {@link ContentObserver}
* constructor, then a call to the {@link #onChange} method is posted to the
* handler's message queue. Otherwise, the {@link #onChange} method is
* invoked immediately on this thread.
*
* @param selfChange True if this is a self-change notification.
* @param uris The Uri of the changed content.
* @param flags Flags indicating details about this change.
*/
public final void dispatchChange(boolean selfChange, @NonNull Iterable<Uri> uris,
@NotifyFlags int flags) {
dispatchChange(selfChange, uris, flags, UserHandle.getCallingUserId());
}
private final class NotificationRunnable implements Runnable {
private final boolean mSelfChange;
private final Uri mUri;
private final int mUserId;
public NotificationRunnable(boolean selfChange, Uri uri, int userId) {
mSelfChange = selfChange;
mUri = uri;
mUserId = userId;
}
@Override
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri, mUserId);
/** @hide */
public final void dispatchChange(boolean selfChange, @NonNull Iterable<Uri> uris,
@NotifyFlags int flags, @UserIdInt int userId) {
if (mHandler == null) {
onChange(selfChange, uris, flags, userId);
} else {
mHandler.post(() -> {
onChange(selfChange, uris, flags, userId);
});
}
}
@@ -228,9 +266,16 @@ public abstract class ContentObserver {
@Override
public void onChange(boolean selfChange, Uri uri, int userId) {
// This is kept intact purely for apps using hidden APIs, to
// redirect to the updated implementation
onChangeEtc(selfChange, new Uri[] { uri }, 0, userId);
}
@Override
public void onChangeEtc(boolean selfChange, Uri[] uris, int flags, int userId) {
ContentObserver contentObserver = mContentObserver;
if (contentObserver != null) {
contentObserver.dispatchChange(selfChange, uri, userId);
contentObserver.dispatchChange(selfChange, Arrays.asList(uris), flags, userId);
}
}

View File

@@ -16,9 +16,14 @@
package android.database;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.content.ContentResolver.NotifyFlags;
import android.net.Uri;
import android.os.*;
import java.util.ArrayList;
/**
* Wraps a BulkCursor around an existing Cursor making it remotable.
@@ -76,9 +81,18 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative
}
@Override
public void onChange(boolean selfChange, Uri uri) {
public void onChange(boolean selfChange, @NonNull Iterable<Uri> uris,
@NotifyFlags int flags, @UserIdInt int userId) {
// Since we deliver changes from the most-specific to least-specific
// overloads, we only need to redirect from the most-specific local
// method to the most-specific remote method
final ArrayList<Uri> asList = new ArrayList<>();
uris.forEach(asList::add);
final Uri[] asArray = asList.toArray(new Uri[asList.size()]);
try {
mRemote.onChange(selfChange, uri, android.os.Process.myUid());
mRemote.onChangeEtc(selfChange, asArray, flags, userId);
} catch (RemoteException ex) {
// Do nothing, the far side is dead
}

View File

@@ -22,8 +22,7 @@ import android.net.Uri;
/**
* @hide
*/
interface IContentObserver
{
interface IContentObserver {
/**
* This method is called when an update occurs to the cursor that is being
* observed. selfUpdate is true if the update was caused by a call to
@@ -31,4 +30,11 @@ interface IContentObserver
*/
@UnsupportedAppUsage
oneway void onChange(boolean selfUpdate, in Uri uri, int userId);
/**
* This method is called when an update occurs to the cursor that is being
* observed. selfUpdate is true if the update was caused by a call to
* commit on the cursor that is being observed.
*/
oneway void onChangeEtc(boolean selfUpdate, in Uri[] uri, int flags, int userId);
}

View File

@@ -83,6 +83,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
/**
* {@hide}
@@ -399,15 +400,22 @@ public final class ContentService extends IContentService.Stub {
public void notifyChange(Uri[] uris, IContentObserver observer,
boolean observerWantsSelfNotifications, int flags, int userHandle,
int targetSdkVersion, String callingPackage) {
final ObserverCollector collector = new ObserverCollector();
for (Uri uri : uris) {
notifyChange(uri, observer, observerWantsSelfNotifications, flags, userHandle,
targetSdkVersion, callingPackage);
targetSdkVersion, callingPackage, collector);
}
final long token = clearCallingIdentity();
try {
collector.dispatch();
} finally {
Binder.restoreCallingIdentity(token);
}
}
public void notifyChange(Uri uri, IContentObserver observer,
boolean observerWantsSelfNotifications, int flags, int userHandle,
int targetSdkVersion, String callingPackage) {
int targetSdkVersion, String callingPackage, ObserverCollector collector) {
if (DEBUG) Slog.d(TAG, "Notifying update of " + uri + " for user " + userHandle
+ " from observer " + observer + ", flags " + Integer.toHexString(flags));
@@ -442,22 +450,9 @@ public final class ContentService extends IContentService.Stub {
// process rather than the caller's process. We will restore this before returning.
long identityToken = clearCallingIdentity();
try {
ArrayList<ObserverCall> calls = new ArrayList<ObserverCall>();
synchronized (mRootNode) {
mRootNode.collectObserversLocked(uri, 0, observer, observerWantsSelfNotifications,
flags, userHandle, calls);
}
final int numCalls = calls.size();
for (int i = 0; i < numCalls; i++) {
// Immediately dispatch notifications to foreground apps that
// are important to the user; all other background observers are
// delayed to avoid stampeding
final ObserverCall oc = calls.get(i);
if (oc.mProcState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) {
oc.run();
} else {
BackgroundThread.getHandler().postDelayed(oc, BACKGROUND_OBSERVER_DELAY);
}
flags, userHandle, collector);
}
if ((flags&ContentResolver.NOTIFY_SYNC_TO_NETWORK) != 0) {
SyncManager syncManager = getSyncManager();
@@ -487,40 +482,84 @@ public final class ContentService extends IContentService.Stub {
}
}
public void notifyChange(Uri uri, IContentObserver observer,
boolean observerWantsSelfNotifications, boolean syncToNetwork,
String callingPackage) {
notifyChange(uri, observer, observerWantsSelfNotifications,
syncToNetwork ? ContentResolver.NOTIFY_SYNC_TO_NETWORK : 0,
UserHandle.getCallingUserId(), Build.VERSION_CODES.CUR_DEVELOPMENT, callingPackage);
}
/** {@hide} */
/**
* Collection of detected change notifications that should be delivered.
* <p>
* To help reduce Binder transaction overhead, this class clusters together
* multiple {@link Uri} where all other arguments are identical.
*/
@VisibleForTesting
public static final class ObserverCall implements Runnable {
final IContentObserver mObserver;
final boolean mSelfChange;
final Uri mUri;
final int mUserId;
final int mProcState;
public static class ObserverCollector {
private final ArrayMap<Key, List<Uri>> collected = new ArrayMap<>();
ObserverCall(IContentObserver observer, boolean selfChange, Uri uri, int userId,
int procState) {
mObserver = observer;
mSelfChange = selfChange;
mUri = uri;
mUserId = userId;
mProcState = procState;
private static class Key {
final IContentObserver observer;
final int uid;
final boolean selfChange;
final int flags;
final int userId;
Key(IContentObserver observer, int uid, boolean selfChange, int flags, int userId) {
this.observer = observer;
this.uid = uid;
this.selfChange = selfChange;
this.flags = flags;
this.userId = userId;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Key)) {
return false;
}
final Key other = (Key) o;
return Objects.equals(observer, other.observer)
&& (uid == other.uid)
&& (selfChange == other.selfChange)
&& (flags == other.flags)
&& (userId == other.userId);
}
@Override
public int hashCode() {
return Objects.hash(observer, uid, selfChange, flags, userId);
}
}
@Override
public void run() {
try {
mObserver.onChange(mSelfChange, mUri, mUserId);
if (DEBUG) Slog.d(TAG, "Notified " + mObserver + " of update at " + mUri);
} catch (RemoteException ignored) {
// We already have a death observer that will clean up if the
// remote process dies
public void collect(IContentObserver observer, int uid, boolean selfChange, Uri uri,
int flags, int userId) {
final Key key = new Key(observer, uid, selfChange, flags, userId);
List<Uri> value = collected.get(key);
if (value == null) {
value = new ArrayList<>();
collected.put(key, value);
}
value.add(uri);
}
public void dispatch() {
for (int i = 0; i < collected.size(); i++) {
final Key key = collected.keyAt(i);
final List<Uri> value = collected.valueAt(i);
final Runnable task = () -> {
try {
key.observer.onChangeEtc(key.selfChange,
value.toArray(new Uri[value.size()]), key.flags, key.userId);
} catch (RemoteException ignored) {
}
};
// Immediately dispatch notifications to foreground apps that
// are important to the user; all other background observers are
// delayed to avoid stampeding
final int procState = LocalServices.getService(ActivityManagerInternal.class)
.getUidProcessState(key.uid);
if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) {
task.run();
} else {
BackgroundThread.getHandler().postDelayed(task, BACKGROUND_OBSERVER_DELAY);
}
}
}
}
@@ -1455,10 +1494,6 @@ public final class ContentService extends IContentService.Stub {
}
}
public static final int INSERT_TYPE = 0;
public static final int UPDATE_TYPE = 1;
public static final int DELETE_TYPE = 2;
private String mName;
private ArrayList<ObserverNode> mChildren = new ArrayList<ObserverNode>();
private ArrayList<ObserverEntry> mObservers = new ArrayList<ObserverEntry>();
@@ -1588,7 +1623,7 @@ public final class ContentService extends IContentService.Stub {
private void collectMyObserversLocked(Uri uri, boolean leaf, IContentObserver observer,
boolean observerWantsSelfNotifications, int flags,
int targetUserHandle, ArrayList<ObserverCall> calls) {
int targetUserHandle, ObserverCollector collector) {
int N = mObservers.size();
IBinder observerBinder = observer == null ? null : observer.asBinder();
for (int i = 0; i < N; i++) {
@@ -1628,10 +1663,8 @@ public final class ContentService extends IContentService.Stub {
if (DEBUG) Slog.d(TAG, "Reporting to " + entry.observer + ": leaf=" + leaf
+ " flags=" + Integer.toHexString(flags)
+ " desc=" + entry.notifyForDescendants);
final int procState = LocalServices.getService(ActivityManagerInternal.class)
.getUidProcessState(entry.uid);
calls.add(new ObserverCall(entry.observer, selfChange, uri,
targetUserHandle, procState));
collector.collect(entry.observer, entry.uid, selfChange, uri, flags,
targetUserHandle);
}
}
}
@@ -1641,21 +1674,21 @@ public final class ContentService extends IContentService.Stub {
*/
public void collectObserversLocked(Uri uri, int index, IContentObserver observer,
boolean observerWantsSelfNotifications, int flags,
int targetUserHandle, ArrayList<ObserverCall> calls) {
int targetUserHandle, ObserverCollector collector) {
String segment = null;
int segmentCount = countUriSegments(uri);
if (index >= segmentCount) {
// This is the leaf node, notify all observers
if (DEBUG) Slog.d(TAG, "Collecting leaf observers @ #" + index + ", node " + mName);
collectMyObserversLocked(uri, true, observer, observerWantsSelfNotifications,
flags, targetUserHandle, calls);
flags, targetUserHandle, collector);
} else if (index < segmentCount){
segment = getUriSegment(uri, index);
if (DEBUG) Slog.d(TAG, "Collecting non-leaf observers @ #" + index + " / "
+ segment);
// Notify any observers at this level who are interested in descendants
collectMyObserversLocked(uri, false, observer, observerWantsSelfNotifications,
flags, targetUserHandle, calls);
flags, targetUserHandle, collector);
}
int N = mChildren.size();
@@ -1664,7 +1697,7 @@ public final class ContentService extends IContentService.Stub {
if (segment == null || node.mName.equals(segment)) {
// We found the child,
node.collectObserversLocked(uri, index + 1, observer,
observerWantsSelfNotifications, flags, targetUserHandle, calls);
observerWantsSelfNotifications, flags, targetUserHandle, collector);
if (segment != null) {
break;
}

View File

@@ -16,30 +16,52 @@
package com.android.server.content;
import java.util.ArrayList;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.content.ContentResolver;
import android.database.ContentObserver;
import android.database.IContentObserver;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.ArraySet;
import com.android.server.content.ContentService.ObserverCall;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.LocalServices;
import com.android.server.content.ContentService.ObserverCollector;
import com.android.server.content.ContentService.ObserverNode;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import java.util.Arrays;
/**
* atest FrameworksServicesTests:com.android.server.content.ObserverNodeTest
*/
@SmallTest
public class ObserverNodeTest extends AndroidTestCase {
static class TestObserver extends ContentObserver {
@RunWith(AndroidJUnit4.class)
public class ObserverNodeTest {
static class TestObserver extends ContentObserver {
public TestObserver() {
super(new Handler(Looper.getMainLooper()));
}
}
@Test
public void testUri() {
final int myUserHandle = UserHandle.myUserId();
@@ -65,15 +87,15 @@ public class ObserverNodeTest extends AndroidTestCase {
0, 0, myUserHandle);
}
ArrayList<ObserverCall> calls = new ArrayList<ObserverCall>();
for (int i = nums.length - 1; i >=0; --i) {
root.collectObserversLocked(uris[i], 0, null, false, 0, myUserHandle, calls);
assertEquals(nums[i], calls.size());
calls.clear();
final ObserverCollector collector = mock(ObserverCollector.class);
root.collectObserversLocked(uris[i], 0, null, false, 0, myUserHandle, collector);
verify(collector, times(nums[i])).collect(
any(), anyInt(), anyBoolean(), any(), anyInt(), anyInt());
}
}
@Test
public void testUriNotNotify() {
final int myUserHandle = UserHandle.myUserId();
@@ -95,12 +117,67 @@ public class ObserverNodeTest extends AndroidTestCase {
0, 0, myUserHandle);
}
ArrayList<ObserverCall> calls = new ArrayList<ObserverCall>();
for (int i = uris.length - 1; i >=0; --i) {
root.collectObserversLocked(uris[i], 0, null, false, 0, myUserHandle, calls);
assertEquals(nums[i], calls.size());
calls.clear();
final ObserverCollector collector = mock(ObserverCollector.class);
root.collectObserversLocked(uris[i], 0, null, false, 0, myUserHandle, collector);
verify(collector, times(nums[i])).collect(
any(), anyInt(), anyBoolean(), any(), anyInt(), anyInt());
}
}
@Test
public void testCluster() throws Exception {
final int myUserHandle = UserHandle.myUserId();
// Assume everything is foreground during our test
final ActivityManagerInternal ami = mock(ActivityManagerInternal.class);
when(ami.getUidProcessState(anyInt()))
.thenReturn(ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND);
LocalServices.removeServiceForTest(ActivityManagerInternal.class);
LocalServices.addService(ActivityManagerInternal.class, ami);
final IContentObserver observer = mock(IContentObserver.class);
when(observer.asBinder()).thenReturn(new Binder());
final ObserverNode root = new ObserverNode("");
root.addObserverLocked(Uri.parse("content://authority/"), observer,
true, root, 0, 1000, myUserHandle);
final ObserverCollector collector = new ObserverCollector();
root.collectObserversLocked(Uri.parse("content://authority/1"), 0, null, false,
0, myUserHandle, collector);
root.collectObserversLocked(Uri.parse("content://authority/1"), 0, null, false,
ContentResolver.NOTIFY_INSERT, myUserHandle, collector);
root.collectObserversLocked(Uri.parse("content://authority/2"), 0, null, false,
ContentResolver.NOTIFY_INSERT, myUserHandle, collector);
root.collectObserversLocked(Uri.parse("content://authority/2"), 0, null, false,
ContentResolver.NOTIFY_UPDATE, myUserHandle, collector);
collector.dispatch();
// We should only cluster when all other arguments are equal
verify(observer).onChangeEtc(eq(false), argThat(new UriSetMatcher(
Uri.parse("content://authority/1"))),
eq(0), anyInt());
verify(observer).onChangeEtc(eq(false), argThat(new UriSetMatcher(
Uri.parse("content://authority/1"),
Uri.parse("content://authority/2"))),
eq(ContentResolver.NOTIFY_INSERT), anyInt());
verify(observer).onChangeEtc(eq(false), argThat(new UriSetMatcher(
Uri.parse("content://authority/2"))),
eq(ContentResolver.NOTIFY_UPDATE), anyInt());
}
private static class UriSetMatcher implements ArgumentMatcher<Uri[]> {
private final ArraySet<Uri> uris;
public UriSetMatcher(Uri... uris) {
this.uris = new ArraySet<>(Arrays.asList(uris));
}
@Override
public boolean matches(Uri[] uris) {
final ArraySet<Uri> test = new ArraySet<>(Arrays.asList(uris));
return this.uris.equals(test);
}
}
}