Package captureplugin.drivers.topfield.connector

Source Code of captureplugin.drivers.topfield.connector.TopfieldConnector$SatelliteInfo

/**
* Created on 20.06.2010
*/
package captureplugin.drivers.topfield.connector;

import java.awt.Window;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Authenticator;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import captureplugin.drivers.topfield.TopfieldConfiguration;
import captureplugin.drivers.topfield.TopfieldServiceInfo;
import captureplugin.drivers.topfield.TopfieldTimerEntry;
import captureplugin.drivers.topfield.TopfieldTimerMode;
import captureplugin.drivers.utils.ProgramTime;
import devplugin.Channel;
import devplugin.Date;
import devplugin.Plugin;
import devplugin.Program;

/**
* Connector to the device.
*
* @author Wolfgang Reh
*/
public class TopfieldConnector {
  /**
   * Authenticator for HTTP access authentication.
   *
   * @author Wolfgang Reh
   */
  private static final class AccessAuthenticator extends Authenticator {
    private final String username;
    private final char[] password;

    public AccessAuthenticator(String username, char[] password) {
      this.username = username;
      this.password = password;
    }

    @Override
    protected PasswordAuthentication getPasswordAuthentication() {
      return new PasswordAuthentication(username, password);
    }
  }

  /**
   * Container for information about one satellite.
   *
   * @author Wolfgang Reh
   */
  private static final class SatelliteInfo {
    private final int index;
    private final int serviceCount;
    private final String name;

    /**
     * @param index
     *          The satellite index
     * @param count
     *          The number of services on this satellite
     * @param name
     *          The name of the satellite
     */
    public SatelliteInfo(int index, int count, String name) {
      this.index = index;
      this.serviceCount = count;
      this.name = name;
    }

    /**
     * @return the index
     */
    public int getIndex() {
      return (index);
    }

    /**
     * @return the serviceCount
     */
    public int getServiceCount() {
      return (serviceCount);
    }

    /**
     * @return the name
     */
    public String getName() {
      return (name);
    }
  }

  private static final String NEW_TIMER_FORM = "/TimerNewEntryForm.htm";
  private static final String INSERT_TIMER_PAGE = "/InsertTimerSchedule.cgi";
  private static final String DELETE_TIMER_PAGE = "/DeleteTimerSchedule.cgi";
  private static final String TIMER_LIST_PAGE = "/TimerSettings.htm";
  private static final String INFO_CREATE_FORMAT = "satelliteInfo%s\\[(\\d+)\\]\\s*=\\s*new CreateServiceInfo\\((\\d+),\\s*(\\d+),\\s*\"([^\"]*)\"\\);";
  private static final String INFO_CHANNEL_FORMAT = "satelliteInfo%s\\[%d\\]\\.optionsValue\\[(\\d+)\\]\\s*=\\s*(\\d+);";
  private static final String INFO_SERVICE_FORMAT = "satelliteInfo%s\\[%d\\]\\.options\\[%d\\]\\s*=\\s*\"([^\"]*)\";";
  private static final String INFO_TUNER_FORMAT = "satelliteInfo%s\\[%d\\]\\.tunerNum\\[%d\\]\\s*=\\s*(\\d+);";
  private static final String RECORD_FORMAT = "RecordOnOff=1&Type=%d&Satellite=%d&Service=%d&Tuner=%d&Mode=%d&"
      + "DateDay=%d&DateMonth=%d&DateYear=%d&StartTimeHour=%d&StartTimeMinute=%d&DurationHour=%d&DurationMinute=%d&FileName=%s";
  private static final String SWITCH_FORMAT = "RecordOnOff=0&Type=%d&Satellite=%d&Service=%d&Tuner=%d&Mode=%d&"
      + "DateDay=%d&DateMonth=%d&DateYear=%d&StartTimeHour=%d&StartTimeMinute=%d&DurationHour=%d&DurationMinute=%d";
  private static final String DELETE_FORMAT = "EntryNum=%d";
  private static final String REPLY_ALERT = "alert(";
  private static final Pattern TIMER_ENTRY_PATTERN = Pattern
      .compile("new\\s*CreateTimerEntry\\((\\d+),\\s*(\\d+),\\s*(\\d+),\\s*\\\"([^\\\"]*)\\\",\\s*\\\"((\\d+)\\.\\s*([^\\\"]*))\\\",\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),\\s*\\\"([^\\\"]*)\\\"\\);");
  private static final Pattern RECEIVER_ADD_TIME_PATTERN = Pattern.compile("\\(-(\\d+)/\\+(\\d)\\)");
  private static final String ENCODING_UTF8 = "UTF-8";

