1c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler/* 2d6d68b49aaf5b0fbcce89097ca4c02d73bcea4ddJustin Klaassen * Copyright (C) 2016 The Android Open Source Project 3c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler * 4c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler * Licensed under the Apache License, Version 2.0 (the "License"); 5c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler * you may not use this file except in compliance with the License. 6c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler * You may obtain a copy of the License at 7c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler * 8d6d68b49aaf5b0fbcce89097ca4c02d73bcea4ddJustin Klaassen * http://www.apache.org/licenses/LICENSE-2.0 9c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler * 10c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler * Unless required by applicable law or agreed to in writing, software 11c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler * distributed under the License is distributed on an "AS IS" BASIS, 12c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler * See the License for the specific language governing permissions and 14c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler * limitations under the License. 15c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler */ 16c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler 17c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandlerpackage com.android.deskclock; 18c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler 19c0743272ad924bc97d335d695be3e6b32a83dd7dAdrian Roosimport android.app.AlarmManager; 20ed2084787cec0f02e6cead215d409d6f2f60f737Alon Albertimport android.content.BroadcastReceiver; 21ed2084787cec0f02e6cead215d409d6f2f60f737Alon Albertimport android.content.Context; 22ed2084787cec0f02e6cead215d409d6f2f60f737Alon Albertimport android.content.Intent; 23ed2084787cec0f02e6cead215d409d6f2f60f737Alon Albertimport android.content.IntentFilter; 2490dc136d444ba29fe8db6b20872022b18f18dc94John Spurlockimport android.content.res.Configuration; 2540e4b8c5bda04416feaa3f41539c241479d4f046Annie Chinimport android.database.ContentObserver; 26c624a3fb698c13312a5e14114c37f45e3b3438bcJustin Klaassenimport android.net.Uri; 2790dc136d444ba29fe8db6b20872022b18f18dc94John Spurlockimport android.os.Handler; 2840e4b8c5bda04416feaa3f41539c241479d4f046Annie Chinimport android.provider.Settings; 2917b4ca405a8b0d154f4d8354ab62044cd25b2204Daniel Sandlerimport android.service.dreams.DreamService; 30c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandlerimport android.view.View; 313af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieuximport android.view.ViewTreeObserver.OnPreDrawListener; 323a4ba0db218b830af3dd17fde2952125a2e50fdcIsaac Katzenelsonimport android.widget.TextClock; 33c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler 343af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieuximport com.android.deskclock.data.DataModel; 353af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieuximport com.android.deskclock.uidata.UiDataModel; 3678b8e1513e24c58ffea6ee4edbebdce85c248f6fSam Blitzstein 373af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieuxpublic final class Screensaver extends DreamService { 38f10aaa263d364ecca5c6f3ee39ef96faf9eac83dBudi Kusmiantoro 39310210dab3e97b1defe0870ecbc25b1451e90392Justin Klaassen private static final LogUtils.Logger LOGGER = new LogUtils.Logger("Screensaver"); 40f10aaa263d364ecca5c6f3ee39ef96faf9eac83dBudi Kusmiantoro 413af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux private final OnPreDrawListener mStartPositionUpdater = new StartPositionUpdater(); 423af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux private MoveScreensaverRunnable mPositionUpdater; 43c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler 44c5b45b80a280794ca131b9857c5885c337d4d6e1Isaac Katzenelson private String mDateFormat; 45c5b45b80a280794ca131b9857c5885c337d4d6e1Isaac Katzenelson private String mDateFormatForAccessibility; 46e269bd8658721a71fd9d42084b280042c5258945Daniel Sandler 47b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen private View mContentView; 48b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen private View mMainClockView; 49b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen private TextClock mDigitalClock; 50b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen private AnalogClock mAnalogClock; 51b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen 5240e4b8c5bda04416feaa3f41539c241479d4f046Annie Chin /* Register ContentObserver to see alarm changes for pre-L */ 53d6d68b49aaf5b0fbcce89097ca4c02d73bcea4ddJustin Klaassen private final ContentObserver mSettingsContentObserver = 54d6d68b49aaf5b0fbcce89097ca4c02d73bcea4ddJustin Klaassen Utils.isLOrLater() ? null : new ContentObserver(new Handler()) { 55d6d68b49aaf5b0fbcce89097ca4c02d73bcea4ddJustin Klaassen @Override 56d6d68b49aaf5b0fbcce89097ca4c02d73bcea4ddJustin Klaassen public void onChange(boolean selfChange) { 57d6d68b49aaf5b0fbcce89097ca4c02d73bcea4ddJustin Klaassen Utils.refreshAlarm(Screensaver.this, mContentView); 58d6d68b49aaf5b0fbcce89097ca4c02d73bcea4ddJustin Klaassen } 59d6d68b49aaf5b0fbcce89097ca4c02d73bcea4ddJustin Klaassen }; 6040e4b8c5bda04416feaa3f41539c241479d4f046Annie Chin 613af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux // Runs every midnight or when the time changes and refreshes the date. 62eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas private final Runnable mMidnightUpdater = new Runnable() { 63eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas @Override 64eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas public void run() { 65eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView); 66eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas } 67eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas }; 68eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas 69eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas /** 703af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux * Receiver to alarm clock changes. 71eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas */ 723af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux private final BroadcastReceiver mAlarmChangedReceiver = new BroadcastReceiver() { 73eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas @Override 74eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas public void onReceive(Context context, Intent intent) { 753af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux Utils.refreshAlarm(Screensaver.this, mContentView); 76eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas } 77eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas }; 78eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas 7990dc136d444ba29fe8db6b20872022b18f18dc94John Spurlock @Override 8090dc136d444ba29fe8db6b20872022b18f18dc94John Spurlock public void onCreate() { 81310210dab3e97b1defe0870ecbc25b1451e90392Justin Klaassen LOGGER.v("Screensaver created"); 82b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen 83b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen setTheme(R.style.Theme_DeskClock); 8490dc136d444ba29fe8db6b20872022b18f18dc94John Spurlock super.onCreate(); 85c5b45b80a280794ca131b9857c5885c337d4d6e1Isaac Katzenelson 86c5b45b80a280794ca131b9857c5885c337d4d6e1Isaac Katzenelson mDateFormat = getString(R.string.abbrev_wday_month_day_no_year); 87c5b45b80a280794ca131b9857c5885c337d4d6e1Isaac Katzenelson mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year); 889a1fd04f15b653a6600629aee41c7d3fd7d843b3Daniel Sandler } 899a1fd04f15b653a6600629aee41c7d3fd7d843b3Daniel Sandler 90e269bd8658721a71fd9d42084b280042c5258945Daniel Sandler @Override 9190dc136d444ba29fe8db6b20872022b18f18dc94John Spurlock public void onAttachedToWindow() { 92310210dab3e97b1defe0870ecbc25b1451e90392Justin Klaassen LOGGER.v("Screensaver attached to window"); 9390dc136d444ba29fe8db6b20872022b18f18dc94John Spurlock super.onAttachedToWindow(); 94c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler 953af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux setContentView(R.layout.desk_clock_saver); 96b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen 97b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen mContentView = findViewById(R.id.saver_container); 98b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen mMainClockView = mContentView.findViewById(R.id.main_clock); 99b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen mDigitalClock = (TextClock) mMainClockView.findViewById(R.id.digital_clock); 100b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen mAnalogClock = (AnalogClock) mMainClockView.findViewById(R.id.analog_clock); 101b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen 102b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen setClockStyle(); 103b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen Utils.setClockIconTypeface(mContentView); 104b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen Utils.setTimeFormat(mDigitalClock, false); 105b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen mAnalogClock.enableSeconds(false); 106b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen 107de7bc34a9e062b175343ac464440a4d6eaceaab6Sean Stout mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE 108de7bc34a9e062b175343ac464440a4d6eaceaab6Sean Stout | View.SYSTEM_UI_FLAG_IMMERSIVE 109de7bc34a9e062b175343ac464440a4d6eaceaab6Sean Stout | View.SYSTEM_UI_FLAG_FULLSCREEN 110de7bc34a9e062b175343ac464440a4d6eaceaab6Sean Stout | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 111de7bc34a9e062b175343ac464440a4d6eaceaab6Sean Stout | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); 1123af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux 113b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen mPositionUpdater = new MoveScreensaverRunnable(mContentView, mMainClockView); 1143af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux 11557ca681913fa5b9cd86a7342a91cfe4cc5b910dfDaniel Sandler // We want the screen saver to exit upon user interaction. 11657ca681913fa5b9cd86a7342a91cfe4cc5b910dfDaniel Sandler setInteractive(false); 1173d6adf080cb4a1469244e393807f6921b9f1149bJohn Spurlock setFullscreen(true); 1181a7820902f3a6428f0bb586f8f1b5a2824838cb0Daniel Sandler 119eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas // Setup handlers for time reference changes and date updates. 120b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen if (Utils.isLOrLater()) { 121b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen registerReceiver(mAlarmChangedReceiver, 122b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen new IntentFilter(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)); 123b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen } 124eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas 1253af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux if (mSettingsContentObserver != null) { 126c624a3fb698c13312a5e14114c37f45e3b3438bcJustin Klaassen @SuppressWarnings("deprecation") 127c624a3fb698c13312a5e14114c37f45e3b3438bcJustin Klaassen final Uri uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED); 128c624a3fb698c13312a5e14114c37f45e3b3438bcJustin Klaassen getContentResolver().registerContentObserver(uri, false, mSettingsContentObserver); 12940e4b8c5bda04416feaa3f41539c241479d4f046Annie Chin } 13040e4b8c5bda04416feaa3f41539c241479d4f046Annie Chin 1313af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView); 132b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen Utils.refreshAlarm(this, mContentView); 1333af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux 1343af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux startPositionUpdater(); 1353af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater, 100); 136c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler } 137c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler 138c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler @Override 13990dc136d444ba29fe8db6b20872022b18f18dc94John Spurlock public void onDetachedFromWindow() { 140310210dab3e97b1defe0870ecbc25b1451e90392Justin Klaassen LOGGER.v("Screensaver detached from window"); 14190dc136d444ba29fe8db6b20872022b18f18dc94John Spurlock super.onDetachedFromWindow(); 14290dc136d444ba29fe8db6b20872022b18f18dc94John Spurlock 1433af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux if (mSettingsContentObserver != null) { 14440e4b8c5bda04416feaa3f41539c241479d4f046Annie Chin getContentResolver().unregisterContentObserver(mSettingsContentObserver); 14540e4b8c5bda04416feaa3f41539c241479d4f046Annie Chin } 14640e4b8c5bda04416feaa3f41539c241479d4f046Annie Chin 1473af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux UiDataModel.getUiDataModel().removePeriodicCallback(mMidnightUpdater); 1483af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux stopPositionUpdater(); 1493af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux 150eb3a1f0714e209a8335d84142778465aa6b44c5cRobyn Coultas // Tear down handlers for time reference changes and date updates. 151b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen if (Utils.isLOrLater()) { 152b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen unregisterReceiver(mAlarmChangedReceiver); 153b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen } 1543af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux } 1553af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux 1563af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux @Override 1573af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux public void onConfigurationChanged(Configuration newConfig) { 158310210dab3e97b1defe0870ecbc25b1451e90392Justin Klaassen LOGGER.v("Screensaver configuration changed"); 1593af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux super.onConfigurationChanged(newConfig); 1603af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux 161696a2dfdf1418addb286861c8a70223ce7c31431Sean Stout startPositionUpdater(); 162c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler } 163c57490dff5bfbf601d4b708fdae029df99f807b2Daniel Sandler 1644560461b08b1660fa5776523d8344df0d8d23f1dItzhak Katzenelson private void setClockStyle() { 16534142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux Utils.setScreensaverClockStyle(mDigitalClock, mAnalogClock); 1663af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux final boolean dimNightMode = DataModel.getDataModel().getScreensaverNightModeOn(); 167b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen Utils.dimClockView(dimNightMode, mMainClockView); 16878b8e1513e24c58ffea6ee4edbebdce85c248f6fSam Blitzstein setScreenBright(!dimNightMode); 1698f873a2bca1277f37cc8d08655d73385e5508232Daniel Sandler } 1709a1fd04f15b653a6600629aee41c7d3fd7d843b3Daniel Sandler 1713af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux /** 1723af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux * The {@link #mContentView} will be drawn shortly. When that draw occurs, the position updater 1733af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux * callback will also be executed to choose a random position for the time display as well as 1743af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux * schedule future callbacks to move the time display each minute. 1753af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux */ 1763af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux private void startPositionUpdater() { 177696a2dfdf1418addb286861c8a70223ce7c31431Sean Stout if (mContentView != null) { 178696a2dfdf1418addb286861c8a70223ce7c31431Sean Stout mContentView.getViewTreeObserver().addOnPreDrawListener(mStartPositionUpdater); 179696a2dfdf1418addb286861c8a70223ce7c31431Sean Stout } 1803af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux } 1813a4ba0db218b830af3dd17fde2952125a2e50fdcIsaac Katzenelson 1823af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux /** 1833af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux * This activity is no longer in the foreground; position callbacks should be removed. 1843af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux */ 1853af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux private void stopPositionUpdater() { 186696a2dfdf1418addb286861c8a70223ce7c31431Sean Stout if (mContentView != null) { 187696a2dfdf1418addb286861c8a70223ce7c31431Sean Stout mContentView.getViewTreeObserver().removeOnPreDrawListener(mStartPositionUpdater); 188696a2dfdf1418addb286861c8a70223ce7c31431Sean Stout } 1893af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux mPositionUpdater.stop(); 1903af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux } 19178b8e1513e24c58ffea6ee4edbebdce85c248f6fSam Blitzstein 1923af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux private final class StartPositionUpdater implements OnPreDrawListener { 1933af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux /** 1943af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux * This callback occurs after initial layout has completed. It is an appropriate place to 195b8c4512b4a6d3e78d426e7e67b502e4735e365cfJustin Klaassen * select a random position for {@link #mMainClockView} and schedule future callbacks to update 1963af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux * its position. 1973af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux * 1983af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux * @return {@code true} to continue with the drawing pass 1993af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux */ 2003af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux @Override 2013af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux public boolean onPreDraw() { 2023af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux if (mContentView.getViewTreeObserver().isAlive()) { 2033af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux // (Re)start the periodic position updater. 2043af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux mPositionUpdater.start(); 205c5b45b80a280794ca131b9857c5885c337d4d6e1Isaac Katzenelson 2063af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux // This listener must now be removed to avoid starting the position updater again. 2073af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux mContentView.getViewTreeObserver().removeOnPreDrawListener(mStartPositionUpdater); 2083af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux } 2093af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux return true; 2103af168c834d73487f8f614f0aaafbf6f9a850f0fJames Lemieux } 21190dc136d444ba29fe8db6b20872022b18f18dc94John Spurlock } 212310210dab3e97b1defe0870ecbc25b1451e90392Justin Klaassen} 213