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

Commit 7ebbcf16 authored by Nate Myren's avatar Nate Myren Committed by Automerger Merge Worker
Browse files

Merge "Add attribution info to start callbacks" into sc-dev am: 5d97f292

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/15446590

Change-Id: If5ddeb244befcaea57a3e53f741b94ec5bfd6dd0
parents f69b24a5 5d97f292
Loading
Loading
Loading
Loading
+57 −3
Original line number Original line Diff line number Diff line
@@ -7205,6 +7205,34 @@ public class AppOpsManager {
     * @hide
     * @hide
     */
     */
    public interface OnOpStartedListener {
    public interface OnOpStartedListener {

        /**
         * Represents a start operation that was unsuccessful
         * @hide
         */
        public int START_TYPE_FAILED = 0;

        /**
         * Represents a successful start operation
         * @hide
         */
        public int START_TYPE_STARTED = 1;

        /**
         * Represents an operation where a restricted operation became unrestricted, and resumed.
         * @hide
         */
        public int START_TYPE_RESUMED = 2;

        /** @hide */
        @Retention(RetentionPolicy.SOURCE)
        @IntDef(flag = true, prefix = { "TYPE_" }, value = {
            START_TYPE_FAILED,
            START_TYPE_STARTED,
            START_TYPE_RESUMED
        })
        public @interface StartedType {}

        /**
        /**
         * Called when an op was started.
         * Called when an op was started.
         *
         *
@@ -7213,11 +7241,35 @@ public class AppOpsManager {
         * @param uid The UID performing the operation.
         * @param uid The UID performing the operation.
         * @param packageName The package performing the operation.
         * @param packageName The package performing the operation.
         * @param attributionTag The attribution tag performing the operation.
         * @param attributionTag The attribution tag performing the operation.
         * @param flags The flags of this op
         * @param flags The flags of this op.
         * @param result The result of the start.
         * @param result The result of the start.
         */
         */
        void onOpStarted(int op, int uid, String packageName, String attributionTag,
        void onOpStarted(int op, int uid, String packageName, String attributionTag,
                @OpFlags int flags, @Mode int result);
                @OpFlags int flags, @Mode int result);

        /**
         * Called when an op was started.
         *
         * Note: This is only for op starts. It is not called when an op is noted or stopped.
         * By default, unless this method is overridden, no code will be executed for resume
         * events.
         * @param op The op code.
         * @param uid The UID performing the operation.
         * @param packageName The package performing the operation.
         * @param attributionTag The attribution tag performing the operation.
         * @param flags The flags of this op.
         * @param result The result of the start.
         * @param startType The start type of this start event. Either failed, resumed, or started.
         * @param attributionFlags The location of this started op in an attribution chain.
         * @param attributionChainId The ID of the attribution chain of this op, if it is in one.
         */
        default void onOpStarted(int op, int uid, String packageName, String attributionTag,
                @OpFlags int flags, @Mode int result, @StartedType int startType,
                @AttributionFlags int attributionFlags, int attributionChainId) {
            if (startType != START_TYPE_RESUMED) {
                onOpStarted(op, uid, packageName, attributionTag, flags, result);
            }
        }
    }
    }


    AppOpsManager(Context context, IAppOpsService service) {
    AppOpsManager(Context context, IAppOpsService service) {
@@ -7858,8 +7910,10 @@ public class AppOpsManager {
             cb = new IAppOpsStartedCallback.Stub() {
             cb = new IAppOpsStartedCallback.Stub() {
                 @Override
                 @Override
                 public void opStarted(int op, int uid, String packageName, String attributionTag,
                 public void opStarted(int op, int uid, String packageName, String attributionTag,
                         int flags, int mode) {
                         int flags, int mode, int startType, int attributionFlags,
                     callback.onOpStarted(op, uid, packageName, attributionTag, flags, mode);
                         int attributionChainId) {
                     callback.onOpStarted(op, uid, packageName, attributionTag, flags, mode,
                             startType, attributionFlags, attributionChainId);
                 }
                 }
             };
             };
             mStartedWatchers.put(callback, cb);
             mStartedWatchers.put(callback, cb);
+72 −12
Original line number Original line Diff line number Diff line
@@ -31,7 +31,9 @@ import static android.app.AppOpsManager.OPSTR_FINE_LOCATION;
import static android.app.AppOpsManager.OPSTR_PHONE_CALL_CAMERA;
import static android.app.AppOpsManager.OPSTR_PHONE_CALL_CAMERA;
import static android.app.AppOpsManager.OPSTR_PHONE_CALL_MICROPHONE;
import static android.app.AppOpsManager.OPSTR_PHONE_CALL_MICROPHONE;
import static android.app.AppOpsManager.OPSTR_RECORD_AUDIO;
import static android.app.AppOpsManager.OPSTR_RECORD_AUDIO;
import static android.app.AppOpsManager.OP_CAMERA;
import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED;
import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED;
import static android.app.AppOpsManager.OP_RECORD_AUDIO;
import static android.media.AudioSystem.MODE_IN_COMMUNICATION;
import static android.media.AudioSystem.MODE_IN_COMMUNICATION;
import static android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
import static android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;


@@ -63,7 +65,8 @@ import java.util.Objects;
 *
 *
 * @hide
 * @hide
 */
 */
public class PermissionUsageHelper implements AppOpsManager.OnOpActiveChangedListener {
public class PermissionUsageHelper implements AppOpsManager.OnOpActiveChangedListener,
        AppOpsManager.OnOpStartedListener {


    /** Whether to show the mic and camera icons.  */
    /** Whether to show the mic and camera icons.  */
    private static final String PROPERTY_CAMERA_MIC_ICONS_ENABLED = "camera_mic_icons_enabled";
    private static final String PROPERTY_CAMERA_MIC_ICONS_ENABLED = "camera_mic_icons_enabled";
@@ -160,9 +163,10 @@ public class PermissionUsageHelper implements AppOpsManager.OnOpActiveChangedLis
        mUserContexts = new ArrayMap<>();
        mUserContexts = new ArrayMap<>();
        mUserContexts.put(Process.myUserHandle(), mContext);
        mUserContexts.put(Process.myUserHandle(), mContext);
        // TODO ntmyren: make this listen for flag enable/disable changes
        // TODO ntmyren: make this listen for flag enable/disable changes
        String[] ops = { OPSTR_CAMERA, OPSTR_RECORD_AUDIO };
        String[] opStrs = { OPSTR_CAMERA, OPSTR_RECORD_AUDIO };
        mContext.getSystemService(AppOpsManager.class).startWatchingActive(ops,
        mAppOpsManager.startWatchingActive(opStrs, context.getMainExecutor(), this);
                context.getMainExecutor(), this);
        int[] ops = { OP_CAMERA, OP_RECORD_AUDIO };
        mAppOpsManager.startWatchingStarted(ops, this);
    }
    }


    private Context getUserContext(UserHandle user) {
    private Context getUserContext(UserHandle user) {
@@ -182,25 +186,65 @@ public class PermissionUsageHelper implements AppOpsManager.OnOpActiveChangedLis
    public void onOpActiveChanged(@NonNull String op, int uid, @NonNull String packageName,
    public void onOpActiveChanged(@NonNull String op, int uid, @NonNull String packageName,
            @Nullable String attributionTag, boolean active, @AttributionFlags int attributionFlags,
            @Nullable String attributionTag, boolean active, @AttributionFlags int attributionFlags,
            int attributionChainId) {
            int attributionChainId) {
        if (attributionChainId == ATTRIBUTION_CHAIN_ID_NONE
        if (active) {
                || attributionFlags == ATTRIBUTION_FLAGS_NONE
            // Started callback handles these
                || (attributionFlags & ATTRIBUTION_FLAG_TRUSTED) == 0) {
            // If this is not a chain, or it is untrusted, return
            return;
            return;
        }
        }


        if (!active) {
        // if any link in the chain is finished, remove the chain. Then, find any other chains that
            // if any link in the chain is finished, remove the chain.
        // contain this op/package/uid/tag combination, and remove them, as well.
        // TODO ntmyren: be smarter about this
        // TODO ntmyren: be smarter about this
        mAttributionChains.remove(attributionChainId);
        mAttributionChains.remove(attributionChainId);
        int numChains = mAttributionChains.size();
        ArrayList<Integer> toRemove = new ArrayList<>();
        for (int i = 0; i < numChains; i++) {
            int chainId = mAttributionChains.keyAt(i);
            ArrayList<AccessChainLink> chain = mAttributionChains.valueAt(i);
            int chainSize = chain.size();
            for (int j = 0; j < chainSize; j++) {
                AccessChainLink link = chain.get(j);
                if (link.packageAndOpEquals(op, packageName, attributionTag, uid)) {
                    toRemove.add(chainId);
                    break;
                }
            }
        }
        mAttributionChains.removeAll(toRemove);
    }

    @Override
    public void onOpStarted(int op, int uid, String packageName, String attributionTag,
                @AppOpsManager.OpFlags int flags, @AppOpsManager.Mode int result) {
       // not part of an attribution chain. Do nothing
    }

    @Override
    public void onOpStarted(int op, int uid, String packageName, String attributionTag,
            @AppOpsManager.OpFlags int flags, @AppOpsManager.Mode int result,
            @StartedType int startedType, @AttributionFlags int attributionFlags,
            int attributionChainId) {
        if (startedType == START_TYPE_FAILED || attributionChainId == ATTRIBUTION_CHAIN_ID_NONE
                || attributionFlags == ATTRIBUTION_FLAGS_NONE
                || (attributionFlags & ATTRIBUTION_FLAG_TRUSTED) == 0) {
            // If this is not a successful start, or it is not a chain, or it is untrusted, return
            return;
            return;
        }
        }
        addLinkToChainIfNotPresent(AppOpsManager.opToPublicName(op), packageName, uid,
                attributionTag, attributionFlags, attributionChainId);
    }

    private void addLinkToChainIfNotPresent(String op, String packageName, int uid,
            String attributionTag, int attributionFlags, int attributionChainId) {


        ArrayList<AccessChainLink> currentChain = mAttributionChains.computeIfAbsent(
        ArrayList<AccessChainLink> currentChain = mAttributionChains.computeIfAbsent(
                attributionChainId, k -> new ArrayList<>());
                attributionChainId, k -> new ArrayList<>());
        AccessChainLink link = new AccessChainLink(op, packageName, attributionTag, uid,
        AccessChainLink link = new AccessChainLink(op, packageName, attributionTag, uid,
                attributionFlags);
                attributionFlags);


        if (currentChain.contains(link)) {
            return;
        }

        int currSize = currentChain.size();
        int currSize = currentChain.size();
        if (currSize == 0 || link.isEnd() || !currentChain.get(currSize - 1).isEnd()) {
        if (currSize == 0 || link.isEnd() || !currentChain.get(currSize - 1).isEnd()) {
            // if the list is empty, this link is the end, or the last link in the current chain
            // if the list is empty, this link is the end, or the last link in the current chain
@@ -613,5 +657,21 @@ public class PermissionUsageHelper implements AppOpsManager.OnOpActiveChangedLis
        public boolean isStart() {
        public boolean isStart() {
            return (flags & ATTRIBUTION_FLAG_RECEIVER) != 0;
            return (flags & ATTRIBUTION_FLAG_RECEIVER) != 0;
        }
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof AccessChainLink)) {
                return false;
            }
            AccessChainLink other = (AccessChainLink) obj;
            return other.flags == flags && packageAndOpEquals(other.usage.op,
                    other.usage.packageName, other.usage.attributionTag, other.usage.uid);
        }

        public boolean packageAndOpEquals(String op, String packageName, String attributionTag,
                int uid) {
            return Objects.equals(op, usage.op) && Objects.equals(packageName, usage.packageName)
                    && Objects.equals(attributionTag, usage.attributionTag) && uid == usage.uid;
        }
    }
    }
}
}
+2 −1
Original line number Original line Diff line number Diff line
@@ -18,5 +18,6 @@ package com.android.internal.app;


// Iterface to observe op starts
// Iterface to observe op starts
oneway interface IAppOpsStartedCallback {
oneway interface IAppOpsStartedCallback {
    void opStarted(int op, int uid, String packageName, String attributionTag, int flags, int mode);
    void opStarted(int op, int uid, String packageName, String attributionTag, int flags, int mode,
    int startedType, int attributionFlags, int attributionChainId);
}
}
+23 −8
Original line number Original line Diff line number Diff line
@@ -45,6 +45,9 @@ import static android.app.AppOpsManager.OP_NONE;
import static android.app.AppOpsManager.OP_PLAY_AUDIO;
import static android.app.AppOpsManager.OP_PLAY_AUDIO;
import static android.app.AppOpsManager.OP_RECORD_AUDIO;
import static android.app.AppOpsManager.OP_RECORD_AUDIO;
import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD;
import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD;
import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_FAILED;
import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_RESUMED;
import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_STARTED;
import static android.app.AppOpsManager.OpEventProxyInfo;
import static android.app.AppOpsManager.OpEventProxyInfo;
import static android.app.AppOpsManager.RestrictionBypass;
import static android.app.AppOpsManager.RestrictionBypass;
import static android.app.AppOpsManager.SAMPLING_STRATEGY_BOOT_TIME_SAMPLING;
import static android.app.AppOpsManager.SAMPLING_STRATEGY_BOOT_TIME_SAMPLING;
@@ -1238,6 +1241,11 @@ public class AppOpsService extends IAppOpsService.Stub {
                    scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid, parent.packageName,
                    scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid, parent.packageName,
                            tag, true, event.getAttributionFlags(), event.getAttributionChainId());
                            tag, true, event.getAttributionFlags(), event.getAttributionChainId());
                }
                }
                // Note: this always sends MODE_ALLOWED, even if the mode is FOREGROUND
                // TODO ntmyren: figure out how to get the real mode.
                scheduleOpStartedIfNeededLocked(parent.op, parent.uid, parent.packageName,
                        tag, event.getFlags(), MODE_ALLOWED, START_TYPE_RESUMED,
                        event.getAttributionFlags(), event.getAttributionChainId());
            }
            }
            mPausedInProgressEvents = null;
            mPausedInProgressEvents = null;
        }
        }