  private final TopfieldConfiguration configuration;
  private final ArrayList<TopfieldServiceInfo> channels = new ArrayList<TopfieldServiceInfo>();

  /**
   * Create a connector.
   *
   * @param configuration
   *          The configuration of the device
   */
  public TopfieldConnector(TopfieldConfiguration configuration) {
    this.configuration = configuration;
    Authenticator.setDefault(new AccessAuthenticator(configuration.getUsername(), configuration.getPasswordChars()));
  }

  /**
   * Read the contents of the given page.
   *
   * @param page
   *          The page to read
   * @return A <code>StringBuffer</code> containing the page contents
   * @throws TopfieldConnectionException
   *           If the device could not be contacted
   */
  private StringBuffer readDeviceData(String page) throws TopfieldConnectionException {
    try {
      URL deviceURL = configuration.getDeviceURL(page);
      URLConnection connection = deviceURL.openConnection();
      connection.setConnectTimeout(configuration.getConnectionTimeout());
      InputStream contentStream = (InputStream) connection.getContent();
      BufferedReader in = new BufferedReader(new InputStreamReader(contentStream));
      StringBuffer content = new StringBuffer();
      String line;
      while ((line = in.readLine()) != null) {
        content.append(line);
      }
      in.close();
      configuration.setDeviceUnreachable(false);
      return content;
    } catch (MalformedURLException e) {
      configuration.setDeviceUnreachable(true);
      throw new TopfieldConnectionException(e);
    } catch (IOException e) {
      configuration.setDeviceUnreachable(true);
      throw new TopfieldConnectionException(e);
    }
  }

  /**
   * Get the channels from the device.
   *
   * @return The list of currently tuned channels
   * @throws TopfieldConnectionException
   *           If the device could not be contacted
   */
  public ArrayList<TopfieldServiceInfo> getDeviceChannels() throws TopfieldConnectionException {
    StringBuffer content = readDeviceData(NEW_TIMER_FORM);
    gatherChannels(content, TopfieldServiceType.TV);
    gatherChannels(content, TopfieldServiceType.RADIO);
    return channels;
  }

  /**
   * Collect the channels from the received content.
   *
   * @param webPage
   *          The received content
   * @param type
   *          Collect TV or radio channel
   */
  private void gatherChannels(StringBuffer webPage, TopfieldServiceType type) {
    ArrayList<SatelliteInfo> satellites = new ArrayList<SatelliteInfo>();
    Matcher infoMatcher = Pattern.compile(String.format(INFO_CREATE_FORMAT, type)).matcher(webPage);
    while (infoMatcher.find()) {
      satellites.add(new SatelliteInfo(Integer.parseInt(infoMatcher.group(1)), Integer.parseInt(infoMatcher.group(2)),
          infoMatcher.group(4)));
    }
    for (SatelliteInfo info : satellites) {
      Matcher channelMatcher = Pattern.compile(String.format(INFO_CHANNEL_FORMAT, type, info.getIndex())).matcher(
          webPage);
      while (channelMatcher.find()) {
        int subIndex = Integer.parseInt(channelMatcher.group(1));
        int serviceIndex = Integer.parseInt(channelMatcher.group(2));
        Matcher serviceMatcher = Pattern.compile(String.format(INFO_SERVICE_FORMAT, type, info.getIndex(), subIndex))
            .matcher(webPage);
        Matcher tunerMatcher = Pattern.compile(String.format(INFO_TUNER_FORMAT, type, info.getIndex(), subIndex))
            .matcher(webPage);
        serviceMatcher.find();
        tunerMatcher.find();
        String serviceName = serviceMatcher.group(1);
        serviceName = serviceName.substring(serviceName.indexOf(" ") + 1);
        TopfieldServiceInfo serviceInfo = new TopfieldServiceInfo(serviceIndex, info.getName(), info.getIndex(),
            serviceName, Integer.parseInt(tunerMatcher.group(1)), type == TopfieldServiceType.TV);
        channels.add(serviceInfo);
      }
    }
  }

