1 /*
2  * Copyright (C) 2024 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 package com.android.nfc;
18 
19 import android.content.Context;
20 import android.os.Binder;
21 import android.os.Process;
22 import android.os.RemoteException;
23 import android.text.TextUtils;
24 
25 import com.android.modules.utils.BasicShellCommandHandler;
26 
27 import java.io.PrintWriter;
28 
29 /**
30  * Interprets and executes 'adb shell cmd nfc [args]'.
31  *
32  * To add new commands:
33  * - onCommand: Add a case "<command>" execute. Return a 0
34  *   if command executed successfully.
35  * - onHelp: add a description string.
36  *
37  * Permissions: currently root permission is required for some commands. Others will
38  * enforce the corresponding API permissions.
39  */
40 public class NfcShellCommand extends BasicShellCommandHandler {
41     private static final int DISABLE_POLLING_FLAGS = 0x1000;
42     private static final int ENABLE_POLLING_FLAGS = 0x0000;
43 
44     // These don't require root access. However, these do perform permission checks in the
45     // corresponding binder methods in mNfcService.mNfcAdapter.
46     // Note: Any time you invoke a method from an internal class, consider making it privileged
47     // since these shell commands are available on production builds, we don't want apps to use
48     // this command to bypass security restrictions. mNfcService.mNfcAdapter binder
49     // methods already enforce permissions of the invoking shell (non-rooted shell has limited
50     // set of privileges).
51     private static final String[] NON_PRIVILEGED_COMMANDS = {
52             "help",
53             "disable-nfc",
54             "enable-nfc",
55             "status",
56     };
57     private final NfcService mNfcService;
58     private final Context mContext;
59 
NfcShellCommand(NfcService nfcService, Context context)60     NfcShellCommand(NfcService nfcService, Context context) {
61         mNfcService = nfcService;
62         mContext = context;
63     }
64 
65     @Override
onCommand(String cmd)66     public int onCommand(String cmd) {
67         // Treat no command as help command.
68         if (cmd == null || cmd.equals("")) {
69             cmd = "help";
70         }
71         // Explicit exclusion from root permission
72         if (ArrayUtils.indexOf(NON_PRIVILEGED_COMMANDS, cmd) == -1) {
73             final int uid = Binder.getCallingUid();
74             if (uid != Process.ROOT_UID) {
75                 throw new SecurityException(
76                         "Uid " + uid + " does not have access to " + cmd + " nfc command "
77                                 + "(or such command doesn't exist)");
78             }
79         }
80 
81         final PrintWriter pw = getOutPrintWriter();
82         try {
83             switch (cmd) {
84                 case "status":
85                     printStatus(pw);
86                     return 0;
87                 case "disable-nfc":
88                     String stringSaveState = getNextArg();
89                     boolean saveState = false;
90                     if (TextUtils.equals(stringSaveState, "[persist]")) {
91                         saveState = true;
92                     }
93                     mNfcService.mNfcAdapter.disable(saveState, mContext.getPackageName());
94                     return 0;
95                 case "enable-nfc":
96                     mNfcService.mNfcAdapter.enable(mContext.getPackageName());
97                     return 0;
98                 case "set-reader-mode":
99                     boolean enable_polling =
100                             getNextArgRequiredTrueOrFalse("enable-polling", "disable-polling");
101                     int flags = enable_polling ? ENABLE_POLLING_FLAGS : DISABLE_POLLING_FLAGS;
102                     mNfcService.mNfcAdapter.setReaderMode(new Binder(), null, flags, null);
103                     return 0;
104                 case "set-observe-mode":
105                     boolean enable = getNextArgRequiredTrueOrFalse("enable", "disable");
106                     mNfcService.mNfcAdapter.setObserveMode(enable, mContext.getPackageName());
107                     return 0;
108                 case "set-controller-always-on":
109                     boolean enableAlwaysOn = getNextArgRequiredTrueOrFalse("enable", "disable");
110                     mNfcService.mNfcAdapter.setControllerAlwaysOn(enableAlwaysOn);
111                     return 0;
112                 case "set-discovery-tech":
113                     int pollTech = Integer.parseInt(getNextArg());
114                     int listenTech = Integer.parseInt(getNextArg());
115                     mNfcService.mNfcAdapter.updateDiscoveryTechnology(
116                             new Binder(), pollTech, listenTech);
117                     return 0;
118                 default:
119                     return handleDefaultCommands(cmd);
120             }
121         } catch (IllegalArgumentException e) {
122             pw.println("Invalid args for " + cmd + ": ");
123             e.printStackTrace(pw);
124             return -1;
125         } catch (Exception e) {
126             pw.println("Exception while executing nfc shell command" + cmd + ": ");
127             e.printStackTrace(pw);
128             return -1;
129         }
130     }
131 
argTrueOrFalse(String arg, String trueString, String falseString)132     private static boolean argTrueOrFalse(String arg, String trueString, String falseString) {
133         if (trueString.equals(arg)) {
134             return true;
135         } else if (falseString.equals(arg)) {
136             return false;
137         } else {
138             throw new IllegalArgumentException("Expected '" + trueString + "' or '" + falseString
139                     + "' as next arg but got '" + arg + "'");
140         }
141 
142     }
143 
getNextArgRequiredTrueOrFalse(String trueString, String falseString)144     private boolean getNextArgRequiredTrueOrFalse(String trueString, String falseString)
145             throws IllegalArgumentException {
146         String nextArg = getNextArgRequired();
147         return argTrueOrFalse(nextArg, trueString, falseString);
148     }
149 
printStatus(PrintWriter pw)150     private void printStatus(PrintWriter pw) throws RemoteException {
151         pw.println("Nfc is " + (mNfcService.isNfcEnabled() ? "enabled" : "disabled"));
152     }
153 
onHelpNonPrivileged(PrintWriter pw)154     private void onHelpNonPrivileged(PrintWriter pw) {
155         pw.println("  status");
156         pw.println("    Gets status of UWB stack");
157         pw.println("  enable-nfc");
158         pw.println("    Toggle NFC on");
159         pw.println("  disable-nfc [persist]");
160         pw.println("    Toggle NFC off (optionally make it persistent)");
161     }
162 
onHelpPrivileged(PrintWriter pw)163     private void onHelpPrivileged(PrintWriter pw) {
164         pw.println("  set-observe-mode enable|disable");
165         pw.println("    Enable or disable observe mode.");
166         pw.println("  set-reader-mode enable-polling|disable-polling");
167         pw.println("    Enable or reader mode polling");
168         pw.println("  set-controller-always-on enable|disable");
169         pw.println("    Enable or disable controller always on");
170         pw.println("  set-discovery-tech poll-mask|listen-mask");
171     }
172 
173     @Override
onHelp()174     public void onHelp() {
175         final PrintWriter pw = getOutPrintWriter();
176         pw.println("NFC (Near-field communication) commands:");
177         pw.println("  help or -h");
178         pw.println("    Print this help text.");
179         onHelpNonPrivileged(pw);
180         if (Binder.getCallingUid() == Process.ROOT_UID) {
181             onHelpPrivileged(pw);
182         }
183         pw.println();
184     }
185 }
186