Loading services/backup/java/com/android/server/backup/TransportManager.java +0 −5 Original line number Diff line number Diff line Loading @@ -29,14 +29,12 @@ import android.content.pm.ResolveInfo; import android.os.RemoteException; import android.os.UserHandle; import android.util.ArrayMap; import android.util.EventLog; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.backup.IBackupTransport; import com.android.internal.util.Preconditions; import com.android.server.EventLogTags; import com.android.server.backup.transport.OnTransportRegisteredListener; import com.android.server.backup.transport.TransportClient; import com.android.server.backup.transport.TransportClientManager; Loading Loading @@ -574,8 +572,6 @@ public class TransportManager { return BackupManager.ERROR_TRANSPORT_UNAVAILABLE; } EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, transportString, 1); int result; try { String transportName = transport.name(); Loading @@ -587,7 +583,6 @@ public class TransportManager { result = BackupManager.SUCCESS; } catch (RemoteException e) { Slog.e(TAG, "Transport " + transportString + " died while registering"); EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, transportString, 0); result = BackupManager.ERROR_TRANSPORT_UNAVAILABLE; } Loading services/backup/java/com/android/server/backup/transport/TransportClient.java +45 −0 Original line number Diff line number Diff line Loading @@ -29,12 +29,14 @@ import android.os.IBinder; import android.os.Looper; import android.os.UserHandle; import android.util.ArrayMap; import android.util.EventLog; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.backup.IBackupTransport; import com.android.internal.util.Preconditions; import com.android.server.EventLogTags; import com.android.server.backup.TransportManager; import java.lang.annotation.Retention; Loading Loading @@ -419,10 +421,45 @@ public class TransportClient { @GuardedBy("mStateLock") private void setStateLocked(@State int state, @Nullable IBackupTransport transport) { log(Log.VERBOSE, "State: " + stateToString(mState) + " => " + stateToString(state)); onStateTransition(mState, state); mState = state; mTransport = transport; } private void onStateTransition(int oldState, int newState) { String transport = mTransportComponent.flattenToShortString(); int bound = transitionThroughState(oldState, newState, State.BOUND_AND_CONNECTING); int connected = transitionThroughState(oldState, newState, State.CONNECTED); if (bound != Transition.NO_TRANSITION) { int value = (bound == Transition.UP) ? 1 : 0; // 1 is bound, 0 is not bound EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, transport, value); } if (connected != Transition.NO_TRANSITION) { int value = (connected == Transition.UP) ? 1 : 0; // 1 is connected, 0 is not connected EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_CONNECTION, transport, value); } } /** * Returns: * * <ul> * <li>{@link Transition#UP}, if oldState < stateReference <= newState * <li>{@link Transition#DOWN}, if oldState >= stateReference > newState * <li>{@link Transition#NO_TRANSITION}, otherwise */ @Transition private int transitionThroughState( @State int oldState, @State int newState, @State int stateReference) { if (oldState < stateReference && stateReference <= newState) { return Transition.UP; } if (oldState >= stateReference && stateReference > newState) { return Transition.DOWN; } return Transition.NO_TRANSITION; } @GuardedBy("mStateLock") private void checkStateIntegrityLocked() { switch (mState) { Loading Loading @@ -481,6 +518,14 @@ public class TransportClient { // CharSequence time = DateFormat.format("yyyy-MM-dd HH:mm:ss", System.currentTimeMillis()); } @IntDef({Transition.DOWN, Transition.NO_TRANSITION, Transition.UP}) @Retention(RetentionPolicy.SOURCE) private @interface Transition { int DOWN = -1; int NO_TRANSITION = 0; int UP = 1; } @IntDef({State.UNUSABLE, State.IDLE, State.BOUND_AND_CONNECTING, State.CONNECTED}) @Retention(RetentionPolicy.SOURCE) private @interface State { Loading services/core/java/com/android/server/EventLogTags.logtags +1 −0 Original line number Diff line number Diff line Loading @@ -133,6 +133,7 @@ option java_package com.android.server 2846 full_backup_cancelled (Package|3),(Message|3) 2850 backup_transport_lifecycle (Transport|3),(Bound|1|1) 2851 backup_transport_connection (Transport|3),(Connected|1|1) # --------------------------- Loading services/robotests/src/com/android/server/backup/transport/TransportClientTest.java +82 −2 Original line number Diff line number Diff line Loading @@ -35,8 +35,10 @@ import android.os.Looper; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; import com.android.internal.backup.IBackupTransport; import com.android.server.EventLogTags; import com.android.server.backup.TransportManager; import com.android.server.testing.FrameworkRobolectricTestRunner; import com.android.server.testing.ShadowEventLog; import com.android.server.testing.SystemLoaderClasses; import org.junit.Before; import org.junit.Test; Loading @@ -48,7 +50,7 @@ import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLooper; @RunWith(FrameworkRobolectricTestRunner.class) @Config(manifest = Config.NONE, sdk = 26) @Config(manifest = Config.NONE, sdk = 26, shadows = {ShadowEventLog.class}) @SystemLoaderClasses({TransportManager.class, TransportClient.class}) @Presubmit public class TransportClientTest { Loading @@ -60,6 +62,7 @@ public class TransportClientTest { @Mock private IBackupTransport.Stub mIBackupTransport; private TransportClient mTransportClient; private ComponentName mTransportComponent; private String mTransportString; private Intent mBindIntent; private ShadowLooper mShadowLooper; Loading @@ -71,6 +74,7 @@ public class TransportClientTest { mShadowLooper = shadowOf(mainLooper); mTransportComponent = new ComponentName(PACKAGE_NAME, PACKAGE_NAME + ".transport.Transport"); mTransportString = mTransportComponent.flattenToShortString(); mBindIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST).setComponent(mTransportComponent); mTransportClient = new TransportClient( Loading Loading @@ -161,7 +165,7 @@ public class TransportClientTest { } @Test public void testConnectAsync_whenFrameworkDoesntBind_releasesConnection() throws Exception { public void testConnectAsync_whenFrameworkDoesNotBind_releasesConnection() throws Exception { when(mContext.bindServiceAsUser( eq(mBindIntent), any(ServiceConnection.class), Loading Loading @@ -234,6 +238,82 @@ public class TransportClientTest { .onTransportConnectionResult(isNull(), eq(mTransportClient)); } @Test public void testConnectAsync_beforeFrameworkCall_logsBoundTransition() { ShadowEventLog.clearEvents(); mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 1); } @Test public void testConnectAsync_afterOnServiceConnected_logsBoundAndConnectedTransitions() { ShadowEventLog.clearEvents(); mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); connection.onServiceConnected(mTransportComponent, mIBackupTransport); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 1); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 1); } @Test public void testConnectAsync_afterOnBindingDied_logsBoundAndUnboundTransitions() { ShadowEventLog.clearEvents(); mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); connection.onBindingDied(mTransportComponent); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 1); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0); } @Test public void testUnbind_whenConnected_logsDisconnectedAndUnboundTransitions() { mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); connection.onServiceConnected(mTransportComponent, mIBackupTransport); ShadowEventLog.clearEvents(); mTransportClient.unbind("caller1"); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 0); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0); } @Test public void testOnServiceDisconnected_whenConnected_logsDisconnectedAndUnboundTransitions() { mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); connection.onServiceConnected(mTransportComponent, mIBackupTransport); ShadowEventLog.clearEvents(); connection.onServiceDisconnected(mTransportComponent); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 0); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0); } @Test public void testOnBindingDied_whenConnected_logsDisconnectedAndUnboundTransitions() { mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); connection.onServiceConnected(mTransportComponent, mIBackupTransport); ShadowEventLog.clearEvents(); connection.onBindingDied(mTransportComponent); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 0); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0); } private void assertEventLogged(int tag, Object... values) { assertThat(ShadowEventLog.hasEvent(tag, values)).isTrue(); } private ServiceConnection verifyBindServiceAsUserAndCaptureServiceConnection(Context context) { ArgumentCaptor<ServiceConnection> connectionCaptor = ArgumentCaptor.forClass(ServiceConnection.class); Loading services/robotests/src/com/android/server/testing/ShadowEventLog.java 0 → 100644 +71 −0 Original line number Diff line number Diff line /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.server.testing; import android.util.EventLog; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; @Implements(EventLog.class) public class ShadowEventLog { private final static LinkedHashSet<Entry> ENTRIES = new LinkedHashSet<>(); @Implementation public static int writeEvent(int tag, Object... values) { ENTRIES.add(new Entry(tag, Arrays.asList(values))); // Currently we don't care about the return value, if we do, estimate it correctly return 0; } public static boolean hasEvent(int tag, Object... values) { return ENTRIES.contains(new Entry(tag, Arrays.asList(values))); } public static void clearEvents() { ENTRIES.clear(); } public static class Entry { public final int tag; public final List<Object> values; public Entry(int tag, List<Object> values) { this.tag = tag; this.values = values; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Entry entry = (Entry) o; return tag == entry.tag && values.equals(entry.values); } @Override public int hashCode() { int result = tag; result = 31 * result + values.hashCode(); return result; } } } Loading
services/backup/java/com/android/server/backup/TransportManager.java +0 −5 Original line number Diff line number Diff line Loading @@ -29,14 +29,12 @@ import android.content.pm.ResolveInfo; import android.os.RemoteException; import android.os.UserHandle; import android.util.ArrayMap; import android.util.EventLog; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.backup.IBackupTransport; import com.android.internal.util.Preconditions; import com.android.server.EventLogTags; import com.android.server.backup.transport.OnTransportRegisteredListener; import com.android.server.backup.transport.TransportClient; import com.android.server.backup.transport.TransportClientManager; Loading Loading @@ -574,8 +572,6 @@ public class TransportManager { return BackupManager.ERROR_TRANSPORT_UNAVAILABLE; } EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, transportString, 1); int result; try { String transportName = transport.name(); Loading @@ -587,7 +583,6 @@ public class TransportManager { result = BackupManager.SUCCESS; } catch (RemoteException e) { Slog.e(TAG, "Transport " + transportString + " died while registering"); EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, transportString, 0); result = BackupManager.ERROR_TRANSPORT_UNAVAILABLE; } Loading
services/backup/java/com/android/server/backup/transport/TransportClient.java +45 −0 Original line number Diff line number Diff line Loading @@ -29,12 +29,14 @@ import android.os.IBinder; import android.os.Looper; import android.os.UserHandle; import android.util.ArrayMap; import android.util.EventLog; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.backup.IBackupTransport; import com.android.internal.util.Preconditions; import com.android.server.EventLogTags; import com.android.server.backup.TransportManager; import java.lang.annotation.Retention; Loading Loading @@ -419,10 +421,45 @@ public class TransportClient { @GuardedBy("mStateLock") private void setStateLocked(@State int state, @Nullable IBackupTransport transport) { log(Log.VERBOSE, "State: " + stateToString(mState) + " => " + stateToString(state)); onStateTransition(mState, state); mState = state; mTransport = transport; } private void onStateTransition(int oldState, int newState) { String transport = mTransportComponent.flattenToShortString(); int bound = transitionThroughState(oldState, newState, State.BOUND_AND_CONNECTING); int connected = transitionThroughState(oldState, newState, State.CONNECTED); if (bound != Transition.NO_TRANSITION) { int value = (bound == Transition.UP) ? 1 : 0; // 1 is bound, 0 is not bound EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, transport, value); } if (connected != Transition.NO_TRANSITION) { int value = (connected == Transition.UP) ? 1 : 0; // 1 is connected, 0 is not connected EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_CONNECTION, transport, value); } } /** * Returns: * * <ul> * <li>{@link Transition#UP}, if oldState < stateReference <= newState * <li>{@link Transition#DOWN}, if oldState >= stateReference > newState * <li>{@link Transition#NO_TRANSITION}, otherwise */ @Transition private int transitionThroughState( @State int oldState, @State int newState, @State int stateReference) { if (oldState < stateReference && stateReference <= newState) { return Transition.UP; } if (oldState >= stateReference && stateReference > newState) { return Transition.DOWN; } return Transition.NO_TRANSITION; } @GuardedBy("mStateLock") private void checkStateIntegrityLocked() { switch (mState) { Loading Loading @@ -481,6 +518,14 @@ public class TransportClient { // CharSequence time = DateFormat.format("yyyy-MM-dd HH:mm:ss", System.currentTimeMillis()); } @IntDef({Transition.DOWN, Transition.NO_TRANSITION, Transition.UP}) @Retention(RetentionPolicy.SOURCE) private @interface Transition { int DOWN = -1; int NO_TRANSITION = 0; int UP = 1; } @IntDef({State.UNUSABLE, State.IDLE, State.BOUND_AND_CONNECTING, State.CONNECTED}) @Retention(RetentionPolicy.SOURCE) private @interface State { Loading
services/core/java/com/android/server/EventLogTags.logtags +1 −0 Original line number Diff line number Diff line Loading @@ -133,6 +133,7 @@ option java_package com.android.server 2846 full_backup_cancelled (Package|3),(Message|3) 2850 backup_transport_lifecycle (Transport|3),(Bound|1|1) 2851 backup_transport_connection (Transport|3),(Connected|1|1) # --------------------------- Loading
services/robotests/src/com/android/server/backup/transport/TransportClientTest.java +82 −2 Original line number Diff line number Diff line Loading @@ -35,8 +35,10 @@ import android.os.Looper; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; import com.android.internal.backup.IBackupTransport; import com.android.server.EventLogTags; import com.android.server.backup.TransportManager; import com.android.server.testing.FrameworkRobolectricTestRunner; import com.android.server.testing.ShadowEventLog; import com.android.server.testing.SystemLoaderClasses; import org.junit.Before; import org.junit.Test; Loading @@ -48,7 +50,7 @@ import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLooper; @RunWith(FrameworkRobolectricTestRunner.class) @Config(manifest = Config.NONE, sdk = 26) @Config(manifest = Config.NONE, sdk = 26, shadows = {ShadowEventLog.class}) @SystemLoaderClasses({TransportManager.class, TransportClient.class}) @Presubmit public class TransportClientTest { Loading @@ -60,6 +62,7 @@ public class TransportClientTest { @Mock private IBackupTransport.Stub mIBackupTransport; private TransportClient mTransportClient; private ComponentName mTransportComponent; private String mTransportString; private Intent mBindIntent; private ShadowLooper mShadowLooper; Loading @@ -71,6 +74,7 @@ public class TransportClientTest { mShadowLooper = shadowOf(mainLooper); mTransportComponent = new ComponentName(PACKAGE_NAME, PACKAGE_NAME + ".transport.Transport"); mTransportString = mTransportComponent.flattenToShortString(); mBindIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST).setComponent(mTransportComponent); mTransportClient = new TransportClient( Loading Loading @@ -161,7 +165,7 @@ public class TransportClientTest { } @Test public void testConnectAsync_whenFrameworkDoesntBind_releasesConnection() throws Exception { public void testConnectAsync_whenFrameworkDoesNotBind_releasesConnection() throws Exception { when(mContext.bindServiceAsUser( eq(mBindIntent), any(ServiceConnection.class), Loading Loading @@ -234,6 +238,82 @@ public class TransportClientTest { .onTransportConnectionResult(isNull(), eq(mTransportClient)); } @Test public void testConnectAsync_beforeFrameworkCall_logsBoundTransition() { ShadowEventLog.clearEvents(); mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 1); } @Test public void testConnectAsync_afterOnServiceConnected_logsBoundAndConnectedTransitions() { ShadowEventLog.clearEvents(); mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); connection.onServiceConnected(mTransportComponent, mIBackupTransport); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 1); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 1); } @Test public void testConnectAsync_afterOnBindingDied_logsBoundAndUnboundTransitions() { ShadowEventLog.clearEvents(); mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); connection.onBindingDied(mTransportComponent); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 1); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0); } @Test public void testUnbind_whenConnected_logsDisconnectedAndUnboundTransitions() { mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); connection.onServiceConnected(mTransportComponent, mIBackupTransport); ShadowEventLog.clearEvents(); mTransportClient.unbind("caller1"); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 0); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0); } @Test public void testOnServiceDisconnected_whenConnected_logsDisconnectedAndUnboundTransitions() { mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); connection.onServiceConnected(mTransportComponent, mIBackupTransport); ShadowEventLog.clearEvents(); connection.onServiceDisconnected(mTransportComponent); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 0); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0); } @Test public void testOnBindingDied_whenConnected_logsDisconnectedAndUnboundTransitions() { mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); connection.onServiceConnected(mTransportComponent, mIBackupTransport); ShadowEventLog.clearEvents(); connection.onBindingDied(mTransportComponent); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 0); assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0); } private void assertEventLogged(int tag, Object... values) { assertThat(ShadowEventLog.hasEvent(tag, values)).isTrue(); } private ServiceConnection verifyBindServiceAsUserAndCaptureServiceConnection(Context context) { ArgumentCaptor<ServiceConnection> connectionCaptor = ArgumentCaptor.forClass(ServiceConnection.class); Loading
services/robotests/src/com/android/server/testing/ShadowEventLog.java 0 → 100644 +71 −0 Original line number Diff line number Diff line /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.server.testing; import android.util.EventLog; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; @Implements(EventLog.class) public class ShadowEventLog { private final static LinkedHashSet<Entry> ENTRIES = new LinkedHashSet<>(); @Implementation public static int writeEvent(int tag, Object... values) { ENTRIES.add(new Entry(tag, Arrays.asList(values))); // Currently we don't care about the return value, if we do, estimate it correctly return 0; } public static boolean hasEvent(int tag, Object... values) { return ENTRIES.contains(new Entry(tag, Arrays.asList(values))); } public static void clearEvents() { ENTRIES.clear(); } public static class Entry { public final int tag; public final List<Object> values; public Entry(int tag, List<Object> values) { this.tag = tag; this.values = values; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Entry entry = (Entry) o; return tag == entry.tag && values.equals(entry.values); } @Override public int hashCode() { int result = tag; result = 31 * result + values.hashCode(); return result; } } }