  /**
   * Add a R-timer.
   *
   * @param parent
   *          The parent window calling this method
   * @param service
   *          The service to record
   * @param recordTime
   *          The time to record
   * @param repeat
   *          The repetition of the timer
   * @return <code>true</code> if the timer could be set
   * @throws TopfieldConnectionException
   *           If the device could not be contacted
   * @throws TopfieldServiceException
   *           If the service changed since last assignment
   * @throws TopfieldTunerException
   *           If no tuner could be found for the recording
   */
  public boolean addRecording(Window parent, TopfieldServiceInfo service, ProgramTime recordTime,
      TopfieldTimerMode repeat) throws TopfieldConnectionException, TopfieldServiceException, TopfieldTunerException {
    return addTimer(parent, service, recordTime, true, repeat);
  }

  /**
   * Add a P-timer.
   *
   * @param parent
   *          The parent window calling this method
   * @param service
   *          The service to switch to
   * @param switchTime
   *          The time when to switch
   * @return <code>true</code> if the timer could be set
   * @throws TopfieldConnectionException
   *           If the device could not be contacted
   * @throws TopfieldServiceException
   *           If the service changed since last assignment
   * @throws TopfieldTunerException
   *           If no tuner could be found for the timer
   */
  public boolean addPTimer(Window parent, TopfieldServiceInfo service, ProgramTime switchTime)
      throws TopfieldConnectionException, TopfieldServiceException, TopfieldTunerException {
    return addTimer(parent, service, switchTime, false, TopfieldTimerMode.ONE_TIME);
  }

  /**
   * Add a timer.
   *
   * @param parent
   *          The parent window calling this method
   * @param service
   *          The service to record
   * @param recordTime
   *          The time to record
   * @param record
   *          <code>true</code> creates a recording, <code>false</code> a
   *          P-timer
   * @param repeat
   *          The repetition of the timer
   * @return <code>true</code> if the timer could be set
   * @throws TopfieldConnectionException
   *           If the device could not be contacted
   * @throws TopfieldServiceException
   *           If the service changed since last assignment
   * @throws TopfieldTunerException
   *           If no tuner could be found for the recording
   */
  private boolean addTimer(Window parent, TopfieldServiceInfo service, ProgramTime recordTime, boolean record,
      TopfieldTimerMode repeat) throws TopfieldConnectionException, TopfieldServiceException, TopfieldTunerException {

    // RecordOnOff ...... 0 = Switch on only, 1 = Record
    // Type ............. 0 = TV, 1 = Radio
    // Satellite ........ Satellite index
    // Service .......... Service index
    // Tuner ............ Tuner number
    // Mode ............. 0 = One Time, 1 = Every Day, 2 = Every Weekend,
    // .. . . . . . . . . 3 = Weekly, 4 = Every Weekday
    // DateDay .......... Day
    // DateMonth ........ Month
    // DateYear ......... Year
    // StartTimeHour ... Start Hour
    // StartTimeMinute . Start Minute
    // DurationHour .... Duration Hour
    // DurationMinute .. Duration Minute
    // FileName ........ File name

    StringBuffer formContent = readDeviceData(NEW_TIMER_FORM);
    gatherChannels(formContent, (service.isTV() ? TopfieldServiceType.TV : TopfieldServiceType.RADIO));
    if (configuration.isCorrectTime()) {
      correctTime(formContent, recordTime);
    }
    if (channels.contains(service)) {
      Calendar recordStart = recordTime.getStartAsCalendar();
      Integer tuner = selectTuner(service, recordTime);
      if (tuner == null) {
        throw new TopfieldTunerException();
      }

      String request;
      try {
        if (record) {
          request = String.format(RECORD_FORMAT, (service.isTV() ? 0 : 1), service.getSatelliteNumber(), service
              .getChannelNumber(), tuner, repeat.toNumber(), recordStart.get(Calendar.DAY_OF_MONTH), recordStart
              .get(Calendar.MONTH) + 1, recordStart.get(Calendar.YEAR), recordStart.get(Calendar.HOUR_OF_DAY),
              recordStart.get(Calendar.MINUTE), recordTime.getLength() / 60, recordTime.getLength() % 60, URLEncoder
                  .encode(recordTime.getTitle(), ENCODING_UTF8));
        } else {
          request = String.format(SWITCH_FORMAT, (service.isTV() ? 0 : 1), service.getSatelliteNumber(), service
              .getChannelNumber(), tuner, repeat.toNumber(), recordStart.get(Calendar.DAY_OF_MONTH), recordStart
              .get(Calendar.MONTH) + 1, recordStart.get(Calendar.YEAR), recordStart.get(Calendar.HOUR_OF_DAY),
              recordStart.get(Calendar.MINUTE), recordTime.getLength() / 60, recordTime.getLength() % 60);
        }

        URL deviceURL = configuration.getDeviceURL(INSERT_TIMER_PAGE);
        URLConnection connection = deviceURL.openConnection();
        connection.setConnectTimeout(configuration.getConnectionTimeout());
        connection.setDoOutput(true);
        OutputStreamWriter connectionWriter = new OutputStreamWriter(connection.getOutputStream());
        connectionWriter.write(request);
        connectionWriter.close();
        InputStream contentStream = connection.getInputStream();
        BufferedReader in = new BufferedReader(new InputStreamReader(contentStream));
        boolean insertOK = true;
        String line;
        while ((line = in.readLine()) != null) {
          if (line.contains(REPLY_ALERT)) {
            insertOK = false;
            break;
          }
        }
        in.close();
        getTimerList();
        configuration.setDeviceUnreachable(false);
        return insertOK;
      } catch (MalformedURLException e) {
        configuration.setDeviceUnreachable(true);
        throw new TopfieldConnectionException(e);
      } catch (IOException e) {
        configuration.setDeviceUnreachable(true);
        throw new TopfieldConnectionException(e);
      }
    } else {
      throw new TopfieldServiceException();
    }
  }