@@ -3945,13 +3953,15 @@ public class AppOpsService extends IAppOpsService.Stub {
        }
        }


        boolean isRestricted = false;
        boolean isRestricted = false;
        int startType = START_TYPE_FAILED;
        synchronized (this) {
        synchronized (this) {
            final Ops ops = getOpsLocked(uid, packageName, attributionTag,
            final Ops ops = getOpsLocked(uid, packageName, attributionTag,
                    pvr.isAttributionTagValid, pvr.bypass, /* edit */ true);
                    pvr.isAttributionTagValid, pvr.bypass, /* edit */ true);
            if (ops == null) {
            if (ops == null) {
                if (!dryRun) {
                if (!dryRun) {
                    scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
                    scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
                            flags, AppOpsManager.MODE_IGNORED);
                            flags, AppOpsManager.MODE_IGNORED, startType, attributionFlags,
                            attributionChainId);
                }
                }
                if (DEBUG) Slog.d(TAG, "startOperation: no op for code " + code + " uid " + uid
                if (DEBUG) Slog.d(TAG, "startOperation: no op for code " + code + " uid " + uid
                        + " package " + packageName + " flags: "
                        + " package " + packageName + " flags: "
@@ -3977,7 +3987,7 @@ public class AppOpsService extends IAppOpsService.Stub {
                    if (!dryRun) {
                    if (!dryRun) {
                        attributedOp.rejected(uidState.state, flags);
                        attributedOp.rejected(uidState.state, flags);
                        scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
                        scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
                                flags, uidMode);
                                flags, uidMode, startType, attributionFlags, attributionChainId);
                    }
                    }
                    return new SyncNotedAppOp(uidMode, code, attributionTag, packageName);
                    return new SyncNotedAppOp(uidMode, code, attributionTag, packageName);
                }
                }
@@ -3993,7 +4003,7 @@ public class AppOpsService extends IAppOpsService.Stub {
                    if (!dryRun) {
                    if (!dryRun) {
                        attributedOp.rejected(uidState.state, flags);
                        attributedOp.rejected(uidState.state, flags);
                        scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
                        scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
                                flags, mode);
                                flags, mode, startType, attributionFlags, attributionChainId);
                    }
                    }
                    return new SyncNotedAppOp(mode, code, attributionTag, packageName);
                    return new SyncNotedAppOp(mode, code, attributionTag, packageName);
                }
                }
