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

Commit 6a56584b authored by Felipe Salazar's avatar Felipe Salazar
Browse files

Update DockObserver to consume extcon uevents

Bug: b/201683309, b/216506604
Test: ran on device, atest FrameworksServicesTests:DockObserverTest

Change-Id: I91b8b82fa8044ee3014f629354c92e0370964521
parent 86d5713d
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -5634,4 +5634,13 @@
    <!-- The amount of time after becoming non-interactive (in ms) after which
         Low Power Standby can activate. -->
    <integer name="config_lowPowerStandbyNonInteractiveTimeout">5000</integer>


    <!-- Mapping to select an Intent.EXTRA_DOCK_STATE value from extcon state
         key-value pairs. Each entry is evaluated in order and is of the form:
            "[EXTRA_DOCK_STATE value],key1=value1,key2=value2[,...]"
         An entry with no key-value pairs is valid and can be used as a wildcard.
     -->
    <string-array name="config_dockExtconStateMapping">
    </string-array>
</resources>
+2 −0
Original line number Diff line number Diff line
@@ -4675,6 +4675,8 @@

  <java-symbol type="string" name="config_deviceSpecificDeviceStatePolicyProvider" />

  <java-symbol type="array" name="config_dockExtconStateMapping" />

  <java-symbol type="string" name="notification_channel_abusive_bg_apps"/>
  <java-symbol type="string" name="notification_title_abusive_bg_apps"/>
  <java-symbol type="string" name="notification_content_abusive_bg_apps"/>
+152 −37
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@ package com.android.server;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.media.Ringtone;
import android.media.RingtoneManager;
@@ -32,15 +31,21 @@ import android.os.SystemClock;
import android.os.UEventObserver;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
import com.android.server.ExtconUEventObserver.ExtconInfo;

import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * DockObserver monitors for a docking station.
@@ -48,9 +53,6 @@ import java.io.PrintWriter;
final class DockObserver extends SystemService {
    private static final String TAG = "DockObserver";

    private static final String DOCK_UEVENT_MATCH = "DEVPATH=/devices/virtual/switch/dock";
    private static final String DOCK_STATE_PATH = "/sys/class/switch/dock/state";

    private static final int MSG_DOCK_STATE_CHANGED = 0;

    private final PowerManager mPowerManager;
@@ -69,6 +71,92 @@ final class DockObserver extends SystemService {

    private final boolean mAllowTheaterModeWakeFromDock;

    private final List<ExtconStateConfig> mExtconStateConfigs;

    static final class ExtconStateProvider {
        private final Map<String, String> mState;

        ExtconStateProvider(Map<String, String> state) {
            mState = state;
        }

        String getValue(String key) {
            return mState.get(key);
        }


        static ExtconStateProvider fromString(String stateString) {
            Map<String, String> states = new HashMap<>();
            String[] lines = stateString.split("\n");
            for (String line : lines) {
                String[] fields = line.split("=");
                if (fields.length == 2) {
                    states.put(fields[0], fields[1]);
                } else {
                    Slog.e(TAG, "Invalid line: " + line);
                }
            }
            return new ExtconStateProvider(states);
        }

        static ExtconStateProvider fromFile(String stateFilePath) {
            char[] buffer = new char[1024];
            try (FileReader file = new FileReader(stateFilePath)) {
                int len = file.read(buffer, 0, 1024);
                String stateString = (new String(buffer, 0, len)).trim();
                return ExtconStateProvider.fromString(stateString);
            } catch (FileNotFoundException e) {
                Slog.w(TAG, "No state file found at: " + stateFilePath);
                return new ExtconStateProvider(new HashMap<>());
            } catch (Exception e) {
                Slog.e(TAG, "" , e);
                return new ExtconStateProvider(new HashMap<>());
            }
        }
    }

    /**
     * Represents a mapping from extcon state to EXTRA_DOCK_STATE value. Each
     * instance corresponds to an entry in config_dockExtconStateMapping.
     */
    private static final class ExtconStateConfig {

        // The EXTRA_DOCK_STATE that will be used if the extcon key-value pairs match
        public final int extraStateValue;

        // A list of key-value pairs that must be present in the extcon state for a match
        // to be considered. An empty list is considered a matching wildcard.
        public final List<Pair<String, String>> keyValuePairs = new ArrayList<>();

        ExtconStateConfig(int extraStateValue) {
            this.extraStateValue = extraStateValue;
        }
    }

    private static List<ExtconStateConfig> loadExtconStateConfigs(Context context) {
        String[] rows = context.getResources().getStringArray(
            com.android.internal.R.array.config_dockExtconStateMapping);
        try {
            ArrayList<ExtconStateConfig> configs = new ArrayList<>();
            for (String row : rows) {
                String[] rowFields = row.split(",");
                ExtconStateConfig config = new ExtconStateConfig(Integer.parseInt(rowFields[0]));
                for (int i = 1; i < rowFields.length; i++) {
                    String[] keyValueFields = rowFields[i].split("=");
                    if (keyValueFields.length != 2) {
                        throw new IllegalArgumentException("Invalid key-value: " + rowFields[i]);
                    }
                    config.keyValuePairs.add(Pair.create(keyValueFields[0], keyValueFields[1]));
                }
                configs.add(config);
            }
            return configs;
        } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
            Slog.e(TAG, "Could not parse extcon state config", e);
            return new ArrayList<>();
        }
    }