  /**
   * Correct the time according to the set pre- and postroll time on the
   * receiver.
   *
   * @param formContent
   *          The content of the new timer form
   * @param recordTime
   *          The time to modify
   */
  private void correctTime(StringBuffer formContent, ProgramTime recordTime) {
    Matcher timeMatcher = RECEIVER_ADD_TIME_PATTERN.matcher(formContent);
    if (timeMatcher.find()) {
      int receiverPreroll = Integer.parseInt(timeMatcher.group(1));
      int receiverPostroll = Integer.parseInt(timeMatcher.group(2));

      recordTime.addMinutesToStart(receiverPreroll);
      recordTime.addMinutesToEnd(receiverPostroll * -1);
    }
  }

  /**
   * Select a tuner.
   *
   * @param service
   *          The service to record
   * @param recordTime
   *          The time the recording should run
   * @return The selected tuner or <code>null</code> if no tuner found
   * @throws TopfieldConnectionException
   *           If the device could not be contacted
   */
  private Integer selectTuner(TopfieldServiceInfo service, ProgramTime recordTime) throws TopfieldConnectionException {
    if (configuration.isUseTuner4() && (service.getTuner() == 3)) {
      return 4;
    } else {
      getTimerList();
      HashSet<Integer> usedTuners = new HashSet<Integer>();
      for (TopfieldTimerEntry timerEntry : configuration.getTimerEntries()) {
        if (((timerEntry.getProgram().getStart().compareTo(recordTime.getStart()) >= 0) && (timerEntry.getProgram()
            .getStart().compareTo(recordTime.getEnd()) <= 0))
            || ((timerEntry.getProgram().getEnd().compareTo(recordTime.getStart()) >= 0) && (timerEntry.getProgram()
                .getEnd().compareTo(recordTime.getEnd()) <= 0))
            || ((timerEntry.getProgram().getStart().compareTo(recordTime.getStart()) <= 0) && (timerEntry.getProgram()
                .getEnd().compareTo(recordTime.getEnd()) >= 0))) {
          // This program overlaps with the new recording.
          usedTuners.add(timerEntry.getEntryTuner());
        }
      }
      switch (service.getTuner()) {
      case 1:
        if (!usedTuners.contains(1)) {
          return 1;
        }
        break;
      case 2:
        if (!usedTuners.contains(2)) {
          return 2;
        }
        break;
      case 3:
        if (usedTuners.isEmpty()) {
          return 1;
        } else if (usedTuners.contains(1) && !usedTuners.contains(2)) {
          return 2;
        } else if (!usedTuners.contains(1) && usedTuners.contains(2)) {
          return 1;
        }
        break;
      }
    }

    return null;
  }

