content: /*
   * Copyright (C) 2018 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 android.annotation.Nullable;
  import android.os.UEventObserver;
  import android.util.ArrayMap;
  import android.util.Slog;
  
  import java.io.File;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.regex.Pattern;
  
  /**
   * A specialized UEventObserver that receives UEvents from the kernel for devices in the {@code
   * /sys/class/extcon}. directory
   *
   * <p>Subclass ExtconUEventObserver, implementing {@link #onUEvent(ExtconInfo, UEvent)}, then call
   * startObserving() with a ExtconInfo to observe. The UEvent thread will then call your onUEvent()
   * method when a UEvent occurs that matches the path of your ExtconInfos.
   *
   * <p>Call stopObserving() to stop receiving UEvents.
   *
   * <p>There is only one UEvent thread per process, even if that process has multiple UEventObserver
   * subclass instances. The UEvent thread starts when the startObserving() is called for the first
   * time in that process. Once started the UEvent thread will not stop (although it can stop
   * notifying UEventObserver's via stopObserving()).
   *
   * @hide
   */
  public abstract class ExtconUEventObserver extends UEventObserver {
      private static final String TAG = "ExtconUEventObserver";
      private static final boolean LOG = false;
      private static final String SELINUX_POLICIES_NEED_TO_BE_CHANGED =
              "This probably means the selinux policies need to be changed.";
  
      private final Map<String, ExtconInfo> mExtconInfos = new ArrayMap<>();
  
      @Override
      public final void onUEvent(UEvent event) {
          String devPath = event.get("DEVPATH");
          ExtconInfo info = mExtconInfos.get(devPath);
          if (info != null) {
              onUEvent(info, event);
          } else {
              Slog.w(TAG, "No match found for DEVPATH of " + event + " in " + mExtconInfos);
          }
      }
  
      /**
       * Subclasses of ExtconUEventObserver should override this method to handle UEvents.
       *
       * @param extconInfo that matches the {@code DEVPATH} of {@code event}
       * @param event      the event
       */
      protected abstract void onUEvent(ExtconInfo extconInfo, UEvent event);
  
      /** Starts observing {@link ExtconInfo#getDevicePath()}. */
      public void startObserving(ExtconInfo extconInfo) {
          String devicePath = extconInfo.getDevicePath();
          if (devicePath == null) {
              Slog.wtf(TAG, "Unable to start observing  " + extconInfo.getName()
                      + " because the device path is null. " + SELINUX_POLICIES_NEED_TO_BE_CHANGED);
          } else {
              mExtconInfos.put(devicePath, extconInfo);
              if (LOG) Slog.v(TAG, "Observing  " + devicePath);
              startObserving("DEVPATH=" + devicePath);
          }
      }
  
      /** An External Connection to watch. */
      public static final class ExtconInfo {
          private static final String TAG = "ExtconInfo";
  
          /** Returns a new list of all external connections whose name matches {@code regex}. */
          public static List<ExtconInfo> getExtconInfos(@Nullable String regex) {
              if (!extconExists()) {
                  return new ArrayList<>(0);  // Always return a new list.
              }
              Pattern p = regex == null ? null : Pattern.compile(regex);
              File file = new File("/sys/class/extcon");
              File[] files = file.listFiles();
              if (files == null) {
                  Slog.wtf(TAG, file + " exists " + file.exists() + " isDir " + file.isDirectory()
                          + " but listFiles returns null. "
                          + SELINUX_POLICIES_NEED_TO_BE_CHANGED);
                  return new ArrayList<>(0);  // Always return a new list.
              } else {
                  ArrayList list = new ArrayList(files.length);
                  for (File f : files) {
                      String name = f.getName();
                      if (p == null || p.matcher(name).matches()) {
                          ExtconInfo uei = new ExtconInfo(name);
                          list.add(uei);
                          if (LOG) Slog.d(TAG, name + " matches " + regex);
                      } else {
                          if (LOG) Slog.d(TAG, name + " does not match " + regex);
                      }
                  }
                  return list;
              }
          }
  
          private final String mName;
  
          public ExtconInfo(String name) {
              mName = name;
          }
  
          /** The name of the external connection */
          public String getName() {
              return mName;
          }
  
          /**
           * The path to the device for this external connection.
           *
           * <p><b>NOTE</b> getting this path involves resolving a symlink.
           *
           * @return the device path, or null if it not found.
           */
          @Nullable
          public String getDevicePath() {
              try {
                  String extconPath = String.format(Locale.US, "/sys/class/extcon/%s", mName);
                  File devPath = new File(extconPath);
                  if (devPath.exists()) {
                      String canonicalPath = devPath.getCanonicalPath();
                      int start = canonicalPath.indexOf("/devices");
                      return canonicalPath.substring(start);
                  }
                  return null;
              } catch (IOException e) {
                  Slog.e(TAG, "Could not get the extcon device path for " + mName, e);
                  return null;
              }
          }
  
          /** The path to the state file */
          public String getStatePath() {
              return String.format(Locale.US, "/sys/class/extcon/%s/state", mName);
          }
      }
  
      /** Does the {@code /sys/class/extcon/<name>} directory exist */
      public static boolean namedExtconDirExists(String name) {
          File extconDir = new File("/sys/class/extcon/" + name);
          return extconDir.exists() && extconDir.isDirectory();
      }
  
      /** Does the {@code /sys/class/extcon} directory exist */
      public static boolean extconExists() {
          File extconDir = new File("/sys/class/extcon");
          return extconDir.exists() && extconDir.isDirectory();
      }
  }
  