diff --git a/build.gradle.kts b/build.gradle.kts index 0af76808..ad09e054 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,6 +9,7 @@ import com.github.jk1.license.render.TextReportRenderer import org.objectweb.asm.ClassVisitor import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes.CHECKCAST +import org.objectweb.asm.Opcodes.INVOKESTATIC buildscript { dependencies { @@ -139,6 +140,9 @@ android { } } +/** + * Fix PosixFilePermission class type check issue. + */ abstract class FixPosixFilePermissionClassVisitorFactory : AsmClassVisitorFactory { @@ -155,7 +159,10 @@ abstract class FixPosixFilePermissionClassVisitorFactory : exceptions: Array? ): MethodVisitor { if (name == "attributesToPermissions") { // org.apache.sshd.sftp.common.SftpHelper.attributesToPermissions - return object : MethodVisitor(instrumentationContext.apiVersion.get(), super.visitMethod(access, name, descriptor, signature, exceptions)) { + return object : MethodVisitor( + instrumentationContext.apiVersion.get(), + super.visitMethod(access, name, descriptor, signature, exceptions) + ) { override fun visitTypeInsn(opcode: Int, type: String?) { // We need to prevent Android Desugar modifying the `PosixFilePermission` classname. // @@ -187,12 +194,74 @@ abstract class FixPosixFilePermissionClassVisitorFactory : interface Params : InstrumentationParameters } +/** + * Collections.unmodifiableXXX is not exist when Android API level is lower than 26. + * So we replace the call to Collections.unmodifiableXXX with the original collection by removing the call. + */ +abstract class FixCollectionsClassVisitorFactory : + AsmClassVisitorFactory { + override fun createClassVisitor( + classContext: ClassContext, + nextClassVisitor: ClassVisitor + ): ClassVisitor { + return object : ClassVisitor(instrumentationContext.apiVersion.get(), nextClassVisitor) { + override fun visitMethod( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + exceptions: Array? + ): MethodVisitor { + return object : MethodVisitor( + instrumentationContext.apiVersion.get(), + super.visitMethod(access, name, descriptor, signature, exceptions) + ) { + override fun visitMethodInsn( + opcode: Int, + type: String?, + name: String?, + descriptor: String?, + isInterface: Boolean + ) { + val backportClass = "org/kde/kdeconnect/Helpers/CollectionsBackport" + + if (opcode == INVOKESTATIC && type == "java/util/Collections") { + val replaceRules = mapOf( + "unmodifiableNavigableSet" to "(Ljava/util/NavigableSet;)Ljava/util/NavigableSet;", + "unmodifiableSet" to "(Ljava/util/Set;)Ljava/util/Set;", + "unmodifiableNavigableMap" to "(Ljava/util/NavigableMap;)Ljava/util/NavigableMap;", + "emptyNavigableMap" to "()Ljava/util/NavigableMap;") + if (name in replaceRules && descriptor == replaceRules[name]) { + super.visitMethodInsn(opcode, backportClass, name, descriptor, isInterface) + val calleeClass = classContext.currentClassData.className + println("Replace Collections.$name call with CollectionsBackport.$name from $calleeClass success.") + return + } + } + super.visitMethodInsn(opcode, type, name, descriptor, isInterface) + } + } + } + } + } + + override fun isInstrumentable(classData: ClassData): Boolean { + return classData.className.startsWith("org.apache.sshd") // We only need to fix the Apache SSHD library + } + + interface Params : InstrumentationParameters +} + androidComponents { onVariants { variant -> variant.instrumentation.transformClassesWith( FixPosixFilePermissionClassVisitorFactory::class.java, InstrumentationScope.ALL ) { } + variant.instrumentation.transformClassesWith( + FixCollectionsClassVisitorFactory::class.java, + InstrumentationScope.ALL + ) { } } } diff --git a/src/org/kde/kdeconnect/Helpers/CollectionsBackport.java b/src/org/kde/kdeconnect/Helpers/CollectionsBackport.java new file mode 100644 index 00000000..f4957785 --- /dev/null +++ b/src/org/kde/kdeconnect/Helpers/CollectionsBackport.java @@ -0,0 +1,869 @@ +/* + * SPDX-FileCopyrightText: 2014 The Android Open Source Project + * SPDX-FileCopyrightText: 1997, 2021, Oracle and/or its affiliates. All rights reserved + * + * SPDX-FileCopyrightText: 2024 ShellWen Chen + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ +package org.kde.kdeconnect.Helpers; + +import android.os.Build; +import android.os.Build.VERSION; + +import androidx.annotation.RequiresApi; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.Map; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.Objects; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.Spliterator; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** @noinspection unused*/ +public final class CollectionsBackport { + public static NavigableSet unmodifiableNavigableSet(NavigableSet s) { + if (VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return Collections.unmodifiableNavigableSet(s); + } else { + return new UnmodifiableNavigableSetBackport(s); + } + } + + public static Set unmodifiableSet(Set s) { + if (VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return Collections.unmodifiableSet(s); + } else { + return new UnmodifiableSetBackport(s); + } + } + + public static Collection unmodifiableCollection(Collection c) { + if (VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return Collections.unmodifiableCollection(c); + } else { + return new UnmodifiableCollectionBackport(c); + } + } + + public static NavigableMap unmodifiableNavigableMap(NavigableMap m) { + if (VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return Collections.unmodifiableNavigableMap(m); + } else { + return new UnmodifiableNavigableMapBackport<>(m); + } + } + + public static Map unmodifiableMap(Map m) { + if (VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return Collections.unmodifiableMap(m); + } else { + return new UnmodifiableMapBackport(m); + } + } + + public static NavigableSet emptyNavigableSet() { + return (NavigableSet) UnmodifiableNavigableSetBackport.EMPTY_NAVIGABLE_SET; + } + + public static NavigableMap emptyNavigableMap() { + return (NavigableMap) UnmodifiableNavigableMapBackport.EMPTY_NAVIGABLE_MAP; + } + + static boolean eq(Object o1, Object o2) { + return o1 == null ? o2 == null : o1.equals(o2); + } + + static class UnmodifiableNavigableSetBackport + extends UnmodifiableSortedSetBackport + implements NavigableSet, Serializable { + + /** + * A singleton empty unmodifiable navigable set used for + * {@link #emptyNavigableSet()}. + * + * @param type of elements, if there were any, and bounds + */ + private static class EmptyNavigableSet extends UnmodifiableNavigableSetBackport + implements Serializable { + public EmptyNavigableSet() { + super(new TreeSet<>()); + } + + @java.io.Serial + private Object readResolve() { + return EMPTY_NAVIGABLE_SET; + } + } + + @SuppressWarnings("rawtypes") + private static final NavigableSet EMPTY_NAVIGABLE_SET = + new EmptyNavigableSet<>(); + + /** + * The instance we are protecting. + */ + @SuppressWarnings("serial") // Conditionally serializable + private final NavigableSet ns; + + UnmodifiableNavigableSetBackport(NavigableSet s) { + super(s); + ns = s; + } + + public E lower(E e) { + return ns.lower(e); + } + + public E floor(E e) { + return ns.floor(e); + } + + public E ceiling(E e) { + return ns.ceiling(e); + } + + public E higher(E e) { + return ns.higher(e); + } + + public E pollFirst() { + throw new UnsupportedOperationException(); + } + + public E pollLast() { + throw new UnsupportedOperationException(); + } + + public NavigableSet descendingSet() { + return new UnmodifiableNavigableSetBackport<>(ns.descendingSet()); + } + + public Iterator descendingIterator() { + return descendingSet().iterator(); + } + + public NavigableSet subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive) { + return new UnmodifiableNavigableSetBackport<>( + ns.subSet(fromElement, fromInclusive, toElement, toInclusive)); + } + + public NavigableSet headSet(E toElement, boolean inclusive) { + return new UnmodifiableNavigableSetBackport<>( + ns.headSet(toElement, inclusive)); + } + + public NavigableSet tailSet(E fromElement, boolean inclusive) { + return new UnmodifiableNavigableSetBackport<>( + ns.tailSet(fromElement, inclusive)); + } + } + + static class UnmodifiableSortedSetBackport + extends UnmodifiableSetBackport + implements SortedSet, Serializable { + @SuppressWarnings("serial") // Conditionally serializable + private final SortedSet ss; + + UnmodifiableSortedSetBackport(SortedSet s) { + super(s); + ss = s; + } + + public Comparator comparator() { + return ss.comparator(); + } + + public SortedSet subSet(E fromElement, E toElement) { + return new UnmodifiableSortedSetBackport<>(ss.subSet(fromElement, toElement)); + } + + public SortedSet headSet(E toElement) { + return new UnmodifiableSortedSetBackport<>(ss.headSet(toElement)); + } + + public SortedSet tailSet(E fromElement) { + return new UnmodifiableSortedSetBackport<>(ss.tailSet(fromElement)); + } + + public E first() { + return ss.first(); + } + + public E last() { + return ss.last(); + } + } + + static class UnmodifiableSetBackport extends UnmodifiableCollectionBackport + implements Set, Serializable { + + UnmodifiableSetBackport(Set s) { + super(s); + } + + public boolean equals(Object o) { + return o == this || c.equals(o); + } + + public int hashCode() { + return c.hashCode(); + } + } + + static class UnmodifiableCollectionBackport implements Collection, Serializable { + + @SuppressWarnings("serial") // Conditionally serializable + final Collection c; + + UnmodifiableCollectionBackport(Collection c) { + if (c == null) + throw new NullPointerException(); + this.c = c; + } + + public int size() { + return c.size(); + } + + public boolean isEmpty() { + return c.isEmpty(); + } + + public boolean contains(Object o) { + return c.contains(o); + } + + public Object[] toArray() { + return c.toArray(); + } + + public T[] toArray(T[] a) { + return c.toArray(a); + } + + @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) + public T[] toArray(IntFunction f) { + return c.toArray(f); + } + + public String toString() { + return c.toString(); + } + + public Iterator iterator() { + return new Iterator() { + private final Iterator i = c.iterator(); + + public boolean hasNext() { + return i.hasNext(); + } + + public E next() { + return i.next(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void forEachRemaining(Consumer action) { + // Use backing collection version + i.forEachRemaining(action); + } + }; + } + + public boolean add(E e) { + throw new UnsupportedOperationException(); + } + + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + public boolean containsAll(Collection coll) { + return c.containsAll(coll); + } + + public boolean addAll(Collection coll) { + throw new UnsupportedOperationException(); + } + + public boolean removeAll(Collection coll) { + throw new UnsupportedOperationException(); + } + + public boolean retainAll(Collection coll) { + throw new UnsupportedOperationException(); + } + + public void clear() { + throw new UnsupportedOperationException(); + } + + // Override default methods in Collection + @Override + public void forEach(Consumer action) { + c.forEach(action); + } + + @Override + public boolean removeIf(Predicate filter) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + @Override + public Spliterator spliterator() { + return (Spliterator) c.spliterator(); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @SuppressWarnings("unchecked") + @Override + public Stream stream() { + return (Stream) c.stream(); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @SuppressWarnings("unchecked") + @Override + public Stream parallelStream() { + return (Stream) c.parallelStream(); + } + } + + static class UnmodifiableNavigableMapBackport extends UnmodifiableSortedMapBackport implements NavigableMap, Serializable { + private static final long serialVersionUID = -4858195264774772197L; + private static final EmptyNavigableMapBackport EMPTY_NAVIGABLE_MAP = new EmptyNavigableMapBackport(); + private final NavigableMap nm; + + UnmodifiableNavigableMapBackport(NavigableMap m) { + super(m); + this.nm = m; + } + + public K lowerKey(K key) { + return this.nm.lowerKey(key); + } + + public K floorKey(K key) { + return this.nm.floorKey(key); + } + + public K ceilingKey(K key) { + return this.nm.ceilingKey(key); + } + + public K higherKey(K key) { + return this.nm.higherKey(key); + } + + public Map.Entry lowerEntry(K key) { + Map.Entry lower = (Entry) this.nm.lowerEntry(key); + return null != lower ? new UnmodifiableMapBackport.UnmodifiableEntrySetBackport.UnmodifiableEntry(lower) : null; + } + + public Map.Entry floorEntry(K key) { + Map.Entry floor = (Entry) this.nm.floorEntry(key); + return null != floor ? new UnmodifiableMapBackport.UnmodifiableEntrySetBackport.UnmodifiableEntry(floor) : null; + } + + public Map.Entry ceilingEntry(K key) { + Map.Entry ceiling = (Entry) this.nm.ceilingEntry(key); + return null != ceiling ? new UnmodifiableMapBackport.UnmodifiableEntrySetBackport.UnmodifiableEntry(ceiling) : null; + } + + public Map.Entry higherEntry(K key) { + Map.Entry higher = (Entry) this.nm.higherEntry(key); + return null != higher ? new UnmodifiableMapBackport.UnmodifiableEntrySetBackport.UnmodifiableEntry(higher) : null; + } + + public Map.Entry firstEntry() { + Map.Entry first = (Entry) this.nm.firstEntry(); + return null != first ? new UnmodifiableMapBackport.UnmodifiableEntrySetBackport.UnmodifiableEntry(first) : null; + } + + public Map.Entry lastEntry() { + Map.Entry last = (Entry) this.nm.lastEntry(); + return null != last ? new UnmodifiableMapBackport.UnmodifiableEntrySetBackport.UnmodifiableEntry(last) : null; + } + + public Map.Entry pollFirstEntry() { + throw new UnsupportedOperationException(); + } + + public Map.Entry pollLastEntry() { + throw new UnsupportedOperationException(); + } + + public NavigableMap descendingMap() { + return (NavigableMap) CollectionsBackport.unmodifiableNavigableMap(this.nm.descendingMap()); + } + + public NavigableSet navigableKeySet() { + return CollectionsBackport.unmodifiableNavigableSet(this.nm.navigableKeySet()); + } + + public NavigableSet descendingKeySet() { + return CollectionsBackport.unmodifiableNavigableSet(this.nm.descendingKeySet()); + } + + public NavigableMap subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) { + return (NavigableMap) CollectionsBackport.unmodifiableNavigableMap(this.nm.subMap(fromKey, fromInclusive, toKey, toInclusive)); + } + + public NavigableMap headMap(K toKey, boolean inclusive) { + return (NavigableMap) CollectionsBackport.unmodifiableNavigableMap(this.nm.headMap(toKey, inclusive)); + } + + public NavigableMap tailMap(K fromKey, boolean inclusive) { + return (NavigableMap) CollectionsBackport.unmodifiableNavigableMap(this.nm.tailMap(fromKey, inclusive)); + } + + private static class EmptyNavigableMapBackport extends UnmodifiableNavigableMapBackport implements Serializable { + private static final long serialVersionUID = -2239321462712562324L; + + EmptyNavigableMapBackport() { + super(new TreeMap()); + } + + public NavigableSet navigableKeySet() { + return CollectionsBackport.emptyNavigableSet(); + } + + private Object readResolve() { + return UnmodifiableNavigableMapBackport.EMPTY_NAVIGABLE_MAP; + } + } + } + + static class UnmodifiableSortedMapBackport + extends UnmodifiableMapBackport + implements SortedMap, Serializable { + @SuppressWarnings("serial") // Conditionally serializable + private final SortedMap sm; + + UnmodifiableSortedMapBackport(SortedMap m) {super(m); sm = m; } + public Comparator comparator() { return sm.comparator(); } + public SortedMap subMap(K fromKey, K toKey) + { return new UnmodifiableSortedMapBackport<>(sm.subMap(fromKey, toKey)); } + public SortedMap headMap(K toKey) + { return new UnmodifiableSortedMapBackport<>(sm.headMap(toKey)); } + public SortedMap tailMap(K fromKey) + { return new UnmodifiableSortedMapBackport<>(sm.tailMap(fromKey)); } + public K firstKey() { return sm.firstKey(); } + public K lastKey() { return sm.lastKey(); } + } + + private static class UnmodifiableMapBackport implements Map, Serializable { + @java.io.Serial + private static final long serialVersionUID = -1034234728574286014L; + + @SuppressWarnings("serial") // Conditionally serializable + private final Map m; + + UnmodifiableMapBackport(Map m) { + if (m == null) + throw new NullPointerException(); + this.m = m; + } + + public int size() { + return m.size(); + } + + public boolean isEmpty() { + return m.isEmpty(); + } + + public boolean containsKey(Object key) { + return m.containsKey(key); + } + + public boolean containsValue(Object val) { + return m.containsValue(val); + } + + public V get(Object key) { + return m.get(key); + } + + public V put(K key, V value) { + throw new UnsupportedOperationException(); + } + + public V remove(Object key) { + throw new UnsupportedOperationException(); + } + + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + + public void clear() { + throw new UnsupportedOperationException(); + } + + private transient Set keySet; + private transient Set> entrySet; + private transient Collection values; + + public Set keySet() { + if (keySet == null) + keySet = (Set) unmodifiableSet(m.keySet()); + return keySet; + } + + public Set> entrySet() { + if (entrySet == null) + entrySet = new UnmodifiableEntrySetBackport<>(m.entrySet()); + return entrySet; + } + + public Collection values() { + if (values == null) + values = (Collection) unmodifiableCollection(m.values()); + return values; + } + + public boolean equals(Object o) { + return o == this || m.equals(o); + } + + public int hashCode() { + return m.hashCode(); + } + + public String toString() { + return m.toString(); + } + + // Override default methods in Map + @Override + @SuppressWarnings("unchecked") + public V getOrDefault(Object k, V defaultValue) { + // Safe cast as we don't change the value + return ((Map) m).getOrDefault(k, defaultValue); + } + + @Override + public void forEach(BiConsumer action) { + m.forEach(action); + } + + @Override + public void replaceAll(BiFunction function) { + throw new UnsupportedOperationException(); + } + + @Override + public V putIfAbsent(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + throw new UnsupportedOperationException(); + } + + @Override + public V replace(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public V computeIfPresent(K key, + BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public V compute(K key, + BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public V merge(K key, V value, + BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + /** + * We need this class in addition to UnmodifiableSet as + * Map.Entries themselves permit modification of the backing Map + * via their setValue operation. This class is subtle: there are + * many possible attacks that must be thwarted. + * + * @serial include + */ + static class UnmodifiableEntrySetBackport + extends UnmodifiableSetBackport> { + @java.io.Serial + private static final long serialVersionUID = 7854390611657943733L; + + @SuppressWarnings({"unchecked", "rawtypes"}) + UnmodifiableEntrySetBackport(Set> s) { + // Need to cast to raw in order to work around a limitation in the type system + super((Set) s); + } + + static Consumer> entryConsumer( + Consumer> action) { + return e -> action.accept(new UnmodifiableEntry<>(e)); + } + + public void forEach(Consumer> action) { + Objects.requireNonNull(action); + c.forEach(entryConsumer(action)); + } + + static final class UnmodifiableEntrySetSpliterator + implements Spliterator> { + final Spliterator> s; + + UnmodifiableEntrySetSpliterator(Spliterator> s) { + this.s = s; + } + + @Override + public boolean tryAdvance(Consumer> action) { + Objects.requireNonNull(action); + return s.tryAdvance(entryConsumer(action)); + } + + @Override + public void forEachRemaining(Consumer> action) { + Objects.requireNonNull(action); + s.forEachRemaining(entryConsumer(action)); + } + + @Override + public Spliterator> trySplit() { + Spliterator> split = s.trySplit(); + return split == null + ? null + : new UnmodifiableEntrySetSpliterator<>(split); + } + + @Override + public long estimateSize() { + return s.estimateSize(); + } + + @Override + public long getExactSizeIfKnown() { + return s.getExactSizeIfKnown(); + } + + @Override + public int characteristics() { + return s.characteristics(); + } + + @Override + public boolean hasCharacteristics(int characteristics) { + return s.hasCharacteristics(characteristics); + } + + @Override + public Comparator> getComparator() { + return s.getComparator(); + } + } + + @SuppressWarnings("unchecked") + public Spliterator> spliterator() { + return new UnmodifiableEntrySetSpliterator<>( + (Spliterator>) c.spliterator()); + } + + @Override + public Stream> stream() { + return StreamSupport.stream(spliterator(), false); + } + + @Override + public Stream> parallelStream() { + return StreamSupport.stream(spliterator(), true); + } + + public Iterator> iterator() { + return new Iterator>() { + private final Iterator> i = c.iterator(); + + public boolean hasNext() { + return i.hasNext(); + } + + public Map.Entry next() { + return new UnmodifiableEntry<>(i.next()); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + // Seems like an oversight. http://b/110351017 + public void forEachRemaining(Consumer> action) { + i.forEachRemaining(entryConsumer(action)); + } + }; + } + + @SuppressWarnings("unchecked") + public Object[] toArray() { + Object[] a = c.toArray(); + for (int i = 0; i < a.length; i++) + a[i] = new UnmodifiableEntry<>((Map.Entry) a[i]); + return a; + } + + @SuppressWarnings("unchecked") + public T[] toArray(T[] a) { + // We don't pass a to c.toArray, to avoid window of + // vulnerability wherein an unscrupulous multithreaded client + // could get his hands on raw (unwrapped) Entries from c. + Object[] arr = c.toArray(a.length == 0 ? a : Arrays.copyOf(a, 0)); + + for (int i = 0; i < arr.length; i++) + arr[i] = new UnmodifiableEntry<>((Map.Entry) arr[i]); + + if (arr.length > a.length) + return (T[]) arr; + + System.arraycopy(arr, 0, a, 0, arr.length); + if (a.length > arr.length) + a[arr.length] = null; + return a; + } + + /** + * This method is overridden to protect the backing set against + * an object with a nefarious equals function that senses + * that the equality-candidate is Map.Entry and calls its + * setValue method. + */ + public boolean contains(Object o) { + if (!(o instanceof Map.Entry)) + return false; + return c.contains( + new UnmodifiableEntry<>((Map.Entry) o)); + } + + /** + * The next two methods are overridden to protect against + * an unscrupulous List whose contains(Object o) method senses + * when o is a Map.Entry, and calls o.setValue. + */ + public boolean containsAll(Collection coll) { + for (Object e : coll) { + if (!contains(e)) // Invokes safe contains() above + return false; + } + return true; + } + + public boolean equals(Object o) { + if (o == this) + return true; + + // Android-changed: (b/247094511) instanceof pattern variable is not yet supported. + /* + return o instanceof Set s + && s.size() == c.size() + && containsAll(s); // Invokes safe containsAll() above + */ + if (!(o instanceof Set)) + return false; + Set s = (Set) o; + if (s.size() != c.size()) + return false; + return containsAll(s); // Invokes safe containsAll() above + } + + /** + * This "wrapper class" serves two purposes: it prevents + * the client from modifying the backing Map, by short-circuiting + * the setValue method, and it protects the backing Map against + * an ill-behaved Map.Entry that attempts to modify another + * Map Entry when asked to perform an equality check. + */ + private static class UnmodifiableEntry implements Map.Entry { + private Map.Entry e; + + UnmodifiableEntry(Map.Entry e) { + this.e = Objects.requireNonNull(e); + } + + public K getKey() { + return e.getKey(); + } + + public V getValue() { + return e.getValue(); + } + + public V setValue(V value) { + throw new UnsupportedOperationException(); + } + + public int hashCode() { + return e.hashCode(); + } + + public boolean equals(Object o) { + if (this == o) + return true; + // Android-changed: (b/247094511) instanceof pattern variable is not yet + // supported. + /* + return o instanceof Map.Entry t + && eq(e.getKey(), t.getKey()) + && eq(e.getValue(), t.getValue()); + */ + if (!(o instanceof Map.Entry)) + return false; + Map.Entry t = (Map.Entry) o; + return eq(e.getKey(), t.getKey()) && + eq(e.getValue(), t.getValue()); + } + + public String toString() { + return e.toString(); + } + } + } + } +}