  /**
   * Get the list of timers from the device.<br>
   * This method refreshes the list of currently set timer entries in the
   * configuration.
   *
   * @throws TopfieldConnectionException
   *           If the device could not be contacted
   */
  public void getTimerList() throws TopfieldConnectionException {
    ArrayList<TopfieldTimerEntry> timerList = new ArrayList<TopfieldTimerEntry>();
    StringBuffer content = readDeviceData(TIMER_LIST_PAGE);
    Matcher timerEntryMatcher = TIMER_ENTRY_PATTERN.matcher(content);
    while (timerEntryMatcher.find()) {
      if (timerEntryMatcher.group(2).equals("0")) {
        // This is a P-timer => skip it
        continue;
      }

      int entryNumber = Integer.parseInt(timerEntryMatcher.group(1));
      TopfieldServiceType type = TopfieldServiceType.createFromNumber(Integer.parseInt(timerEntryMatcher.group(3)));
      int serviceNumber = Integer.parseInt(timerEntryMatcher.group(6)) - 1;
      int tuner = Integer.parseInt(timerEntryMatcher.group(8));
      TopfieldTimerMode mode = TopfieldTimerMode.createFromNumber(Integer.parseInt(timerEntryMatcher.group(9)));
      int startDay = Integer.parseInt(timerEntryMatcher.group(10));
      int startMonth = Integer.parseInt(timerEntryMatcher.group(11));
      int startYear = Integer.parseInt(timerEntryMatcher.group(12));
      int startHour = Integer.parseInt(timerEntryMatcher.group(13));
      int startMinute = Integer.parseInt(timerEntryMatcher.group(14));
      int length = Integer.parseInt(timerEntryMatcher.group(15));
      String fileName = timerEntryMatcher.group(16);

      Calendar recordingStart = Calendar.getInstance();
      recordingStart.set(Calendar.DAY_OF_MONTH, startDay);
      recordingStart.set(Calendar.MONTH, startMonth - 1);
      recordingStart.set(Calendar.YEAR, startYear);
      recordingStart.set(Calendar.HOUR_OF_DAY, startHour);
      recordingStart.set(Calendar.MINUTE, startMinute);
      recordingStart.set(Calendar.SECOND, 0);
      recordingStart.set(Calendar.MILLISECOND, 0);

      boolean channelHasData;
      do {
        channelHasData = false;
        Calendar recordingEnd = (Calendar) recordingStart.clone();
        recordingEnd.add(Calendar.MINUTE, length);

        int recordingStartMinutes = startHour * 60 + startMinute;
        int recordingEndMinutes = recordingStartMinutes + length;

        Channel tvBrowserChannel = configuration.getChannelForService(serviceNumber, type == TopfieldServiceType.TV);
        if (tvBrowserChannel != null) {
          Iterator<Program> programIterator = Plugin.getPluginManager().getChannelDayProgram(new Date(recordingStart),
              tvBrowserChannel);

          while (programIterator.hasNext()) {
            channelHasData = true;
            Program program = programIterator.next();
            int programStartMinutes = program.getStartTime();
            int programEndMinutes = programStartMinutes + program.getLength();

            if ((programStartMinutes >= recordingStartMinutes) && (programEndMinutes <= recordingEndMinutes)) {
              ProgramTime programTime = new ProgramTime(program, recordingStart.getTime(), recordingEnd.getTime());
              TopfieldTimerEntry timerEntry = new TopfieldTimerEntry(fileName, entryNumber, mode, tuner, programTime);
              timerList.add(timerEntry);
              break;
            }
          }
        }
      } while (channelHasData && nextRecordng(recordingStart, mode));
    }

    configuration.setTimerEntries(timerList);
  }

