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

Commit de527e5d authored by brycelee's avatar brycelee Committed by Bryce Lee
Browse files

Convert DreamOverlayStateController to Kotlin.

Bug: 407646672
Test: atest DreamOverlayStateControllerTest
Flag: EXEMPT refactor
Change-Id: Ibc1835ec3acb33970eac83c067004b5a1b80947a
parent 3c74ec20
Loading
Loading
Loading
Loading
+24 −25
Original line number Diff line number Diff line
@@ -90,7 +90,6 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
@@ -281,9 +280,9 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
            val overlay = IDreamOverlay.Stub.asInterface(proxy)
            val callback = Mockito.mock(IDreamOverlayClientCallback::class.java)
            overlay.getClient(callback)
            val clientCaptor = ArgumentCaptor.forClass(IDreamOverlayClient::class.java)
            val clientCaptor = argumentCaptor<IDreamOverlayClient>()
            verify(callback).onDreamOverlayClient(clientCaptor.capture())
            return clientCaptor.value
            return clientCaptor.lastValue
        }

    @Test
@@ -338,10 +337,10 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
        )
        mMainExecutor.runAllReady()
        verify(mWindowManager).addView(any(), any())
        verify(mStateController).setOverlayActive(false)
        verify(mStateController).setLowLightActive(false)
        verify(mStateController).isOverlayActive = false
        verify(mStateController).isLowLightActive = false
        verify(mStateController).setEntryAnimationsFinished(false)
        verify(mStateController, never()).setOverlayActive(true)
        verify(mStateController, never()).isOverlayActive = true
        verify(mUiEventLogger, never())
            .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START)
        verify(mDreamOverlayCallbackController, never()).onStartDream()
@@ -428,12 +427,12 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {

        verifyNoMoreInteractions(mTouchMonitor)

        val captor = ArgumentCaptor.forClass(DreamOverlayStateController.Callback::class.java)
        val captor = argumentCaptor<DreamOverlayStateController.Callback>()
        verify(mStateController).addCallback(captor.capture())

        whenever(mStateController.areExitAnimationsRunning()).thenReturn(false)

        captor.firstValue.onStateChanged()
        captor.lastValue.onStateChanged()

        // Should only be called once since it should be null during the second reset.
        verify(mTouchMonitor).destroy()
@@ -453,7 +452,7 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
        )
        mMainExecutor.runAllReady()
        assertThat(mService.dreamComponent).isEqualTo(LOW_LIGHT_COMPONENT)
        verify(mStateController).setLowLightActive(true)
        verify(mStateController).isLowLightActive = true
    }

    @Test
@@ -498,8 +497,8 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
        verify(mWindowManager).removeView(mViewCaptor.firstValue)

        // Verify state correctly set.
        verify(mStateController).setOverlayActive(false)
        verify(mStateController).setLowLightActive(false)
        verify(mStateController).isOverlayActive = false
        verify(mStateController).isLowLightActive = false
        verify(mStateController).setEntryAnimationsFinished(false)

        // Verify touch monitor destroyed
@@ -542,7 +541,7 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
                null
            }
            .`when`(mStateController)
            .setOverlayActive(true)
            .isOverlayActive = true

        // Start the dream.
        client.startDream(
@@ -587,8 +586,8 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
        // Verify state correctly set.
        verify(mKeyguardUpdateMonitor).removeCallback(any())
        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.DESTROYED)
        verify(mStateController).setOverlayActive(false)
        verify(mStateController).setLowLightActive(false)
        verify(mStateController).isOverlayActive = false
        verify(mStateController).isLowLightActive = false
        verify(mStateController).setEntryAnimationsFinished(false)
    }

@@ -604,8 +603,8 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
        // Verify state still correctly set.
        verify(mKeyguardUpdateMonitor).removeCallback(any())
        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.DESTROYED)
        verify(mStateController).setOverlayActive(false)
        verify(mStateController).setLowLightActive(false)
        verify(mStateController).isOverlayActive = false
        verify(mStateController).isLowLightActive = false
    }

    @Test
