content: /* Android IMSI-Catcher Detector | (c) AIMSICD Privacy Project
   * -----------------------------------------------------------
   * LICENSE:  http://git.io/vki47 | TERMS:  http://git.io/vki4o
   * -----------------------------------------------------------
   */
  package zz.aimsicd.lite.ui.fragments;
  
  import android.annotation.SuppressLint;
  
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.text.Editable;
  import android.util.Log;
  import android.view.View;
  import android.widget.AdapterView;
  import android.widget.ArrayAdapter;
  import android.widget.Button;
  import android.widget.EditText;
  import android.widget.RelativeLayout;
  import android.widget.Spinner;
  import android.widget.TextView;
  
  import com.stericson.RootShell.RootShell;
  import com.stericson.RootShell.execution.Command;
  import com.stericson.RootShell.execution.Shell;
  
  import java.io.File;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.List;
  
  import io.freefair.android.injection.annotation.InjectView;
  import io.freefair.android.injection.annotation.XmlLayout;
  import io.freefair.android.injection.app.InjectionFragment;
  
  import zz.aimsicd.lite.R;
  import zz.aimsicd.lite.utils.Helpers;
  import zz.aimsicd.lite.utils.atcmd.AtCommandTerminal;
  import zz.aimsicd.lite.utils.atcmd.TtyPrivFile;
  
  
  
  /**
   *  Description:    This is the AT Command Interface or AT Command Processor (ATCoP) that
   *                  allow the user to communicate directly to the baseband processor (BP),
   *                  via old-school AT commands. This can be very useful for debugging radio
   *                  related network problems on those devices that are using this interface.
   *                  The most common baseband hardware that allow for this are those made by
   *                  Qualcomm (MSM) or Mediatek (MTK). Intel XMM based devices have been found
   *                  very difficult or impossible to use this, especailly on Samsung devices.
   *
   *  Requirements:   1) You need to have supported hardware that already has an AT serial device
   *                  enumerated in the Android /dev tree. Some common ones are:
   *                      Qualcomm:   /dev/smd[0,7]
   *                      MTK:        /dev/radio/atci[0-9]
   *                      XMM:        TBA
   *
   *                  2) You need to be rooted as this interface is using a persistent root shell.
   *
   *  Issues:
   *              [ ] Need to increase time for long AT commands like "AT+COPS=?" (~30 sec)
   *              [ ] Need a "no" timeout to watch output for while, or let's make it 10 minutes.
   *                  Perhaps with a manual stop?
   */
  @XmlLayout(R.layout.activity_at_command)
  public class AtCommandFragment extends InjectionFragment {
  
      public static final String TAG = "AICDL";
      public static final String mTAG = "XXX";
  
      //Return value constants
      private static final int SERIAL_INIT_OK = 100;
      private static final int SERIAL_INIT_ERROR = 101;
      private static final int ROOT_UNAVAILABLE = 102;
      private static final int BUSYBOX_UNAVAILABLE = 103;
      private static final List<String> mSerialDevices = new ArrayList<>();
  
      private String mSerialDevice;
      private int mTimeout;
  
      @InjectView(R.id.atcommandView)
      private RelativeLayout mAtCommandLayout;
  
      @InjectView(R.id.at_command_error)
      private TextView mAtCommandError;
  
      @InjectView(R.id.serial_device)
      private TextView mSerialDeviceDisplay;
  
      @InjectView(R.id.response)
      private TextView mAtResponse;
  
      @InjectView(zz.aimsicd.lite.R.id.at_command)
      private EditText mAtCommand;
  
      @InjectView(R.id.serial_device_spinner)
      private Spinner mSerialDeviceSpinner;
  
      @InjectView(zz.aimsicd.lite.R.id.serial_device_spinner_title)
      private TextView mSerialDeviceSpinnerLabel;
  
      private AtCommandTerminal mCommandTerminal;
  
      @InjectView(R.id.timeout_spinner)
      private Spinner timeoutSpinner;
  
      @InjectView(zz.aimsicd.lite.R.id.execute)
      private Button atCommandExecute;
  
      @Override
      public void onViewCreated(View view, Bundle savedInstanceState) {
          super.onViewCreated(view, savedInstanceState);
          atCommandExecute.setOnClickListener(new btnClick());
          mSerialDeviceSpinner.setOnItemSelectedListener(new spinnerListener());
          timeoutSpinner.setOnItemSelectedListener(new timeoutSpinnerListener());
          timeoutSpinner.setSelection(1);
          mTimeout = 5000;
      }
  
      private class timeoutSpinnerListener implements AdapterView.OnItemSelectedListener {
  
          @Override
          public void onItemSelected(AdapterView<?> parentView, View selectedItemView,
                  int position, long id) {
              switch (position) {
                  // Don't forget to also change the arrays.xml
                  case 0: //2 seconds
                      mTimeout = 2000;
                      break;
                  case 1: //5 seconds
                      mTimeout = 5000;
                      break;
                  case 2: //10 seconds
                      mTimeout = 10000;
                      break;
                  case 3: //20 seconds
                      mTimeout = 20000;
                      break;
                  case 4: //30 seconds
                      mTimeout = 30000;
                      break;
                  case 5: // No timeout, when watching output...
                      mTimeout = 600000; // Well, ok, 10 min
                      break;
                  default:
                      mTimeout = 5000;
              }
          }
  
          @Override
          public void onNothingSelected(AdapterView<?> parentView) {
          }
      }
  
      private class spinnerListener implements AdapterView.OnItemSelectedListener {
  
          @Override
          public void onItemSelected(AdapterView<?> parentView, View selectedItemView,
                  int position, long id) {
              mSerialDevice = String.valueOf(mSerialDeviceSpinner.getSelectedItem());
              mSerialDeviceDisplay.setText(mSerialDevice);
              setSerialDevice();
          }
  
          @Override
          public void onNothingSelected(AdapterView<?> parentView) {
  
          }
      }
  
      @Override
      public void onDestroy() {
          super.onDestroy();
          if (mCommandTerminal != null) {
              mCommandTerminal.dispose();
          }
      }
  
      @Override
      public void onResume() {
          super.onResume();
  
          int serialDevice = initSerialDevice();
  
          switch (serialDevice) {
              case SERIAL_INIT_OK:
                  mAtCommandLayout.setVisibility(View.VISIBLE);
                  break;
              case ROOT_UNAVAILABLE:
                  mAtCommandError.setText(zz.aimsicd.lite.R.string.unable_to_acquire_root_access);
                  break;
              case BUSYBOX_UNAVAILABLE:
                  mAtCommandError.setText(zz.aimsicd.lite.R.string.unable_to_detect_busybox);
                  break;
              case SERIAL_INIT_ERROR:
                  mAtCommandError.setText(R.string.unknown_error_trying_to_acquire_serial_device);
                  break;
              default:
                  mAtCommandError.setText(R.string.unknown_error_initialising_at_command_injector);
                  break;
          }
  
      }
  
      private class btnClick implements View.OnClickListener {
  
          @Override
          public void onClick(View v) {
              if (mAtCommand.getText() != null) {
                  String command = mAtCommand.getText().toString();
                  Log.i(TAG, mTAG + "AT Command Detected: " + command);
                  executeAT();
              }
          }
      }
  
      /**
       *  Description:    This is looking for possible serial devices that may be used for ATCoP.
       *
       *  Issues:         This is generally not working since it is very HW and SW dependent.
       *
       *
       * @return
       */
      private int initSerialDevice() {
  
          /*
           * NOTE:
           *
           *      Because of how RootShell is being used the handler has to be disabled.
           *
           *      With the handler disabled absolutely NO UI work can be done in the callback methods
           *      since they will be called on a separate thread.
           *
           *      To work around this, either:
           *
           *      a) Execute all Shell commands in a thread, such as AsyncTask
           *          OR
           *      b) Stop using commandWait (which is a no no...you should never sleep on the
           *         main UI thread) and implement the callback commandFinished/commandTerminated
           *         to determine when to continue on.
           *
           */
          RootShell.handlerEnabled = false;
  
          // Check for root access
          boolean root = RootShell.isAccessGiven();
          if (!root) {
              return ROOT_UNAVAILABLE;
          }
  
          // Check if Busybox is installed
          boolean busybox = RootShell.isBusyboxAvailable();
          if (!busybox) {
              return BUSYBOX_UNAVAILABLE;
          }
  
          try {
              mAtResponse.setText(R.string.at_command_response_looking);
              mSerialDevices.clear();
  
              // THIS IS A BAD IDEA       TODO: Consider removing
              // Use RIL Serial Device details from the System Property
              try {
                  String rilDevice = Helpers.getSystemProp(getActivity(), "rild.libargs", "UNKNOWN");
                  mSerialDevice = ("UNKNOWN".equals(rilDevice) ? rilDevice : rilDevice.substring(3));
  
                  if (!"UNKNOWN".equals(mSerialDevice)) {
                      mSerialDevices.add(mSerialDevice);
                  }
              } catch (StringIndexOutOfBoundsException e) {
                 Log.w(TAG, mTAG + e.getMessage());
                  // ignore, move on
              }
  
              //==================================================================
              // WARNING:  Scraping commands can be masked by aliases in: mkshrc
              //           and even hardcoded in the sh binary or elsewhere.
              //           To get unaliased versions, use: "\\<command>"
              //==================================================================
  
              for (File file : new File("/dev").listFiles()) {
                  String name = file.getName();
                  boolean add = false;
                  // QC: /dev/smd[0-7]
                  if (name.matches("^smd.$")) {
                      add = true;
                  } else if ("radio".equals(name)) {
                      // MTK: /dev/radio/*atci*
                      for (File subfile : file.listFiles()) {
                          String subname = subfile.getName();
                          if (subname.contains("atci")) {
                              add = true;
                              file = subfile;
                          }
                      }
                  }
  
                  if (add) {
                      mSerialDevices.add(file.getAbsolutePath());
                      mAtResponse.append(getString(R.string.at_command_response_found) + file.getAbsolutePath() + "\n");
                  }
              }
  
              // Now try XMM/XGOLD modem config
              File xgold = new File("/system/etc/ril_xgold_radio.cfg");
              if (xgold.exists() && xgold.isFile()) {
                  Command cmd = new Command(1, "\\cat /system/etc/ril_xgold_radio.cfg | "
                                              + "\\grep -E \"atport*|dataport*\"") {
  
                      @Override
                      public void commandOutput(int id, String line) {
                          if (id == 0) {
                              if (!line.trim().isEmpty() && line.contains("/dev/")) {
                                  int place = line.indexOf("=") + 1;
                                  mSerialDevices.add(line.substring(place, line.length() - 1));
                                  mAtResponse.append(getString(R.string.at_command_response_found) + line.substring(place, line.length() - 1) + "\n");
                              }
                          }
                          super.commandOutput(id, line);
                      }
                  };
  
                  Shell shell = RootShell.getShell(true);
                  shell.add(cmd);
                  commandWait(shell, cmd);
              }
  
          } catch (Exception e) {
             Log.e(TAG, mTAG + "InitSerialDevice ", e);
          }
  
          if (!mSerialDevices.isEmpty()) {
              String[] entries = new String[mSerialDevices.size()];
              entries = mSerialDevices.toArray(entries);
              ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(getActivity(),
                      android.R.layout.simple_spinner_item, entries);
              mSerialDeviceSpinner.setAdapter(spinnerAdapter);
              mSerialDeviceSpinner.setVisibility(View.VISIBLE);
              mSerialDeviceSpinnerLabel.setVisibility(View.VISIBLE);
          }
  
          mAtResponse.append(getString(R.string.at_command_response_setup_complete));
          mAtResponse.setVisibility(View.VISIBLE);
  
          return SERIAL_INIT_OK;
      }
  
      private void setSerialDevice() {
          if (mCommandTerminal != null) {
              mCommandTerminal.dispose();
              mCommandTerminal = null;
          }
      }
  
      private AtCommandTerminal getSerialDevice() {
          if (mCommandTerminal == null) {
              try {
                  mCommandTerminal = new TtyPrivFile(mSerialDevice);
                  return mCommandTerminal;
              } catch (IOException e) {
                  mAtResponse.append(e.toString());
              }
          } else {
              return mCommandTerminal;
          }
          return null;
      }
  
      private void executeAT() {
          // It seem that MTK devices doesn't need "\r" but QC devices do.
          // We need a device-type check here, perhaps: gsm.version.ril-impl.
          Editable cmd = mAtCommand.getText();
          if (cmd != null && cmd.length() != 0) {
             Log.d(TAG, mTAG + "ExecuteAT: attempting to send: " + cmd.toString());
  
              if (getSerialDevice() != null) {
                  mCommandTerminal.send(cmd.toString(), new Handler(Looper.getMainLooper()) {
                      @Override
                      public void handleMessage(Message message) {
                          if (message.obj instanceof List) {
                              // Fixme: unchecked cast
                              List<String> lines = ((List<String>) message.obj);
                              StringBuffer response = new StringBuffer();
                              for (String line : lines) {
                                  response.append(line);
                                  response.append('\n');
                              }
                              if (response.length() != 0) {
                                  mAtResponse.append(response);
                              }
  
                          } else if (message.obj instanceof IOException) {
                              mAtResponse.append("IOException: " + ((IOException) message.obj).getMessage() + "\n");
                          }
                      }
                  }.obtainMessage());
              }
          }
  
      }
  
      /**
       * This below method is part of the RootTools Project: https://github.com/Stericson/RootTools
       * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
       *
       * Slightly modified commandWait method as found in RootToolsInternalMethods.java to utilise
       * the user selected timeout value.
       *
       */
      private void commandWait(Shell shell, Command cmd) throws Exception {
          while (!cmd.isFinished()) {
              synchronized (cmd) {
                  try {
                      if (!cmd.isFinished()) {
                          cmd.wait(mTimeout);
                      }
                  } catch (InterruptedException e) {
                     Log.e(TAG, mTAG + e.getMessage());
                  }
              }
              if (!cmd.isExecuting() && !cmd.isFinished()) {
                  Exception e = new Exception();
  
                  if (!shell.isExecuting && !shell.isReading) {
                     Log.w(TAG, mTAG + "Waiting for a command to be executed in a shell that is not executing and not reading! \n\n Command: " + cmd.getCommand());
                      e.setStackTrace(Thread.currentThread().getStackTrace());
                     Log.e(TAG, mTAG + e.getMessage(), e);
  
                  } else if (shell.isExecuting && !shell.isReading) {
                     Log.e(TAG, mTAG + "Waiting for a command to be executed in a shell that is executing but not reading! \n\n Command: " + cmd.getCommand());
                      e.setStackTrace(Thread.currentThread().getStackTrace());
                     Log.e(TAG, mTAG + e.getMessage(), e);
  
                  } else {
                     Log.e(TAG, mTAG + "Waiting for a command to be executed in a shell that is not reading! \n\n Command: " + cmd.getCommand());
                      e.setStackTrace(Thread.currentThread().getStackTrace());
                     Log.e(TAG, mTAG + e.getMessage(), e);
                  }
              }
          }
      }
  }
  