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 an
14 * limitations under the License.
15 */
16
17package com.android.server.usb;
18
19import android.annotation.NonNull;
20import android.annotation.UserIdInt;
21import android.app.PendingIntent;
22import android.app.admin.DevicePolicyManager;
23import android.content.BroadcastReceiver;
24import android.content.ComponentName;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.content.pm.PackageManager;
29import android.hardware.usb.IUsbManager;
30import android.hardware.usb.UsbAccessory;
31import android.hardware.usb.UsbDevice;
32import android.hardware.usb.UsbManager;
33import android.hardware.usb.UsbPort;
34import android.hardware.usb.UsbPortStatus;
35import android.os.Binder;
36import android.os.Bundle;
37import android.os.ParcelFileDescriptor;
38import android.os.UserHandle;
39import android.os.UserManager;
40import android.util.Slog;
41
42import com.android.internal.annotations.GuardedBy;
43import com.android.internal.util.DumpUtils;
44import com.android.internal.util.IndentingPrintWriter;
45import com.android.internal.util.Preconditions;
46import com.android.server.SystemService;
47
48import java.io.File;
49import java.io.FileDescriptor;
50import java.io.PrintWriter;
51
52/**
53 * UsbService manages all USB related state, including both host and device support.
54 * Host related events and calls are delegated to UsbHostManager, and device related
55 * support is delegated to UsbDeviceManager.
56 */
57public class UsbService extends IUsbManager.Stub {
58
59    public static class Lifecycle extends SystemService {
60        private UsbService mUsbService;
61
62        public Lifecycle(Context context) {
63            super(context);
64        }
65
66        @Override
67        public void onStart() {
68            mUsbService = new UsbService(getContext());
69            publishBinderService(Context.USB_SERVICE, mUsbService);
70        }
71
72        @Override
73        public void onBootPhase(int phase) {
74            if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
75                mUsbService.systemReady();
76            } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
77                mUsbService.bootCompleted();
78            }
79        }
80
81        @Override
82        public void onSwitchUser(int newUserId) {
83            mUsbService.onSwitchUser(newUserId);
84        }
85
86        @Override
87        public void onStopUser(int userHandle) {
88            mUsbService.onStopUser(UserHandle.of(userHandle));
89        }
90    }
91
92    private static final String TAG = "UsbService";
93
94    private final Context mContext;
95    private final UserManager mUserManager;
96
97    private UsbDeviceManager mDeviceManager;
98    private UsbHostManager mHostManager;
99    private UsbPortManager mPortManager;
100    private final UsbAlsaManager mAlsaManager;
101
102    private final UsbSettingsManager mSettingsManager;
103
104    /**
105     * The user id of the current user. There might be several profiles (with separate user ids)
106     * per user.
107     */
108    @GuardedBy("mLock")
109    private @UserIdInt int mCurrentUserId;
110
111    private final Object mLock = new Object();
112
113    private UsbUserSettingsManager getSettingsForUser(@UserIdInt int userIdInt) {
114        return mSettingsManager.getSettingsForUser(userIdInt);
115    }
116
117    public UsbService(Context context) {
118        mContext = context;
119
120        mUserManager = context.getSystemService(UserManager.class);
121        mSettingsManager = new UsbSettingsManager(context);
122        mAlsaManager = new UsbAlsaManager(context);
123
124        final PackageManager pm = mContext.getPackageManager();
125        if (pm.hasSystemFeature(PackageManager.FEATURE_USB_HOST)) {
126            mHostManager = new UsbHostManager(context, mAlsaManager, mSettingsManager);
127        }
128        if (new File("/sys/class/android_usb").exists()) {
129            mDeviceManager = new UsbDeviceManager(context, mAlsaManager, mSettingsManager);
130        }
131        if (mHostManager != null || mDeviceManager != null) {
132            mPortManager = new UsbPortManager(context);
133        }
134
135        onSwitchUser(UserHandle.USER_SYSTEM);
136
137        final IntentFilter filter = new IntentFilter();
138        filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
139        filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
140        mContext.registerReceiver(mReceiver, filter, null, null);
141    }
142
143    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
144        @Override
145        public void onReceive(Context context, Intent intent) {
146            final String action = intent.getAction();
147            if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED
148                    .equals(action)) {
149                if (mDeviceManager != null) {
150                    mDeviceManager.updateUserRestrictions();
151                }
152            }
153        }
154    };
155
156    /**
157     * Set new {@link #mCurrentUserId} and propagate it to other modules.
158     *
159     * @param newUserId The user id of the new current user.
160     */
161    private void onSwitchUser(@UserIdInt int newUserId) {
162        synchronized (mLock) {
163            mCurrentUserId = newUserId;
164
165            // The following two modules need to know about the current profile group. If they need
166            // to distinguish by profile of the user, the id has to be passed in the call to the
167            // module.
168            UsbProfileGroupSettingsManager settings =
169                    mSettingsManager.getSettingsForProfileGroup(UserHandle.of(newUserId));
170            if (mHostManager != null) {
171                mHostManager.setCurrentUserSettings(settings);
172            }
173            if (mDeviceManager != null) {
174                mDeviceManager.setCurrentUser(newUserId, settings);
175            }
176        }
177    }
178
179    /**
180     * Execute operations when a user is stopped.
181     *
182     * @param stoppedUser The user that is stopped
183     */
184    private void onStopUser(@NonNull UserHandle stoppedUser) {
185        mSettingsManager.remove(stoppedUser);
186    }
187
188    public void systemReady() {
189        mAlsaManager.systemReady();
190
191        if (mDeviceManager != null) {
192            mDeviceManager.systemReady();
193        }
194        if (mHostManager != null) {
195            mHostManager.systemReady();
196        }
197        if (mPortManager != null) {
198            mPortManager.systemReady();
199        }
200    }
201
202    public void bootCompleted() {
203        if (mDeviceManager != null) {
204            mDeviceManager.bootCompleted();
205        }
206    }
207
208    /* Returns a list of all currently attached USB devices (host mdoe) */
209    @Override
210    public void getDeviceList(Bundle devices) {
211        if (mHostManager != null) {
212            mHostManager.getDeviceList(devices);
213        }
214    }
215
216    /**
217     * Check if the calling user is in the same profile group as the {@link #mCurrentUserId
218     * current user}.
219     *
220     * @return Iff the caller is in the current user's profile group
221     */
222    private boolean isCallerInCurrentUserProfileGroupLocked() {
223        int userIdInt = UserHandle.getCallingUserId();
224
225        long ident = clearCallingIdentity();
226        try {
227            return mUserManager.isSameProfileGroup(userIdInt, mCurrentUserId);
228        } finally {
229            restoreCallingIdentity(ident);
230        }
231    }
232
233    /* Opens the specified USB device (host mode) */
234    @Override
235    public ParcelFileDescriptor openDevice(String deviceName) {
236        ParcelFileDescriptor fd = null;
237
238        if (mHostManager != null) {
239            synchronized (mLock) {
240                if (deviceName != null) {
241                    int userIdInt = UserHandle.getCallingUserId();
242                    boolean isCurrentUser = isCallerInCurrentUserProfileGroupLocked();
243
244                    if (isCurrentUser) {
245                        fd = mHostManager.openDevice(deviceName, getSettingsForUser(userIdInt));
246                    } else {
247                        Slog.w(TAG, "Cannot open " + deviceName + " for user " + userIdInt +
248                               " as user is not active.");
249                    }
250                }
251            }
252        }
253
254        return fd;
255    }
256
257    /* returns the currently attached USB accessory (device mode) */
258    @Override
259    public UsbAccessory getCurrentAccessory() {
260        if (mDeviceManager != null) {
261            return mDeviceManager.getCurrentAccessory();
262        } else {
263            return null;
264        }
265    }
266
267    /* opens the currently attached USB accessory (device mode) */
268    @Override
269    public ParcelFileDescriptor openAccessory(UsbAccessory accessory) {
270        if (mDeviceManager != null) {
271            int userIdInt = UserHandle.getCallingUserId();
272
273            synchronized (mLock) {
274                boolean isCurrentUser = isCallerInCurrentUserProfileGroupLocked();
275
276                if (isCurrentUser) {
277                    return mDeviceManager.openAccessory(accessory, getSettingsForUser(userIdInt));
278                } else {
279                    Slog.w(TAG, "Cannot open " + accessory + " for user " + userIdInt +
280                            " as user is not active.");
281                }
282            }
283        }
284
285        return null;
286    }
287
288    @Override
289    public void setDevicePackage(UsbDevice device, String packageName, int userId) {
290        device = Preconditions.checkNotNull(device);
291
292        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
293
294        UserHandle user = UserHandle.of(userId);
295        mSettingsManager.getSettingsForProfileGroup(user).setDevicePackage(device, packageName,
296                user);
297    }
298
299    @Override
300    public void setAccessoryPackage(UsbAccessory accessory, String packageName, int userId) {
301        accessory = Preconditions.checkNotNull(accessory);
302
303        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
304
305        UserHandle user = UserHandle.of(userId);
306        mSettingsManager.getSettingsForProfileGroup(user).setAccessoryPackage(accessory,
307                packageName, user);
308    }
309
310    @Override
311    public boolean hasDevicePermission(UsbDevice device) {
312        final int userId = UserHandle.getCallingUserId();
313        return getSettingsForUser(userId).hasPermission(device);
314    }
315
316    @Override
317    public boolean hasAccessoryPermission(UsbAccessory accessory) {
318        final int userId = UserHandle.getCallingUserId();
319        return getSettingsForUser(userId).hasPermission(accessory);
320    }
321
322    @Override
323    public void requestDevicePermission(UsbDevice device, String packageName, PendingIntent pi) {
324        final int userId = UserHandle.getCallingUserId();
325        getSettingsForUser(userId).requestPermission(device, packageName, pi);
326    }
327
328    @Override
329    public void requestAccessoryPermission(
330            UsbAccessory accessory, String packageName, PendingIntent pi) {
331        final int userId = UserHandle.getCallingUserId();
332        getSettingsForUser(userId).requestPermission(accessory, packageName, pi);
333    }
334
335    @Override
336    public void grantDevicePermission(UsbDevice device, int uid) {
337        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
338        final int userId = UserHandle.getUserId(uid);
339        getSettingsForUser(userId).grantDevicePermission(device, uid);
340    }
341
342    @Override
343    public void grantAccessoryPermission(UsbAccessory accessory, int uid) {
344        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
345        final int userId = UserHandle.getUserId(uid);
346        getSettingsForUser(userId).grantAccessoryPermission(accessory, uid);
347    }
348
349    @Override
350    public boolean hasDefaults(String packageName, int userId) {
351        packageName = Preconditions.checkStringNotEmpty(packageName);
352
353        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
354
355        UserHandle user = UserHandle.of(userId);
356        return mSettingsManager.getSettingsForProfileGroup(user).hasDefaults(packageName, user);
357    }
358
359    @Override
360    public void clearDefaults(String packageName, int userId) {
361        packageName = Preconditions.checkStringNotEmpty(packageName);
362
363        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
364
365        UserHandle user = UserHandle.of(userId);
366        mSettingsManager.getSettingsForProfileGroup(user).clearDefaults(packageName, user);
367    }
368
369    @Override
370    public boolean isFunctionEnabled(String function) {
371        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
372        return mDeviceManager != null && mDeviceManager.isFunctionEnabled(function);
373    }
374
375    @Override
376    public void setCurrentFunction(String function, boolean usbDataUnlocked) {
377        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
378
379        if (!isSupportedCurrentFunction(function)) {
380            Slog.w(TAG, "Caller of setCurrentFunction() requested unsupported USB function: "
381                    + function);
382            function = UsbManager.USB_FUNCTION_NONE;
383        }
384
385        if (mDeviceManager != null) {
386            mDeviceManager.setCurrentFunctions(function, usbDataUnlocked);
387        } else {
388            throw new IllegalStateException("USB device mode not supported");
389        }
390    }
391
392    private static boolean isSupportedCurrentFunction(String function) {
393        if (function == null) return true;
394
395        switch (function) {
396            case UsbManager.USB_FUNCTION_NONE:
397            case UsbManager.USB_FUNCTION_AUDIO_SOURCE:
398            case UsbManager.USB_FUNCTION_MIDI:
399            case UsbManager.USB_FUNCTION_MTP:
400            case UsbManager.USB_FUNCTION_PTP:
401            case UsbManager.USB_FUNCTION_RNDIS:
402                return true;
403        }
404
405        return false;
406    }
407
408    @Override
409    public void allowUsbDebugging(boolean alwaysAllow, String publicKey) {
410        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
411        mDeviceManager.allowUsbDebugging(alwaysAllow, publicKey);
412    }
413
414    @Override
415    public void denyUsbDebugging() {
416        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
417        mDeviceManager.denyUsbDebugging();
418    }
419
420    @Override
421    public void clearUsbDebuggingKeys() {
422        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
423        mDeviceManager.clearUsbDebuggingKeys();
424    }
425
426    @Override
427    public UsbPort[] getPorts() {
428        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
429
430        final long ident = Binder.clearCallingIdentity();
431        try {
432            return mPortManager != null ? mPortManager.getPorts() : null;
433        } finally {
434            Binder.restoreCallingIdentity(ident);
435        }
436    }
437
438    @Override
439    public UsbPortStatus getPortStatus(String portId) {
440        Preconditions.checkNotNull(portId, "portId must not be null");
441        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
442
443        final long ident = Binder.clearCallingIdentity();
444        try {
445            return mPortManager != null ? mPortManager.getPortStatus(portId) : null;
446        } finally {
447            Binder.restoreCallingIdentity(ident);
448        }
449    }
450
451    @Override
452    public void setPortRoles(String portId, int powerRole, int dataRole) {
453        Preconditions.checkNotNull(portId, "portId must not be null");
454        UsbPort.checkRoles(powerRole, dataRole);
455        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
456
457        final long ident = Binder.clearCallingIdentity();
458        try {
459            if (mPortManager != null) {
460                mPortManager.setPortRoles(portId, powerRole, dataRole, null);
461            }
462        } finally {
463            Binder.restoreCallingIdentity(ident);
464        }
465    }
466
467    @Override
468    public void setUsbDeviceConnectionHandler(ComponentName usbDeviceConnectionHandler) {
469        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
470        synchronized (mLock) {
471            if (mCurrentUserId == UserHandle.getCallingUserId()) {
472                if (mHostManager != null) {
473                    mHostManager.setUsbDeviceConnectionHandler(usbDeviceConnectionHandler);
474                }
475            } else {
476                throw new IllegalArgumentException("Only the current user can register a usb " +
477                        "connection handler");
478            }
479        }
480    }
481
482    @Override
483    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
484        if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return;
485
486        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
487        final long ident = Binder.clearCallingIdentity();
488        try {
489            if (args == null || args.length == 0 || "-a".equals(args[0])) {
490                pw.println("USB Manager State:");
491                pw.increaseIndent();
492                if (mDeviceManager != null) {
493                    mDeviceManager.dump(pw);
494                }
495                if (mHostManager != null) {
496                    mHostManager.dump(pw);
497                }
498                if (mPortManager != null) {
499                    mPortManager.dump(pw);
500                }
501                mAlsaManager.dump(pw);
502
503                mSettingsManager.dump(pw);
504            } else if (args.length == 4 && "set-port-roles".equals(args[0])) {
505                final String portId = args[1];
506                final int powerRole;
507                switch (args[2]) {
508                    case "source":
509                        powerRole = UsbPort.POWER_ROLE_SOURCE;
510                        break;
511                    case "sink":
512                        powerRole = UsbPort.POWER_ROLE_SINK;
513                        break;
514                    case "no-power":
515                        powerRole = 0;
516                        break;
517                    default:
518                        pw.println("Invalid power role: " + args[2]);
519                        return;
520                }
521                final int dataRole;
522                switch (args[3]) {
523                    case "host":
524                        dataRole = UsbPort.DATA_ROLE_HOST;
525                        break;
526                    case "device":
527                        dataRole = UsbPort.DATA_ROLE_DEVICE;
528                        break;
529                    case "no-data":
530                        dataRole = 0;
531                        break;
532                    default:
533                        pw.println("Invalid data role: " + args[3]);
534                        return;
535                }
536                if (mPortManager != null) {
537                    mPortManager.setPortRoles(portId, powerRole, dataRole, pw);
538                    // Note: It might take some time for the side-effects of this operation
539                    // to be fully applied by the kernel since the driver may need to
540                    // renegotiate the USB port mode.  If this proves to be an issue
541                    // during debugging, it might be worth adding a sleep here before
542                    // dumping the new state.
543                    pw.println();
544                    mPortManager.dump(pw);
545                }
546            } else if (args.length == 3 && "add-port".equals(args[0])) {
547                final String portId = args[1];
548                final int supportedModes;
549                switch (args[2]) {
550                    case "ufp":
551                        supportedModes = UsbPort.MODE_UFP;
552                        break;
553                    case "dfp":
554                        supportedModes = UsbPort.MODE_DFP;
555                        break;
556                    case "dual":
557                        supportedModes = UsbPort.MODE_DUAL;
558                        break;
559                    case "none":
560                        supportedModes = 0;
561                        break;
562                    default:
563                        pw.println("Invalid mode: " + args[2]);
564                        return;
565                }
566                if (mPortManager != null) {
567                    mPortManager.addSimulatedPort(portId, supportedModes, pw);
568                    pw.println();
569                    mPortManager.dump(pw);
570                }
571            } else if (args.length == 5 && "connect-port".equals(args[0])) {
572                final String portId = args[1];
573                final int mode;
574                final boolean canChangeMode = args[2].endsWith("?");
575                switch (canChangeMode ? removeLastChar(args[2]) : args[2]) {
576                    case "ufp":
577                        mode = UsbPort.MODE_UFP;
578                        break;
579                    case "dfp":
580                        mode = UsbPort.MODE_DFP;
581                        break;
582                    default:
583                        pw.println("Invalid mode: " + args[2]);
584                        return;
585                }
586                final int powerRole;
587                final boolean canChangePowerRole = args[3].endsWith("?");
588                switch (canChangePowerRole ? removeLastChar(args[3]) : args[3]) {
589                    case "source":
590                        powerRole = UsbPort.POWER_ROLE_SOURCE;
591                        break;
592                    case "sink":
593                        powerRole = UsbPort.POWER_ROLE_SINK;
594                        break;
595                    default:
596                        pw.println("Invalid power role: " + args[3]);
597                        return;
598                }
599                final int dataRole;
600                final boolean canChangeDataRole = args[4].endsWith("?");
601                switch (canChangeDataRole ? removeLastChar(args[4]) : args[4]) {
602                    case "host":
603                        dataRole = UsbPort.DATA_ROLE_HOST;
604                        break;
605                    case "device":
606                        dataRole = UsbPort.DATA_ROLE_DEVICE;
607                        break;
608                    default:
609                        pw.println("Invalid data role: " + args[4]);
610                        return;
611                }
612                if (mPortManager != null) {
613                    mPortManager.connectSimulatedPort(portId, mode, canChangeMode,
614                            powerRole, canChangePowerRole, dataRole, canChangeDataRole, pw);
615                    pw.println();
616                    mPortManager.dump(pw);
617                }
618            } else if (args.length == 2 && "disconnect-port".equals(args[0])) {
619                final String portId = args[1];
620                if (mPortManager != null) {
621                    mPortManager.disconnectSimulatedPort(portId, pw);
622                    pw.println();
623                    mPortManager.dump(pw);
624                }
625            } else if (args.length == 2 && "remove-port".equals(args[0])) {
626                final String portId = args[1];
627                if (mPortManager != null) {
628                    mPortManager.removeSimulatedPort(portId, pw);
629                    pw.println();
630                    mPortManager.dump(pw);
631                }
632            } else if (args.length == 1 && "reset".equals(args[0])) {
633                if (mPortManager != null) {
634                    mPortManager.resetSimulation(pw);
635                    pw.println();
636                    mPortManager.dump(pw);
637                }
638            } else if (args.length == 1 && "ports".equals(args[0])) {
639                if (mPortManager != null) {
640                    mPortManager.dump(pw);
641                }
642            } else {
643                pw.println("Dump current USB state or issue command:");
644                pw.println("  ports");
645                pw.println("  set-port-roles <id> <source|sink|no-power> <host|device|no-data>");
646                pw.println("  add-port <id> <ufp|dfp|dual|none>");
647                pw.println("  connect-port <id> <ufp|dfp><?> <source|sink><?> <host|device><?>");
648                pw.println("    (add ? suffix if mode, power role, or data role can be changed)");
649                pw.println("  disconnect-port <id>");
650                pw.println("  remove-port <id>");
651                pw.println("  reset");
652                pw.println();
653                pw.println("Example USB type C port role switch:");
654                pw.println("  dumpsys usb set-port-roles \"default\" source device");
655                pw.println();
656                pw.println("Example USB type C port simulation with full capabilities:");
657                pw.println("  dumpsys usb add-port \"matrix\" dual");
658                pw.println("  dumpsys usb connect-port \"matrix\" ufp? sink? device?");
659                pw.println("  dumpsys usb ports");
660                pw.println("  dumpsys usb disconnect-port \"matrix\"");
661                pw.println("  dumpsys usb remove-port \"matrix\"");
662                pw.println("  dumpsys usb reset");
663                pw.println();
664                pw.println("Example USB type C port where only power role can be changed:");
665                pw.println("  dumpsys usb add-port \"matrix\" dual");
666                pw.println("  dumpsys usb connect-port \"matrix\" dfp source? host");
667                pw.println("  dumpsys usb reset");
668                pw.println();
669                pw.println("Example USB OTG port where id pin determines function:");
670                pw.println("  dumpsys usb add-port \"matrix\" dual");
671                pw.println("  dumpsys usb connect-port \"matrix\" dfp source host");
672                pw.println("  dumpsys usb reset");
673                pw.println();
674                pw.println("Example USB device-only port:");
675                pw.println("  dumpsys usb add-port \"matrix\" ufp");
676                pw.println("  dumpsys usb connect-port \"matrix\" ufp sink device");
677                pw.println("  dumpsys usb reset");
678            }
679        } finally {
680            Binder.restoreCallingIdentity(ident);
681        }
682    }
683
684    private static final String removeLastChar(String value) {
685        return value.substring(0, value.length() - 1);
686    }
687}
688