  private boolean nextRecordng(Calendar recordingTime, TopfieldTimerMode timerMode) {
    switch (timerMode) {
    case ONE_TIME:
      return false;
    case EVERY_DAY:
      recordingTime.add(Calendar.DAY_OF_MONTH, 1);
      return true;
    case WEEKLY:
      recordingTime.add(Calendar.DAY_OF_MONTH, 7);
      return true;
    case EVERY_WEEKDAY:
      switch (recordingTime.get(Calendar.DAY_OF_WEEK)) {
      case Calendar.SUNDAY:
      case Calendar.MONDAY:
      case Calendar.TUESDAY:
      case Calendar.WEDNESDAY:
      case Calendar.THURSDAY:
        recordingTime.add(Calendar.DAY_OF_MONTH, 1);
        break;
      case Calendar.FRIDAY:
        recordingTime.add(Calendar.DAY_OF_MONTH, 3);
        break;
      case Calendar.SATURDAY:
        recordingTime.add(Calendar.DAY_OF_MONTH, 2);
        break;
      }
      return true;
    case EVERY_WEEKEND:
      switch (recordingTime.get(Calendar.DAY_OF_WEEK)) {
      case Calendar.SUNDAY:
        recordingTime.add(Calendar.DAY_OF_MONTH, 6);
        break;
      case Calendar.MONDAY:
        recordingTime.add(Calendar.DAY_OF_MONTH, 5);
        break;
      case Calendar.TUESDAY:
        recordingTime.add(Calendar.DAY_OF_MONTH, 4);
        break;
      case Calendar.WEDNESDAY:
        recordingTime.add(Calendar.DAY_OF_MONTH, 3);
        break;
      case Calendar.THURSDAY:
        recordingTime.add(Calendar.DAY_OF_MONTH, 2);
        break;
      case Calendar.FRIDAY:
        recordingTime.add(Calendar.DAY_OF_MONTH, 1);
        break;
      case Calendar.SATURDAY:
        recordingTime.add(Calendar.DAY_OF_MONTH, 1);
        break;
      }
      return true;
    default:
      return false;
    }
  }

  /**
   * Delete a timer.
   *
   * @param parent
   *          The parent window calling this method
   * @param entry
   *          The entry to delete
   * @return <code>true</code> if the entry was successfully deleted
   * @throws TopfieldConnectionException
   *           If the device could not be contacted
   */
  public boolean deleteRecording(Window parent, TopfieldTimerEntry entry) throws TopfieldConnectionException {
    String request = String.format(DELETE_FORMAT, entry.getEntryNumber());
    try {
      URL deviceURL = configuration.getDeviceURL(DELETE_TIMER_PAGE);
      URLConnection connection = deviceURL.openConnection();
      connection.setConnectTimeout(configuration.getConnectionTimeout());
      connection.setDoOutput(true);
      OutputStreamWriter connectionWriter = new OutputStreamWriter(connection.getOutputStream());
      connectionWriter.write(request);
      connectionWriter.close();
      // No need to check the answer, it's always OK. Nevertheless we have to
      // read the result or the server on the device won't answer the next
      // request.
      InputStream contentStream = connection.getInputStream();
      BufferedReader in = new BufferedReader(new InputStreamReader(contentStream));
      while (in.readLine() != null) {
        ;
      }
      in.close();
      getTimerList();
      boolean deleteOK = true;
      // Check the list of timers after deletion to determine if the deletion
      // was successful.
      for (TopfieldTimerEntry currentEntry : configuration.getTimerEntries()) {
        if (currentEntry.getProgram().getProgram().equals(entry.getProgram().getProgram())) {
          deleteOK = false;
          break;
        }
      }
      configuration.setDeviceUnreachable(false);
      return deleteOK;
    } catch (MalformedURLException e) {
      configuration.setDeviceUnreachable(true);
      throw new TopfieldConnectionException(e);
    } catch (IOException e) {
      configuration.setDeviceUnreachable(true);
      throw new TopfieldConnectionException(e);
    }
  }
}
TOP

Related Classes of captureplugin.drivers.topfield.connector.TopfieldConnector$SatelliteInfo

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.