@@ -727,12 +726,12 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
            false, /*shouldShowComplication*/
        )
        mMainExecutor.runAllReady()
        val paramsCaptor = ArgumentCaptor.forClass(WindowManager.LayoutParams::class.java)
        val paramsCaptor = argumentCaptor<WindowManager.LayoutParams>()

        // Verify that a new window is added.
        verify(mWindowManager).addView(any(), paramsCaptor.capture())
        assertThat(
                paramsCaptor.value.privateFlags and
                paramsCaptor.lastValue.privateFlags and
                    WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS ==
                    WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
            )
@@ -1000,18 +999,18 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
        )
        mMainExecutor.runAllReady()
        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
        val callbackCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java)
        val callbackCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
        verify(mKeyguardUpdateMonitor).registerCallback(callbackCaptor.capture())

        // Notification shade opens.
        callbackCaptor.value.onShadeExpandedChanged(true)
        callbackCaptor.lastValue.onShadeExpandedChanged(true)
        mMainExecutor.runAllReady()

        // Lifecycle state goes from resumed back to started when the notification shade shows.
        assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED)

        // Notification shade closes.
        callbackCaptor.value.onShadeExpandedChanged(false)
        callbackCaptor.lastValue.onShadeExpandedChanged(false)
        mMainExecutor.runAllReady()

        // Lifecycle state goes back to RESUMED.
@@ -1228,11 +1227,11 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
        val dreamTaskInfo = TaskInfo(mock<ComponentName>(), WindowConfiguration.ACTIVITY_TYPE_DREAM)
        assertThat(matcher.matches(dreamTaskInfo)).isTrue()

        val callbackCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java)
        val callbackCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
        verify(mKeyguardUpdateMonitor).registerCallback(callbackCaptor.capture())

        // Notification shade opens.
        callbackCaptor.value.onShadeExpandedChanged(true)
        callbackCaptor.lastValue.onShadeExpandedChanged(true)
        mMainExecutor.runAllReady()

        verify(gestureInteractor)
@@ -1265,11 +1264,11 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
        mMainExecutor.runAllReady()
        clearInvocations(gestureInteractor)

        val callbackCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java)
        val callbackCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
        verify(mKeyguardUpdateMonitor).registerCallback(callbackCaptor.capture())

        // Notification shade opens.
        callbackCaptor.value.onShadeExpandedChanged(true)
        callbackCaptor.lastValue.onShadeExpandedChanged(true)
        mMainExecutor.runAllReady()

        verify(gestureInteractor)
+0 −448
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 com.android.systemui.dreams;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.systemui.SysuiTestCase;
import com.android.systemui.complication.Complication;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.log.LogBuffer;
import com.android.systemui.log.core.FakeLogBuffer;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.reference.FakeWeakReferenceFactory;
import com.android.systemui.util.time.FakeSystemClock;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import java.util.Collection;

@SmallTest
@RunWith(AndroidJUnit4.class)
@android.platform.test.annotations.EnabledOnRavenwood
public class DreamOverlayStateControllerTest extends SysuiTestCase {
    @Mock
    DreamOverlayStateController.Callback mCallback;

    @Mock
    Complication mComplication;

    @Mock
    private FeatureFlags mFeatureFlags;

    private final LogBuffer mLogBuffer = FakeLogBuffer.Factory.Companion.create();