    public DockObserver(Context context) {
        super(context);

@@ -77,9 +165,25 @@ final class DockObserver extends SystemService {
        mAllowTheaterModeWakeFromDock = context.getResources().getBoolean(
                com.android.internal.R.bool.config_allowTheaterModeWakeFromDock);

        init();  // set initial status
        mExtconStateConfigs = loadExtconStateConfigs(context);

        List<ExtconInfo> infos = ExtconInfo.getExtconInfoForTypes(new String[] {
                ExtconInfo.EXTCON_DOCK
        });

        mObserver.startObserving(DOCK_UEVENT_MATCH);
        if (!infos.isEmpty()) {
            ExtconInfo info = infos.get(0);
            Slog.i(TAG, "Found extcon info devPath: " + info.getDevicePath()
                        + ", statePath: " + info.getStatePath());

            // set initial status
            setDockStateFromProviderLocked(ExtconStateProvider.fromFile(info.getStatePath()));
            mPreviousDockState = mActualDockState;

            mExtconUEventObserver.startObserving(info);
        } else {
            Slog.i(TAG, "No extcon dock device found in this kernel.");
        }
    }

    @Override
@@ -101,26 +205,6 @@ final class DockObserver extends SystemService {
        }
    }

    private void init() {
        synchronized (mLock) {
            try {
                char[] buffer = new char[1024];
                FileReader file = new FileReader(DOCK_STATE_PATH);
                try {
                    int len = file.read(buffer, 0, 1024);
                    setActualDockStateLocked(Integer.parseInt((new String(buffer, 0, len)).trim()));
                    mPreviousDockState = mActualDockState;
                } finally {
                    file.close();
                }
            } catch (FileNotFoundException e) {
                Slog.w(TAG, "This kernel does not have dock station support");
            } catch (Exception e) {
                Slog.e(TAG, "" , e);
            }
        }
    }

