1/*
2 * Copyright (C) 2010 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
17
18package android.hardware.usb;
19
20import android.annotation.Nullable;
21import android.annotation.SdkConstant;
22import android.annotation.SystemService;
23import android.annotation.SdkConstant.SdkConstantType;
24import android.app.PendingIntent;
25import android.content.ComponentName;
26import android.content.Context;
27import android.content.pm.PackageManager.NameNotFoundException;
28import android.os.Bundle;
29import android.os.ParcelFileDescriptor;
30import android.os.Process;
31import android.os.RemoteException;
32import android.util.Log;
33
34import com.android.internal.util.Preconditions;
35
36import java.util.HashMap;
37
38/**
39 * This class allows you to access the state of USB and communicate with USB devices.
40 * Currently only host mode is supported in the public API.
41 *
42 * <div class="special reference">
43 * <h3>Developer Guides</h3>
44 * <p>For more information about communicating with USB hardware, read the
45 * <a href="{@docRoot}guide/topics/connectivity/usb/index.html">USB developer guide</a>.</p>
46 * </div>
47 */
48@SystemService(Context.USB_SERVICE)
49public class UsbManager {
50    private static final String TAG = "UsbManager";
51
52   /**
53     * Broadcast Action:  A sticky broadcast for USB state change events when in device mode.
54     *
55     * This is a sticky broadcast for clients that includes USB connected/disconnected state,
56     * <ul>
57     * <li> {@link #USB_CONNECTED} boolean indicating whether USB is connected or disconnected.
58     * <li> {@link #USB_HOST_CONNECTED} boolean indicating whether USB is connected or
59     *     disconnected as host.
60     * <li> {@link #USB_CONFIGURED} boolean indicating whether USB is configured.
61     * currently zero if not configured, one for configured.
62     * <li> {@link #USB_FUNCTION_ADB} boolean extra indicating whether the
63     * adb function is enabled
64     * <li> {@link #USB_FUNCTION_RNDIS} boolean extra indicating whether the
65     * RNDIS ethernet function is enabled
66     * <li> {@link #USB_FUNCTION_MTP} boolean extra indicating whether the
67     * MTP function is enabled
68     * <li> {@link #USB_FUNCTION_PTP} boolean extra indicating whether the
69     * PTP function is enabled
70     * <li> {@link #USB_FUNCTION_PTP} boolean extra indicating whether the
71     * accessory function is enabled
72     * <li> {@link #USB_FUNCTION_AUDIO_SOURCE} boolean extra indicating whether the
73     * audio source function is enabled
74     * <li> {@link #USB_FUNCTION_MIDI} boolean extra indicating whether the
75     * MIDI function is enabled
76     * </ul>
77     * If the sticky intent has not been found, that indicates USB is disconnected,
78     * USB is not configued, MTP function is enabled, and all the other functions are disabled.
79     *
80     * {@hide}
81     */
82    public static final String ACTION_USB_STATE =
83            "android.hardware.usb.action.USB_STATE";
84
85    /**
86     * Broadcast Action: A broadcast for USB port changes.
87     *
88     * This intent is sent when a USB port is added, removed, or changes state.
89     * <ul>
90     * <li> {@link #EXTRA_PORT} containing the {@link android.hardware.usb.UsbPort}
91     * for the port.
92     * <li> {@link #EXTRA_PORT_STATUS} containing the {@link android.hardware.usb.UsbPortStatus}
93     * for the port, or null if the port has been removed
94     * </ul>
95     *
96     * @hide
97     */
98    public static final String ACTION_USB_PORT_CHANGED =
99            "android.hardware.usb.action.USB_PORT_CHANGED";
100
101   /**
102     * Broadcast Action:  A broadcast for USB device attached event.
103     *
104     * This intent is sent when a USB device is attached to the USB bus when in host mode.
105     * <ul>
106     * <li> {@link #EXTRA_DEVICE} containing the {@link android.hardware.usb.UsbDevice}
107     * for the attached device
108     * </ul>
109     */
110    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
111    public static final String ACTION_USB_DEVICE_ATTACHED =
112            "android.hardware.usb.action.USB_DEVICE_ATTACHED";
113
114   /**
115     * Broadcast Action:  A broadcast for USB device detached event.
116     *
117     * This intent is sent when a USB device is detached from the USB bus when in host mode.
118     * <ul>
119     * <li> {@link #EXTRA_DEVICE} containing the {@link android.hardware.usb.UsbDevice}
120     * for the detached device
121     * </ul>
122     */
123    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
124    public static final String ACTION_USB_DEVICE_DETACHED =
125            "android.hardware.usb.action.USB_DEVICE_DETACHED";
126
127   /**
128     * Broadcast Action:  A broadcast for USB accessory attached event.
129     *
130     * This intent is sent when a USB accessory is attached.
131     * <ul>
132     * <li> {@link #EXTRA_ACCESSORY} containing the {@link android.hardware.usb.UsbAccessory}
133     * for the attached accessory
134     * </ul>
135     */
136    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
137    public static final String ACTION_USB_ACCESSORY_ATTACHED =
138            "android.hardware.usb.action.USB_ACCESSORY_ATTACHED";
139
140   /**
141     * Broadcast Action:  A broadcast for USB accessory detached event.
142     *
143     * This intent is sent when a USB accessory is detached.
144     * <ul>
145     * <li> {@link #EXTRA_ACCESSORY} containing the {@link UsbAccessory}
146     * for the attached accessory that was detached
147     * </ul>
148     */
149    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
150    public static final String ACTION_USB_ACCESSORY_DETACHED =
151            "android.hardware.usb.action.USB_ACCESSORY_DETACHED";
152
153    /**
154     * Boolean extra indicating whether USB is connected or disconnected.
155     * Used in extras for the {@link #ACTION_USB_STATE} broadcast.
156     *
157     * {@hide}
158     */
159    public static final String USB_CONNECTED = "connected";
160
161    /**
162     * Boolean extra indicating whether USB is connected or disconnected as host.
163     * Used in extras for the {@link #ACTION_USB_STATE} broadcast.
164     *
165     * {@hide}
166     */
167    public static final String USB_HOST_CONNECTED = "host_connected";
168
169    /**
170     * Boolean extra indicating whether USB is configured.
171     * Used in extras for the {@link #ACTION_USB_STATE} broadcast.
172     *
173     * {@hide}
174     */
175    public static final String USB_CONFIGURED = "configured";
176
177    /**
178     * Boolean extra indicating whether confidential user data, such as photos, should be
179     * made available on the USB connection. This variable will only be set when the user
180     * has explicitly asked for this data to be unlocked.
181     * Used in extras for the {@link #ACTION_USB_STATE} broadcast.
182     *
183     * {@hide}
184     */
185    public static final String USB_DATA_UNLOCKED = "unlocked";
186
187    /**
188     * Boolean extra indicating whether the intent represents a change in the usb
189     * configuration (as opposed to a state update).
190     *
191     * {@hide}
192     */
193    public static final String USB_CONFIG_CHANGED = "config_changed";
194
195    /**
196     * A placeholder indicating that no USB function is being specified.
197     * Used to distinguish between selecting no function vs. the default function in
198     * {@link #setCurrentFunction(String)}.
199     *
200     * {@hide}
201     */
202    public static final String USB_FUNCTION_NONE = "none";
203
204    /**
205     * Name of the adb USB function.
206     * Used in extras for the {@link #ACTION_USB_STATE} broadcast
207     *
208     * {@hide}
209     */
210    public static final String USB_FUNCTION_ADB = "adb";
211
212    /**
213     * Name of the RNDIS ethernet USB function.
214     * Used in extras for the {@link #ACTION_USB_STATE} broadcast
215     *
216     * {@hide}
217     */
218    public static final String USB_FUNCTION_RNDIS = "rndis";
219
220    /**
221     * Name of the MTP USB function.
222     * Used in extras for the {@link #ACTION_USB_STATE} broadcast
223     *
224     * {@hide}
225     */
226    public static final String USB_FUNCTION_MTP = "mtp";
227
228    /**
229     * Name of the PTP USB function.
230     * Used in extras for the {@link #ACTION_USB_STATE} broadcast
231     *
232     * {@hide}
233     */
234    public static final String USB_FUNCTION_PTP = "ptp";
235
236    /**
237     * Name of the audio source USB function.
238     * Used in extras for the {@link #ACTION_USB_STATE} broadcast
239     *
240     * {@hide}
241     */
242    public static final String USB_FUNCTION_AUDIO_SOURCE = "audio_source";
243
244    /**
245     * Name of the MIDI USB function.
246     * Used in extras for the {@link #ACTION_USB_STATE} broadcast
247     *
248     * {@hide}
249     */
250    public static final String USB_FUNCTION_MIDI = "midi";
251
252    /**
253     * Name of the Accessory USB function.
254     * Used in extras for the {@link #ACTION_USB_STATE} broadcast
255     *
256     * {@hide}
257     */
258    public static final String USB_FUNCTION_ACCESSORY = "accessory";
259
260    /**
261     * Name of extra for {@link #ACTION_USB_PORT_CHANGED}
262     * containing the {@link UsbPort} object for the port.
263     *
264     * @hide
265     */
266    public static final String EXTRA_PORT = "port";
267
268    /**
269     * Name of extra for {@link #ACTION_USB_PORT_CHANGED}
270     * containing the {@link UsbPortStatus} object for the port, or null if the port
271     * was removed.
272     *
273     * @hide
274     */
275    public static final String EXTRA_PORT_STATUS = "portStatus";
276
277    /**
278     * Name of extra for {@link #ACTION_USB_DEVICE_ATTACHED} and
279     * {@link #ACTION_USB_DEVICE_DETACHED} broadcasts
280     * containing the {@link UsbDevice} object for the device.
281     */
282    public static final String EXTRA_DEVICE = "device";
283
284    /**
285     * Name of extra for {@link #ACTION_USB_ACCESSORY_ATTACHED} and
286     * {@link #ACTION_USB_ACCESSORY_DETACHED} broadcasts
287     * containing the {@link UsbAccessory} object for the accessory.
288     */
289    public static final String EXTRA_ACCESSORY = "accessory";
290
291    /**
292     * Name of extra added to the {@link android.app.PendingIntent}
293     * passed into {@link #requestPermission(UsbDevice, PendingIntent)}
294     * or {@link #requestPermission(UsbAccessory, PendingIntent)}
295     * containing a boolean value indicating whether the user granted permission or not.
296     */
297    public static final String EXTRA_PERMISSION_GRANTED = "permission";
298
299    private final Context mContext;
300    private final IUsbManager mService;
301
302    /**
303     * {@hide}
304     */
305    public UsbManager(Context context, IUsbManager service) {
306        mContext = context;
307        mService = service;
308    }
309
310    /**
311     * Returns a HashMap containing all USB devices currently attached.
312     * USB device name is the key for the returned HashMap.
313     * The result will be empty if no devices are attached, or if
314     * USB host mode is inactive or unsupported.
315     *
316     * @return HashMap containing all connected USB devices.
317     */
318    public HashMap<String,UsbDevice> getDeviceList() {
319        HashMap<String,UsbDevice> result = new HashMap<String,UsbDevice>();
320        if (mService == null) {
321            return result;
322        }
323        Bundle bundle = new Bundle();
324        try {
325            mService.getDeviceList(bundle);
326            for (String name : bundle.keySet()) {
327                result.put(name, (UsbDevice)bundle.get(name));
328            }
329            return result;
330        } catch (RemoteException e) {
331            throw e.rethrowFromSystemServer();
332        }
333    }
334
335    /**
336     * Opens the device so it can be used to send and receive
337     * data using {@link android.hardware.usb.UsbRequest}.
338     *
339     * @param device the device to open
340     * @return a {@link UsbDeviceConnection}, or {@code null} if open failed
341     */
342    public UsbDeviceConnection openDevice(UsbDevice device) {
343        try {
344            String deviceName = device.getDeviceName();
345            ParcelFileDescriptor pfd = mService.openDevice(deviceName);
346            if (pfd != null) {
347                UsbDeviceConnection connection = new UsbDeviceConnection(device);
348                boolean result = connection.open(deviceName, pfd, mContext);
349                pfd.close();
350                if (result) {
351                    return connection;
352                }
353            }
354        } catch (Exception e) {
355            Log.e(TAG, "exception in UsbManager.openDevice", e);
356        }
357        return null;
358    }
359
360    /**
361     * Returns a list of currently attached USB accessories.
362     * (in the current implementation there can be at most one)
363     *
364     * @return list of USB accessories, or null if none are attached.
365     */
366    public UsbAccessory[] getAccessoryList() {
367        if (mService == null) {
368            return null;
369        }
370        try {
371            UsbAccessory accessory = mService.getCurrentAccessory();
372            if (accessory == null) {
373                return null;
374            } else {
375                return new UsbAccessory[] { accessory };
376            }
377        } catch (RemoteException e) {
378            throw e.rethrowFromSystemServer();
379        }
380    }
381
382    /**
383     * Opens a file descriptor for reading and writing data to the USB accessory.
384     *
385     * @param accessory the USB accessory to open
386     * @return file descriptor, or null if the accessor could not be opened.
387     */
388    public ParcelFileDescriptor openAccessory(UsbAccessory accessory) {
389        try {
390            return mService.openAccessory(accessory);
391        } catch (RemoteException e) {
392            throw e.rethrowFromSystemServer();
393        }
394    }
395
396    /**
397     * Returns true if the caller has permission to access the device.
398     * Permission might have been granted temporarily via
399     * {@link #requestPermission(UsbDevice, PendingIntent)} or
400     * by the user choosing the caller as the default application for the device.
401     *
402     * @param device to check permissions for
403     * @return true if caller has permission
404     */
405    public boolean hasPermission(UsbDevice device) {
406        if (mService == null) {
407            return false;
408        }
409        try {
410            return mService.hasDevicePermission(device);
411        } catch (RemoteException e) {
412            throw e.rethrowFromSystemServer();
413        }
414    }
415
416    /**
417     * Returns true if the caller has permission to access the accessory.
418     * Permission might have been granted temporarily via
419     * {@link #requestPermission(UsbAccessory, PendingIntent)} or
420     * by the user choosing the caller as the default application for the accessory.
421     *
422     * @param accessory to check permissions for
423     * @return true if caller has permission
424     */
425    public boolean hasPermission(UsbAccessory accessory) {
426        if (mService == null) {
427            return false;
428        }
429        try {
430            return mService.hasAccessoryPermission(accessory);
431        } catch (RemoteException e) {
432            throw e.rethrowFromSystemServer();
433        }
434    }
435
436    /**
437     * Requests temporary permission for the given package to access the device.
438     * This may result in a system dialog being displayed to the user
439     * if permission had not already been granted.
440     * Success or failure is returned via the {@link android.app.PendingIntent} pi.
441     * If successful, this grants the caller permission to access the device only
442     * until the device is disconnected.
443     *
444     * The following extras will be added to pi:
445     * <ul>
446     * <li> {@link #EXTRA_DEVICE} containing the device passed into this call
447     * <li> {@link #EXTRA_PERMISSION_GRANTED} containing boolean indicating whether
448     * permission was granted by the user
449     * </ul>
450     *
451     * @param device to request permissions for
452     * @param pi PendingIntent for returning result
453     */
454    public void requestPermission(UsbDevice device, PendingIntent pi) {
455        try {
456            mService.requestDevicePermission(device, mContext.getPackageName(), pi);
457        } catch (RemoteException e) {
458            throw e.rethrowFromSystemServer();
459        }
460    }
461
462    /**
463     * Requests temporary permission for the given package to access the accessory.
464     * This may result in a system dialog being displayed to the user
465     * if permission had not already been granted.
466     * Success or failure is returned via the {@link android.app.PendingIntent} pi.
467     * If successful, this grants the caller permission to access the accessory only
468     * until the device is disconnected.
469     *
470     * The following extras will be added to pi:
471     * <ul>
472     * <li> {@link #EXTRA_ACCESSORY} containing the accessory passed into this call
473     * <li> {@link #EXTRA_PERMISSION_GRANTED} containing boolean indicating whether
474     * permission was granted by the user
475     * </ul>
476     *
477     * @param accessory to request permissions for
478     * @param pi PendingIntent for returning result
479     */
480    public void requestPermission(UsbAccessory accessory, PendingIntent pi) {
481        try {
482            mService.requestAccessoryPermission(accessory, mContext.getPackageName(), pi);
483        } catch (RemoteException e) {
484            throw e.rethrowFromSystemServer();
485        }
486    }
487
488    /**
489     * Grants permission for USB device without showing system dialog.
490     * Only system components can call this function.
491     * @param device to request permissions for
492     *
493     * {@hide}
494     */
495    public void grantPermission(UsbDevice device) {
496        grantPermission(device, Process.myUid());
497    }
498
499    /**
500     * Grants permission for USB device to given uid without showing system dialog.
501     * Only system components can call this function.
502     * @param device to request permissions for
503     * @uid uid to give permission
504     *
505     * {@hide}
506     */
507    public void grantPermission(UsbDevice device, int uid) {
508        try {
509            mService.grantDevicePermission(device, uid);
510        } catch (RemoteException e) {
511            throw e.rethrowFromSystemServer();
512        }
513    }
514
515    /**
516     * Grants permission to specified package for USB device without showing system dialog.
517     * Only system components can call this function, as it requires the MANAGE_USB permission.
518     * @param device to request permissions for
519     * @param packageName of package to grant permissions
520     *
521     * {@hide}
522     */
523    public void grantPermission(UsbDevice device, String packageName) {
524        try {
525            int uid = mContext.getPackageManager()
526                .getPackageUidAsUser(packageName, mContext.getUserId());
527            grantPermission(device, uid);
528        } catch (NameNotFoundException e) {
529            Log.e(TAG, "Package " + packageName + " not found.", e);
530        }
531    }
532
533    /**
534     * Returns true if the specified USB function is currently enabled when in device mode.
535     * <p>
536     * USB functions represent interfaces which are published to the host to access
537     * services offered by the device.
538     * </p>
539     *
540     * @param function name of the USB function
541     * @return true if the USB function is enabled
542     *
543     * {@hide}
544     */
545    public boolean isFunctionEnabled(String function) {
546        if (mService == null) {
547            return false;
548        }
549        try {
550            return mService.isFunctionEnabled(function);
551        } catch (RemoteException e) {
552            throw e.rethrowFromSystemServer();
553        }
554    }
555
556    /**
557     * Sets the current USB function when in device mode.
558     * <p>
559     * USB functions represent interfaces which are published to the host to access
560     * services offered by the device.
561     * </p><p>
562     * This method is intended to select among primary USB functions.  The system may
563     * automatically activate additional functions such as {@link #USB_FUNCTION_ADB}
564     * or {@link #USB_FUNCTION_ACCESSORY} based on other settings and states.
565     * </p><p>
566     * The allowed values are: {@link #USB_FUNCTION_NONE}, {@link #USB_FUNCTION_AUDIO_SOURCE},
567     * {@link #USB_FUNCTION_MIDI}, {@link #USB_FUNCTION_MTP}, {@link #USB_FUNCTION_PTP},
568     * or {@link #USB_FUNCTION_RNDIS}.
569     * </p><p>
570     * Also sets whether USB data (for example, MTP exposed pictures) should be made available
571     * on the USB connection when in device mode. Unlocking usb data should only be done with
572     * user involvement, since exposing pictures or other data could leak sensitive
573     * user information.
574     * </p><p>
575     * Note: This function is asynchronous and may fail silently without applying
576     * the requested changes.
577     * </p>
578     *
579     * @param function name of the USB function, or null to restore the default function
580     * @param usbDataUnlocked whether user data is accessible
581     *
582     * {@hide}
583     */
584    public void setCurrentFunction(String function, boolean usbDataUnlocked) {
585        try {
586            mService.setCurrentFunction(function, usbDataUnlocked);
587        } catch (RemoteException e) {
588            throw e.rethrowFromSystemServer();
589        }
590    }
591
592    /**
593     * Returns a list of physical USB ports on the device.
594     * <p>
595     * This list is guaranteed to contain all dual-role USB Type C ports but it might
596     * be missing other ports depending on whether the kernel USB drivers have been
597     * updated to publish all of the device's ports through the new "dual_role_usb"
598     * device class (which supports all types of ports despite its name).
599     * </p>
600     *
601     * @return The list of USB ports, or null if none.
602     *
603     * @hide
604     */
605    public UsbPort[] getPorts() {
606        if (mService == null) {
607            return null;
608        }
609        try {
610            return mService.getPorts();
611        } catch (RemoteException e) {
612            throw e.rethrowFromSystemServer();
613        }
614    }
615
616    /**
617     * Gets the status of the specified USB port.
618     *
619     * @param port The port to query.
620     * @return The status of the specified USB port, or null if unknown.
621     *
622     * @hide
623     */
624    public UsbPortStatus getPortStatus(UsbPort port) {
625        Preconditions.checkNotNull(port, "port must not be null");
626
627        try {
628            return mService.getPortStatus(port.getId());
629        } catch (RemoteException e) {
630            throw e.rethrowFromSystemServer();
631        }
632    }
633
634    /**
635     * Sets the desired role combination of the port.
636     * <p>
637     * The supported role combinations depend on what is connected to the port and may be
638     * determined by consulting
639     * {@link UsbPortStatus#isRoleCombinationSupported UsbPortStatus.isRoleCombinationSupported}.
640     * </p><p>
641     * Note: This function is asynchronous and may fail silently without applying
642     * the requested changes.  If this function does cause a status change to occur then
643     * a {@link #ACTION_USB_PORT_CHANGED} broadcast will be sent.
644     * </p>
645     *
646     * @param powerRole The desired power role: {@link UsbPort#POWER_ROLE_SOURCE}
647     * or {@link UsbPort#POWER_ROLE_SINK}, or 0 if no power role.
648     * @param dataRole The desired data role: {@link UsbPort#DATA_ROLE_HOST}
649     * or {@link UsbPort#DATA_ROLE_DEVICE}, or 0 if no data role.
650     *
651     * @hide
652     */
653    public void setPortRoles(UsbPort port, int powerRole, int dataRole) {
654        Preconditions.checkNotNull(port, "port must not be null");
655        UsbPort.checkRoles(powerRole, dataRole);
656
657        Log.d(TAG, "setPortRoles Package:" + mContext.getPackageName());
658        try {
659            mService.setPortRoles(port.getId(), powerRole, dataRole);
660        } catch (RemoteException e) {
661            throw e.rethrowFromSystemServer();
662        }
663    }
664
665    /**
666     * Sets the component that will handle USB device connection.
667     * <p>
668     * Setting component allows to specify external USB host manager to handle use cases, where
669     * selection dialog for an activity that will handle USB device is undesirable.
670     * Only system components can call this function, as it requires the MANAGE_USB permission.
671     *
672     * @param usbDeviceConnectionHandler The component to handle usb connections,
673     * {@code null} to unset.
674     *
675     * {@hide}
676     */
677    public void setUsbDeviceConnectionHandler(@Nullable ComponentName usbDeviceConnectionHandler) {
678        try {
679            mService.setUsbDeviceConnectionHandler(usbDeviceConnectionHandler);
680        } catch (RemoteException e) {
681            throw e.rethrowFromSystemServer();
682        }
683    }
684
685    /** @hide */
686    public static String addFunction(String functions, String function) {
687        if (USB_FUNCTION_NONE.equals(functions)) {
688            return function;
689        }
690        if (!containsFunction(functions, function)) {
691            if (functions.length() > 0) {
692                functions += ",";
693            }
694            functions += function;
695        }
696        return functions;
697    }
698
699    /** @hide */
700    public static String removeFunction(String functions, String function) {
701        String[] split = functions.split(",");
702        for (int i = 0; i < split.length; i++) {
703            if (function.equals(split[i])) {
704                split[i] = null;
705            }
706        }
707        if (split.length == 1 && split[0] == null) {
708            return USB_FUNCTION_NONE;
709        }
710        StringBuilder builder = new StringBuilder();
711        for (int i = 0; i < split.length; i++) {
712            String s = split[i];
713            if (s != null) {
714                if (builder.length() > 0) {
715                    builder.append(",");
716                }
717                builder.append(s);
718            }
719        }
720        return builder.toString();
721    }
722
723    /** @hide */
724    public static boolean containsFunction(String functions, String function) {
725        int index = functions.indexOf(function);
726        if (index < 0) return false;
727        if (index > 0 && functions.charAt(index - 1) != ',') return false;
728        int charAfter = index + function.length();
729        if (charAfter < functions.length() && functions.charAt(charAfter) != ',') return false;
730        return true;
731    }
732}
733