Android Open Source - WsprNetViewer Wspr Net Viewer Sync Adapter






From Project

Back to project page WsprNetViewer.

License

The source code is released under:

Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUC...

If you think the Android project WsprNetViewer listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * Copyright (C) 2014 Joseph D. Glandorf
 *//from   w w w . ja v a 2  s .c  om
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.glandorf1.joe.wsprnetviewer.app.sync;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SyncRequest;
import android.content.SyncResult;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.util.Log;

import com.glandorf1.joe.wsprnetviewer.app.MainActivity;
import com.glandorf1.joe.wsprnetviewer.app.R;
import com.glandorf1.joe.wsprnetviewer.app.Utility;
import com.glandorf1.joe.wsprnetviewer.app.data.WsprNetContract;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import java.util.Vector;

public class WsprNetViewerSyncAdapter extends AbstractThreadedSyncAdapter {
    private static final int WSPR_NOTIFICATION_ID = 3004; // TODO: is NOTIFICATION_ID chosen at random?
    private static final String LOG_TAG = WsprNetViewerSyncAdapter.class.getSimpleName();
    // Default interval at which to sync with wsprnet.org, in seconds.
    public static final int SYNC_INTERVAL = 60 * 60;  // 1 hour
    public static final int SYNC_FLEXTIME = SYNC_INTERVAL / 4; // +/- 15 minutes
    private static final double mBandFrequencyTolerancePercent = 5.;
    private static Double[] mBandFrequency, mBandFrequencyMin, mBandFrequencyMax;
    private static String[] mBandFrequencyStr, mBandNameStr;
    private static int mBandNameIdx = -1;

    private Context mContext;

