1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.server; 17 18 import android.annotation.Nullable; 19 import android.annotation.StringDef; 20 import android.os.FileUtils; 21 import android.os.UEventObserver; 22 import android.text.TextUtils; 23 import android.util.ArrayMap; 24 import android.util.Slog; 25 import com.android.internal.annotations.GuardedBy; 26 import java.io.File; 27 import java.io.IOException; 28 import java.util.ArrayList; 29 import java.util.HashSet; 30 import java.util.List; 31 import java.util.Map; 32 33 /** 34 * A specialized UEventObserver that receives UEvents from the kernel for devices in the {@code 35 * /sys/class/extcon}. directory 36 * 37 * <p>Subclass ExtconUEventObserver, implementing {@link #onUEvent(ExtconInfo, UEvent)}, then call 38 * startObserving() with a ExtconInfo to observe. The UEvent thread will then call your onUEvent() 39 * method when a UEvent occurs that matches the path of your ExtconInfos. 40 * 41 * <p>Call stopObserving() to stop receiving UEvents. 42 * 43 * <p>There is only one UEvent thread per process, even if that process has multiple UEventObserver 44 * subclass instances. The UEvent thread starts when the startObserving() is called for the first 45 * time in that process. Once started the UEvent thread will not stop (although it can stop 46 * notifying UEventObserver's via stopObserving()). 47 * 48 * @hide 49 */ 50 public abstract class ExtconUEventObserver extends UEventObserver { 51 private static final String TAG = "ExtconUEventObserver"; 52 private static final boolean LOG = false; 53 private static final String SELINUX_POLICIES_NEED_TO_BE_CHANGED = 54 "This probably means the selinux policies need to be changed."; 55 56 private final Map<String, ExtconInfo> mExtconInfos = new ArrayMap<>(); 57 58 @Override onUEvent(UEvent event)59 public final void onUEvent(UEvent event) { 60 String devPath = event.get("DEVPATH"); 61 ExtconInfo info = mExtconInfos.get(devPath); 62 if (info != null) { 63 onUEvent(info, event); 64 } else { 65 Slog.w(TAG, "No match found for DEVPATH of " + event + " in " + mExtconInfos); 66 } 67 } 68 69 /** 70 * Subclasses of ExtconUEventObserver should override this method to handle UEvents. 71 * 72 * @param extconInfo that matches the {@code DEVPATH} of {@code event} 73 * @param event the event 74 */ onUEvent(ExtconInfo extconInfo, UEvent event)75 protected abstract void onUEvent(ExtconInfo extconInfo, UEvent event); 76 77 /** Starts observing {@link ExtconInfo#getDevicePath()}. */ startObserving(ExtconInfo extconInfo)78 public void startObserving(ExtconInfo extconInfo) { 79 String devicePath = extconInfo.getDevicePath(); 80 if (devicePath == null) { 81 Slog.wtf(TAG, "Unable to start observing " + extconInfo.getName() 82 + " because the device path is null. " + SELINUX_POLICIES_NEED_TO_BE_CHANGED); 83 } else { 84 mExtconInfos.put(devicePath, extconInfo); 85 if (LOG) Slog.v(TAG, "Observing " + devicePath); 86 startObserving("DEVPATH=" + devicePath); 87 } 88 } 89 90 /** An External Connection to watch. */ 91 public static final class ExtconInfo { 92 /* Copied from drivers/extcon/extcon.c */ 93 94 /* USB external connector */ 95 public static final String EXTCON_USB = "USB"; 96 public static final String EXTCON_USB_HOST = "USB-HOST"; 97 98 /* Charger external connector */ 99 public static final String EXTCON_TA = "TA"; 100 public static final String EXTCON_FAST_CHARGER = "FAST-CHARGER"; 101 public static final String EXTCON_SLOW_CHARGER = "SLOW-CHARGER"; 102 public static final String EXTCON_CHARGE_DOWNSTREAM = "CHARGE-DOWNSTREAM"; 103 104 /* Audio/Video external connector */ 105 public static final String EXTCON_LINE_IN = "LINE-IN"; 106 public static final String EXTCON_LINE_OUT = "LINE-OUT"; 107 public static final String EXTCON_MICROPHONE = "MICROPHONE"; 108 public static final String EXTCON_HEADPHONE = "HEADPHONE"; 109 110 public static final String EXTCON_HDMI = "HDMI"; 111 public static final String EXTCON_MHL = "MHL"; 112 public static final String EXTCON_DVI = "DVI"; 113 public static final String EXTCON_VGA = "VGA"; 114 public static final String EXTCON_SPDIF_IN = "SPDIF-IN"; 115 public static final String EXTCON_SPDIF_OUT = "SPDIF-OUT"; 116 public static final String EXTCON_VIDEO_IN = "VIDEO-IN"; 117 public static final String EXTCON_VIDEO_OUT = "VIDEO-OUT"; 118 119 /* Etc external connector */ 120 public static final String EXTCON_DOCK = "DOCK"; 121 public static final String EXTCON_JIG = "JIG"; 122 public static final String EXTCON_MECHANICAL = "MECHANICAL"; 123 124 @StringDef({ 125 EXTCON_USB, 126 EXTCON_USB_HOST, 127 EXTCON_TA, 128 EXTCON_FAST_CHARGER, 129 EXTCON_SLOW_CHARGER, 130 EXTCON_CHARGE_DOWNSTREAM, 131 EXTCON_LINE_IN, 132 EXTCON_LINE_OUT, 133 EXTCON_MICROPHONE, 134 EXTCON_HEADPHONE, 135 EXTCON_HDMI, 136 EXTCON_MHL, 137 EXTCON_DVI, 138 EXTCON_VGA, 139 EXTCON_SPDIF_IN, 140 EXTCON_SPDIF_OUT, 141 EXTCON_VIDEO_IN, 142 EXTCON_VIDEO_OUT, 143 EXTCON_DOCK, 144 EXTCON_JIG, 145 EXTCON_MECHANICAL, 146 }) 147 148 public @interface ExtconDeviceType {} 149 150 private static final Object sLock = new Object(); 151 private static ExtconInfo[] sExtconInfos = null; 152 153 private final String mName; 154 private final @ExtconDeviceType HashSet<String> mDeviceTypes = new HashSet<>(); 155 156 @GuardedBy("sLock") initExtconInfos()157 private static void initExtconInfos() { 158 if (sExtconInfos != null) { 159 return; 160 } 161 162 File file = new File("/sys/class/extcon"); 163 File[] files = file.listFiles(); 164 if (files == null) { 165 Slog.w(TAG, 166 file + " exists " + file.exists() + " isDir " + file.isDirectory() 167 + " but listFiles returns null." 168 + SELINUX_POLICIES_NEED_TO_BE_CHANGED); 169 sExtconInfos = new ExtconInfo[0]; 170 } else { 171 List<ExtconInfo> list = new ArrayList<>(files.length); 172 for (File f : files) { 173 list.add(new ExtconInfo(f.getName())); 174 } 175 sExtconInfos = list.toArray(new ExtconInfo[0]); 176 } 177 } 178 179 /** 180 * Returns a new list of all external connections for the types given. 181 */ getExtconInfoForTypes( @xtconDeviceType String[] extconTypes)182 public static List<ExtconInfo> getExtconInfoForTypes( 183 @ExtconDeviceType String[] extconTypes) { 184 synchronized (sLock) { 185 initExtconInfos(); 186 } 187 188 List<ExtconInfo> extcons = new ArrayList<ExtconInfo>(); 189 for (ExtconInfo extcon : sExtconInfos) { 190 for (String type : extconTypes) { 191 if (extcon.hasCableType(type)) { 192 extcons.add(extcon); 193 break; 194 } 195 } 196 } 197 198 return extcons; 199 } 200 201 /** True if the given type is supported */ hasCableType(@xtconDeviceType String type)202 public boolean hasCableType(@ExtconDeviceType String type) { 203 return mDeviceTypes.contains(type); 204 } 205 ExtconInfo(String extconName)206 private ExtconInfo(String extconName) { 207 mName = extconName; 208 209 // Retrieve device types from /sys/class/extcon/extcon[X]/cable.[Y]/name 210 File[] cableDirs = FileUtils.listFilesOrEmpty(new File("/sys/class/extcon", mName), 211 (dir, cable) -> cable.startsWith("cable.")); 212 if (cableDirs.length == 0) { 213 Slog.d(TAG, 214 "Unable to list cables in /sys/class/extcon/" + mName + ". " 215 + SELINUX_POLICIES_NEED_TO_BE_CHANGED); 216 } 217 218 for (File cableDir : cableDirs) { 219 String cableCanonicalPath = null; 220 try { 221 cableCanonicalPath = cableDir.getCanonicalPath(); 222 String name = FileUtils.readTextFile(new File(cableDir, "name"), 0, null); 223 name = name.replace("\n", "").replace("\r", ""); 224 if (LOG) { 225 Slog.v(TAG, "Add extcon cable " + cableCanonicalPath); 226 } 227 mDeviceTypes.add(name); 228 } catch (IOException ex) { 229 Slog.w(TAG, 230 "Unable to read " + cableCanonicalPath + "/name. " 231 + SELINUX_POLICIES_NEED_TO_BE_CHANGED, 232 ex); 233 } 234 } 235 } 236 237 /** The name of the external connection */ getName()238 public String getName() { 239 return mName; 240 } 241 242 /** 243 * The path to the device for this external connection. 244 * 245 * <p><b>NOTE</b> getting this path involves resolving a symlink. 246 * 247 * @return the device path, or null if it not found. 248 */ 249 @Nullable getDevicePath()250 public String getDevicePath() { 251 try { 252 String extconPath = TextUtils.formatSimple("/sys/class/extcon/%s", mName); 253 File devPath = new File(extconPath); 254 if (devPath.exists()) { 255 String canonicalPath = devPath.getCanonicalPath(); 256 int start = canonicalPath.indexOf("/devices"); 257 return canonicalPath.substring(start); 258 } 259 return null; 260 } catch (IOException e) { 261 Slog.e(TAG, "Could not get the extcon device path for " + mName, e); 262 return null; 263 } 264 } 265 266 /** The path to the state file */ getStatePath()267 public String getStatePath() { 268 return TextUtils.formatSimple("/sys/class/extcon/%s/state", mName); 269 } 270 } 271 272 /** Does the {@code /sys/class/extcon} directory exist */ extconExists()273 public static boolean extconExists() { 274 File extconDir = new File("/sys/class/extcon"); 275 return extconDir.exists() && extconDir.isDirectory(); 276 } 277 } 278