/* * [SetClock.java] * * Summary: set PC Clock from an atomic clock. * * Copyright: (c) 1999-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 7.3 2005-05-09 * 7.4 2005-07-31 ANT scripts, Javadoc, release source, change message to make button say "done" when complete, * add an about button. * 7.5 2005-07-31 new icon, bigger type. * 7.6 2006-02-05 new icon, bigger type. * 7.7 2006-03-05 reformat with IntelliJ and add Javadoc * 7.8 2007-05-24 IntelliJ inspector lint. * 7.9 2007-06-14 catch error if set time fails. * 8.0 2008-04-06 add build to title, tidy code, correct spelling errors. * 8.1 2008-08-18 use xx.pool.ntp.org to find a nearby time server. * 8.2 2008-09-08 use 3 different pool servers and two probes on each to get a better average. * 8.3 2008-09-23 fix problem with Microsoft C++ runtime library. * 8.4 2010-03-24 allow user to select look and feel. * 8.5 2011-12-22 update JNLP. * 8.6 2012-12-11 add 64-bit DLL for JNI */ package com.mindprod.setclock; import com.mindprod.common18.Build; import com.mindprod.common18.CMPAboutBox; import com.mindprod.common18.FontFactory; import com.mindprod.common18.HybridJ; import com.mindprod.common18.JEButton; import com.mindprod.common18.Laf; import com.mindprod.common18.VersionCheck; import com.mindprod.inwords.TimeInterval; import javax.swing.JApplet; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JTextField; import java.awt.Color; import java.awt.Container; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; import java.util.SimpleTimeZone; import java.util.TimeZone; import static java.lang.System.*; /** * set PC Clock from an atomic clock. *

* JWS app to set system date time to match SNTP time server time from the Internet. Java Web Start Application Was * originally an Applet, but hassles of JNI meant that was abandoned for JWS. We run this as an JWS application, not an * Applet. * * @author Roedy Green, Canadian Mind Products * @version 8.6 2012-12-11 add 64-bit DLL for JNI * @since 1999 */ @SuppressWarnings( { "FieldCanBeLocal" } ) public final class SetClock extends JApplet { /** * true if want additional debugging output */ @SuppressWarnings( { "UnusedDeclaration" } ) private static final boolean DEBUGGING = false; // Futures: // detect changed TimeZone // get good display up quickly // progress note while ask timeserver // recover if timeserver silent // find out why freezes if no diff in time. // list atomic clocks, let user choose. // Try different clocks over time and gradually // home in on a geographically close set with fastest response. // Once in sync, if user changes time, should rethink accurate clock. // Colours. /** * height of Applet box in pixels. Does not include surrounding frame. */ private static final int APPLET_HEIGHT = 325; /** * Width of Applet box in pixels. */ private static final int APPLET_WIDTH = 518; private static final int FIRST_COPYRIGHT_YEAR = 1999; /** * how many times we ask the Time Server for time. We average the results of these probes. */ private static final int PROBES = 6; /** * embedded copyright not displayed */ @SuppressWarnings( { "UnusedDeclaration" } ) private static final String EMBEDDED_COPYRIGHT = "Copyright: (c) 1999-2017 Roedy Green, Canadian Mind Products, " + "http://mindprod.com"; /** * date this version released. */ private static final String RELEASE_DATE = "2012-12-11"; /** * title. */ private static final String TITLE_STRING = "SetClock"; /** * version. */ private static final String VERSION_STRING = "8.6"; private static final Color FOREGROUND_FOR_LABEL = new Color( 0x0000b0 ); /** * DARK_GREEN Colour. */ private static final Color FOREGROUND_FOR_TEXT = new Color( 0x008000 ); /** * for titles */ private static final Color FOREGROUND_FOR_TITLE = new Color( 0xdc143c ); /** * Field RED */ @SuppressWarnings( { "ConstantNamingConvention" } ) private static final Color RED = Color.RED; /** * Colour WHITE. */ private static final Color WHITE = Color.WHITE; /** * Big font. */ private static final Font FONT_BIG = FontFactory.build( "Dialog", Font.BOLD, 13 ); /** * for for titles and About buttons */ private static final Font FONT_FOR_TITLE = FontFactory.build( "Dialog", Font.BOLD, 16 ); /** * for for title second line */ private static final Font FONT_FOR_TITLE2 = FontFactory.build( "Dialog", Font.PLAIN, 14 ); /** * medium size font. */ private static final Font FONT_MEDIUM = FontFactory.build( "Dialog", Font.BOLD, 11 ); /** * small font. */ private static final Font FONT_SMALL = FontFactory.build( "Dialog", Font.PLAIN, 11 ); /** * Local date formatter. */ private static final SimpleDateFormat FLOCAL = new SimpleDateFormat( "EEEE yyyy-MM-dd hh:mm:ss aa" ); /** * TimeZone short : long form. */ private static final SimpleDateFormat FZONE = new SimpleDateFormat( "zz : zzzzzz" ); /** * used to put correction into words */ private static final TimeInterval inWords = new TimeInterval(); private static final TimeZone UTC = new SimpleTimeZone( 0, "UTC" ); static { String dll = "setclock.32"; try { // load nativepcclock.dll on path, set up by Java Web Start // arch is x86 or amd64 if ( System.getProperty( "os.arch" ).equals( "amd64" ) ) { dll = "setclock.64"; } // without .dll suffix System.loadLibrary( dll ); } catch ( Exception e ) { out.println( "Unable to load " + dll ); out.println( e.getMessage() ); System.exit( 1 ); } } /** * Used to ask the background updater to stop running so we can have some cpu cycles for the foreground tasks. */ private Boolean pleaseStop = Boolean.FALSE; /** * thread to continuously update the clock */ private ClockUpdater clockUpdater; /** * Applet ContentPane */ private Container contentPane; /** * best guess at accurate time, updated each second. */ private GregorianCalendar accurateTime; /** * Time according to local PC clock, updated each second. */ private GregorianCalendar pcTime; /** * button to make the time correction */ private JButton setClockButton; /** * control to label the accurate time */ private JLabel accurateTimeLabel; /** * control to label the correction required */ private JLabel correctionLabel; /** * control for the instruction on how to use. */ private JLabel instructionsText; /** * control to label the PC's time */ private JLabel pcTimeLabel; /** * control to label the TimeZone */ private JLabel timeZoneLabel; /** * control for title title */ private JLabel title; /** * title, second line */ private JLabel title2; /** * control to contain the accurate time */ private JTextField accurateTimeText; /** * control to contain the correction required */ private JTextField correctionText; /** * control to contain the PC's time */ private JTextField pcTimeText; /** * control to contain the TimeZone */ private JTextField timeZoneName; /** * 0=start 1=have accurate time 2=accurate time set. */ private int stage = 0; /** * current error in milliseconds between two clocks. Add this to pcTime to get accurateTime */ private long correction; /** * allocate all the Components */ private void buildComponents() { // INIT_CONTROLS contentPane.setLayout( new GridBagLayout() ); title = new JLabel( TITLE_STRING + " " + VERSION_STRING, JLabel.RIGHT ); title.setForeground( FOREGROUND_FOR_TITLE ); title.setFont( FONT_FOR_TITLE ); title2 = new JLabel( "released:" + RELEASE_DATE + " build:" + Build.BUILD_NUMBER ); title2.setFont( FONT_FOR_TITLE2 ); title2.setForeground( FOREGROUND_FOR_TITLE ); timeZoneLabel = new JLabel( "Your Time Zone", JLabel.RIGHT ); timeZoneLabel.setForeground( FOREGROUND_FOR_LABEL ); timeZoneLabel.setFont( FONT_BIG ); timeZoneName = new JTextField(); timeZoneName.setForeground( FOREGROUND_FOR_TEXT ); timeZoneName.setEditable( false ); pcTimeLabel = new JLabel( "Your PC's Clock", JLabel.RIGHT ); pcTimeLabel.setForeground( FOREGROUND_FOR_LABEL ); pcTimeLabel.setFont( FONT_BIG ); pcTimeText = new JTextField(); pcTimeText.setForeground( FOREGROUND_FOR_TEXT ); pcTimeText.setEditable( false ); correctionLabel = new JLabel( "+ Correction", JLabel.RIGHT ); correctionLabel.setForeground( RED ); correctionLabel.setFont( FONT_BIG ); correctionText = new JTextField(); correctionText.setForeground( RED ); correctionText.setEditable( false ); accurateTimeLabel = new JLabel( "= Accurate Clock", JLabel.RIGHT ); accurateTimeLabel.setForeground( FOREGROUND_FOR_LABEL ); accurateTimeLabel.setFont( FONT_BIG ); accurateTimeText = new JTextField(); accurateTimeText.setForeground( FOREGROUND_FOR_LABEL ); accurateTimeText.setEditable( false ); setClockButton = new JEButton( "Get Time" ); setClockButton.setToolTipText( "Get the accurate time from an atomic clock on the web." ); setClockButton.addActionListener( new ActionListener() { /** * Handle Button Push * @param e details of just what the user clicked. */ public void actionPerformed( ActionEvent e ) { final Object object = e.getSource(); if ( object == setClockButton ) { // stop background ClockUpdater so it won't interfere with // us stop(); switch ( stage ) { case 0: // get Time from atomic clock getAtomicTime(); break; case 1: // set both clocks to the same time. setTime(); break; case 2: default: System.exit( 0 ); } // restart background ClockUpdater start(); } } // end actionPerformed } // end anonymous ActionListener ); instructionsText = new JLabel( "To get the accurate time from an atomic clock, click \"Get Time\"", JLabel.CENTER ); instructionsText.setForeground( FOREGROUND_FOR_TEXT ); instructionsText.setFont( FONT_SMALL ); } /** * build a menu with Look & Feel and About across the top */ private void buildMenu() { // turn on anti-alias System.setProperty( "swing.aatext", "true" ); final JMenuBar menubar = new JMenuBar(); setJMenuBar( menubar ); final JMenu lafMenu = Laf.buildLookAndFeelMenu(); if ( lafMenu != null ) { menubar.add( lafMenu ); } final JMenu menuHelp = new JMenu( "Help" ); menubar.add( menuHelp ); final JMenuItem aboutItem = new JMenuItem( "About" ); menuHelp.add( aboutItem ); aboutItem.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { // open about frame new CMPAboutBox( TITLE_STRING, VERSION_STRING, "Set your hardware clock from an accurate atomic timeserver clock on the web.", "Currently only works on Windows 32/64-bit.", "freeware", RELEASE_DATE, FIRST_COPYRIGHT_YEAR, "Roedy Green", "SETCLOCK", "1.6" ); } } ); } /** * display Accurate local Time Used by both refresh and background updater */ private void displayAccurateTime() { // intern to reduce garbage collection frequency // Avoid flicker. final String t; if ( stage > 0 ) { FLOCAL.setTimeZone( TimeZone.getDefault() ); t = FLOCAL.format( accurateTime.getTime() ).intern(); } else { t = ""; } if ( !t.equals( accurateTimeText.getText() ) ) { accurateTimeText.setText( t ); } } // end displayAccurateTime /** * display correction */ private void displayCorrection() { if ( stage > 0 ) { if ( correction < 0 ) { correctionLabel.setText( "- Correction" ); correctionText.setText( inWords.toWords( -correction ) ); } else { correctionLabel.setText( "+ Correction" ); correctionText.setText( inWords.toWords( correction ) ); } } else { correctionLabel.setText( "+ Correction" ); correctionText.setText( "" ); } } // end displayCorrection /** * display PC local time. Used by both refresh and background updater */ private void displayPCTime() { // intern to reduce garbage collection frequency // Avoid flicker, since can easily tell if no change. FLOCAL.setTimeZone( TimeZone.getDefault() ); final String t = FLOCAL.format( pcTime.getTime() ).intern(); if ( !t.equals( pcTimeText.getText() ) ) { pcTimeText.setText( t ); } } // end displayPCTime /** * display local TimeZone info */ private void displayTimeZone() { // unfortunately this will not change if user changes TimeZone // behind our back. FZONE.setTimeZone( TimeZone.getDefault() ); // Get TimeZone offset in minutes final long millis = pcTime.get( GregorianCalendar.ZONE_OFFSET ) + pcTime.get( GregorianCalendar.DST_OFFSET ); final int minutes = ( int ) ( millis / ( 1000 * 60 ) ); // display Timezone offset shortname : longname : (GMT-hh:mm) timeZoneName.setText( FZONE.format( pcTime.getTime() ) + " (GMT" + hhmm( minutes ) + ")" ); } // end displayTimeZone /** * Ask time server website for accurate time. */ private void getAtomicTime() { Thread.yield(); if ( ( correction = getPoolCorrection() ) == Long.MAX_VALUE ) { if ( ( correction = getSingleServerCorrection( "north-america.pool.ntp.org" ) ) == Long.MAX_VALUE ) { if ( ( correction = getSingleServerCorrection( "128.118.25.3" ) ) == Long.MAX_VALUE ) { throw new IllegalArgumentException( "None of the atomic clock timeservers are responding" ); } } } // if we get here, correction contains the correction to our pc clock. stage = 1; updateBothTimes(); refresh(); } // end getAtomicTime /** * get correction using 0.ca.pool.ntp.org time servers * * @return correction in ms */ long getPoolCorrection() { final String country = Locale.getDefault().getCountry().toLowerCase(); long total = 0; for ( int i = 0; i < PROBES; i++ ) { final String nearbySNTPServer = i % 3 + "." + country + ".pool.ntp.org"; out.println( "using timeserver " + nearbySNTPServer ); final long adjust = MiniSNTP.correction( nearbySNTPServer ); if ( adjust == Long.MAX_VALUE ) { return Long.MAX_VALUE; } total += adjust; } return ( total + PROBES / 2 ) / PROBES; } /** * get correction using 0.ca.pool.ntp.org time servers * * @param nearbySNTPServer name of timeserver, e.g. " 0.ca.pool.ntp.org" * * @return correction in ms */ long getSingleServerCorrection( String nearbySNTPServer ) { out.println( "using timeserver " + nearbySNTPServer ); long total = 0; for ( int i = 0; i < PROBES; i++ ) { final long adjust = MiniSNTP.correction( nearbySNTPServer ); if ( adjust == Long.MAX_VALUE ) { return Long.MAX_VALUE; } total += adjust; } return ( total + PROBES / 2 ) / PROBES; } /** * format minutes as -HH:MM with lead zeros * * @param mins minutes, may be over 60. * * @return hours:minutes string */ private String hhmm( int mins ) { if ( mins == 0 ) { return "+0"; } final boolean negative = ( mins < 0 ); if ( negative ) { mins = -mins; } final char m2 = ( char ) ( mins % 10 + '0' ); mins /= 10; final char m1 = ( char ) ( mins % 6 + '0' ); mins /= 6; final char h2 = ( char ) ( mins % 10 + '0' ); mins /= 10; final char h1 = ( char ) ( mins % 10 + '0' ); StringBuilder result = new StringBuilder( 5 ); result.append( negative ? '-' : '+' ); result.append( h1 ); result.append( h2 ); result.append( ':' ); result.append( m1 ); result.append( m2 ); return result.toString(); } // end hhmm /** * layout components in a GridBag */ private void layoutComponents() { // add components // --0---------1--------------------2---- // title -------title2------------------- 0 // "yourtz" tz ------------------------ 1 // "pcclock" pcTime--------------------- 2 // "Corr" corr----------------------- 3 // ----------- GETTIME ------------------- 4 // -----------instructions---------------- 5 // x y w h wtx wty anchor fill T L B R padx pady contentPane.add( title, new GridBagConstraints( 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.NORTHEAST, GridBagConstraints.NONE, new Insets( 10, 10, 5, 5 ), 0, 0 ) ); contentPane.add( title2, new GridBagConstraints( 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.NORTHEAST, GridBagConstraints.NONE, new Insets( 10, 5, 5, 5 ), 0, 0 ) ); contentPane.add( timeZoneLabel, new GridBagConstraints( 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 5, 10, 5, 5 ), 0, 0 ) ); contentPane.add( timeZoneName, new GridBagConstraints( 1, 1, 2, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets( 5, 5, 5, 10 ), 0, 0 ) ); contentPane.add( pcTimeLabel, new GridBagConstraints( 0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 5, 10, 5, 5 ), 0, 0 ) ); contentPane.add( pcTimeText, new GridBagConstraints( 1, 2, 2, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets( 5, 5, 5, 10 ), 0, 0 ) ); contentPane.add( correctionLabel, new GridBagConstraints( 0, 3, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 5, 10, 5, 5 ), 0, 0 ) ); contentPane.add( correctionText, new GridBagConstraints( 1, 3, 2, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets( 5, 5, 5, 10 ), 0, 0 ) ); contentPane.add( accurateTimeLabel, new GridBagConstraints( 0, 4, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 10, 10, 5, 5 ), 0, 0 ) ); contentPane.add( accurateTimeText, new GridBagConstraints( 1, 4, 2, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets( 5, 5, 5, 10 ), 0, 0 ) ); contentPane.add( setClockButton, new GridBagConstraints( 1, 5, 1, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets( 10, 5, 10, 5 ), 10, 10 ) ); contentPane.add( instructionsText, new GridBagConstraints( 0, 6, 3, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets( 5, 10, 10, 10 ), 0, 0 ) ); } /** * nativeSetClock wants UTC time. natural year, month, day -- not zero based. return true if all went ok. * * @param year year e.g. 2010 * @param month 1-based month * @param day 1-based day * @param hour 0-based 24-hour * @param minute 0-based minute * @param second 0-based seconds * @param millis 0-based milliseconds * * @return true if all went ok. */ native boolean nativeSetClock( int year, int month, int day, int hour, int minute, int second, int millis ); /** * Update all variable parts of the screen; Not used by background updater, which just updates some fields. */ private void refresh() { // display accurate time displayAccurateTime(); // display pc time displayPCTime(); // display correction displayCorrection(); // may grow or shrink appreciably. correctionText.invalidate(); // display TimeZone offset -HH:MM + name displayTimeZone(); // may grow or shrink appreciably timeZoneName.invalidate(); if ( stage > 0 ) { if ( correction == 0 ) { stage = 2; setClockButton.setText( "Exit" ); setClockButton.setToolTipText( "Exit the program." ); report( "Your PC clock is now set accurately. Click Exit." ); } else { setClockButton.setText( "Set Clock" ); setClockButton.setToolTipText( "Set your PC clock to the accurate atomic time." ); report( "To change your PC clock to the accurate date/time shown, click \"Set Clock\"" ); } } else { setClockButton.setText( "Get Time" ); setClockButton.setToolTipText( "Get the accurate time from an atomic clock on the web." ); report( "To get the accurate time from an atomic timeserver clock on the web, click \"Get Time\"" ); } validate(); repaint(); } // end refresh /** * keep user informed what is going on. * * @param r what to tell the user */ private void report( String r ) { instructionsText.setText( r ); instructionsText.invalidate(); validate(); repaint(); } // end report /** * Set date and Time * * @param d contains UTC date/time * * @return true if all went ok */ boolean setClock( Date d ) { // take Timestamp apart into fields Calendar c = new GregorianCalendar( UTC ); c.setTime( d ); int year = c.get( Calendar.YEAR ); int month = c.get( Calendar.MONTH ) + 1; int day = c.get( Calendar.DAY_OF_MONTH ); int hour = c.get( Calendar.HOUR_OF_DAY ); int minute = c.get( Calendar.MINUTE ); int second = c.get( Calendar.SECOND ); int millis = c.get( Calendar.MILLISECOND ); try { return nativeSetClock( year, month, day, hour, minute, second, millis ); } catch ( Exception e ) { out.println( "Unable to access setclock.32.dll or setclock.64.dll." ); out.println( e.getMessage() ); return false; } } /** * Set PC time from atomic clock correction */ private void setTime() { // set both clocks to the same time. synch(); refresh(); } // end SetTime /** * Synchronise the PC clock to the server clock. Set the PC system clock, using a native method. */ private void synch() { if ( stage == 0 ) { return; } Thread.yield(); updateBothTimes(); Date timestamp = accurateTime.getTime(); boolean success; try { success = setClock( timestamp ); } catch ( Exception e ) { report( "Exception accessing setClock method." ); err.println(); err.println( e.getMessage() ); err.println( "Exception accessing setClock method." ); err.println(); return; } if ( success ) { correction = 0; updateBothTimes(); refresh(); } else { report( "Windows refused to permit setting system time." ); err.println( "Windows refused to permit setting system time." ); } } // end synch /** * update values for both pcTime and accurateTime using the local clock. Presumes correction is already calculated. * accurateTime is computed from the correction. */ private void updateBothTimes() { pcTime = new GregorianCalendar();// init to current time, current // TimeZone final Date d = pcTime.getTime(); d.setTime( d.getTime() + correction ); accurateTime.setTime( d ); } // end updateBothTimes /** * We run only as JWS not as Applet. * * @param args command line arguments ignored. */ public static void main( String[] args ) { out.println( "<>classpath<>" ); out.println( System.getProperty( "java.class.path" ) ); out.println( "<>library path<>" ); out.println( System.getProperty( "java.library.path" ) ); HybridJ.fireup( new SetClock(), TITLE_STRING + " " + VERSION_STRING, APPLET_WIDTH, APPLET_HEIGHT ); } // end main /** * Called by the browser or Applet viewer to inform * this Applet that it is being reclaimed and that it should destroy * any resources that it has allocated. */ @Override public void destroy() { accurateTime = null; accurateTimeLabel = null; accurateTimeText = null; clockUpdater = null; contentPane = null; correctionLabel = null; correctionText = null; instructionsText = null; pcTime = null; pcTimeLabel = null; pcTimeText = null; setClockButton = null; timeZoneLabel = null; timeZoneName = null; title2 = null; title = null; } /** * Called by the browser or Applet viewer to inform * this Applet that it has been loaded into the system. */ @Override public void init() { if ( !VersionCheck.isJavaVersionOK( 1, 7, 0, this ) ) { return; } buildMenu(); // also initial L&F contentPane = this.getContentPane(); contentPane.setBackground( WHITE ); contentPane.setFont( FONT_MEDIUM ); accurateTime = new GregorianCalendar(); pcTime = new GregorianCalendar(); buildComponents(); layoutComponents(); updateBothTimes(); refresh(); validate(); setVisible( true ); } // end init /** * standard Applet start -- called whenever we scroll onscreen */ public void start() { if ( clockUpdater == null ) { clockUpdater = new ClockUpdater(); clockUpdater.start(); // Give updater slightly lower priority so that setClock button // thread will have no problem executing. // Unfortunately, the updater thread never sleeps. clockUpdater.setPriority( Thread.NORM_PRIORITY - 1 ); } } // end start /** * standard Applet stop -- called whenever we scroll offscreen */ public void stop() { if ( clockUpdater != null && clockUpdater.isAlive() ) { pleaseStop = Boolean.TRUE; // wait till does actually stop // TKM do this properly with some sort of wait-notify while ( pleaseStop ) { try { // sleep for 0.1 second to give ClockUpdater task a chance // to stop Thread.sleep( 100 ); } catch ( InterruptedException e ) { // no problem } } // end while } // end if clockUpdater = null; } // end stop /** * Inner class to update the accurate and inaccurate clock displays * I wrote this originally for AWT. Today, I would have used a Swing Timer * to simplify the code. Swing would then handle the looping. */ private final class ClockUpdater extends Thread { /** * Method run ... */ @SuppressWarnings( { "UnnecessaryLabelOnBreakStatement", "UnusedLabel" } ) public void run() { //outerWhile: while ( true ) { updateBothTimes(); long oldTime = pcTime.getTime().getTime(); long oldZone = pcTime.get( GregorianCalendar.ZONE_OFFSET ) + pcTime.get( GregorianCalendar.DST_OFFSET ); innerWhile: while ( true ) { // TimeZone, correction are constant displayPCTime(); displayAccurateTime(); // we dare not sleep. If user changes clock while // we are asleep, we might never wake up. try { // sleep for 0.1 second anyway, just to avoid hogging // all the cpu Thread.sleep( 100 ); } catch ( InterruptedException e ) { // nothing } // However, we give other threads a kick at the can every // time // through the loop. Thread.yield(); if ( pleaseStop ) { pleaseStop = Boolean.FALSE; return; } updateBothTimes(); final long newTime = pcTime.getTime().getTime(); final long newZone = pcTime.get( GregorianCalendar.ZONE_OFFSET ) + pcTime .get( GregorianCalendar.DST_OFFSET ); if ( newZone != oldZone || newTime < oldTime || newTime > oldTime + 500 ) { // Need to recalculate the correction, clock just // hiccoughed break; } // end if oldTime = newTime; oldZone = newZone; } // end innerWhile // Somebody probably fiddled with the system clock // behind our back. // Get new correction. stage = 0; getAtomicTime(); updateBothTimes(); } // end outerWhile } // end run } }