    private void setActualDockStateLocked(int newState) {
        mActualDockState = newState;
        if (!mUpdatesStopped) {
@@ -234,19 +318,50 @@ final class DockObserver extends SystemService {
        }
    };

    private final UEventObserver mObserver = new UEventObserver() {
        @Override
        public void onUEvent(UEventObserver.UEvent event) {
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                Slog.v(TAG, "Dock UEVENT: " + event.toString());
    private int getDockedStateExtraValue(ExtconStateProvider state) {
        for (ExtconStateConfig config : mExtconStateConfigs) {
            boolean match = true;
            for (Pair<String, String> keyValue : config.keyValuePairs) {
                String stateValue = state.getValue(keyValue.first);
                match = match && keyValue.second.equals(stateValue);
                if (!match) {
                    break;
                }
            }

            try {
            if (match) {
                return config.extraStateValue;
            }
        }

        return Intent.EXTRA_DOCK_STATE_DESK;
    }

    @VisibleForTesting
    void setDockStateFromProviderForTesting(ExtconStateProvider provider) {
        synchronized (mLock) {
                    setActualDockStateLocked(Integer.parseInt(event.get("SWITCH_STATE")));
            setDockStateFromProviderLocked(provider);
        }
    }

    private void setDockStateFromProviderLocked(ExtconStateProvider provider) {
        int state = Intent.EXTRA_DOCK_STATE_UNDOCKED;
        if ("1".equals(provider.getValue("DOCK"))) {
            state = getDockedStateExtraValue(provider);
        }
        setActualDockStateLocked(state);
    }

    private final ExtconUEventObserver mExtconUEventObserver = new ExtconUEventObserver() {
        @Override
        public void onUEvent(ExtconInfo extconInfo, UEventObserver.UEvent event) {
            synchronized (mLock) {
                String stateString = event.get("STATE");
                if (stateString != null) {
                    setDockStateFromProviderLocked(ExtconStateProvider.fromString(stateString));
                } else {
                    Slog.e(TAG, "Extcon event missing STATE: " + event);
                }
            } catch (NumberFormatException e) {
                Slog.e(TAG, "Could not parse switch state from event " + event);
            }
        }
    };
+134 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.server;

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

import android.content.Intent;
import android.os.Looper;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;

import androidx.test.core.app.ApplicationProvider;

import com.android.internal.R;
import com.android.internal.util.test.BroadcastInterceptingContext;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.concurrent.ExecutionException;

@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class DockObserverTest {

    @Rule
    public TestableContext mContext =
            new TestableContext(ApplicationProvider.getApplicationContext(), null);

    private final BroadcastInterceptingContext mInterceptingContext =
            new BroadcastInterceptingContext(mContext);

    BroadcastInterceptingContext.FutureIntent updateExtconDockState(DockObserver observer,
            String extconDockState) {
        BroadcastInterceptingContext.FutureIntent futureIntent =
                mInterceptingContext.nextBroadcastIntent(Intent.ACTION_DOCK_EVENT);
        observer.setDockStateFromProviderForTesting(
                DockObserver.ExtconStateProvider.fromString(extconDockState));
        TestableLooper.get(this).processAllMessages();
        return futureIntent;
    }

    DockObserver observerWithMappingConfig(String[] configEntries) {
        mContext.getOrCreateTestableResources().addOverride(
                R.array.config_dockExtconStateMapping,
                configEntries);
        return new DockObserver(mInterceptingContext);
    }

    void assertDockEventIntentWithExtraThenUndock(DockObserver observer, String extconDockState,
            int expectedExtra) throws ExecutionException, InterruptedException {
        assertThat(updateExtconDockState(observer, extconDockState)
                .get().getIntExtra(Intent.EXTRA_DOCK_STATE, -1))
                .isEqualTo(expectedExtra);
        assertThat(updateExtconDockState(observer, "DOCK=0")
                .get().getIntExtra(Intent.EXTRA_DOCK_STATE, -1))
                .isEqualTo(Intent.EXTRA_DOCK_STATE_UNDOCKED);
    }

    @Before
    public void setUp() {
        if (Looper.myLooper() == null) {
            Looper.prepare();
        }
    }

    @Test
    public void testDockIntentBroadcast_onlyAfterBootReady()
            throws ExecutionException, InterruptedException {
        DockObserver observer = new DockObserver(mInterceptingContext);
        BroadcastInterceptingContext.FutureIntent futureIntent =
                updateExtconDockState(observer, "DOCK=1");
        updateExtconDockState(observer, "DOCK=1").assertNotReceived();
        // Last boot phase reached
        observer.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
        TestableLooper.get(this).processAllMessages();
        assertThat(futureIntent.get().getIntExtra(Intent.EXTRA_DOCK_STATE, -1))
                .isEqualTo(Intent.EXTRA_DOCK_STATE_DESK);
    }

    @Test
    public void testDockIntentBroadcast_customConfigResource()
            throws ExecutionException, InterruptedException {
        DockObserver observer = observerWithMappingConfig(
                new String[] {"2,KEY1=1,KEY2=2", "3,KEY3=3"});
        observer.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);

        // Mapping should not match
        assertDockEventIntentWithExtraThenUndock(observer, "DOCK=1",
                Intent.EXTRA_DOCK_STATE_DESK);
        assertDockEventIntentWithExtraThenUndock(observer, "DOCK=1\nKEY1=1",
                Intent.EXTRA_DOCK_STATE_DESK);
        assertDockEventIntentWithExtraThenUndock(observer, "DOCK=1\nKEY2=2",
                Intent.EXTRA_DOCK_STATE_DESK);

        // 1st mapping now matches
        assertDockEventIntentWithExtraThenUndock(observer, "DOCK=1\nKEY2=2\nKEY1=1",
                Intent.EXTRA_DOCK_STATE_CAR);

        // 2nd mapping now matches
        assertDockEventIntentWithExtraThenUndock(observer, "DOCK=1\nKEY3=3",
                Intent.EXTRA_DOCK_STATE_LE_DESK);
    }

    @Test
    public void testDockIntentBroadcast_customConfigResourceWithWildcard()
            throws ExecutionException, InterruptedException {
        DockObserver observer = observerWithMappingConfig(new String[] {
                "2,KEY2=2",
                "3,KEY3=3",
                "4"
        });
        observer.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
        assertDockEventIntentWithExtraThenUndock(observer, "DOCK=1\nKEY5=5",
                Intent.EXTRA_DOCK_STATE_HE_DESK);
    }
}