//----------------------------------------------------------------------------------- // // Distributed under MIT Licence // See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE. // //----------------------------------------------------------------------------------- // // GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely // tested on a Venu 2 device. The source code is provided at: // https://github.com/house-of-abbey/GarminHomeAssistant. // // P A Abbey & J D Abbey & Someone0nEarth & moesterheld, 31 October 2023 // //----------------------------------------------------------------------------------- using Toybox.Graphics; using Toybox.Lang; using Toybox.WatchUi; using Toybox.Timer; using Toybox.Attention; using Toybox.Time; //! Pin digit used for number 0..9 // class PinDigit extends WatchUi.Selectable { private var mDigit as Lang.Number; //! Class Constructor //! //! @param digit The digit this instance of the class represents and to display. //! @param stepX Horizontal spacing. //! @param stepY Vertical spacing. // function initialize(digit as Lang.Number, stepX as Lang.Number, stepY as Lang.Number) { var marginX = stepX * 0.05; // 5% margin on all sides var marginY = stepY * 0.05; var x = (digit == 0) ? stepX : stepX * ((digit+2) % 3); // layout '0' in 2nd col, others ltr in 3 columns x += marginX + HomeAssistantPinConfirmationView.MARGIN_X; var y = (digit == 0) ? stepY * 4 : (digit <= 3) ? stepY : (digit <=6) ? stepY * 2 : stepY * 3; // layout '0' in bottom row (5), others top to bottom in 3 rows (2-4) (row 1 is reserved for masked pin) y += marginY; var width = stepX - (marginX * 2); var height = stepY - (marginY * 2); var button = new PinDigitButton({ :width => width, :height => height, :label => digit }); var buttonTouched = new PinDigitButton({ :width => width, :height => height, :label => digit, :touched => true }); // initialize selectable Selectable.initialize({ :stateDefault => button, :stateHighlighted => buttonTouched, :locX => x, :locY => y, :width => width, :height => height }); mDigit = digit; } //! Return the digit 0..9 represented by this button // function getDigit() as Lang.Number { return mDigit; } //! Customised drawing of a PIN digit's button. // class PinDigitButton extends WatchUi.Drawable { private var mText as Lang.Number; private var mTouched as Lang.Boolean = false; //! Class Constructor //! //! @param options See `Drawable.initialize()`, but with `:label` and `:touched` added.
//! {
//!   :label as Lang.Number, // The digit 0..9 to display
//!   :touched as Lang.Boolean, // Should the digit be filled to indicate it has been pressed?
//!   + those required by `Drawable.initialize()`
//! } // function initialize(options) { Drawable.initialize(options); mText = options.get(:label); mTouched = options.get(:touched); } //! Draw the PIN digit button. //! //! @param dc Device context // function draw(dc as Graphics.Dc) { if (mTouched) { dc.setColor(Graphics.COLOR_ORANGE, Graphics.COLOR_ORANGE); } else { dc.setColor(Graphics.COLOR_DK_GRAY, Graphics.COLOR_DK_GRAY); } dc.fillCircle(locX + width / 2, locY + height / 2, height / 2); // circle fill dc.setColor(Graphics.COLOR_LT_GRAY, Graphics.COLOR_LT_GRAY); dc.setPenWidth(3); dc.drawCircle(locX + width / 2, locY + height / 2, height / 2); // circle outline dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); dc.drawText(locX+width / 2, locY+height / 2, Graphics.FONT_TINY, mText, Graphics.TEXT_JUSTIFY_CENTER|Graphics.TEXT_JUSTIFY_VCENTER); // center text in circle } } } //! Pin Confirmation dialog and logic. // class HomeAssistantPinConfirmationView extends WatchUi.View { //! Margin on left & right side of screen (overall prettier and works better on round displays) static const MARGIN_X = 20; //! Indicates how many digits have been entered so far. var mPinMask as Lang.String = ""; //! Class Constructor // function initialize() { View.initialize(); } //! Construct the view. //! //! @param dc Device context // function onLayout(dc as Graphics.Dc) as Void { var stepX = (dc.getWidth() - MARGIN_X * 2) / 3; // three columns var stepY = dc.getHeight() / 5; // five rows (first row for masked pin entry) var digits = []; for (var i=0; i<=9; i++) { digits.add(new PinDigit(i, stepX, stepY)); } // draw digits setLayout(digits); } //! Update the view. //! //! @param dc Device context // function onUpdate(dc as Graphics.Dc) as Void { View.onUpdate(dc); if (mPinMask.length() != 0) { dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK); dc.drawText(dc.getWidth()/2, dc.getHeight()/10, Graphics.FONT_SYSTEM_SMALL, mPinMask, Graphics.TEXT_JUSTIFY_CENTER|Graphics.TEXT_JUSTIFY_VCENTER); } } //! Update the PIN mask displayed. //! //! @param length Number of `*` characters to use for the mask string. // function updatePinMask(length as Lang.Number) { mPinMask = ""; for (var i=0; i; private var mLockedUntil as Lang.Number or Null; //! Class Constructor // function initialize() { // System.println("PinFailures initialize() Initializing PIN failures from storage"); var failures = Application.Storage.getValue(PinFailures.STORAGE_KEY_FAILURES); mFailures = (failures == null) ? [] : failures; mLockedUntil = Application.Storage.getValue(PinFailures.STORAGE_KEY_LOCKED); } //! Record a PIN entry failure. If too many have occurred lock the application. // function addFailure() { mFailures.add(Time.now().value()); // System.println("PinFailures addFailure() " + mFailures.size() + " PIN confirmation failures recorded"); if (mFailures.size() >= Globals.scPinMaxFailures) { // System.println("PinFailures addFailure() Too many failures detected"); var oldestFailureOutdate = new Time.Moment(mFailures[0]).add(new Time.Duration(Globals.scPinMaxFailureMinutes * 60)); // System.println("PinFailures addFailure() Oldest failure: " + oldestFailureOutdate.value() + " Now:" + Time.now().value()); if (new Time.Moment(Time.now().value()).greaterThan(oldestFailureOutdate)) { // System.println("PinFailures addFailure() Pruning oldest outdated failure"); mFailures = mFailures.slice(1, null); } else { mFailures = []; mLockedUntil = Time.now().add(new Time.Duration(Globals.scPinLockTimeMinutes * Time.Gregorian.SECONDS_PER_MINUTE)).value(); Application.Storage.setValue(STORAGE_KEY_LOCKED, mLockedUntil); // System.println("PinFailures addFailure() Locked until " + mLockedUntil); } } Application.Storage.setValue(STORAGE_KEY_FAILURES, mFailures); } //! Clear the record of previous PIN entry failures, e.g. because the correct PIN has now been entered //! within tolerance. // function reset() { // System.println("PinFailures reset() Resetting failures"); mFailures = []; mLockedUntil = null; Application.Storage.deleteValue(STORAGE_KEY_FAILURES); Application.Storage.deleteValue(STORAGE_KEY_LOCKED); } //! Retrieve the remaining time the application must be locked out for. // function getLockedUntilSeconds() as Lang.Number { return new Time.Moment(mLockedUntil).subtract(Time.now()).value(); } //! Is the application currently locked out? If the application is no longer locked out, then clear the //! stored values used to determine this state. //! //! @return Boolean indicating if the application is currently locked out. // function isLocked() as Lang.Boolean { if (mLockedUntil == null) { return false; } var isLocked = new Time.Moment(Time.now().value()).lessThan(new Time.Moment(mLockedUntil)); if (!isLocked) { mLockedUntil = null; Application.Storage.deleteValue(STORAGE_KEY_LOCKED); } return isLocked; } }