    public WsprNetViewerSyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
        Log.d(LOG_TAG, "Creating SyncAdapter");
        mContext = context;
    }


    @Override
    public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) {
        // TODO:  verify if wsprSpotQuery is no longer needed
        String wsprSpotQuery = "";
            wsprSpotQuery = Utility.getPreferredGridsquare(mContext);
        // If there's no gridsquare code, there's nothing to look up.
        if (wsprSpotQuery.length() == 0) {
            return;
        }

        try {
            // Get data from live from website.
            // For url_wsprnet_spots, drupal must be true!
            // For url_wsprnet_spots_old, drupal must be false!
          //String source = mContext.getString(R.string.url_wsprnet_spots);
            String source = mContext.getString(R.string.url_wsprnet_spots_old);
            boolean drupal = false; // true= drupal url; false= old database format
            Document wsprHtmlDoc = getWsprData(mContext, source);
            // TODO: 'maxSpots' is unused in queries until it is known how to submit a direct Drupal query to wspr web site.
            int maxSpots = 1000;
            getWsprDataFromTags(mContext, wsprHtmlDoc, maxSpots, wsprSpotQuery, drupal);
        } catch (Throwable e) {
            Log.e(LOG_TAG, e.getMessage(), e);
            e.printStackTrace();
        }
        return;
    }

    /**
     * Helper method to handle insertion of a new gridsquare in the wspr database.
     *
     * @param gridsquareSetting The gridsquare string used to request updates from the server.
     * @param cityName        A human-readable city name, e.g "Mountain View"
     * @param lat             the latitude of the city
     * @param lon             the longitude of the city
     * @return the row ID of the added gridsquare.
     */
    public long addGridsquare(Context context, String gridsquareSetting, String cityName, String countryName, double lat, double lon) {

        // First, check if the gridsquare with this city name exists in the db
        Cursor cursor = context.getContentResolver().query(
                WsprNetContract.GridSquareEntry.CONTENT_URI,
                new String[]{WsprNetContract.GridSquareEntry._ID},
                WsprNetContract.GridSquareEntry.COLUMN_GRIDSQUARE_SETTING + " = ?",
                new String[]{gridsquareSetting},
                null);

        if (cursor.moveToFirst()) {
            int gridsquareIdIndex = cursor.getColumnIndex(WsprNetContract.GridSquareEntry._ID);
            return cursor.getLong(gridsquareIdIndex);
        } else {
            ContentValues gridsquareValues = new ContentValues();
            gridsquareValues.put(WsprNetContract.GridSquareEntry.COLUMN_GRIDSQUARE_SETTING, gridsquareSetting);
            gridsquareValues.put(WsprNetContract.GridSquareEntry.COLUMN_CITY_NAME, cityName);
            gridsquareValues.put(WsprNetContract.GridSquareEntry.COLUMN_COUNTRY_NAME, countryName);
            gridsquareValues.put(WsprNetContract.GridSquareEntry.COLUMN_COORD_LAT, lat);
            gridsquareValues.put(WsprNetContract.GridSquareEntry.COLUMN_COORD_LONG, lon);

            Uri gridsquareInsertUri = context.getContentResolver()
                    .insert(WsprNetContract.GridSquareEntry.CONTENT_URI, gridsquareValues);

            return ContentUris.parseId(gridsquareInsertUri);
        }
    }


    /**
     * Obtain the Document containing the wspr data in HTML format.
     * Wspr data is from "source"m which can be a file in this app's private area (for testing),
     * a built-in String resource (for testing), or "live" from the wsprnet.org web site.
     * Call from 'doInBackground()', etc.
     */
    public Document getWsprData(Context context, String source) {
        // initialize document to "empty" page
        Document wsprHtmlDoc = Jsoup.parse(context.getString(R.string.html_empty_page));
        try {
            Log.d("Utility", "Connecting to [" + source + "]");
            // Allow html to come from an external app-private file, resource string, or live web site.
            if (source.startsWith(context.getString(R.string.file_wsprnet_spots_prefix))) {
                // get data from file
//                // TODO: get this working--can't load the file that was manually dropped onto SD card; maybe
//                // todo:   USB debug connection prevents local SD card access?
//                File pdir = context.getExternalFilesDir(null);
//                File[] files = pdir.listFiles();
//                for (File inFile : files) {
//                    if (!inFile.isDirectory()) {
//                        Log.d(LOG_TAG, inFile.getName() + ", " + inFile.getAbsolutePath() + ", " + inFile.getCanonicalPath());
//                    }
//                }
//                String fname = pdir.toString() + source.replaceFirst("file:", "");
//                File input = new File(fname);
//                wsprHtmlDoc = Jsoup.parse(input, "UTF-8");
            } else if ((source.startsWith(context.getString(R.string.html_wsprnet_spots_prefix)))) {
                // get data from internal string
//                String html = context.getString(R.string.html_wsprnet_spots_data11)
//                        + context.getString(R.string.html_wsprnet_spots_data12)
//                        + context.getString(R.string.html_wsprnet_spots_data13)
//                        + context.getString(R.string.html_wsprnet_spots_data14);
//                wsprHtmlDoc = Jsoup.parse(URLDecoder.decode(html, "UTF-8"));
            } else if ((source.startsWith(context.getString(R.string.url_wsprnet_spots)))) {
                // Get wspr info from http://www.wsprnet.org/drupal/wsprnet/spots; e.g.:
                wsprHtmlDoc = Jsoup.connect(source).get();
            } else if ((source.startsWith(context.getString(R.string.url_wsprnet_spots_old_base)))) {
                // Get wspr info from http://www.wsprnet.org/drupal/wsprnet/spots; e.g.:
                wsprHtmlDoc = Jsoup.connect(source).get();
            }
            Log.d("getWsprData", "Connected to ["+source+"]");
        } // try
        catch(Throwable t) {
            t.printStackTrace();
        }
        return wsprHtmlDoc;
    } // getWsprData()

    /**
     * Returns name of frequency band, or "" if not within a valid frequency band.
     * @param context
     * @param idx - pass in 'mBandNameIdx', as set by getFrequencyBandCheck()
     * @return Returns name of frequency band, or "" if not within a valid frequency band.
     */
    public String getFrequencyBandName(Context context, int idx) {
        if ((mBandNameStr == null) || (mBandNameStr.length <= 0)) {
            Resources res = context.getResources();
            mBandNameStr = res.getStringArray(R.array.pref_notify_band_options);
        }
        if ((idx >= 0) && (idx < mBandNameStr.length)) {
            return mBandNameStr[idx];
        } else {
            return "";
        }
    }

    /**
     * Determines if a frequency is in one of the bands in R.array.pref_notify_band_values.
     * Checks if it is also in the notification band-- if it is, then set mBandNameIdx.
     * @param context
     * @param freqMhz - a frequency in MHz as returned by a specific WSPR report
     * @param tolerancePercent - freqMhz is in the band if within +/-tolerance of the band's center frequency
     * @return Returns -1 if not within a valid frequency band; also sets mBandNameIdx.
     */
    public double getFrequencyBandCheck(Context context, double freqMhz, double tolerancePercent) {
        double band = -1;
        if ((mBandFrequencyStr == null) || (mBandFrequencyStr.length <= 0)) {
            Resources res = context.getResources();
            mBandFrequencyStr = res.getStringArray(R.array.pref_notify_band_values);
        }
        if ((mBandFrequencyStr != null) && mBandFrequencyStr.length > 0) {
            if ((mBandFrequency == null) || (mBandFrequency.length <= 0)) {
                mBandFrequency = new Double[mBandFrequencyStr.length];
                mBandFrequencyMin = new Double[mBandFrequencyStr.length];
                mBandFrequencyMax = new Double[mBandFrequencyStr.length];
                if ((tolerancePercent < 1.) || (tolerancePercent > 20.)) {
                    tolerancePercent = 5.;
                }
                for (int i = 0; i < mBandFrequencyStr.length; i++) {
                    mBandFrequency[i] = Double.parseDouble(mBandFrequencyStr[i]);
                    mBandFrequencyMin[i] = mBandFrequency[i] - (mBandFrequency[i] * (tolerancePercent / 100.));
                    mBandFrequencyMax[i] = mBandFrequency[i] + (mBandFrequency[i] * (tolerancePercent / 100.));
                }
            }
            if ((mBandFrequency != null) && (mBandFrequency.length > 0)) {
                for (int i = 0; i < mBandFrequency.length; i++) {
                    if ((mBandFrequencyMin[i] <= freqMhz) && (freqMhz <= mBandFrequencyMax[i])) {
                        band = mBandFrequency[i];
                        break;
                    }
                }
            }
        }
        return band;
    }

    /**
     * Checks if frequency is in the notification band; sets mBandNameIdx if it is.
     * @param freqMhz - a frequency in MHz as returned by a specific WSPR report
     * @return Returns false if not within a valid frequency band; otherwise true.
     */
    public boolean frequencyBandNotifyCheck(double freqMhz,
                                        double notifyBandMHzMin, double notifyBandMHzMax) {
        double band = -1;
        boolean ok = false;
        if ((mBandFrequency != null) && (mBandFrequency.length > 0)) {
            for (int i = 0; i < mBandFrequency.length; i++) {
                if ((mBandFrequencyMin[i] <= freqMhz) && (freqMhz <= mBandFrequencyMax[i])) {
                    band = mBandFrequency[i];
                    if ((notifyBandMHzMin <= band) && (band <= notifyBandMHzMax)) {
                        mBandNameIdx = i; // save for getFrequencyBandName()
                        ok = true;
                    }
                    break;
                }
            }
        }
        return ok;
    }


    /**
     * Clean up and parse the raw timestamp from the html.
     * Since there may be multiple records with the same timestamp (the resolution is only 1 minute),
     * make sure there is a mechanism to distinguish each of them.
     */
    public String parseTimestamp(String timestampStr) {
        SimpleDateFormat timestampFormatIn = new SimpleDateFormat(Utility.TIMESTAMP_FORMAT_WSPR);
        // TODO: getting parse exceptions-but maybe only in debug mode; SDF may not be suitable for
        //       use in static modules.
        //       Investigate using something like joda-time: http://www.joda.org/joda-time/
        try {
            Date inputTimestamp = timestampFormatIn.parse(timestampStr);
            inputTimestamp.setTime(inputTimestamp.getTime()); // + millisecondOffset); // can add a fake ms value to make timestamp unique
            String dbTimestamp = WsprNetContract.getDbTimestampString(inputTimestamp);
            return dbTimestamp;
        } catch (ParseException e) {
            Log.e(LOG_TAG, e.getMessage(), e);
            //e.printStackTrace();
            return timestampStr;
        }
    }

    /**
     * Converts Date class to a string representation, used for easy comparison and database lookup.
     * @param timestamp The input timestamp
     * @return a WSPR-format representation of the timestamp.
     */
    public static long getShortTimestamp(Date timestamp){
        // Because the API returns a unix timestamp (measured in seconds),
        // it must be converted to milliseconds in order to be converted to valid timestamp.
        SimpleDateFormat sdf = new SimpleDateFormat(WsprNetContract.TIMESTAMP_FORMAT_DB_SHORT);
        String s = sdf.format(timestamp);
        long i = 0;
        try {
            i = Long.parseLong(s);
        } catch (Exception e) {
            // nothing to do
            i = -1;
        }
        return i;
    }


    /**
     * Parse the Document containing the wspr data in HTML format.
     * Call from 'doInBackground()', etc.
     */
    public void getWsprDataFromTags(Context context, Document wsprHtml, int maxSpots,
                                           String gridsquareSetting, boolean drupal)
            throws Throwable {

        // These are the names of the objects that need to be extracted.
        // column# 0             1       2           3    4       5      6     7          8     9       10
        //   Timestamp          Call  MHz          SNR  Drift  Grid  Pwr  Reporter  RGrid  km      az
        //   2014-08-25 20:40   DL8EDC  7.040186   -4    0     JO31le    5    LA3JJ/L   JO59bh   925     11

        // Gridsquare information
        final String WSPRNET_IDX_CITY = "city_name";
        final String WSPRNET_IDX_COUNTRY_NAME = "name";
        //final String WSPRNET_IDX_COORD = "coord";
        final String WSPRNET_IDX_COORD_LAT = "lat";
        final String WSPRNET_IDX_COORD_LONG = "lon";

        // Wspr information html element indices for their 'new' drupal interface.
        //   e.g.: http://wsprnet.org/drupal/wsprnet/spots
        final int WSPRNET_IDX_TIMESTAMP = 0;
        final int WSPRNET_IDX_TX_CALLSIGN = 1;
        final int WSPRNET_IDX_TX_FREQ_MHZ = 2;
        final int WSPRNET_IDX_RX_SNR = 3;
        final int WSPRNET_IDX_RX_DRIFT = 4;
        final int WSPRNET_IDX_TX_GRIDSQUARE = 5;
        final int WSPRNET_IDX_TX_POWER = 6;
        final int WSPRNET_IDX_RX_CALLSIGN = 7;
        final int WSPRNET_IDX_RX_GRIDSQUARE = 8;
        final int WSPRNET_IDX_DISTANCE = 9;
        final int WSPRNET_IDX_AZIMUTH = 10;

        // Wspr information html element indices for their 'old' url query interface.
        //   e.g.: http://wsprnet.org/olddb?mode=html&band=all&limit=10&findcall=&findreporter=&sort=date
        final int WSPRNET_IDX_OLDDB_TIMESTAMP = 0;
        final int WSPRNET_IDX_OLDDB_TX_CALLSIGN = 1;
        final int WSPRNET_IDX_OLDDB_TX_FREQ_MHZ = 2;
        final int WSPRNET_IDX_OLDDB_RX_SNR = 3;
        final int WSPRNET_IDX_OLDDB_RX_DRIFT = 4;
        final int WSPRNET_IDX_OLDDB_TX_GRIDSQUARE = 5;
        final int WSPRNET_IDX_OLDDB_TX_POWER_DBM = 6;
        final int WSPRNET_IDX_OLDDB_TX_POWER_W = 7;
        final int WSPRNET_IDX_OLDDB_RX_CALLSIGN = 8;
        final int WSPRNET_IDX_OLDDB_RX_GRIDSQUARE = 9;
        final int WSPRNET_IDX_OLDDB_DISTANCE_KM = 10;
        final int WSPRNET_IDX_OLDDB_DISTANCE_MILES = 11;
        
        // Notification calculations
        double minSNR  = Utility.getNotifyMinSNR(context);
        double notifyBandMHz = Utility.getNotifyBand(context),
               notifyBandMHzMin = notifyBandMHz - 0.001, notifyBandMHzMax = notifyBandMHz + 0.001;
        if (notifyBandMHz < 0.00001) {
            notifyBandMHzMin = 0;
            notifyBandMHzMax = 1e300;
        }
        String bandName = "";
        mBandNameIdx = -1; // reset which band was found for notification
        int nHits = 0, nHitsSnr = 0, nHitsBand = 0, nHitsDistance = 0, nHitsTxCall = 0, nHitsRxCall = 0, nHitsTxGrid = 0, nHitsRxGrid = 0;
        double notifyMinTxRxKm = Utility.getNotifyTxRxKm(context);
        // Get the tx/rx notify callsigns, but configure for wildcard matching with regex's.
        String displayTxCallsign = Utility.getNotifyCallsign(context, true),
               displayRxCallsign = Utility.getNotifyCallsign(context, false);
        String displayTxGridsquare = Utility.getNotifyGridsquare(context, true),
               displayRxGridsquare = Utility.getNotifyGridsquare(context, false);
        String notifyTxCallsign = Utility.filterCleanupMatch(displayTxCallsign),
                notifyRxCallsign = Utility.filterCleanupMatch(displayRxCallsign);
        String notifyTxGridsquare = Utility.filterCleanupMatch(displayTxGridsquare),
                notifyRxGridsquare = Utility.filterCleanupMatch(displayRxGridsquare);
        boolean snrOk = false, bandOk = false, distanceOk = false, txCallOk = false, rxCallOk = false, txGridOk = false, rxGridOk = false;
        boolean snrEna = true,
                bandEna = (notifyBandMHz < 0.00001),
                distanceEna = (notifyMinTxRxKm >= 0.001),
                txCallEna = (notifyTxCallsign.length() > 0),
                rxCallEna = (notifyRxCallsign.length() > 0),
                txGridEna = (notifyTxGridsquare.length() > 0),
                rxGridEna = (notifyRxGridsquare.length() > 0);

        // Delete items older than the cutoff period specified in the settings menu.
        // TODO: It might be easier to use System.currentTimeMillis(), which returns time in UTC.  BUT,
        // todo: Date objects seem to work in the local time zone; wasn't able to initialize one to UTC.
        Calendar cal = Calendar.getInstance();
        cal.setTime(new Date());
        TimeZone tz = TimeZone.getDefault();
        int offsetUTC = tz.getOffset(cal.getTimeInMillis()) / 1000;
        int seconds = Utility.cutoffSeconds(context);
        cal.add(Calendar.SECOND, -offsetUTC);
        cal.add(Calendar.SECOND, -seconds);
        String cutoffTimestamp = WsprNetContract.getDbTimestampString(cal.getTime());
        int d;
        d = context.getContentResolver().delete(WsprNetContract.SignalReportEntry.CONTENT_URI,
                WsprNetContract.SignalReportEntry.COLUMN_TIMESTAMPTEXT + " <= ?",
                new String[]{cutoffTimestamp});
        Log.v(LOG_TAG, "getWsprDataFromTags: deleted " + Integer.toString(d) + " old items.");

        // Get the cutoff date for notifications.
        cal.setTime(new Date());
        seconds = Utility.updateIntervalSeconds(context);
        cal.add(Calendar.SECOND, -offsetUTC);
        cal.add(Calendar.SECOND, -seconds);
        long cutoffNotifyTimeMin = getShortTimestamp(cal.getTime());
        cal.add(Calendar.SECOND, 2*seconds);
        long cutoffNotifyTimeMax = getShortTimestamp(cal.getTime());
        long iTimestamp = 0;

        try {
            // TODO: get city name, lat/long from gridsquare; determine how to look this up
            String cityName = context.getString(R.string.unknown_city); // generic text until the city/country is looked up
            String countryName = context.getString(R.string.unknown_country);
            double cityLatitude = Utility.gridsquareToLatitude(gridsquareSetting);
            double cityLongitude = Utility.gridsquareToLongitude(gridsquareSetting);

            Log.v(LOG_TAG, cityName + ", with coord: " + cityLatitude + " " + cityLongitude);

            // Insert the gridsquare into the database.
            long locationID = addGridsquare(context, gridsquareSetting, cityName, countryName, cityLatitude, cityLongitude);
            Elements wsprHeader, wsprHeader1, wsprHeader2; // TODO: someday, match up header name instead of relying on a fixed column #
            Elements wsprData;
            if (drupal == true) {
                wsprHeader = wsprHtml.select("div#block-system-main.block.block-system div.content table tbody tr:eq(0)");
                wsprData = wsprHtml.select("div#block-system-main.block.block-system div.content table tbody tr:gt(0)");
            } else {
                wsprHeader1 = wsprHtml.select("html body table tbody tr:eq(0)");
                wsprHeader2 = wsprHtml.select("html body table tbody tr:eq(1)");
                wsprData   = wsprHtml.select("html body table tbody tr:gt(1)");
            }

            // Get and insert the new wspr information into the database
            Vector<ContentValues> cVVector = new Vector<ContentValues>(wsprData.size());

            for (int i = 0; (i < wsprData.size()) && (i < maxSpots); i++) {
                Elements wsprTDRow = wsprData.get(i).select("td");  // table data row split into <td> elements
                // These are the values that will be collected.
                // column# 0             1       2           3    4       5      6     7          8     9       10
                //   Timestamp          Call  MHz          SNR  Drift  Grid  Pwr  Reporter  RGrid  km      az

                String timestamp, txCallsign, txGridsquare, rxCallsign, rxGridsquare;
                Double txFreqMhz, rxSnr, rxDrift, txPower, kmDistance, azimuth;
                if (drupal == true) {
                    // Wspr information  for the 'drupal' url query interface.
                    // Get wspr info from http://www.wsprnet.org/drupal/wsprnet/spots; e.g.:
                    //   <table>
                    //   <tr>  <th's> Timestamp          Call  MHz          SNR  Drift  Grid  Pwr  Reporter  RGrid  km      az
                    //   <tr>  <td's> 2014-08-25 20:40   DL8EDC  7.040186   -4    0     JO31le    5    LA3JJ/L   JO59bh   925     11
                    //   <tr>  <td's> 2014-08-25 20:40   DL8EDC  7.040183   -9    0     JO31le    5    OZ2ABB   JO65au   618     31
                    //   <tr>  <td's> 2014-08-25 20:40   DL8EDC  7.040178   -14   0     JO31le    5    OH7FES   KP53bh   1919    37
                    //    ... </table>
                    // Note: each item in the header or row is a <td> (but not shown above.)
                    // Use the Firefox plugin Firebug to determine the html structure:
                    //   highlight one of the table rows, right-click on the corresponding <TR> element in the
                    //   plugin, then select "Copy CSS Path"; clipboard contains, e.g.:
                    //     html.js body.html.not-front.not-logged-in.one-sidebar.sidebar-first.page-wsprnet.page-wsprnet-spots div#page div#middlecontainer div#main div#squeeze div#squeeze-content div#inner-content div.region.region-content div#block-system-main.block.block-system div.content table tbody tr
                    //Elements wsprHeader = wsprHtml.select("div#block-system-main.block.block-system div.content table tbody tr:eq(0)");
                    //Elements wsprData = wsprHtml.select("div#block-system-main.block.block-system div.content table tbody tr:gt(0)");
                    //Element wsprOneRow = wsprData.get(0);  // syntax to get specific element #
                    //Elements wsprTDRow = wsprRow.select("th"); // syntax to get header elements
                    //Elements wsprTDRow = wsprRow.select("td"); // syntax to get data elements

                    // Get rid of "&nbsp;" (non-break space character)
                    // Save timestamp as: "yyyyMMddHHmmssSSS"
                    timestamp = parseTimestamp(wsprTDRow.get(WSPRNET_IDX_TIMESTAMP).text()
                            .replace(Utility.NBSP, ' ').replace(" .", ".").replace(".0000", "").trim());
                    txCallsign = wsprTDRow.get(WSPRNET_IDX_TX_CALLSIGN).text().replace(Utility.NBSP, ' ').trim().toUpperCase();
                    txFreqMhz = Double.parseDouble(wsprTDRow.get(WSPRNET_IDX_TX_FREQ_MHZ).text().replace(Utility.NBSP, ' ').trim());
                    rxSnr = Double.parseDouble(wsprTDRow.get(WSPRNET_IDX_RX_SNR).text().replace(Utility.NBSP, ' ').trim());
                    rxDrift = Double.parseDouble(wsprTDRow.get(WSPRNET_IDX_RX_DRIFT).text().replace(Utility.NBSP, ' ').trim());
                    txGridsquare = wsprTDRow.get(WSPRNET_IDX_TX_GRIDSQUARE).text().replace(Utility.NBSP, ' ').trim(); // mixed case!
                    txPower = Double.parseDouble(wsprTDRow.get(WSPRNET_IDX_TX_POWER).text().replace(Utility.NBSP, ' ').trim());
                    rxCallsign = wsprTDRow.get(WSPRNET_IDX_RX_CALLSIGN).text().replace(Utility.NBSP, ' ').trim().toUpperCase();
                    rxGridsquare = wsprTDRow.get(WSPRNET_IDX_RX_GRIDSQUARE).text().replace(Utility.NBSP, ' ').trim(); // mixed case!
                    kmDistance = Double.parseDouble(wsprTDRow.get(WSPRNET_IDX_DISTANCE).text().replace(Utility.NBSP, ' ').trim());
                    azimuth = Double.parseDouble(wsprTDRow.get(WSPRNET_IDX_AZIMUTH).text().replace(Utility.NBSP, ' ').trim());
                } else {
                    // Wspr information  for the 'old' url query interface.
                    //   e.g.: http://wsprnet.org/olddb?mode=html&band=all&limit=10&findcall=&findreporter=&sort=date
                    // Save timestamp as: "yyyyMMddHHmmssSSS"
                    timestamp = parseTimestamp(wsprTDRow.get(WSPRNET_IDX_OLDDB_TIMESTAMP).text()
                            .replace(Utility.NBSP, ' ').replace(" .", ".").replace(".0000", "").trim());
                    txCallsign = wsprTDRow.get(WSPRNET_IDX_OLDDB_TX_CALLSIGN).text().replace(Utility.NBSP, ' ').trim().toUpperCase();
                    txFreqMhz = Double.parseDouble(wsprTDRow.get(WSPRNET_IDX_OLDDB_TX_FREQ_MHZ).text().replace(Utility.NBSP, ' ').trim());
                    rxSnr = Double.parseDouble(wsprTDRow.get(WSPRNET_IDX_OLDDB_RX_SNR).text().replace(Utility.NBSP, ' ').trim());
                    rxDrift = Double.parseDouble(wsprTDRow.get(WSPRNET_IDX_OLDDB_RX_DRIFT).text().replace(Utility.NBSP, ' ').trim());
                    txGridsquare = wsprTDRow.get(WSPRNET_IDX_OLDDB_TX_GRIDSQUARE).text().replace(Utility.NBSP, ' ').trim(); // mixed case!
                    txPower = Double.parseDouble(wsprTDRow.get(WSPRNET_IDX_OLDDB_TX_POWER_DBM).text().replace(Utility.NBSP, ' ').trim());
                    rxCallsign = wsprTDRow.get(WSPRNET_IDX_OLDDB_RX_CALLSIGN).text().replace(Utility.NBSP, ' ').trim().toUpperCase();
                    rxGridsquare = wsprTDRow.get(WSPRNET_IDX_OLDDB_RX_GRIDSQUARE).text().replace(Utility.NBSP, ' ').trim(); // mixed case!
                    kmDistance = Double.parseDouble(wsprTDRow.get(WSPRNET_IDX_OLDDB_DISTANCE_KM).text().replace(Utility.NBSP, ' ').trim());
                  //miDistance = Double.parseDouble(wsprTDRow.get(WSPRNET_IDX_OLDDB_DISTANCE_MILES).text().replace(Utility.NBSP, ' ').trim());
                    // azimuth not provided; must calculate it ourselves
                    azimuth = Utility.latLongToAzimuth(Utility.gridsquareToLatitude(txGridsquare), Utility.gridsquareToLongitude(txGridsquare),
                                                       Utility.gridsquareToLatitude(rxGridsquare), Utility.gridsquareToLongitude(rxGridsquare));
                }  // parse the html

                // Collect the values together.
                ContentValues wsprValues = new ContentValues();
                wsprValues.put(WsprNetContract.SignalReportEntry.COLUMN_LOC_KEY, locationID);
                wsprValues.put(WsprNetContract.SignalReportEntry.COLUMN_TIMESTAMPTEXT, timestamp);
                wsprValues.put(WsprNetContract.SignalReportEntry.COLUMN_TX_CALLSIGN, txCallsign);
                wsprValues.put(WsprNetContract.SignalReportEntry.COLUMN_TX_FREQ_MHZ, txFreqMhz);
                wsprValues.put(WsprNetContract.SignalReportEntry.COLUMN_RX_SNR, rxSnr);
                wsprValues.put(WsprNetContract.SignalReportEntry.COLUMN_RX_DRIFT, rxDrift);
                wsprValues.put(WsprNetContract.SignalReportEntry.COLUMN_TX_GRIDSQUARE, txGridsquare);
                wsprValues.put(WsprNetContract.SignalReportEntry.COLUMN_TX_POWER, txPower);
                wsprValues.put(WsprNetContract.SignalReportEntry.COLUMN_RX_CALLSIGN, rxCallsign);
                wsprValues.put(WsprNetContract.SignalReportEntry.COLUMN_RX_GRIDSQUARE, rxGridsquare);
                wsprValues.put(WsprNetContract.SignalReportEntry.COLUMN_DISTANCE, kmDistance);
                wsprValues.put(WsprNetContract.SignalReportEntry.COLUMN_AZIMUTH, azimuth);
                cVVector.add(wsprValues);

                // Are any reports significant enough to notify the user?
                // For now, notify user if the SNR (signal-to-noise ratio) in a report for a particular
                // band is above a threshold.  The SNR and frequency band are user preferences.
                // TODO: determine the full criteria for notifications.  E.g.:
                //         a specific frequency band has opened up,
                //         maybe to a particular region,
                //         maybe some minimum number of reports at a minimum SNR.
                try {
                    iTimestamp = Long.parseLong(timestamp.substring(0, WsprNetContract.TIMESTAMP_FORMAT_DB_SHORT.length()));
                    if (   (cutoffNotifyTimeMin > 0) && (cutoffNotifyTimeMax > 0)
                        && (cutoffNotifyTimeMin <= iTimestamp) && (iTimestamp < cutoffNotifyTimeMax)) {
                        // getFrequencyBandCheck() will check what band the TX frequency is in.
                        // frequencyBandNotifyCheck will check if it is in the notification band.
                        double bandMHz = getFrequencyBandCheck(context, txFreqMhz, mBandFrequencyTolerancePercent);
                        bandOk = !bandEna || frequencyBandNotifyCheck(bandMHz, notifyBandMHzMin, notifyBandMHzMax);
                        snrOk = !snrEna || (rxSnr >= minSNR);
                        distanceOk = !distanceEna || (kmDistance >= notifyMinTxRxKm);
                        txCallOk = !txCallEna || txCallsign.matches(notifyTxCallsign);
                        rxCallOk = !rxCallEna || rxCallsign.matches(notifyRxCallsign);
                        txGridOk = !txGridEna || txGridsquare.toUpperCase().matches(notifyTxGridsquare);
                        rxGridOk = !rxGridEna || rxGridsquare.toUpperCase().matches(notifyRxGridsquare);
                        if (bandOk && snrOk && distanceOk && txCallOk && rxCallOk && txGridOk && rxGridOk) {
                            nHits++;
                            nHitsBand += (bandEna && bandOk) ? 1 : 0;
                            nHitsSnr += (snrEna && snrOk) ? 1 : 0;
                            nHitsDistance += (distanceEna && distanceOk) ? 1 : 0;
                            nHitsTxCall += (txCallEna && txCallOk) ? 1 : 0;
                            nHitsRxCall += (rxCallEna && rxCallOk) ? 1 : 0;
                            nHitsTxGrid += (txGridEna && txGridEna) ? 1 : 0;
                            nHitsRxGrid += (rxGridEna && rxGridEna) ? 1 : 0;
                        }
                    }
                } catch (Exception e) {
                  // nothing to do
                }
            } // parse html tags

            // Insert items into database.
            if (cVVector.size() > 0) {
                ContentValues[] cvArray = new ContentValues[cVVector.size()];
                cVVector.toArray(cvArray);
                int ii = context.getContentResolver().bulkInsert(WsprNetContract.SignalReportEntry.CONTENT_URI, cvArray);
                Log.v(LOG_TAG, "getWsprDataFromTags: inserted " + cVVector.size() + "(" + Integer.toString(ii) + ") items");
            }

            // Remove items with an unreasonable timestamp (>24 hours from now); otherwise, they're displayed forever!
            // TODO: don't insert these in the first place!
            cal.setTime(new Date());
            cal.add(Calendar.HOUR, 24);
            String tomorrowTimestamp = WsprNetContract.getDbTimestampString(cal.getTime());
            d = context.getContentResolver().delete(WsprNetContract.SignalReportEntry.CONTENT_URI,
                  WsprNetContract.SignalReportEntry.COLUMN_TIMESTAMPTEXT + " > ?",
                  new String[]{tomorrowTimestamp});
            Log.v(LOG_TAG, "getWsprDataFromTags: deleted " + Integer.toString(d) + " invalid items.");

            // Did any reports meet the notification criteria?
            if (nHits > 0) {
                String description = "";
                if (notifyBandMHz < 0.00001) {
                    bandName = "---";
                } else {
                    bandName = getFrequencyBandName(context, mBandNameIdx);
                    //description += context.getString(R.string.band_open) + ":";
                }
                if (nHitsTxCall > 0) {
                    description += " " + context.getString(R.string.pref_filter_label_tx_callsign) + "=" + displayTxCallsign + ";";
                }
                if (nHitsRxCall > 0) {
                    description += " " + context.getString(R.string.pref_filter_label_rx_callsign) + "=" + displayRxCallsign + ";";
                }
                if (nHitsTxGrid > 0) {
                    description += " " + context.getString(R.string.pref_filter_label_tx_gridsquare) + "=" + displayTxGridsquare + ";";
                }
                if (nHitsRxGrid > 0) {
                    description += " " + context.getString(R.string.pref_filter_label_rx_gridsquare) + "=" + displayRxGridsquare + ";";
                }
                if (nHitsDistance > 0) {
                    // TODO: Display either km or miles.  See SettingsActivity.java, onPreferenceChange().
                    description += " distance>=" + Utility.formatDistance(context, notifyMinTxRxKm, Utility.isMetric(context) ) + "km;";
                }
                notifyWspr(context, bandName, description, minSNR);
            }

        } catch(Exception e) {
            Log.d(LOG_TAG, "getWsprDataFromTags exception: " + e.toString());

        }
    } // getWsprDataFromTags()


    // Make a notification to the user about propagation conditions; they'll want to get on the air now!
    // Notifications must be enabled in the user preferences, and don't notify any more often than
    // specified in the preferences (the "discard data after ..." cutoff value does double duty.)
    private static void notifyWspr(Context context, String bandName, String description, double snr) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        String displayNotificationsKey = context.getString(R.string.pref_enable_notifications_key);

        // Get whether notifications are enabled in preferences.
        boolean notificationsEnabled =
                prefs.getBoolean(displayNotificationsKey,
                        Boolean.parseBoolean(context.getString(R.string.pref_enable_notifications_default)));

        if (notificationsEnabled) {
            // Don't notify more often than the user-preference cutoff interval.
            // pref_last_notification is only stored; it is not displayed in the Settings menu.
            // pref_last_notification gets saved below, in editor.putLong(lastNotificationKey, ...).
            long prefMillis = 1000*(long)Utility.cutoffSeconds(context);
            String lastNotificationKey = context.getString(R.string.pref_last_notification);
            long lastNotification = prefs.getLong(lastNotificationKey, 0);
            Date now  = new Date(System.currentTimeMillis());
            Date last = new Date(lastNotification);

            if ((System.currentTimeMillis() - lastNotification) >= prefMillis) {
                // It's been long enough since the last notification; send a new one now.

                int iconId = Utility.getIconResourceForWsprCondition(snr);
                String title = context.getString(R.string.app_name);

                // Define the text of the wspr notification.
                String contentText = String.format(context.getString(R.string.format_notification),
                        description,
                        bandName,
                        Utility.formatSnr(context, snr));

                // NotificationCompatBuilder builds backward-compatible notifications.
                NotificationCompat.Builder mBuilder =
                        new NotificationCompat.Builder(context)
                                .setSmallIcon(iconId)
                                .setContentTitle(title)
                                .setContentText(contentText);

                // Open this app if the user clicks on the notification.
                Intent resultIntent = new Intent(context, MainActivity.class);
                TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
                stackBuilder.addParentStack(MainActivity.class);
                stackBuilder.addNextIntent(resultIntent);
                PendingIntent resultPendingIntent =
                        stackBuilder.getPendingIntent(
                                0,
                                PendingIntent.FLAG_UPDATE_CURRENT
                        );
                mBuilder.setContentIntent(resultPendingIntent);
                NotificationManager mNotificationManager =
                        (NotificationManager) context
                                .getSystemService(Context.NOTIFICATION_SERVICE);
                // mId allows you to update the notification later on.
                mNotificationManager.notify(WSPR_NOTIFICATION_ID, mBuilder.build());

                //refreshing last sync
                SharedPreferences.Editor editor = prefs.edit();
                editor.putLong(lastNotificationKey, System.currentTimeMillis());
                editor.commit();
            }
        }
    } // notifyWspr()


    /**
     * Helper function to make the adapter sync now.
     *
     * @param context The application context
     */
    public static void syncImmediately(Context context) {
        // Pass the settings flags by inserting them in a bundle
        Bundle settingsBundle = new Bundle();
        settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
        settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
        /*
         * Request the sync for the default account, authority, and manual sync settings
         */
        ContentResolver.requestSync(getSyncAccount(context), context.getString(R.string.content_authority), settingsBundle);
    }


    /**
     * Helper method to schedule the sync adapter periodic execution
     */
    public static void configurePeriodicSync(Context context, int syncInterval, int flexTime) {
        Account account = getSyncAccount(context);
        String authority = context.getString(R.string.content_authority);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            // we can enable inexact timers in our periodic sync
            SyncRequest request = new SyncRequest.Builder().
                    syncPeriodic(syncInterval, flexTime).
                    setSyncAdapter(account, authority).build();
            ContentResolver.requestSync(request);
        } else {
            ContentResolver.addPeriodicSync(account,
                    authority, new Bundle(), syncInterval);
        }
    }

    /**
     * Create a new dummy account for the sync adapter
     *
     * @param context The application context
     */
    public static Account getSyncAccount(Context context) {
        // Get an instance of the Android account manager
        AccountManager accountManager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);

        // Create the account type and default account
        // app_name = WsprNetViewer
        // sync_account_type = wsprnetviewer.joe.glandorf1.com
        Account newAccount = new Account(context.getString(R.string.app_name), context.getString(R.string.sync_account_type));

        // If the password doesn't exist, the account doesn't exist
        if (null == accountManager.getPassword(newAccount)) {
            // Add the account and account type, no password or user data
            // If successful, return the Account object, otherwise report an error.
        boolean ret = accountManager.addAccountExplicitly(newAccount, "", null);
            if (!ret) {
                return null;
            }

            // If you don't set android:syncable="true" in
            // in your <provider> element in the manifest,
            // then call context.setIsSyncable(account, AUTHORITY, 1)
            // here.
            onAccountCreated(newAccount, context);
        }
        return newAccount;
    }

    private static void onAccountCreated(Account newAccount, Context context) {

        // Schedule the sync for periodic execution
        WsprNetViewerSyncAdapter.configurePeriodicSync(context, SYNC_INTERVAL, SYNC_FLEXTIME);

        // Without calling setSyncAutomatically, our periodic sync will not be enabled.
        ContentResolver.setSyncAutomatically(newAccount, context.getString(R.string.content_authority), true);

        // Let's do a sync to get things started.
        syncImmediately(context);
    }


    public static void initializeSyncAdapter(Context context) {
        getSyncAccount(context);
    }
}




