Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f0202a95 authored by Narayan Kamath's avatar Narayan Kamath
Browse files

WorkSource: Add support for chaining attribution.



WorkSource objects now hold references to zero or more WorkChain
objects, each of which represent a "chain" of attribution.

This change also changes most WorkSource APIs to be able to
deal with WorkChains. Several APIs do not necessarily make sense with
WorkChains and have been left unmodified :

- boolean diff(WorkSource): Does not account for WorkChains for now.
  This is a public API so we shouln't change it unless we decide to
  make WorkChains public.
- setReturningDiffs(WorkSource): Used internally only, will be removed
  in a future change.
- addReturningNewbs(WorkSource): Used for testing only, fill be removed
  in a future change.

In addition, two new (hidden) APIs have been added to add and query
WorkChains. These APIs have only been added to facilitate testing. They
will most likely change when we add non-test users.

Co-Authored-By: default avatarYang Lu <yanglu@google.com>

Test: WorkSourceTest
Bug: 62390666
Change-Id: Iff361eb98e079c7b2146c092dc27a3618a813c94
parent 26b71b8c
Loading
Loading
Loading
Loading
+316 −18
Original line number Diff line number Diff line
package android.os;

import android.annotation.Nullable;
import android.os.WorkSourceProto;
import android.util.Log;
import android.util.proto.ProtoOutputStream;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;

/**
 * Describes the source of some work that may be done by someone else.
@@ -19,6 +22,8 @@ public class WorkSource implements Parcelable {
    int[] mUids;
    String[] mNames;

    private ArrayList<WorkChain> mChains;

    /**
     * Internal statics to avoid object allocations in some operations.
     * The WorkSource object itself is not thread safe, but we need to
@@ -39,6 +44,7 @@ public class WorkSource implements Parcelable {
     */
    public WorkSource() {
        mNum = 0;
        mChains = null;
    }

    /**
@@ -48,6 +54,7 @@ public class WorkSource implements Parcelable {
    public WorkSource(WorkSource orig) {
        if (orig == null) {
            mNum = 0;
            mChains = null;
            return;
        }
        mNum = orig.mNum;
@@ -58,6 +65,16 @@ public class WorkSource implements Parcelable {
            mUids = null;
            mNames = null;
        }

        if (orig.mChains != null) {
            // Make a copy of all WorkChains that exist on |orig| since they are mutable.
            mChains = new ArrayList<>(orig.mChains.size());
            for (WorkChain chain : orig.mChains) {
                mChains.add(new WorkChain(chain));
            }
        } else {
            mChains = null;
        }
    }

    /** @hide */
@@ -65,6 +82,7 @@ public class WorkSource implements Parcelable {
        mNum = 1;
        mUids = new int[] { uid, 0 };
        mNames = null;
        mChains = null;
    }

    /** @hide */
@@ -75,12 +93,21 @@ public class WorkSource implements Parcelable {
        mNum = 1;
        mUids = new int[] { uid, 0 };
        mNames = new String[] { name, null };
        mChains = null;
    }

    WorkSource(Parcel in) {
        mNum = in.readInt();
        mUids = in.createIntArray();
        mNames = in.createStringArray();

        int numChains = in.readInt();
        if (numChains > 0) {
            mChains = new ArrayList<>(numChains);
            in.readParcelableList(mChains, WorkChain.class.getClassLoader());
        } else {
            mChains = null;
        }
    }

    /** @hide */
@@ -99,7 +126,8 @@ public class WorkSource implements Parcelable {
    }

    /**
     * Clear names from this WorkSource.  Uids are left intact.
     * Clear names from this WorkSource. Uids are left intact. WorkChains if any, are left
     * intact.
     *
     * <p>Useful when combining with another WorkSource that doesn't have names.
     * @hide
@@ -127,11 +155,16 @@ public class WorkSource implements Parcelable {
     */
    public void clear() {
        mNum = 0;
        if (mChains != null) {
            mChains.clear();
        }
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof WorkSource && !diff((WorkSource)o);
        return o instanceof WorkSource
            && !diff((WorkSource) o)
            && Objects.equals(mChains, ((WorkSource) o).mChains);
    }

    @Override
