content: /* Android IMSI-Catcher Detector | (c) AIMSICD Privacy Project
   * -----------------------------------------------------------
   * LICENSE:  http://git.io/vki47 | TERMS:  http://git.io/vki4o
   * -----------------------------------------------------------
   */
  
  package zz.aimsicd.lite.smsdetection;
  
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.ServiceConnection;
  import android.os.IBinder;
  import android.support.annotation.StringRes;
  import android.support.v7.app.AlertDialog;
  import android.util.Log;
  import android.view.WindowManager;
  
  import java.io.BufferedReader;
  import java.io.DataOutputStream;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.util.ArrayList;
  import java.util.List;
  
  import zz.aimsicd.lite.R;
  import zz.aimsicd.lite.adapters.AIMSICDDbAdapter;
  import zz.aimsicd.lite.service.AimsicdService;
  import zz.aimsicd.lite.utils.MiscUtils;
  
  
  
  
  /**
   * Description: Detects mysterious SMS by scraping Logcat entries.
   *
   *
   * NOTES:   For this to work better Samsung users might have to set their Debug Level to High
   * in SysDump menu *#9900# or *#*#9900#*#*
   *
   * This is by no means a complete detection method but gives us something to work off.
   *
   * For latest list of working phones/models, please see:
   * https://github.com/SecUpwN/Android-IMSI-Catcher-Detector/issues/532
   *
   * PHONE:Samsung S5      MODEL:SM-G900F      ANDROID_VER:4.4.2   TYPE0:YES MWI:YES
   * PHONE:Samsung S4-min  MODEL:GT-I9195      ANDROID_VER:4.2.2   TYPE0:YES MWI:YES
   * PHONE:Sony Xperia J   MODEL:ST260i        ANDROID_VER:4.1.2   TYPE0:NO  MWI:YES
   *
   * To Use:
   *
   * SmsDetector smsDetector = new SmsDetector(context);
   *
   * smsDetector.startSmsDetection();
   * smsDetector.stopSmsDetection();
   *
   *
   * TODO:
   * [ ] Add more mTAG to the detection Log items
   *
   * @author Paul Kinsella @banjaxbanjo
   */
  public final class SmsDetector extends Thread {
  
      public static final String TAG = "AICDL";
      public static final String mTAG = "SmsDetector";
  
      private AimsicdService mAIMSICDService;
      private boolean mBound;
      private AIMSICDDbAdapter mDbAdapter;
      private Context mContext;
      private final String[] LOADED_DETECTION_STRINGS;
      private static final int TYPE0 = 1, MWI = 2, WAP = 3;
  
      // TODO: replace this with retrieval from AIMSICDDbAdapter
      private static final int LOGCAT_BUFFER_MAX_SIZE = 100;
  
      /**
       * To correctly detect sms data and phone numbers on wap, we need at least
       * 10 lines after line which indicates wap communication
       */
      private static final int LOGCAT_WAP_EXTRA_LINES = 10;
  
      private static boolean isRunning = false;
  
      public SmsDetector(Context context) {
          mContext = context;
          mDbAdapter = new AIMSICDDbAdapter(context);
  
          List<AdvanceUserItems> silent_string = mDbAdapter.getDetectionStrings();
  
          LOADED_DETECTION_STRINGS = new String[silent_string.size()];
          for (int x = 0; x < silent_string.size(); x++) {
              LOADED_DETECTION_STRINGS[x] = silent_string.get(x).getDetection_string()
                      + "#" + silent_string.get(x).getDetection_type();
          }
      }
  
      public static boolean getSmsDetectionState() {
          return isRunning;
      }
  
      public static void setSmsDetectionState(boolean isRunning) {
          SmsDetector.isRunning = isRunning;
      }
  
      public void startPopUpInfo(SmsType smsType) {
          MiscUtils.showNotification(
                  mContext,
                  mContext.getString(smsType.getAlert()),
                  mContext.getString(R.string.app_name_short) + " - " + mContext.getString(smsType.getTitle()),
                  R.drawable.sense_danger,
                  true);
  
          AlertDialog alertDialog = new AlertDialog.Builder(mContext)
                  .setTitle(smsType.getTitle())
                  .setMessage(smsType.getMessage())
                  .setIcon(R.drawable.sense_danger)
                  .create();
          alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
          alertDialog.show();
      }
  
      public void startSmsDetection() {
          Intent intent = new Intent(mContext, AimsicdService.class);
          mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
          start();
          Log.i(TAG, mTAG + "SMS detection started");
      }
  
      public void stopSmsDetection() {
          setSmsDetectionState(false);
          // Unbind from the service
          if (mBound) {
              mContext.unbindService(mConnection);
              mBound = false;
          }
          Log.i(TAG, mTAG + "SMS detection stopped");
      }
  
      @Override
      public void run() {
          setSmsDetectionState(true);
  
          BufferedReader mLogcatReader;
          try {
              Thread.sleep(500);
  
              String MODE = "logcat -v time -b radio -b main\n";
              Runtime r = Runtime.getRuntime();
              Process process = r.exec("su");
              DataOutputStream dos = new DataOutputStream(process.getOutputStream());
  
              dos.writeBytes(MODE);
              dos.flush();
              dos.close();
  
              mLogcatReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
          } catch (InterruptedException | IOException e) {
             Log.e(TAG, mTAG + "Exception while initializing LogCat (time, radio, main) reader", e);
              return;
          }
  
          String logcatLine;
          List<String> logcatLines = new ArrayList<>();
          while (getSmsDetectionState()) {
              try {
                  logcatLine = mLogcatReader.readLine();
                  if (logcatLines.size() <= LOGCAT_BUFFER_MAX_SIZE || logcatLine != null) {
                      logcatLines.add(logcatLine);
                  } else if (logcatLines.size() == 0) {
                      /**
                       * Sleep only when there is no more input, not after going through buffer
                       * to not unnecessary slow down the process
                       * */
                      Thread.sleep(1000);
                  } else {
                      /**
                       * In moment, where there are no data
                       * we check the current buffer and clear it
                       * */
                      String[] outLines = new String[logcatLines.size()];
                      logcatLines.toArray(outLines);
  
                      for (int counter = 0; counter < logcatLines.size(); counter++) {
                          String bufferedLine = logcatLines.get(counter);
                          switch (checkForSms(bufferedLine)) {
                              case TYPE0:
                                  parseTypeZeroSms(outLines, MiscUtils.logcatTimeStampParser(bufferedLine));
                                  break;
                              case MWI:
                                  parseMwiSms(outLines, MiscUtils.logcatTimeStampParser(bufferedLine));
                                  break;
                              case WAP:
                                  int remainingLinesInBuffer = logcatLines.size() - counter - LOGCAT_WAP_EXTRA_LINES;
                                  if (remainingLinesInBuffer < 0) {
                                      /**
                                       * we need to go forward a few more lines to get data
                                       * and store it in post buffer array
                                       * */
                                      String[] wapPostLines = new String[Math.abs(remainingLinesInBuffer)];
                                      String extraLine;
                                      for (int x = 0; x < Math.abs(remainingLinesInBuffer); x++) {
                                          extraLine = mLogcatReader.readLine();
                                          if (extraLine != null) {
                                              wapPostLines[x] = extraLine;
                                          }
                                      }
  
                                      /**
                                       * We'll add the extra lines to logcat buffer, so we don't miss anything
                                       * on detection cycle continue
                                       * */
                                      int insertCounter = logcatLines.size();
                                      for (String postLine : wapPostLines) {
                                          logcatLines.add(counter + insertCounter, postLine);
                                          insertCounter++;
                                      }
                                  }
  
                                  /**
                                   * Will readout from LogcatBuffer remaining lines, or next LOGCAT_WAP_EXTRA_LINES lines
                                   * depending on how many are available
                                   * */
                                  int availableLines = Math.min(logcatLines.size() - counter - LOGCAT_WAP_EXTRA_LINES, LOGCAT_WAP_EXTRA_LINES);
                                  String[] nextAvailableLines = new String[availableLines];
                                  for (int nextLine = 0; nextLine < availableLines; nextLine++) {
                                      nextAvailableLines[nextLine] = logcatLines.get(counter + nextLine);
                                  }
  
                                  parseWapPushSms(outLines, nextAvailableLines, MiscUtils.logcatTimeStampParser(bufferedLine));
                                  break;
                          }
                          counter++;
                      }
  
                      logcatLines.clear();
                  }
  
              } catch (IOException e) {
                 Log.e(TAG, mTAG + "IO Exception", e);
              } catch (InterruptedException e) {
                 Log.e(TAG, mTAG + "Interrupted Exception", e);
              }
          }
  
          try {
              mLogcatReader.close();
          } catch (IOException ee) {
             Log.e(TAG, mTAG + "IOE Error closing BufferedReader", ee);
          }
      }
  
      private int checkForSms(String line) {
          //0 - null 1 = TYPE0, 2 = MWI, 3 = WAPPUSH
          for (String LOADED_DETECTION_STRING : LOADED_DETECTION_STRINGS) {
              //looping through detection strings to see does logcat line match
              // memory optimized and precaution for LOADED_DETECTION_STRING being not filled
              String[] splitDetectionString = LOADED_DETECTION_STRING == null ? null : LOADED_DETECTION_STRING.split("#");
              if (splitDetectionString == null || splitDetectionString.length < 2 || splitDetectionString[0] == null || splitDetectionString[1] == null) {
                 Log.d(TAG, mTAG + "Broken detection string: " + LOADED_DETECTION_STRING);
                  // skip broken detection string
                  continue;
              }
              if (line.contains(splitDetectionString[0])) {
                  if ("TYPE0".equalsIgnoreCase(splitDetectionString[1])) {
                      Log.i(TAG, mTAG + "TYPE0 detected");
                      return TYPE0;
                  } else if ("MWI".equalsIgnoreCase(splitDetectionString[1])) {
                      Log.i(TAG, mTAG + "MWI detected");
                      return MWI;
                  } else if ("WAPPUSH".equalsIgnoreCase(splitDetectionString[1])) {
                      Log.i(TAG, mTAG + "WAPPUSH detected");
                      return WAP;
                  }
  
              }
              // This is currently unused, but keeping as an example of possible data contents
              // else if (line.contains("BroadcastReceiver action: android.provider.Telephony.SMS_RECEIVED")) {
              // Log.i(TAG, mTAG + "SMS found");
              // return 0;
              // }
          }
          return 0;
      }
  
      private void parseTypeZeroSms(String[] bufferLines, String logcat_timestamp) {
  
          CapturedSmsData capturedSms = new CapturedSmsData();
          String smsText = findSmsData(bufferLines, null);
          String num = findSmsNumber(bufferLines, null);
  
          capturedSms.setSenderNumber(num == null ? "null" : num);
          capturedSms.setSenderMsg(smsText == null ? "null" : num);
          capturedSms.setSmsTimestamp(logcat_timestamp);
          capturedSms.setSmsType("TYPE0");
          capturedSms.setCurrent_lac(mAIMSICDService.getCellTracker().getMonitorCell().getLAC());
          capturedSms.setCurrent_cid(mAIMSICDService.getCellTracker().getMonitorCell().getCID());
          capturedSms.setCurrent_nettype(mAIMSICDService.getCell().getRAT());
          int isRoaming = 0;
  
          if ("true".equals(mAIMSICDService.getCellTracker().getDevice().isRoaming())) {
              isRoaming = 1;
          }
          capturedSms.setCurrent_roam_status(isRoaming);
          capturedSms.setCurrent_gps_lat(mAIMSICDService.lastKnownLocation().getLatitudeInDegrees());
          capturedSms.setCurrent_gps_lon(mAIMSICDService.lastKnownLocation().getLongitudeInDegrees());
  
          // Only alert if the timestamp is not in the data base
          if (!mDbAdapter.isTimeStampInDB(logcat_timestamp)) {
              mDbAdapter.storeCapturedSms(capturedSms);
              mDbAdapter.toEventLog(3, "Detected Type-0 SMS");
              startPopUpInfo(SmsType.SILENT);
          } else {
             Log.d(TAG, mTAG + "Detected Sms already logged");
          }
  
      }
  
      private void parseMwiSms(String[] logcatLines, String logcat_timestamp) {
  
          CapturedSmsData capturedSms = new CapturedSmsData();
          String smsText = findSmsData(logcatLines, null);
          String num = findSmsNumber(logcatLines, null);
  
          capturedSms.setSenderNumber(num == null ? "null" : num);
          capturedSms.setSenderMsg(smsText == null ? "null" : smsText);
          capturedSms.setSmsTimestamp(logcat_timestamp);
          capturedSms.setSmsType("MWI");
          capturedSms.setCurrent_lac(mAIMSICDService.getCellTracker().getMonitorCell().getLAC());
          capturedSms.setCurrent_cid(mAIMSICDService.getCellTracker().getMonitorCell().getCID());
          capturedSms.setCurrent_nettype(mAIMSICDService.getCell().getRAT());
          int isRoaming = 0;
          if ("true".equals(mAIMSICDService.getCellTracker().getDevice().isRoaming())) {
              isRoaming = 1;
          }
          capturedSms.setCurrent_roam_status(isRoaming);
          capturedSms.setCurrent_gps_lat(mAIMSICDService.lastKnownLocation().getLatitudeInDegrees());
          capturedSms.setCurrent_gps_lon(mAIMSICDService.lastKnownLocation().getLongitudeInDegrees());
  
          //only alert if timestamp is not in the data base
          if (!mDbAdapter.isTimeStampInDB(logcat_timestamp)) {
              mDbAdapter.storeCapturedSms(capturedSms);
              mDbAdapter.toEventLog(4, "Detected MWI SMS");
              startPopUpInfo(SmsType.MWI);
          } else {
             Log.d(TAG, mTAG + " Detected Sms already logged");
          }
      }
  
      private void parseWapPushSms(String[] logcatLines, String[] postWapMessageLines, String logcat_timestamp) {
          CapturedSmsData capturedSms = new CapturedSmsData();
          String smsText = findSmsData(logcatLines, postWapMessageLines);
          String num = findSmsNumber(logcatLines, postWapMessageLines);
  
          capturedSms.setSenderNumber(num == null ? "null" : num);
          capturedSms.setSenderMsg(smsText == null ? "null" : smsText);
          capturedSms.setSmsTimestamp(logcat_timestamp);
          capturedSms.setSmsType("WAPPUSH");
          capturedSms.setCurrent_lac(mAIMSICDService.getCellTracker().getMonitorCell().getLAC());
          capturedSms.setCurrent_cid(mAIMSICDService.getCellTracker().getMonitorCell().getCID());
          capturedSms.setCurrent_nettype(mAIMSICDService.getCell().getRAT());
          int isRoaming = 0;
          if ("true".equals(mAIMSICDService.getCellTracker().getDevice().isRoaming())) {
              isRoaming = 1;
          }
          capturedSms.setCurrent_roam_status(isRoaming);
          capturedSms.setCurrent_gps_lat(mAIMSICDService.lastKnownLocation().getLatitudeInDegrees());
          capturedSms.setCurrent_gps_lon(mAIMSICDService.lastKnownLocation().getLongitudeInDegrees());
  
          //only alert if timestamp is not in the data base
          if (!mDbAdapter.isTimeStampInDB(logcat_timestamp)) {
              mDbAdapter.storeCapturedSms(capturedSms);
              mDbAdapter.toEventLog(6, "Detected WAPPUSH SMS");
              startPopUpInfo(SmsType.WAP_PUSH);
          } else {
             Log.d(TAG, mTAG + "Detected SMS already logged");
          }
      }
  
      private String findSmsData(String[] preBuffer, String[] postBuffer) {
          //check pre buffer for number and sms msg
          if (preBuffer != null) {
              for (String preBufferLine : preBuffer) {
                  if (preBufferLine != null) {
                      if (preBufferLine.contains("SMS message body (raw):") && preBufferLine.contains("'")) {
                          preBufferLine = preBufferLine.substring(preBufferLine.indexOf("'") + 1,
                                  preBufferLine.length() - 1);
                          return preBufferLine;
                      }
                  }
              }
              //check post buffer for number and sms msg
              if (postBuffer != null) {
                  for (int x = 0; x < postBuffer.length; x++) {
                      if (postBuffer[x] != null) {
                          String testLine = preBuffer[x];
                          if (testLine.contains("SMS message body (raw):") && testLine.contains("'")) {
                              testLine = testLine.substring(testLine.indexOf("'") + 1,
                                      testLine.length() - 1);
                              return testLine;
                          }
                      }
                  }
              }
          }
          return null;
      }
  
      private String findSmsNumber(String[] preBuffer, String[] postBuffer) {
          //check pre buffer for number and sms msg
          if (preBuffer != null) {
              for (String preBufferLine : preBuffer) {
                  if (preBufferLine != null) {
                      if (preBufferLine.contains("SMS originating address:") && preBufferLine.contains("+")) {
                          return preBufferLine.substring(preBufferLine.indexOf("+"));
                      } else if (preBufferLine.contains("OrigAddr")) {
                          preBufferLine = preBufferLine.substring(preBufferLine.indexOf("OrigAddr")).replace("OrigAddr", "").trim();
                          return preBufferLine;
                      }
                  }
              }
          }
          //check post buffer for number and sms msg
          if (postBuffer != null) {
              for (String postBufferLine : postBuffer) {
                  if (postBufferLine != null) {
                      if (postBufferLine.contains("SMS originating address:") && postBufferLine.contains("+")) {
                          return postBufferLine.substring(postBufferLine.indexOf("+"));
                      } else if (postBufferLine.contains("OrigAddr")) {
                          postBufferLine = postBufferLine.substring(postBufferLine.indexOf("OrigAddr")).replace("OrigAddr", "").trim();
                          return postBufferLine;
                      }
                  }
              }
  
          }
          return null;
      }
  
      private final ServiceConnection mConnection = new ServiceConnection() {
          @Override
          public void onServiceConnected(ComponentName name, IBinder service) {
              mAIMSICDService = ((AimsicdService.AimscidBinder) service).getService();
              mBound = true;
          }
  
          @Override
          public void onServiceDisconnected(ComponentName arg0) {
              Log.i(TAG, mTAG + "Disconnected SMS Detection Service");
              mBound = false;
          }
      };
  
      public enum SmsType {
          SILENT(
                  R.string.alert_silent_sms_detected,
                  R.string.typezero_header,
                  R.string.typezero_data
          ),
          MWI(
                  R.string.alert_mwi_detected,
                  R.string.typemwi_header,
                  R.string.typemwi_data
          ),
          WAP_PUSH(
                  R.string.alert_silent_wap_sms_detected,
                  R.string.typewap_header,
                  R.string.typewap_data
          );
  
          @StringRes
          private int alert;
  
          @StringRes
          private int title;
  
          @StringRes
          private int message;
  
          SmsType(@StringRes int alert,
                  @StringRes int title,
                  @StringRes int message) {
              this.alert = alert;
              this.title = title;
              this.message = message;
          }
  
          @StringRes
          public int getAlert() {
              return alert;
          }
  
          @StringRes
          public int getTitle() {
              return title;
          }
  
          @StringRes
          public int getMessage() {
              return message;
          }
      }
  }
  