Java Source Code List

com.glandorf1.joe.wsprnetviewer.app.ApplicationTest.java
com.glandorf1.joe.wsprnetviewer.app.DetailActivity.java
com.glandorf1.joe.wsprnetviewer.app.DetailFragment.java
com.glandorf1.joe.wsprnetviewer.app.FullTestSuite.java
com.glandorf1.joe.wsprnetviewer.app.MainActivity.java
com.glandorf1.joe.wsprnetviewer.app.SettingsActivity.java
com.glandorf1.joe.wsprnetviewer.app.SimpleEula.java
com.glandorf1.joe.wsprnetviewer.app.TestDb.java
com.glandorf1.joe.wsprnetviewer.app.TestProvider.java
com.glandorf1.joe.wsprnetviewer.app.Utility.java
com.glandorf1.joe.wsprnetviewer.app.WsprAdapter.java
com.glandorf1.joe.wsprnetviewer.app.WsprFragment.java
com.glandorf1.joe.wsprnetviewer.app.data.WsprNetContract.java
com.glandorf1.joe.wsprnetviewer.app.data.WsprNetCustomView.java
com.glandorf1.joe.wsprnetviewer.app.data.WsprNetDbHelper.java
com.glandorf1.joe.wsprnetviewer.app.data.WsprNetProvider.java
com.glandorf1.joe.wsprnetviewer.app.sync.WsprNetViewerAuthenticatorService.java
com.glandorf1.joe.wsprnetviewer.app.sync.WsprNetViewerAuthenticator.java
com.glandorf1.joe.wsprnetviewer.app.sync.WsprNetViewerSyncAdapter.java
com.glandorf1.joe.wsprnetviewer.app.sync.WsprNetViewerSyncService.java