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

Commit 332f702f authored by Yuting's avatar Yuting
Browse files

[DeviceAwareAppOp] Fix the bug that misuses device Id in proxy and proxied attribution source

There is a bug in AppOpsService#startProxyOperationImpl() and AppOpsService#noteProxyOperationImpl() that it uses virtual device Id from proxy attribution source as the device Id in proxied attribution source. The two device Ids can be different and should be treated differently.

Bug: 337340961
Test: presubmit
Change-Id: Ie996c421fd805dea3b57061677466f33cbe226d8
parent 1fc80a64
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -472,6 +472,20 @@ public final class AttributionSource implements Parcelable {
        return null;
    }

    /**
     * @return The next package's device Id from its context.
     * This device ID is used for permissions checking during attribution source validation.
     *
     * @hide
     */
    public int getNextDeviceId() {
        if (mAttributionSourceState.next != null
                && mAttributionSourceState.next.length > 0) {
            return mAttributionSourceState.next[0].deviceId;
        }
        return Context.DEVICE_ID_DEFAULT;
    }

    /**
     * Checks whether this attribution source can be trusted. That is whether
     * the app it refers to created it and provided to the attribution chain.
+53 −33
Original line number Diff line number Diff line
@@ -2909,10 +2909,12 @@ public class AppOpsService extends IAppOpsService.Stub {
        final int proxyUid = attributionSource.getUid();
        final String proxyPackageName = attributionSource.getPackageName();
        final String proxyAttributionTag = attributionSource.getAttributionTag();
        final int proxiedUid = attributionSource.getNextUid();
        final int proxyVirtualDeviceId = attributionSource.getDeviceId();

        final int proxiedUid = attributionSource.getNextUid();
        final String proxiedPackageName = attributionSource.getNextPackageName();
        final String proxiedAttributionTag = attributionSource.getNextAttributionTag();
        final int proxiedVirtualDeviceId = attributionSource.getNextDeviceId();

        verifyIncomingProxyUid(attributionSource);
        verifyIncomingOp(code);
@@ -2949,7 +2951,8 @@ public class AppOpsService extends IAppOpsService.Stub {

            final SyncNotedAppOp proxyReturn = noteOperationUnchecked(code, proxyUid,
                    resolveProxyPackageName, proxyAttributionTag, proxyVirtualDeviceId,
                    Process.INVALID_UID, null, null, proxyFlags, !isProxyTrusted,
                    Process.INVALID_UID, null, null,
                    Context.DEVICE_ID_DEFAULT, proxyFlags, !isProxyTrusted,
                    "proxy " + message, shouldCollectMessage);
            if (proxyReturn.getOpMode() != AppOpsManager.MODE_ALLOWED) {
                return new SyncNotedAppOp(proxyReturn.getOpMode(), code, proxiedAttributionTag,
@@ -2967,9 +2970,9 @@ public class AppOpsService extends IAppOpsService.Stub {
        final int proxiedFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXIED
                : AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED;
        return noteOperationUnchecked(code, proxiedUid, resolveProxiedPackageName,
                proxiedAttributionTag, proxyVirtualDeviceId, proxyUid, resolveProxyPackageName,
                proxyAttributionTag, proxiedFlags, shouldCollectAsyncNotedOp, message,
                shouldCollectMessage);
                proxiedAttributionTag, proxiedVirtualDeviceId, proxyUid, resolveProxyPackageName,
                proxyAttributionTag, proxyVirtualDeviceId, proxiedFlags, shouldCollectAsyncNotedOp,
                message, shouldCollectMessage);
    }

    @Override
@@ -3020,14 +3023,14 @@ public class AppOpsService extends IAppOpsService.Stub {
        }
        return noteOperationUnchecked(code, uid, resolvedPackageName, attributionTag,
                virtualDeviceId, Process.INVALID_UID, null, null,
                AppOpsManager.OP_FLAG_SELF, shouldCollectAsyncNotedOp, message,
                shouldCollectMessage);
                Context.DEVICE_ID_DEFAULT, AppOpsManager.OP_FLAG_SELF, shouldCollectAsyncNotedOp,
                message, shouldCollectMessage);
    }

    private SyncNotedAppOp noteOperationUnchecked(int code, int uid, @NonNull String packageName,
            @Nullable String attributionTag, int virtualDeviceId, int proxyUid,
            String proxyPackageName, @Nullable String proxyAttributionTag, @OpFlags int flags,
            boolean shouldCollectAsyncNotedOp, @Nullable String message,
            String proxyPackageName, @Nullable String proxyAttributionTag, int proxyVirtualDeviceId,
            @OpFlags int flags, boolean shouldCollectAsyncNotedOp, @Nullable String message,
            boolean shouldCollectMessage) {
        PackageVerificationResult pvr;
        try {
@@ -3158,8 +3161,9 @@ public class AppOpsService extends IAppOpsService.Stub {
            }
            scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag,
                    virtualDeviceId, flags, AppOpsManager.MODE_ALLOWED);

            attributedOp.accessed(proxyUid, proxyPackageName, proxyAttributionTag,
                    uidState.getState(), flags);
                    getPersistentId(proxyVirtualDeviceId), uidState.getState(), flags);

            if (shouldCollectAsyncNotedOp) {
                collectAsyncNotedOp(uid, packageName, code, attributionTag, flags, message,
@@ -3525,9 +3529,9 @@ public class AppOpsService extends IAppOpsService.Stub {
        }

        return startOperationUnchecked(clientId, code, uid, packageName, attributionTag,
                virtualDeviceId, Process.INVALID_UID, null, null, OP_FLAG_SELF,
                startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage,
                attributionFlags, attributionChainId);
                virtualDeviceId, Process.INVALID_UID, null, null, Context.DEVICE_ID_DEFAULT,
                OP_FLAG_SELF, startIfModeDefault, shouldCollectAsyncNotedOp, message,
                shouldCollectMessage, attributionFlags, attributionChainId);
    }

    /** @deprecated Use {@link #startProxyOperationWithState} instead. */
@@ -3565,18 +3569,32 @@ public class AppOpsService extends IAppOpsService.Stub {
        final int proxyUid = attributionSource.getUid();
        final String proxyPackageName = attributionSource.getPackageName();
        final String proxyAttributionTag = attributionSource.getAttributionTag();
        final int proxiedUid = attributionSource.getNextUid();
        final int proxyVirtualDeviceId = attributionSource.getDeviceId();

        final int proxiedUid = attributionSource.getNextUid();
        final String proxiedPackageName = attributionSource.getNextPackageName();
        final String proxiedAttributionTag = attributionSource.getNextAttributionTag();
        final int proxiedVirtualDeviceId = attributionSource.getNextDeviceId();

        verifyIncomingProxyUid(attributionSource);
        verifyIncomingOp(code);
        if (!isValidVirtualDeviceId(proxyVirtualDeviceId)) {
            Slog.w(TAG, "startProxyOperationImpl returned MODE_IGNORED as virtualDeviceId "
                    + proxyVirtualDeviceId + " is invalid");
            return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, proxiedAttributionTag,
                    proxiedPackageName);
            Slog.w(
                    TAG,
                    "startProxyOperationImpl returned MODE_IGNORED as proxyVirtualDeviceId "
                            + proxyVirtualDeviceId
                            + " is invalid");
            return new SyncNotedAppOp(
                    AppOpsManager.MODE_IGNORED, code, proxiedAttributionTag, proxiedPackageName);
        }
        if (!isValidVirtualDeviceId(proxiedVirtualDeviceId)) {
            Slog.w(
                    TAG,
                    "startProxyOperationImpl returned MODE_IGNORED as proxiedVirtualDeviceId "
                            + proxiedVirtualDeviceId
                            + " is invalid");
            return new SyncNotedAppOp(
                    AppOpsManager.MODE_IGNORED, code, proxiedAttributionTag, proxiedPackageName);
        }
        if (!isIncomingPackageValid(proxyPackageName, UserHandle.getUserId(proxyUid))
                || !isIncomingPackageValid(proxiedPackageName, UserHandle.getUserId(proxiedUid))) {
@@ -3618,7 +3636,7 @@ public class AppOpsService extends IAppOpsService.Stub {
            // Test if the proxied operation will succeed before starting the proxy operation
            final SyncNotedAppOp testProxiedOp = startOperationDryRun(code,
                    proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag,
                    proxyVirtualDeviceId, resolvedProxyPackageName, proxiedFlags,
                    proxiedVirtualDeviceId, resolvedProxyPackageName, proxiedFlags,
                    startIfModeDefault);

            if (!shouldStartForMode(testProxiedOp.getOpMode(), startIfModeDefault)) {
@@ -3630,7 +3648,7 @@ public class AppOpsService extends IAppOpsService.Stub {

            final SyncNotedAppOp proxyAppOp = startOperationUnchecked(clientId, code, proxyUid,
                    resolvedProxyPackageName, proxyAttributionTag, proxyVirtualDeviceId,
                    Process.INVALID_UID, null, null, proxyFlags,
                    Process.INVALID_UID, null, null, Context.DEVICE_ID_DEFAULT, proxyFlags,
                    startIfModeDefault, !isProxyTrusted, "proxy " + message,
                    shouldCollectMessage, proxyAttributionFlags, attributionChainId);
            if (!shouldStartForMode(proxyAppOp.getOpMode(), startIfModeDefault)) {
@@ -3639,9 +3657,10 @@ public class AppOpsService extends IAppOpsService.Stub {
        }

        return startOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName,
                proxiedAttributionTag, proxyVirtualDeviceId, proxyUid, resolvedProxyPackageName,
                proxyAttributionTag, proxiedFlags, startIfModeDefault, shouldCollectAsyncNotedOp,
                message, shouldCollectMessage, proxiedAttributionFlags, attributionChainId);
                proxiedAttributionTag, proxiedVirtualDeviceId, proxyUid, resolvedProxyPackageName,
                proxyAttributionTag, proxyVirtualDeviceId, proxiedFlags, startIfModeDefault,
                shouldCollectAsyncNotedOp, message, shouldCollectMessage, proxiedAttributionFlags,
                attributionChainId);
    }

    private boolean shouldStartForMode(int mode, boolean startIfModeDefault) {
@@ -3651,9 +3670,10 @@ public class AppOpsService extends IAppOpsService.Stub {
    private SyncNotedAppOp startOperationUnchecked(IBinder clientId, int code, int uid,
            @NonNull String packageName, @Nullable String attributionTag, int virtualDeviceId,
            int proxyUid, String proxyPackageName, @Nullable String proxyAttributionTag,
            @OpFlags int flags, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp,
            @Nullable String message, boolean shouldCollectMessage,
            @AttributionFlags int attributionFlags, int attributionChainId) {
            int proxyVirtualDeviceId, @OpFlags int flags, boolean startIfModeDefault,
            boolean shouldCollectAsyncNotedOp, @Nullable String message,
            boolean shouldCollectMessage, @AttributionFlags int attributionFlags,
            int attributionChainId) {
        PackageVerificationResult pvr;
        try {
            pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName);
@@ -3748,13 +3768,13 @@ public class AppOpsService extends IAppOpsService.Stub {
                    + " flags: " + AppOpsManager.flagsToString(flags));
            try {
                if (isRestricted) {
                    attributedOp.createPaused(clientId, proxyUid, proxyPackageName,
                            proxyAttributionTag, virtualDeviceId, uidState.getState(), flags,
                            attributionFlags, attributionChainId);
                    attributedOp.createPaused(clientId, virtualDeviceId, proxyUid, proxyPackageName,
                            proxyAttributionTag, getPersistentId(proxyVirtualDeviceId),
                            uidState.getState(), flags, attributionFlags, attributionChainId);
                } else {
                    attributedOp.started(clientId, proxyUid, proxyPackageName,
                            proxyAttributionTag, virtualDeviceId, uidState.getState(), flags,
                            attributionFlags, attributionChainId);
                    attributedOp.started(clientId, virtualDeviceId, proxyUid, proxyPackageName,
                            proxyAttributionTag, getPersistentId(proxyVirtualDeviceId),
                            uidState.getState(), flags, attributionFlags, attributionChainId);
                    startType = START_TYPE_STARTED;
                }
            } catch (RemoteException e) {
@@ -4943,7 +4963,7 @@ public class AppOpsService extends IAppOpsService.Stub {

        if (accessTime > 0) {
            attributedOp.accessed(accessTime, accessDuration, proxyUid, proxyPkg,
                    proxyAttributionTag, uidState, opFlags);
                    proxyAttributionTag, PERSISTENT_DEVICE_ID_DEFAULT, uidState, opFlags);
        }
        if (rejectTime > 0) {
            attributedOp.rejected(rejectTime, uidState, opFlags);
+49 −45
Original line number Diff line number Diff line
@@ -24,7 +24,6 @@ import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.companion.virtual.VirtualDeviceManager;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
@@ -95,16 +94,17 @@ final class AttributedOp {
     *
     * @param proxyUid            The uid of the proxy
     * @param proxyPackageName    The package name of the proxy
     * @param proxyAttributionTag the attributionTag in the proxies package
     * @param proxyAttributionTag The attributionTag in the proxies package
     * @param proxyDeviceId       The device Id of the proxy
     * @param uidState            UID state of the app noteOp/startOp was called for
     * @param flags               OpFlags of the call
     */
    public void accessed(int proxyUid, @Nullable String proxyPackageName,
            @Nullable String proxyAttributionTag, @AppOpsManager.UidState int uidState,
            @AppOpsManager.OpFlags int flags) {
            @Nullable String proxyAttributionTag, @Nullable String proxyDeviceId,
            @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags) {
        long accessTime = System.currentTimeMillis();
        accessed(accessTime, -1, proxyUid, proxyPackageName,
                proxyAttributionTag, uidState, flags);
        accessed(accessTime, -1, proxyUid, proxyPackageName, proxyAttributionTag, proxyDeviceId,
                uidState, flags);

        mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
                parent.packageName, tag, uidState, flags, accessTime,
@@ -118,14 +118,16 @@ final class AttributedOp {
     * @param duration            The duration of the event
     * @param proxyUid            The uid of the proxy
     * @param proxyPackageName    The package name of the proxy
     * @param proxyAttributionTag the attributionTag in the proxies package
     * @param proxyAttributionTag The attributionTag in the proxies package
     * @param proxyDeviceId       The device Id of the proxy
     * @param uidState            UID state of the app noteOp/startOp was called for
     * @param flags               OpFlags of the call
     */
    @SuppressWarnings("GuardedBy") // Lock is held on mAppOpsService
    public void accessed(long noteTime, long duration, int proxyUid,
            @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
            @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags) {
            @Nullable String proxyDeviceId, @AppOpsManager.UidState int uidState,
            @AppOpsManager.OpFlags int flags) {
        long key = makeKey(uidState, flags);

        if (mAccessEvents == null) {
@@ -135,7 +137,7 @@ final class AttributedOp {
        AppOpsManager.OpEventProxyInfo proxyInfo = null;
        if (proxyUid != Process.INVALID_UID) {
            proxyInfo = mAppOpsService.mOpEventProxyInfoPool.acquire(proxyUid, proxyPackageName,
                            proxyAttributionTag, VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
                    proxyAttributionTag, proxyDeviceId);
        }

        AppOpsManager.NoteOpEvent existingEvent = mAccessEvents.get(key);
@@ -189,35 +191,36 @@ final class AttributedOp {
     * Update state when start was called
     *
     * @param clientId            Id of the startOp caller
     * @param virtualDeviceId     The virtual device id of the startOp caller
     * @param proxyUid            The UID of the proxy app
     * @param proxyPackageName    The package name of the proxy app
     * @param proxyAttributionTag The attribution tag of the proxy app
     * @param proxyDeviceId       The device id of the proxy app
     * @param uidState            UID state of the app startOp is called for
     * @param flags               The proxy flags
     * @param attributionFlags    The attribution flags associated with this operation.
     * @param attributionChainId  The if of the attribution chain this operations is a part of.
     * @param attributionChainId  The if of the attribution chain this operations is a part of
     */
    public void started(@NonNull IBinder clientId, int proxyUid,
    public void started(@NonNull IBinder clientId, int virtualDeviceId, int proxyUid,
            @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
            int proxyVirtualDeviceId, @AppOpsManager.UidState int uidState,
            @Nullable String proxyDeviceId, @AppOpsManager.UidState int uidState,
            @AppOpsManager.OpFlags int flags, @AppOpsManager.AttributionFlags int attributionFlags,
            int attributionChainId) throws RemoteException {
        startedOrPaused(clientId, proxyUid, proxyPackageName,
                proxyAttributionTag, proxyVirtualDeviceId, uidState, flags,
                /* triggeredByUidStateChange */ false, /* isStarted */ true, attributionFlags,
                attributionChainId);
        startedOrPaused(clientId, virtualDeviceId, proxyUid, proxyPackageName, proxyAttributionTag,
                proxyDeviceId, uidState, flags, attributionFlags, attributionChainId, false,
                true);
    }

    @SuppressWarnings("GuardedBy") // Lock is held on mAppOpsService
    private void startedOrPaused(@NonNull IBinder clientId, int proxyUid,
    private void startedOrPaused(@NonNull IBinder clientId, int virtualDeviceId, int proxyUid,
            @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
            int proxyVirtualDeviceId, @AppOpsManager.UidState int uidState,
            @AppOpsManager.OpFlags int flags, boolean triggeredByUidStateChange,
            boolean isStarted, @AppOpsManager.AttributionFlags int attributionFlags,
            int attributionChainId) throws RemoteException {
            @Nullable String proxyDeviceId, @AppOpsManager.UidState int uidState,
            @AppOpsManager.OpFlags int flags, @AppOpsManager.AttributionFlags int attributionFlags,
            int attributionChainId, boolean triggeredByUidStateChange, boolean isStarted)
            throws RemoteException {
        if (!triggeredByUidStateChange && !parent.isRunning() && isStarted) {
            mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid,
                    parent.packageName, tag, proxyVirtualDeviceId, true, attributionFlags,
                    parent.packageName, tag, virtualDeviceId, true, attributionFlags,
                    attributionChainId);
        }

@@ -233,9 +236,9 @@ final class AttributedOp {
        InProgressStartOpEvent event = events.get(clientId);
        if (event == null) {
            event = mAppOpsService.mInProgressStartOpEventPool.acquire(startTime,
                    SystemClock.elapsedRealtime(), clientId, tag, proxyVirtualDeviceId,
                    SystemClock.elapsedRealtime(), clientId, tag, virtualDeviceId,
                    PooledLambda.obtainRunnable(AppOpsService::onClientDeath, this, clientId),
                    proxyUid, proxyPackageName, proxyAttributionTag, uidState, flags,
                    proxyUid, proxyPackageName, proxyAttributionTag, proxyDeviceId, uidState, flags,
                    attributionFlags, attributionChainId);
            events.put(clientId, event);
        } else {
@@ -366,15 +369,14 @@ final class AttributedOp {
    /**
     * Create an event that will be started, if the op is unpaused.
     */
    public void createPaused(@NonNull IBinder clientId, int proxyUid,
            @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
            int proxyVirtualDeviceId, @AppOpsManager.UidState int uidState,
            @AppOpsManager.OpFlags int flags,
            @AppOpsManager.AttributionFlags int attributionFlags,
    public void createPaused(@NonNull IBinder clientId, int virtualDeviceId,
            int proxyUid, @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
            @Nullable String proxyDeviceId, @AppOpsManager.UidState int uidState,
            @AppOpsManager.OpFlags int flags, @AppOpsManager.AttributionFlags int attributionFlags,
            int attributionChainId) throws RemoteException {
        startedOrPaused(clientId, proxyUid, proxyPackageName, proxyAttributionTag,
                proxyVirtualDeviceId, uidState, flags, false, false,
                attributionFlags, attributionChainId);
        startedOrPaused(clientId, virtualDeviceId, proxyUid, proxyPackageName, proxyAttributionTag,
                proxyDeviceId, uidState, flags, attributionFlags, attributionChainId, false,
                false);
    }

    /**
@@ -496,16 +498,16 @@ final class AttributedOp {
                    // Call started() to add a new start event object and then add the
                    // previously removed unfinished start counts back
                    if (proxy != null) {
                        startedOrPaused(event.getClientId(), proxy.getUid(),
                                proxy.getPackageName(), proxy.getAttributionTag(),
                                event.getVirtualDeviceId(), newState, event.getFlags(),
                                true, isRunning,
                                event.getAttributionFlags(), event.getAttributionChainId());
                        startedOrPaused(event.getClientId(), event.getVirtualDeviceId(),
                                proxy.getUid(), proxy.getPackageName(), proxy.getAttributionTag(),
                                proxy.getDeviceId(), newState, event.getFlags(),
                                event.getAttributionFlags(), event.getAttributionChainId(), true,
                                isRunning);
                    } else {
                        startedOrPaused(event.getClientId(), Process.INVALID_UID, null, null,
                                event.getVirtualDeviceId(), newState, event.getFlags(), true,
                                isRunning, event.getAttributionFlags(),
                                event.getAttributionChainId());
                        startedOrPaused(event.getClientId(), event.getVirtualDeviceId(),
                                Process.INVALID_UID, null, null, null,
                                newState, event.getFlags(), event.getAttributionFlags(),
                                event.getAttributionChainId(), true, isRunning);
                    }

                    events = isRunning ? mInProgressEvents : mPausedInProgressEvents;
@@ -847,7 +849,8 @@ final class AttributedOp {
        InProgressStartOpEvent acquire(long startTime, long elapsedTime, @NonNull IBinder clientId,
                @Nullable String attributionTag, int virtualDeviceId,  @NonNull Runnable onDeath,
                int proxyUid, @Nullable String proxyPackageName,
                @Nullable String proxyAttributionTag, @AppOpsManager.UidState int uidState,
                @Nullable String proxyAttributionTag, @Nullable String proxyDeviceId,
                @AppOpsManager.UidState int uidState,
                @AppOpsManager.OpFlags int flags, @AppOpsManager.AttributionFlags
                int attributionFlags, int attributionChainId) throws RemoteException {

@@ -856,7 +859,7 @@ final class AttributedOp {
            AppOpsManager.OpEventProxyInfo proxyInfo = null;
            if (proxyUid != Process.INVALID_UID) {
                proxyInfo = mOpEventProxyInfoPool.acquire(proxyUid, proxyPackageName,
                        proxyAttributionTag, VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
                        proxyAttributionTag, proxyDeviceId);
            }

            if (recycled != null) {
@@ -880,7 +883,8 @@ final class AttributedOp {
            super(maxUnusedPooledObjects);
        }

        AppOpsManager.OpEventProxyInfo acquire(@IntRange(from = 0) int uid,
        AppOpsManager.OpEventProxyInfo acquire(
                @IntRange(from = 0) int uid,
                @Nullable String packageName,
                @Nullable String attributionTag,
                @Nullable String deviceId) {
@@ -890,7 +894,7 @@ final class AttributedOp {
                return recycled;
            }

            return new AppOpsManager.OpEventProxyInfo(uid, packageName, attributionTag);
            return new AppOpsManager.OpEventProxyInfo(uid, packageName, attributionTag, deviceId);
        }
    }
}
+149 −0

File added.

Preview size limit exceeded, changes collapsed.