@@ -4011,12 +4021,14 @@ public class AppOpsService extends IAppOpsService.Stub {
                        attributedOp.started(clientId, proxyUid, proxyPackageName,
                        attributedOp.started(clientId, proxyUid, proxyPackageName,
                                proxyAttributionTag, uidState.state, flags, attributionFlags,
                                proxyAttributionTag, uidState.state, flags, attributionFlags,
                                attributionChainId);
                                attributionChainId);
                        startType = START_TYPE_STARTED;
                    }
                    }
                } catch (RemoteException e) {
                } catch (RemoteException e) {
                    throw new RuntimeException(e);
                    throw new RuntimeException(e);
                }
                }
                scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, flags,
                scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, flags,
                        isRestricted ? MODE_IGNORED : MODE_ALLOWED);
                        isRestricted ? MODE_IGNORED : MODE_ALLOWED, startType, attributionFlags,
                        attributionChainId);
            }
            }
        }
        }


@@ -4187,7 +4199,9 @@ public class AppOpsService extends IAppOpsService.Stub {
    }
    }


    private void scheduleOpStartedIfNeededLocked(int code, int uid, String pkgName,
    private void scheduleOpStartedIfNeededLocked(int code, int uid, String pkgName,
            String attributionTag, @OpFlags int flags, @Mode int result) {
            String attributionTag, @OpFlags int flags, @Mode int result,
            @AppOpsManager.OnOpStartedListener.StartedType int startedType,
            @AttributionFlags int attributionFlags, int attributionChainId) {
        ArraySet<StartedCallback> dispatchedCallbacks = null;
        ArraySet<StartedCallback> dispatchedCallbacks = null;
        final int callbackListCount = mStartedWatchers.size();
        final int callbackListCount = mStartedWatchers.size();
        for (int i = 0; i < callbackListCount; i++) {
        for (int i = 0; i < callbackListCount; i++) {
@@ -4213,12 +4227,13 @@ public class AppOpsService extends IAppOpsService.Stub {
        mHandler.sendMessage(PooledLambda.obtainMessage(
        mHandler.sendMessage(PooledLambda.obtainMessage(
                AppOpsService::notifyOpStarted,
                AppOpsService::notifyOpStarted,
                this, dispatchedCallbacks, code, uid, pkgName, attributionTag, flags,
                this, dispatchedCallbacks, code, uid, pkgName, attributionTag, flags,
                result));
                result, startedType, attributionFlags, attributionChainId));
    }
    }


    private void notifyOpStarted(ArraySet<StartedCallback> callbacks,
    private void notifyOpStarted(ArraySet<StartedCallback> callbacks,
            int code, int uid, String packageName, String attributionTag, @OpFlags int flags,
            int code, int uid, String packageName, String attributionTag, @OpFlags int flags,
            @Mode int result) {
            @Mode int result, @AppOpsManager.OnOpStartedListener.StartedType int startedType,
            @AttributionFlags int attributionFlags, int attributionChainId) {
        final long identity = Binder.clearCallingIdentity();
        final long identity = Binder.clearCallingIdentity();
        try {
        try {
            final int callbackCount = callbacks.size();
            final int callbackCount = callbacks.size();
@@ -4226,7 +4241,7 @@ public class AppOpsService extends IAppOpsService.Stub {
                final StartedCallback callback = callbacks.valueAt(i);
                final StartedCallback callback = callbacks.valueAt(i);
                try {
                try {
                    callback.mCallback.opStarted(code, uid, packageName, attributionTag, flags,
                    callback.mCallback.opStarted(code, uid, packageName, attributionTag, flags,
                            result);
                            result, startedType, attributionFlags, attributionChainId);
                } catch (RemoteException e) {
                } catch (RemoteException e) {
                    /* do nothing */
                    /* do nothing */
                }
                }
+9 −3
Original line number Original line Diff line number Diff line
@@ -64,12 +64,16 @@ public class AppOpsStartedWatcherTest {
                .times(1)).onOpStarted(eq(AppOpsManager.OP_FINE_LOCATION),
                .times(1)).onOpStarted(eq(AppOpsManager.OP_FINE_LOCATION),
                eq(Process.myUid()), eq(getContext().getPackageName()),
                eq(Process.myUid()), eq(getContext().getPackageName()),
                eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
                eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
                eq(AppOpsManager.MODE_ALLOWED));
                eq(AppOpsManager.MODE_ALLOWED), eq(OnOpStartedListener.START_TYPE_STARTED),
                eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
                eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
        inOrder.verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
        inOrder.verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
                .times(1)).onOpStarted(eq(AppOpsManager.OP_CAMERA),
                .times(1)).onOpStarted(eq(AppOpsManager.OP_CAMERA),
                eq(Process.myUid()), eq(getContext().getPackageName()),
                eq(Process.myUid()), eq(getContext().getPackageName()),
                eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
                eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
                eq(AppOpsManager.MODE_ALLOWED));
                eq(AppOpsManager.MODE_ALLOWED), eq(OnOpStartedListener.START_TYPE_STARTED),
                eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
                eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));


        // Stop watching
        // Stop watching
        appOpsManager.stopWatchingStarted(listener);
        appOpsManager.stopWatchingStarted(listener);
@@ -94,7 +98,9 @@ public class AppOpsStartedWatcherTest {
                .times(2)).onOpStarted(eq(AppOpsManager.OP_CAMERA),
                .times(2)).onOpStarted(eq(AppOpsManager.OP_CAMERA),
                eq(Process.myUid()), eq(getContext().getPackageName()),
                eq(Process.myUid()), eq(getContext().getPackageName()),
                eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
                eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
                eq(AppOpsManager.MODE_ALLOWED));
                eq(AppOpsManager.MODE_ALLOWED), eq(OnOpStartedListener.START_TYPE_STARTED),
                eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
                eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
        verifyNoMoreInteractions(listener);
        verifyNoMoreInteractions(listener);


        // Finish up
        // Finish up