@@ -145,6 +178,11 @@ public class WorkSource implements Parcelable {
                result = ((result << 4) | (result >>> 28)) ^ mNames[i].hashCode();
            }
        }

        if (mChains != null) {
            result = ((result << 4) | (result >>> 28)) ^ mChains.hashCode();
        }

        return result;
    }

@@ -153,6 +191,8 @@ public class WorkSource implements Parcelable {
     * @param other The WorkSource to compare against.
     * @return If there is a difference, true is returned.
     */
    // TODO: This is a public API so it cannot be renamed. Because it is used in several places,
    // we keep its semantics the same and ignore any differences in WorkChains (if any).
    public boolean diff(WorkSource other) {
        int N = mNum;
        if (N != other.mNum) {
@@ -175,12 +215,15 @@ public class WorkSource implements Parcelable {

    /**
     * Replace the current contents of this work source with the given
     * work source.  If <var>other</var> is null, the current work source
     * work source.  If {@code other} is null, the current work source
     * will be made empty.
     */
    public void set(WorkSource other) {
        if (other == null) {
            mNum = 0;
            if (mChains != null) {
                mChains.clear();
            }
            return;
        }
        mNum = other.mNum;
@@ -203,6 +246,18 @@ public class WorkSource implements Parcelable {
            mUids = null;
            mNames = null;
        }

        if (other.mChains != null) {
            if (mChains != null) {
                mChains.clear();
            } else {
                mChains = new ArrayList<>(other.mChains.size());
            }

            for (WorkChain chain : other.mChains) {
                mChains.add(new WorkChain(chain));
            }
        }
    }

    /** @hide */
@@ -211,6 +266,7 @@ public class WorkSource implements Parcelable {
        if (mUids == null) mUids = new int[2];
        mUids[0] = uid;
        mNames = null;
        mChains.clear();
    }

    /** @hide */
@@ -225,9 +281,21 @@ public class WorkSource implements Parcelable {
        }
        mUids[0] = uid;
        mNames[0] = name;
        mChains.clear();
    }

    /** @hide */
    /**
     * Legacy API, DO NOT USE: Only deals with flat UIDs and tags. No chains are transferred, and no
     * differences in chains are returned. This will be removed once its callers have been
     * rewritten.
     *
     * NOTE: This is currently only used in GnssLocationProvider.
     *
     * @hide
     * @deprecated for internal use only. WorkSources are opaque and no external callers should need
     *     to be aware of internal differences.
     */
    @Deprecated
    public WorkSource[] setReturningDiffs(WorkSource other) {
        synchronized (sTmpWorkSource) {
            sNewbWork = null;
@@ -251,11 +319,34 @@ public class WorkSource implements Parcelable {
     */
    public boolean add(WorkSource other) {
        synchronized (sTmpWorkSource) {
            return updateLocked(other, false, false);
            boolean uidAdded = updateLocked(other, false, false);

            boolean chainAdded = false;
            if (other.mChains != null) {
                // NOTE: This is quite an expensive operation, especially if the number of chains
                // is large. We could look into optimizing it if it proves problematic.
                if (mChains == null) {
                    mChains = new ArrayList<>(other.mChains.size());
                }

                for (WorkChain wc : other.mChains) {
                    if (!mChains.contains(wc)) {
                        mChains.add(new WorkChain(wc));
                    }
                }
            }

    /** @hide */
            return uidAdded || chainAdded;
        }
    }

    /**
     * Legacy API: DO NOT USE. Only in use from unit tests.
     *
     * @hide
     * @deprecated meant for unit testing use only. Will be removed in a future API revision.
     */
    @Deprecated
    public WorkSource addReturningNewbs(WorkSource other) {
        synchronized (sTmpWorkSource) {
            sNewbWork = null;
@@ -311,22 +402,14 @@ public class WorkSource implements Parcelable {
        return true;
    }

    /** @hide */
    public WorkSource addReturningNewbs(int uid) {
        synchronized (sTmpWorkSource) {
            sNewbWork = null;
            sTmpWorkSource.mUids[0] = uid;
            updateLocked(sTmpWorkSource, false, true);
            return sNewbWork;
        }
    }

    public boolean remove(WorkSource other) {
        if (mNum <= 0 || other.mNum <= 0) {
            return false;
        }

        boolean uidRemoved = false;
        if (mNames == null && other.mNames == null) {
            return removeUids(other);
            uidRemoved = removeUids(other);
        } else {
            if (mNames == null) {
                throw new IllegalArgumentException("Other " + other + " has names, but target "
@@ -336,8 +419,44 @@ public class WorkSource implements Parcelable {
                throw new IllegalArgumentException("Target " + this + " has names, but other "
                        + other + " does not");
            }
            return removeUidsAndNames(other);
            uidRemoved = removeUidsAndNames(other);
        }

        boolean chainRemoved = false;
        if (other.mChains != null) {
            if (mChains != null) {
                chainRemoved = mChains.removeAll(other.mChains);
            }
        } else if (mChains != null) {
            mChains.clear();
            chainRemoved = true;
        }

        return uidRemoved || chainRemoved;
    }

    /**
     * Create a new {@code WorkChain} associated with this WorkSource and return it.
     *
     * @hide
     */
    public WorkChain createWorkChain() {
        if (mChains == null) {
            mChains = new ArrayList<>(4);
        }

        final WorkChain wc = new WorkChain();
        mChains.add(wc);

        return wc;
    }

    /**
     * @return the list of {@code WorkChains} associated with this {@code WorkSource}.
     * @hide
     */
    public ArrayList<WorkChain> getWorkChains() {
        return mChains;
    }

    private boolean removeUids(WorkSource other) {
@@ -648,6 +767,167 @@ public class WorkSource implements Parcelable {
        }
    }

    /**
     * Represents an attribution chain for an item of work being performed. An attribution chain is
     * an indexed list of {code (uid, tag)} nodes. The node at {@code index == 0} is the initiator
     * of the work, and the node at the highest index performs the work. Nodes at other indices
     * are intermediaries that facilitate the work. Examples :
     *
     * (1) Work being performed by uid=2456 (no chaining):
     * <pre>
     * WorkChain {
     *   mUids = { 2456 }
     *   mTags = { null }
     *   mSize = 1;
     * }
     * </pre>
     *
     * (2) Work being performed by uid=2456 (from component "c1") on behalf of uid=5678:
     *
     * <pre>
     * WorkChain {
     *   mUids = { 5678, 2456 }
     *   mTags = { null, "c1" }
     *   mSize = 1
     * }
     * </pre>
     *
     * Attribution chains are mutable, though the only operation that can be performed on them
     * is the addition of a new node at the end of the attribution chain to represent
     *
     * @hide
     */
    public static class WorkChain implements Parcelable {
        private int mSize;
        private int[] mUids;
        private String[] mTags;

        // @VisibleForTesting
        public WorkChain() {
            mSize = 0;
            mUids = new int[4];
            mTags = new String[4];
        }

        // @VisibleForTesting
        public WorkChain(WorkChain other) {
            mSize = other.mSize;
            mUids = other.mUids.clone();
            mTags = other.mTags.clone();
        }

        private WorkChain(Parcel in) {
            mSize = in.readInt();
            mUids = in.createIntArray();
            mTags = in.createStringArray();
        }

        /**
         * Append a node whose uid is {@code uid} and whose optional tag is {@code tag} to this
         * {@code WorkChain}.
         */
        public WorkChain addNode(int uid, @Nullable String tag) {
            if (mSize == mUids.length) {
                resizeArrays();
            }

            mUids[mSize] = uid;
            mTags[mSize] = tag;
            mSize++;

            return this;
        }

        // TODO: The following three trivial getters are purely for testing and will be removed
        // once we have higher level logic in place, e.g for serializing this WorkChain to a proto,
        // diffing it etc.
        //
        // @VisibleForTesting
        public int[] getUids() {
            return mUids;
        }
        // @VisibleForTesting
        public String[] getTags() {
            return mTags;
        }
        // @VisibleForTesting
        public int getSize() {
            return mSize;
        }

        private void resizeArrays() {
            final int newSize = mSize * 2;
            int[] uids = new int[newSize];
            String[] tags = new String[newSize];

            System.arraycopy(mUids, 0, uids, 0, mSize);
            System.arraycopy(mTags, 0, tags, 0, mSize);

            mUids = uids;
            mTags = tags;
        }

        @Override
        public String toString() {
            StringBuilder result = new StringBuilder("WorkChain{");
            for (int i = 0; i < mSize; ++i) {
                if (i != 0) {
                    result.append(", ");
                }
                result.append("(");
                result.append(mUids[i]);
                if (mTags[i] != null) {
                    result.append(", ");
                    result.append(mTags[i]);
                }
                result.append(")");
            }

            result.append("}");
            return result.toString();
        }

        @Override
        public int hashCode() {
            return (mSize + 31 * Arrays.hashCode(mUids)) * 31 + Arrays.hashCode(mTags);
        }

        @Override
        public boolean equals(Object o) {
            if (o instanceof WorkChain) {
                WorkChain other = (WorkChain) o;

                return mSize == other.mSize
                    && Arrays.equals(mUids, other.mUids)
                    && Arrays.equals(mTags, other.mTags);
            }

            return false;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(mSize);
            dest.writeIntArray(mUids);
            dest.writeStringArray(mTags);
        }

        public static final Parcelable.Creator<WorkChain> CREATOR =
                new Parcelable.Creator<WorkChain>() {
                    public WorkChain createFromParcel(Parcel in) {
                        return new WorkChain(in);
                    }
                    public WorkChain[] newArray(int size) {
                        return new WorkChain[size];
                    }
                };
    }

    @Override
    public int describeContents() {
        return 0;
@@ -658,6 +938,13 @@ public class WorkSource implements Parcelable {
        dest.writeInt(mNum);
        dest.writeIntArray(mUids);
        dest.writeStringArray(mNames);

        if (mChains == null) {
            dest.writeInt(-1);
        } else {
            dest.writeInt(mChains.size());
            dest.writeParcelableList(mChains, flags);
        }
    }

    @Override
@@ -674,6 +961,17 @@ public class WorkSource implements Parcelable {
                result.append(mNames[i]);
            }
        }

        if (mChains != null) {
            result.append(" chains=");
            for (int i = 0; i < mChains.size(); ++i) {
                if (i != 0) {
                    result.append(", ");
                }
                result.append(mChains.get(i));
            }
        }

        result.append("}");
        return result.toString();
    }
+208 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2008 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 android.os;

import android.os.WorkSource.WorkChain;

import junit.framework.TestCase;

import java.util.List;

/**
 * Provides unit tests for hidden / unstable WorkSource APIs that are not CTS testable.
 *
 * These tests will be moved to CTS when finalized.
 */
public class WorkSourceTest extends TestCase {
    public void testWorkChain_add() {
        WorkChain wc1 = new WorkChain();
        wc1.addNode(56, null);

        assertEquals(56, wc1.getUids()[0]);
        assertEquals(null, wc1.getTags()[0]);
        assertEquals(1, wc1.getSize());

        wc1.addNode(57, "foo");
        assertEquals(56, wc1.getUids()[0]);
        assertEquals(null, wc1.getTags()[0]);
        assertEquals(57, wc1.getUids()[1]);
        assertEquals("foo", wc1.getTags()[1]);

        assertEquals(2, wc1.getSize());
    }

    public void testWorkChain_equalsHashCode() {
        WorkChain wc1 = new WorkChain();
        WorkChain wc2 = new WorkChain();

        assertEquals(wc1, wc2);
        assertEquals(wc1.hashCode(), wc2.hashCode());

        wc1.addNode(1, null);
        wc2.addNode(1, null);
        assertEquals(wc1, wc2);
        assertEquals(wc1.hashCode(), wc2.hashCode());

        wc1.addNode(2, "tag");
        wc2.addNode(2, "tag");
        assertEquals(wc1, wc2);
        assertEquals(wc1.hashCode(), wc2.hashCode());

        wc1 = new WorkChain();
        wc2 = new WorkChain();
        wc1.addNode(5, null);
        wc2.addNode(6, null);
        assertFalse(wc1.equals(wc2));
        assertFalse(wc1.hashCode() == wc2.hashCode());

        wc1 = new WorkChain();
        wc2 = new WorkChain();
        wc1.addNode(5, "tag1");
        wc2.addNode(5, "tag2");
        assertFalse(wc1.equals(wc2));
        assertFalse(wc1.hashCode() == wc2.hashCode());
    }

    public void testWorkChain_constructor() {
        WorkChain wc1 = new WorkChain();
        wc1.addNode(1, "foo")
            .addNode(2, null)
            .addNode(3, "baz");

        WorkChain wc2 = new WorkChain(wc1);
        assertEquals(wc1, wc2);

        wc1.addNode(4, "baz");
        assertFalse(wc1.equals(wc2));
    }

    public void testDiff_workChains() {
        WorkSource ws1 = new WorkSource();
        ws1.add(50);
        ws1.createWorkChain().addNode(52, "foo");
        WorkSource ws2 = new WorkSource();
        ws2.add(50);
        ws2.createWorkChain().addNode(60, "bar");

        // Diffs don't take WorkChains into account for the sake of backward compatibility.
        assertFalse(ws1.diff(ws2));
        assertFalse(ws2.diff(ws1));
    }

    public void testEquals_workChains() {
        WorkSource ws1 = new WorkSource();
        ws1.add(50);
        ws1.createWorkChain().addNode(52, "foo");

        WorkSource ws2 = new WorkSource();
        ws2.add(50);
        ws2.createWorkChain().addNode(52, "foo");

        assertEquals(ws1, ws2);

        // Unequal number of WorkChains.
        ws2.createWorkChain().addNode(53, "baz");
        assertFalse(ws1.equals(ws2));

        // Different WorkChain contents.
        WorkSource ws3 = new WorkSource();
        ws3.add(50);
        ws3.createWorkChain().addNode(60, "bar");

        assertFalse(ws1.equals(ws3));
        assertFalse(ws3.equals(ws1));
    }

    public void testWorkSourceParcelling() {
        WorkSource ws = new WorkSource();

        WorkChain wc = ws.createWorkChain();
        wc.addNode(56, "foo");
        wc.addNode(75, "baz");
        WorkChain wc2 = ws.createWorkChain();
        wc2.addNode(20, "foo2");
        wc2.addNode(30, "baz2");

        Parcel p = Parcel.obtain();
        ws.writeToParcel(p, 0);
        p.setDataPosition(0);

        WorkSource unparcelled = WorkSource.CREATOR.createFromParcel(p);

        assertEquals(unparcelled, ws);
    }

    public void testSet_workChains() {
        WorkSource ws1 = new WorkSource();
        ws1.add(50);

        WorkSource ws2 = new WorkSource();
        ws2.add(60);
        WorkChain wc = ws2.createWorkChain();
        wc.addNode(75, "tag");

        ws1.set(ws2);

        // Assert that the WorkChains are copied across correctly to the new WorkSource object.
        List<WorkChain> workChains = ws1.getWorkChains();
        assertEquals(1, workChains.size());

        assertEquals(1, workChains.get(0).getSize());
        assertEquals(75, workChains.get(0).getUids()[0]);
        assertEquals("tag", workChains.get(0).getTags()[0]);

        // Also assert that a deep copy of workchains is made, so the addition of a new WorkChain
        // or the modification of an existing WorkChain has no effect.
        ws2.createWorkChain();
        assertEquals(1, ws1.getWorkChains().size());

        wc.addNode(50, "tag2");
        assertEquals(1, ws1.getWorkChains().size());
        assertEquals(1, ws1.getWorkChains().get(0).getSize());
    }

    public void testSet_nullWorkChain() {
        WorkSource ws = new WorkSource();
        ws.add(60);
        WorkChain wc = ws.createWorkChain();
        wc.addNode(75, "tag");

        ws.set(null);
        assertEquals(0, ws.getWorkChains().size());
    }

    public void testAdd_workChains() {
        WorkSource ws = new WorkSource();
        ws.createWorkChain().addNode(70, "foo");

        WorkSource ws2 = new WorkSource();
        ws2.createWorkChain().addNode(60, "tag");

        ws.add(ws2);

        // Check that the new WorkChain is added to the end of the list.
        List<WorkChain> workChains = ws.getWorkChains();
        assertEquals(2, workChains.size());
        assertEquals(1, workChains.get(1).getSize());
        assertEquals(60, ws.getWorkChains().get(1).getUids()[0]);
        assertEquals("tag", ws.getWorkChains().get(1).getTags()[0]);

        // Adding the same WorkChain twice should be a no-op.
        ws.add(ws2);
        assertEquals(2, workChains.size());
    }
}