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

Commit 3f83c514 authored by Felipe Salazar's avatar Felipe Salazar Committed by Android (Google) Code Review
Browse files

Merge "Update DockObserver to consume extcon uevents"

parents d2d8485b 6a56584b
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -5648,4 +5648,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
@@ -4679,6 +4679,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);
    }
}