    final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());

    final FakeWeakReferenceFactory mWeakReferenceFactory = new FakeWeakReferenceFactory();

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);

        when(mFeatureFlags.isEnabled(Flags.ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS)).thenReturn(false);
    }

    @Test
    public void testStateChange_overlayActive() {
        final DreamOverlayStateController stateController = getDreamOverlayStateController();
        stateController.addCallback(mCallback);
        stateController.setOverlayActive(true);
        mExecutor.runAllReady();

        verify(mCallback).onStateChanged();
        assertThat(stateController.isOverlayActive()).isTrue();

        Mockito.clearInvocations(mCallback);
        stateController.setOverlayActive(true);
        mExecutor.runAllReady();
        verify(mCallback, never()).onStateChanged();

        stateController.setOverlayActive(false);
        mExecutor.runAllReady();
        verify(mCallback).onStateChanged();
        assertThat(stateController.isOverlayActive()).isFalse();
    }

    @Test
    public void testCallback() {
        final DreamOverlayStateController stateController = getDreamOverlayStateController();
        stateController.addCallback(mCallback);

        // Add complication and verify callback is notified.
        stateController.addComplication(mComplication);

        mExecutor.runAllReady();

        verify(mCallback, times(1)).onComplicationsChanged();

        final Collection<Complication> complications = stateController.getComplications();
        assertEquals(complications.size(), 1);
        assertTrue(complications.contains(mComplication));

        clearInvocations(mCallback);

        // Remove complication and verify callback is notified.
        stateController.removeComplication(mComplication);
        mExecutor.runAllReady();
        verify(mCallback, times(1)).onComplicationsChanged();
        assertTrue(stateController.getComplications().isEmpty());
    }

    @Test
    public void testNotifyOnCallbackAdd() {
        final DreamOverlayStateController stateController = getDreamOverlayStateController();

        stateController.addComplication(mComplication);
        mExecutor.runAllReady();

        // Verify callback occurs on add when an overlay is already present.
        stateController.addCallback(mCallback);
        mExecutor.runAllReady();
        verify(mCallback, times(1)).onComplicationsChanged();
    }

    @Test
    public void testComplicationFilteringWhenShouldShowComplications() {
        final DreamOverlayStateController stateController = getDreamOverlayStateController();
        stateController.setShouldShowComplications(true);

        final Complication alwaysAvailableComplication = Mockito.mock(Complication.class);
        final Complication weatherComplication = Mockito.mock(Complication.class);
        when(alwaysAvailableComplication.getRequiredTypeAvailability())
                .thenReturn(Complication.COMPLICATION_TYPE_NONE);
        when(weatherComplication.getRequiredTypeAvailability())
                .thenReturn(Complication.COMPLICATION_TYPE_WEATHER);

        stateController.addComplication(alwaysAvailableComplication);
        stateController.addComplication(weatherComplication);

        final DreamOverlayStateController.Callback callback =
                Mockito.mock(DreamOverlayStateController.Callback.class);

        stateController.addCallback(callback);
        mExecutor.runAllReady();

        {
            final Collection<Complication> complications = stateController.getComplications();
            assertThat(complications.contains(alwaysAvailableComplication)).isTrue();
            assertThat(complications.contains(weatherComplication)).isFalse();
        }

        stateController.setAvailableComplicationTypes(Complication.COMPLICATION_TYPE_WEATHER);
        mExecutor.runAllReady();
        verify(callback).onAvailableComplicationTypesChanged();

        {
            final Collection<Complication> complications = stateController.getComplications();
            assertThat(complications.contains(alwaysAvailableComplication)).isTrue();
            assertThat(complications.contains(weatherComplication)).isTrue();
        }

    }

    @Test
    public void testComplicationFilteringWhenShouldHideComplications() {
        final DreamOverlayStateController stateController = getDreamOverlayStateController();
        stateController.setShouldShowComplications(true);

        final Complication alwaysAvailableComplication = Mockito.mock(Complication.class);
        final Complication weatherComplication = Mockito.mock(Complication.class);
        when(alwaysAvailableComplication.getRequiredTypeAvailability())
                .thenReturn(Complication.COMPLICATION_TYPE_NONE);
        when(weatherComplication.getRequiredTypeAvailability())
                .thenReturn(Complication.COMPLICATION_TYPE_WEATHER);

        stateController.addComplication(alwaysAvailableComplication);
        stateController.addComplication(weatherComplication);

        final DreamOverlayStateController.Callback callback =
                Mockito.mock(DreamOverlayStateController.Callback.class);

        stateController.setAvailableComplicationTypes(Complication.COMPLICATION_TYPE_WEATHER);
        stateController.addCallback(callback);
        mExecutor.runAllReady();

        {
            clearInvocations(callback);
            stateController.setShouldShowComplications(true);
            mExecutor.runAllReady();

            verify(callback).onAvailableComplicationTypesChanged();
            final Collection<Complication> complications = stateController.getComplications();
            assertThat(complications.contains(alwaysAvailableComplication)).isTrue();
            assertThat(complications.contains(weatherComplication)).isTrue();
        }

        {
            clearInvocations(callback);
            stateController.setShouldShowComplications(false);
            mExecutor.runAllReady();

            verify(callback).onAvailableComplicationTypesChanged();
            final Collection<Complication> complications = stateController.getComplications();
            assertThat(complications.contains(alwaysAvailableComplication)).isTrue();
            assertThat(complications.contains(weatherComplication)).isFalse();
        }
    }

    @Test
    public void testComplicationWithNoTypeNotFiltered() {
        final Complication complication = Mockito.mock(Complication.class);
        final DreamOverlayStateController stateController = getDreamOverlayStateController();
        stateController.addComplication(complication);
        mExecutor.runAllReady();
        assertThat(stateController.getComplications(true).contains(complication))
                .isTrue();
    }

    @Test
    public void testComplicationsNotShownForHomeControlPanelDream() {
        final Complication complication = Mockito.mock(Complication.class);
        final DreamOverlayStateController stateController = getDreamOverlayStateController();

        // Add a complication and verify it's returned in getComplications.
        stateController.addComplication(complication);
        mExecutor.runAllReady();
        assertThat(stateController.getComplications().contains(complication))
                .isTrue();

        stateController.setHomeControlPanelActive(true);
        mExecutor.runAllReady();

        assertThat(stateController.getComplications()).isEmpty();
    }

    @Test
    public void testComplicationsNotShownForLowLight() {
        final Complication complication = Mockito.mock(Complication.class);
        final DreamOverlayStateController stateController = getDreamOverlayStateController();

        // Add a complication and verify it's returned in getComplications.
        stateController.addComplication(complication);
        mExecutor.runAllReady();
        assertThat(stateController.getComplications().contains(complication))
                .isTrue();

        stateController.setLowLightActive(true);
        mExecutor.runAllReady();

        assertThat(stateController.getComplications()).isEmpty();
    }

    @Test
    public void testNotifyLowLightChanged() {
        final DreamOverlayStateController stateController = getDreamOverlayStateController();

        stateController.addCallback(mCallback);
        mExecutor.runAllReady();
        assertThat(stateController.isLowLightActive()).isFalse();

        stateController.setLowLightActive(true);

        mExecutor.runAllReady();
        verify(mCallback, times(1)).onStateChanged();
        assertThat(stateController.isLowLightActive()).isTrue();
    }

    @Test
    public void testNotifyLowLightExit() {
        final DreamOverlayStateController stateController = getDreamOverlayStateController();

        stateController.addCallback(mCallback);
        mExecutor.runAllReady();
        assertThat(stateController.isLowLightActive()).isFalse();

        // Turn low light on then off to trigger the exiting callback.
        stateController.setLowLightActive(true);
        stateController.setLowLightActive(false);

        // Callback was only called once, when
        mExecutor.runAllReady();
        verify(mCallback, times(1)).onExitLowLight();
        assertThat(stateController.isLowLightActive()).isFalse();

        // Set with false again, which should not cause the callback to trigger again.
        stateController.setLowLightActive(false);
        mExecutor.runAllReady();
        verify(mCallback, times(1)).onExitLowLight();
    }

    @Test
    public void testNotifyEntryAnimationsFinishedChanged() {
        final DreamOverlayStateController stateController = getDreamOverlayStateController();

        stateController.addCallback(mCallback);
        mExecutor.runAllReady();
        assertThat(stateController.areEntryAnimationsFinished()).isFalse();

        stateController.setEntryAnimationsFinished(true);
        mExecutor.runAllReady();

        verify(mCallback, times(1)).onStateChanged();
        assertThat(stateController.areEntryAnimationsFinished()).isTrue();
    }

    @Test
    public void testNotifyDreamOverlayStatusBarVisibleChanged() {
        final DreamOverlayStateController stateController = getDreamOverlayStateController();

        stateController.addCallback(mCallback);
        mExecutor.runAllReady();
        assertThat(stateController.isDreamOverlayStatusBarVisible()).isFalse();

        stateController.setDreamOverlayStatusBarVisible(true);
        mExecutor.runAllReady();

        verify(mCallback, times(1)).onStateChanged();
        assertThat(stateController.isDreamOverlayStatusBarVisible()).isTrue();
    }

    @Test
    public void testNotifyHasAssistantAttentionChanged() {
        final DreamOverlayStateController stateController = getDreamOverlayStateController();

        stateController.addCallback(mCallback);
        mExecutor.runAllReady();
        assertThat(stateController.hasAssistantAttention()).isFalse();

        stateController.setHasAssistantAttention(true);
        mExecutor.runAllReady();

        verify(mCallback, times(1)).onStateChanged();
        assertThat(stateController.hasAssistantAttention()).isTrue();
    }

    @Test
    public void testShouldShowComplicationsSetToFalse_stillShowsHomeControls_featureEnabled() {
        when(mFeatureFlags.isEnabled(Flags.ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS)).thenReturn(true);

        final DreamOverlayStateController stateController = getDreamOverlayStateController();
        stateController.setShouldShowComplications(true);

        final Complication homeControlsComplication = Mockito.mock(Complication.class);
        when(homeControlsComplication.getRequiredTypeAvailability())
                .thenReturn(Complication.COMPLICATION_TYPE_HOME_CONTROLS);

        stateController.addComplication(homeControlsComplication);

        final DreamOverlayStateController.Callback callback =
                Mockito.mock(DreamOverlayStateController.Callback.class);

        stateController.setAvailableComplicationTypes(
                Complication.COMPLICATION_TYPE_HOME_CONTROLS);
        stateController.addCallback(callback);
        mExecutor.runAllReady();

        {
            clearInvocations(callback);
            stateController.setShouldShowComplications(true);
            mExecutor.runAllReady();

            verify(callback).onAvailableComplicationTypesChanged();
            final Collection<Complication> complications = stateController.getComplications();
            assertThat(complications.contains(homeControlsComplication)).isTrue();
        }

        {
            clearInvocations(callback);
            stateController.setShouldShowComplications(false);
            mExecutor.runAllReady();

            verify(callback).onAvailableComplicationTypesChanged();
            final Collection<Complication> complications = stateController.getComplications();
            assertThat(complications.contains(homeControlsComplication)).isTrue();
        }
    }

    @Test
    public void testHomeControlsDoNotShowIfNotAvailable_featureEnabled() {
        when(mFeatureFlags.isEnabled(Flags.ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS)).thenReturn(true);

        final DreamOverlayStateController stateController = getDreamOverlayStateController();
        stateController.setShouldShowComplications(true);

        final Complication homeControlsComplication = Mockito.mock(Complication.class);
        when(homeControlsComplication.getRequiredTypeAvailability())
                .thenReturn(Complication.COMPLICATION_TYPE_HOME_CONTROLS);

        stateController.addComplication(homeControlsComplication);

        final DreamOverlayStateController.Callback callback =
                Mockito.mock(DreamOverlayStateController.Callback.class);

        stateController.addCallback(callback);
        mExecutor.runAllReady();

        // No home controls since it is not available.
        assertThat(stateController.getComplications()).doesNotContain(homeControlsComplication);

        stateController.setAvailableComplicationTypes(Complication.COMPLICATION_TYPE_HOME_CONTROLS
                | Complication.COMPLICATION_TYPE_WEATHER);
        mExecutor.runAllReady();
        assertThat(stateController.getComplications()).contains(homeControlsComplication);
    }

    @Test
    public void testCallbacksIgnoredWhenWeakReferenceCleared() {
        final DreamOverlayStateController.Callback callback1 = Mockito.mock(
                DreamOverlayStateController.Callback.class);
        final DreamOverlayStateController.Callback callback2 = Mockito.mock(
                DreamOverlayStateController.Callback.class);

        final DreamOverlayStateController stateController = getDreamOverlayStateController();
        stateController.addCallback(callback1);
        stateController.addCallback(callback2);
        mExecutor.runAllReady();

        // Simulate callback1 getting GC'd by clearing the reference
        mWeakReferenceFactory.clear(callback1);
        stateController.setOverlayActive(true);
        mExecutor.runAllReady();

        // Callback2 should still be called, but never callback1
        verify(callback1, never()).onStateChanged();
        verify(callback2).onStateChanged();
        assertThat(stateController.isOverlayActive()).isTrue();
    }

    private DreamOverlayStateController getDreamOverlayStateController() {
        return new DreamOverlayStateController(
                mExecutor,
                mFeatureFlags,
                mLogBuffer,
                mWeakReferenceFactory
        );
    }
}
+355 −0

File added.

Preview size limit exceeded, changes collapsed.

+0 −417

File deleted.

Preview size limit exceeded, changes collapsed.

+386 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading