From 66149737791ec5dd10f00ee3a0c83b7951ef57b2 Mon Sep 17 00:00:00 2001 From: Evan Severson Date: Mon, 7 Mar 2022 09:28:32 -0800 Subject: [PATCH] Use ambient light sensor to adjust camera privacy light brightness If there is a light sensor available at boot then we will watch for changing values and compute the integral of the values over the past specified interval. Light sensor values are transformed with a logorithm since it was observed that values increase very quickly with addition of artificial light although as a human it feels that brightness is increasing linearly (some light research tells this is called Fechner's law) Bug: 216107392 Test: CameraPrivacyLightControlllerTest Change-Id: I94742d099d6a5d074173a21e629a3044f0a10c9a --- core/res/res/values/colors.xml | 3 +- core/res/res/values/config.xml | 5 + core/res/res/values/symbols.xml | 5 +- .../CameraPrivacyLightController.java | 186 +++++++++++++++- .../CameraPrivacyLightControllerTest.java | 208 +++++++++++++++++- 5 files changed, 387 insertions(+), 20 deletions(-) diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index 5df3dde82b3ec..b515abc4000fa 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -448,5 +448,6 @@ #546E7A - #FFFFFF + #FFFFFF + #FFFFFF diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 21abd2237bfa3..eec5869d8a682 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5790,4 +5790,9 @@ + + + 3000 + + 4 diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 678dd9ffa6658..5bcef689a9c98 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4754,7 +4754,10 @@ - + + + + diff --git a/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java b/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java index 750d4004cb604..fd6ec065d421c 100644 --- a/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java +++ b/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java @@ -16,44 +16,105 @@ package com.android.server.sensorprivacy; +import static android.hardware.SensorManager.SENSOR_DELAY_NORMAL; + +import android.annotation.ColorInt; import android.app.AppOpsManager; import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; import android.hardware.lights.Light; import android.hardware.lights.LightState; import android.hardware.lights.LightsManager; import android.hardware.lights.LightsRequest; +import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.Looper; +import android.os.SystemClock; import android.permission.PermissionManager; import android.util.ArraySet; +import android.util.Pair; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.FgThread; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.concurrent.Executor; -class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener { +class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener, + SensorEventListener { + @VisibleForTesting + static final double LIGHT_VALUE_MULTIPLIER = 1 / Math.log(1.1); + + private final Handler mHandler; + private final Executor mExecutor; private final Context mContext; + private final AppOpsManager mAppOpsManager; private final LightsManager mLightsManager; + private final SensorManager mSensorManager; private final Set mActivePackages = new ArraySet<>(); private final Set mActivePhonePackages = new ArraySet<>(); - private final int mCameraPrivacyLightColor; - private final List mCameraLights = new ArrayList<>(); - private final AppOpsManager mAppOpsManager; private LightsManager.LightsSession mLightsSession = null; + @ColorInt + private final int mDayColor; + @ColorInt + private final int mNightColor; + + private final Sensor mLightSensor; + + private boolean mIsAmbientLightListenerRegistered = false; + private final long mMovingAverageIntervalMillis; + /** When average of the time integral over the past {@link #mMovingAverageIntervalMillis} + * milliseconds of the log_1.1(lux(t)) is greater than this value, use the daytime brightness + * else use nighttime brightness. */ + private final long mNightThreshold; + private final ArrayDeque> mAmbientLightValues = new ArrayDeque<>(); + /** Tracks the Riemann sum of {@link #mAmbientLightValues} to avoid O(n) operations when sum is + * needed */ + private long mAlvSum = 0; + private int mLastLightColor = 0; + /** The elapsed real time that the ALS was started watching */ + private long mElapsedTimeStartedReading; + + private final Object mDelayedUpdateToken = new Object(); + + // Can't mock static native methods, workaround for testing + private long mElapsedRealTime = -1; + CameraPrivacyLightController(Context context) { + this(context, FgThread.get().getLooper()); + } + + @VisibleForTesting + CameraPrivacyLightController(Context context, Looper looper) { mContext = context; + mHandler = new Handler(looper); + mExecutor = new HandlerExecutor(mHandler); + mAppOpsManager = mContext.getSystemService(AppOpsManager.class); mLightsManager = mContext.getSystemService(LightsManager.class); + mSensorManager = mContext.getSystemService(SensorManager.class); - mCameraPrivacyLightColor = mContext.getColor(R.color.camera_privacy_light); + mDayColor = mContext.getColor(R.color.camera_privacy_light_day); + mNightColor = mContext.getColor(R.color.camera_privacy_light_night); + mMovingAverageIntervalMillis = mContext.getResources() + .getInteger(R.integer.config_cameraPrivacyLightAlsAveragingIntervalMillis); + mNightThreshold = (long) (Math.log(mContext.getResources() + .getInteger(R.integer.config_cameraPrivacyLightAlsNightThreshold)) + * LIGHT_VALUE_MULTIPLIER); List lights = mLightsManager.getLights(); for (int i = 0; i < lights.size(); i++) { @@ -64,12 +125,60 @@ class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedLis } if (mCameraLights.isEmpty()) { + mLightSensor = null; return; } mAppOpsManager.startWatchingActive( new String[] {AppOpsManager.OPSTR_CAMERA, AppOpsManager.OPSTR_PHONE_CALL_CAMERA}, - FgThread.getExecutor(), this); + mExecutor, this); + + // It may be useful in the future to configure devices to know which lights are near which + // sensors so that we can control individual lights based on their environment. + mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); + } + + private void addElement(long time, int value) { + if (mAmbientLightValues.isEmpty()) { + // Eliminate the size == 1 edge case and assume the light value has been constant for + // the previous interval + mAmbientLightValues.add(new Pair<>(time - getCurrentIntervalMillis() - 1, value)); + } + Pair lastElement = mAmbientLightValues.peekLast(); + mAmbientLightValues.add(new Pair<>(time, value)); + + mAlvSum += (time - lastElement.first) * lastElement.second; + removeObsoleteData(time); + } + + private void removeObsoleteData(long time) { + while (mAmbientLightValues.size() > 1) { + Pair element0 = mAmbientLightValues.pollFirst(); // NOTICE: POLL + Pair element1 = mAmbientLightValues.peekFirst(); // NOTICE: PEEK + if (element1.first > time - getCurrentIntervalMillis()) { + mAmbientLightValues.addFirst(element0); + break; + } + mAlvSum -= (element1.first - element0.first) * element0.second; + } + } + + /** + * Gives the Riemann sum of {@link #mAmbientLightValues} where the part of the interval that + * stretches outside the time window is removed and the time since the last change is added in. + */ + private long getLiveAmbientLightTotal() { + if (mAmbientLightValues.isEmpty()) { + return mAlvSum; + } + long time = getElapsedRealTime(); + removeObsoleteData(time); + + Pair firstElement = mAmbientLightValues.peekFirst(); + Pair lastElement = mAmbientLightValues.peekLast(); + + return mAlvSum - Math.max(0, time - getCurrentIntervalMillis() - firstElement.first) + * firstElement.second + (time - lastElement.first) * lastElement.second; } @Override @@ -93,10 +202,16 @@ class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedLis } private void updateLightSession() { + if (Looper.myLooper() != mHandler.getLooper()) { + mHandler.post(this::updateLightSession); + return; + } + Set exemptedPackages = PermissionManager.getIndicatorExemptedPackages(mContext); boolean shouldSessionEnd = exemptedPackages.containsAll(mActivePackages) && exemptedPackages.containsAll(mActivePhonePackages); + updateSensorListener(shouldSessionEnd); if (shouldSessionEnd) { if (mLightsSession == null) { @@ -106,20 +221,73 @@ class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedLis mLightsSession.close(); mLightsSession = null; } else { - if (mLightsSession != null) { + int lightColor; + if (mLightSensor != null && getLiveAmbientLightTotal() + < getCurrentIntervalMillis() * mNightThreshold) { + lightColor = mNightColor; + } else { + lightColor = mDayColor; + } + + if (mLastLightColor == lightColor && mLightsSession != null) { return; } + mLastLightColor = lightColor; LightsRequest.Builder requestBuilder = new LightsRequest.Builder(); for (int i = 0; i < mCameraLights.size(); i++) { requestBuilder.addLight(mCameraLights.get(i), new LightState.Builder() - .setColor(mCameraPrivacyLightColor) + .setColor(lightColor) .build()); } - mLightsSession = mLightsManager.openSession(Integer.MAX_VALUE); + if (mLightsSession == null) { + mLightsSession = mLightsManager.openSession(Integer.MAX_VALUE); + } + mLightsSession.requestLights(requestBuilder.build()); } } + + private void updateSensorListener(boolean shouldSessionEnd) { + if (shouldSessionEnd && mIsAmbientLightListenerRegistered) { + mSensorManager.unregisterListener(this); + mIsAmbientLightListenerRegistered = false; + } + if (!shouldSessionEnd && !mIsAmbientLightListenerRegistered && mLightSensor != null) { + mSensorManager.registerListener(this, mLightSensor, SENSOR_DELAY_NORMAL, mHandler); + mIsAmbientLightListenerRegistered = true; + mElapsedTimeStartedReading = getElapsedRealTime(); + } + } + + private long getElapsedRealTime() { + return mElapsedRealTime == -1 ? SystemClock.elapsedRealtime() : mElapsedRealTime; + } + + @VisibleForTesting + void setElapsedRealTime(long time) { + mElapsedRealTime = time; + } + + @Override + public void onSensorChanged(SensorEvent event) { + // Using log space to represent human sensation (Fechner's Law) instead of lux + // because lux values causes bright flashes to skew the average very high. + addElement(event.timestamp, Math.max(0, + (int) (Math.log(event.values[0]) * LIGHT_VALUE_MULTIPLIER))); + updateLightSession(); + mHandler.removeCallbacksAndMessages(mDelayedUpdateToken); + mHandler.postDelayed(CameraPrivacyLightController.this::updateLightSession, + mDelayedUpdateToken, mMovingAverageIntervalMillis); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} + + private long getCurrentIntervalMillis() { + return Math.min(mMovingAverageIntervalMillis, + getElapsedRealTime() - mElapsedTimeStartedReading); + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/CameraPrivacyLightControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/CameraPrivacyLightControllerTest.java index fa3e05a6b0012..20cfd59973c36 100644 --- a/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/CameraPrivacyLightControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/CameraPrivacyLightControllerTest.java @@ -24,20 +24,33 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import android.app.AppOpsManager; import android.content.Context; +import android.content.res.Resources; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; import android.hardware.lights.Light; +import android.hardware.lights.LightState; import android.hardware.lights.LightsManager; import android.hardware.lights.LightsRequest; +import android.os.Handler; +import android.os.Looper; import android.permission.PermissionManager; import android.util.ArraySet; import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.internal.R; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -53,26 +66,43 @@ import java.util.stream.Collectors; public class CameraPrivacyLightControllerTest { + private int mDayColor = 1; + private int mNightColor = 0; + private int mCameraPrivacyLightAlsAveragingIntervalMillis = 5000; + private int mCameraPrivacyLightAlsNightThreshold = (int) getLightSensorValue(15); + private MockitoSession mMockitoSession; @Mock private Context mContext; + @Mock + private Resources mResources; + @Mock private LightsManager mLightsManager; @Mock private AppOpsManager mAppOpsManager; + @Mock + private SensorManager mSensorManager; + @Mock private LightsManager.LightsSession mLightsSession; + @Mock + private Sensor mLightSensor; + private ArgumentCaptor mAppOpsListenerCaptor = ArgumentCaptor.forClass(AppOpsManager.OnOpActiveChangedListener.class); private ArgumentCaptor mLightsRequestCaptor = ArgumentCaptor.forClass(LightsRequest.class); + private ArgumentCaptor mLightSensorListenerCaptor = + ArgumentCaptor.forClass(SensorEventListener.class); + private Set mExemptedPackages = new ArraySet<>(); private List mLights = new ArrayList<>(); @@ -86,11 +116,22 @@ public class CameraPrivacyLightControllerTest { .spyStatic(PermissionManager.class) .startMocking(); + doReturn(mDayColor).when(mContext).getColor(R.color.camera_privacy_light_day); + doReturn(mNightColor).when(mContext).getColor(R.color.camera_privacy_light_night); + + doReturn(mResources).when(mContext).getResources(); + doReturn(mCameraPrivacyLightAlsAveragingIntervalMillis).when(mResources) + .getInteger(R.integer.config_cameraPrivacyLightAlsAveragingIntervalMillis); + doReturn(mCameraPrivacyLightAlsNightThreshold).when(mResources) + .getInteger(R.integer.config_cameraPrivacyLightAlsNightThreshold); + doReturn(mLightsManager).when(mContext).getSystemService(LightsManager.class); doReturn(mAppOpsManager).when(mContext).getSystemService(AppOpsManager.class); + doReturn(mSensorManager).when(mContext).getSystemService(SensorManager.class); doReturn(mLights).when(mLightsManager).getLights(); doReturn(mLightsSession).when(mLightsManager).openSession(anyInt()); + doReturn(mLightSensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT); doReturn(mExemptedPackages) .when(() -> PermissionManager.getIndicatorExemptedPackages(any())); @@ -107,7 +148,7 @@ public class CameraPrivacyLightControllerTest { @Test public void testAppsOpsListenerNotRegisteredWithoutCameraLights() { mLights.add(getNextLight(false)); - new CameraPrivacyLightController(mContext); + createCameraPrivacyLightController(); verify(mAppOpsManager, times(0)).startWatchingActive(any(), any(), any()); } @@ -116,7 +157,7 @@ public class CameraPrivacyLightControllerTest { public void testAppsOpsListenerRegisteredWithCameraLight() { mLights.add(getNextLight(true)); - new CameraPrivacyLightController(mContext); + createCameraPrivacyLightController(); verify(mAppOpsManager, times(1)).startWatchingActive(any(), any(), any()); } @@ -128,14 +169,13 @@ public class CameraPrivacyLightControllerTest { mLights.add(getNextLight(r.nextBoolean())); } - new CameraPrivacyLightController(mContext); + createCameraPrivacyLightController(); // Verify no session has been opened at this point. verify(mLightsManager, times(0)).openSession(anyInt()); // Set camera op as active. - verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture()); - mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, 10101, "pkg1", true); + openCamera(); // Verify session has been opened exactly once verify(mLightsManager, times(1)).openSession(anyInt()); @@ -161,7 +201,7 @@ public class CameraPrivacyLightControllerTest { public void testWillOnlyOpenOnceWhenTwoPackagesStartOp() { mLights.add(getNextLight(true)); - new CameraPrivacyLightController(mContext); + createCameraPrivacyLightController(); verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture()); @@ -176,7 +216,7 @@ public class CameraPrivacyLightControllerTest { public void testWillCloseOnFinishOp() { mLights.add(getNextLight(true)); - new CameraPrivacyLightController(mContext); + createCameraPrivacyLightController(); verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture()); @@ -192,7 +232,7 @@ public class CameraPrivacyLightControllerTest { public void testWillCloseOnFinishOpForAllPackages() { mLights.add(getNextLight(true)); - new CameraPrivacyLightController(mContext); + createCameraPrivacyLightController(); int numUids = 100; List uids = new ArrayList<>(numUids); @@ -226,7 +266,7 @@ public class CameraPrivacyLightControllerTest { mLights.add(getNextLight(true)); mExemptedPackages.add("pkg1"); - new CameraPrivacyLightController(mContext); + createCameraPrivacyLightController(); verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture()); @@ -235,6 +275,147 @@ public class CameraPrivacyLightControllerTest { verify(mLightsManager, times(0)).openSession(anyInt()); } + @Test + public void testNoLightSensor() { + mLights.add(getNextLight(true)); + doReturn(null).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT); + + createCameraPrivacyLightController(); + + openCamera(); + + verify(mLightsSession).requestLights(mLightsRequestCaptor.capture()); + LightsRequest lightsRequest = mLightsRequestCaptor.getValue(); + for (LightState lightState : lightsRequest.getLightStates()) { + assertEquals(mDayColor, lightState.getColor()); + } + } + + @Test + public void testALSListenerNotRegisteredUntilCameraIsOpened() { + mLights.add(getNextLight(true)); + Sensor sensor = mock(Sensor.class); + doReturn(sensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT); + + CameraPrivacyLightController cplc = createCameraPrivacyLightController(); + + verify(mSensorManager, never()).registerListener(any(SensorEventListener.class), + any(Sensor.class), anyInt(), any(Handler.class)); + + openCamera(); + + verify(mSensorManager, times(1)).registerListener(mLightSensorListenerCaptor.capture(), + any(Sensor.class), anyInt(), any(Handler.class)); + + mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, 10001, "pkg", false); + verify(mSensorManager, times(1)).unregisterListener(mLightSensorListenerCaptor.getValue()); + } + + @Ignore + @Test + public void testDayColor() { + testBrightnessToColor(20, mDayColor); + } + + @Ignore + @Test + public void testNightColor() { + testBrightnessToColor(10, mNightColor); + } + + private void testBrightnessToColor(int brightnessValue, int color) { + mLights.add(getNextLight(true)); + Sensor sensor = mock(Sensor.class); + doReturn(sensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT); + + CameraPrivacyLightController cplc = createCameraPrivacyLightController(); + cplc.setElapsedRealTime(0); + + openCamera(); + + verify(mSensorManager).registerListener(mLightSensorListenerCaptor.capture(), + any(Sensor.class), anyInt(), any(Handler.class)); + SensorEventListener sensorListener = mLightSensorListenerCaptor.getValue(); + float[] sensorEventValues = new float[1]; + SensorEvent sensorEvent = new SensorEvent(sensor, 0, 0, sensorEventValues); + + sensorEventValues[0] = getLightSensorValue(brightnessValue); + sensorListener.onSensorChanged(sensorEvent); + + verify(mLightsSession, atLeastOnce()).requestLights(mLightsRequestCaptor.capture()); + for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) { + assertEquals(color, lightState.getColor()); + } + } + + @Ignore + @Test + public void testDayToNightTransistion() { + mLights.add(getNextLight(true)); + Sensor sensor = mock(Sensor.class); + doReturn(sensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT); + + CameraPrivacyLightController cplc = createCameraPrivacyLightController(); + cplc.setElapsedRealTime(0); + + openCamera(); + // There will be an initial call at brightness 0 + verify(mLightsSession, times(1)).requestLights(any(LightsRequest.class)); + + verify(mSensorManager).registerListener(mLightSensorListenerCaptor.capture(), + any(Sensor.class), anyInt(), any(Handler.class)); + SensorEventListener sensorListener = mLightSensorListenerCaptor.getValue(); + + onSensorEvent(cplc, sensorListener, sensor, 0, 20); + + // 5 sec avg = 20 + onSensorEvent(cplc, sensorListener, sensor, 5000, 30); + + verify(mLightsSession, times(2)).requestLights(mLightsRequestCaptor.capture()); + for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) { + assertEquals(mDayColor, lightState.getColor()); + } + + // 5 sec avg = 22 + + onSensorEvent(cplc, sensorListener, sensor, 6000, 10); + + // 5 sec avg = 18 + + onSensorEvent(cplc, sensorListener, sensor, 8000, 5); + + // Should have always been day + verify(mLightsSession, times(2)).requestLights(mLightsRequestCaptor.capture()); + for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) { + assertEquals(mDayColor, lightState.getColor()); + } + + // 5 sec avg = 12 + + onSensorEvent(cplc, sensorListener, sensor, 10000, 5); + + // Should now be night + verify(mLightsSession, times(3)).requestLights(mLightsRequestCaptor.capture()); + for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) { + assertEquals(mNightColor, lightState.getColor()); + } + } + + private void onSensorEvent(CameraPrivacyLightController cplc, + SensorEventListener sensorListener, Sensor sensor, long timestamp, int value) { + cplc.setElapsedRealTime(timestamp); + sensorListener.onSensorChanged(new SensorEvent(sensor, 0, timestamp, + new float[] {getLightSensorValue(value)})); + } + + // Use the test thread so that the test is deterministic + private CameraPrivacyLightController createCameraPrivacyLightController() { + if (Looper.myLooper() == null) { + Looper.prepare(); + } + return new CameraPrivacyLightController(mContext, Looper.myLooper()); + } + private Light getNextLight(boolean cameraType) { Light light = ExtendedMockito.mock(Light.class); if (cameraType) { @@ -245,4 +426,13 @@ public class CameraPrivacyLightControllerTest { doReturn(mNextLightId++).when(light).getId(); return light; } + + private float getLightSensorValue(int i) { + return (float) Math.exp(i / CameraPrivacyLightController.LIGHT_VALUE_MULTIPLIER); + } + + private void openCamera() { + verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture()); + mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, 10001, "pkg", true); + } }