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.os.UEventObserver;
20 import android.util.ArrayMap;
21 import android.util.Slog;
22 
23 import java.io.File;
24 import java.io.IOException;
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Map;
29 import java.util.regex.Pattern;
30 
31 /**
32  * A specialized UEventObserver that receives UEvents from the kernel for devices in the {@code
33  * /sys/class/extcon}. directory
34  *
35  * <p>Subclass ExtconUEventObserver, implementing {@link #onUEvent(ExtconInfo, UEvent)}, then call
36  * startObserving() with a ExtconInfo to observe. The UEvent thread will then call your onUEvent()
37  * method when a UEvent occurs that matches the path of your ExtconInfos.
38  *
39  * <p>Call stopObserving() to stop receiving UEvents.
40  *
41  * <p>There is only one UEvent thread per process, even if that process has multiple UEventObserver
42  * subclass instances. The UEvent thread starts when the startObserving() is called for the first
43  * time in that process. Once started the UEvent thread will not stop (although it can stop
44  * notifying UEventObserver's via stopObserving()).
45  *
46  * @hide
47  */
48 public abstract class ExtconUEventObserver extends UEventObserver {
49     private static final String TAG = "ExtconUEventObserver";
50     private static final boolean LOG = false;
51     private static final String SELINUX_POLICIES_NEED_TO_BE_CHANGED =
52             "This probably means the selinux policies need to be changed.";
53 
54     private final Map<String, ExtconInfo> mExtconInfos = new ArrayMap<>();
55 
56     @Override
onUEvent(UEvent event)57     public final void onUEvent(UEvent event) {
58         String devPath = event.get("DEVPATH");
59         ExtconInfo info = mExtconInfos.get(devPath);
60         if (info != null) {
61             onUEvent(info, event);
62         } else {
63             Slog.w(TAG, "No match found for DEVPATH of " + event + " in " + mExtconInfos);
64         }
65     }
66 
67     /**
68      * Subclasses of ExtconUEventObserver should override this method to handle UEvents.
69      *
70      * @param extconInfo that matches the {@code DEVPATH} of {@code event}
71      * @param event      the event
72      */
onUEvent(ExtconInfo extconInfo, UEvent event)73     protected abstract void onUEvent(ExtconInfo extconInfo, UEvent event);
74 
75     /** Starts observing {@link ExtconInfo#getDevicePath()}. */
startObserving(ExtconInfo extconInfo)76     public void startObserving(ExtconInfo extconInfo) {
77         String devicePath = extconInfo.getDevicePath();
78         if (devicePath == null) {
79             Slog.wtf(TAG, "Unable to start observing  " + extconInfo.getName()
80                     + " because the device path is null. " + SELINUX_POLICIES_NEED_TO_BE_CHANGED);
81         } else {
82             mExtconInfos.put(devicePath, extconInfo);
83             if (LOG) Slog.v(TAG, "Observing  " + devicePath);
84             startObserving("DEVPATH=" + devicePath);
85         }
86     }
87 
88     /** An External Connection to watch. */
89     public static final class ExtconInfo {
90         private static final String TAG = "ExtconInfo";
91 
92         /** Returns a new list of all external connections whose name matches {@code regex}. */
getExtconInfos(@ullable String regex)93         public static List<ExtconInfo> getExtconInfos(@Nullable String regex) {
94             if (!extconExists()) {
95                 return new ArrayList<>(0);  // Always return a new list.
96             }
97             Pattern p = regex == null ? null : Pattern.compile(regex);
98             File file = new File("/sys/class/extcon");
99             File[] files = file.listFiles();
100             if (files == null) {
101                 Slog.wtf(TAG, file + " exists " + file.exists() + " isDir " + file.isDirectory()
102                         + " but listFiles returns null. "
103                         + SELINUX_POLICIES_NEED_TO_BE_CHANGED);
104                 return new ArrayList<>(0);  // Always return a new list.
105             } else {
106                 ArrayList list = new ArrayList(files.length);
107                 for (File f : files) {
108                     String name = f.getName();
109                     if (p == null || p.matcher(name).matches()) {
110                         ExtconInfo uei = new ExtconInfo(name);
111                         list.add(uei);
112                         if (LOG) Slog.d(TAG, name + " matches " + regex);
113                     } else {
114                         if (LOG) Slog.d(TAG, name + " does not match " + regex);
115                     }
116                 }
117                 return list;
118             }
119         }
120 
121         private final String mName;
122 
ExtconInfo(String name)123         public ExtconInfo(String name) {
124             mName = name;
125         }
126 
127         /** The name of the external connection */
getName()128         public String getName() {
129             return mName;
130         }
131 
132         /**
133          * The path to the device for this external connection.
134          *
135          * <p><b>NOTE</b> getting this path involves resolving a symlink.
136          *
137          * @return the device path, or null if it not found.
138          */
139         @Nullable
getDevicePath()140         public String getDevicePath() {
141             try {
142                 String extconPath = String.format(Locale.US, "/sys/class/extcon/%s", mName);
143                 File devPath = new File(extconPath);
144                 if (devPath.exists()) {
145                     String canonicalPath = devPath.getCanonicalPath();
146                     int start = canonicalPath.indexOf("/devices");
147                     return canonicalPath.substring(start);
148                 }
149                 return null;
150             } catch (IOException e) {
151                 Slog.e(TAG, "Could not get the extcon device path for " + mName, e);
152                 return null;
153             }
154         }
155 
156         /** The path to the state file */
getStatePath()157         public String getStatePath() {
158             return String.format(Locale.US, "/sys/class/extcon/%s/state", mName);
159         }
160     }
161 
162     /** Does the {@code /sys/class/extcon/<name>} directory exist */
namedExtconDirExists(String name)163     public static boolean namedExtconDirExists(String name) {
164         File extconDir = new File("/sys/class/extcon/" + name);
165         return extconDir.exists() && extconDir.isDirectory();
166     }
167 
168     /** Does the {@code /sys/class/extcon} directory exist */
extconExists()169     public static boolean extconExists() {
170         File extconDir = new File("/sys/class/extcon");
171         return extconDir.exists() && extconDir.isDirectory();
172     }
173 }
174