From 9f4c1a7fe6fad1f206e86d7894927139451f225b Mon Sep 17 00:00:00 2001 From: boltgolt Date: Mon, 4 Jan 2021 23:51:49 +0100 Subject: [PATCH] Added entire onboarding flow --- howdy-gtk/src/{ui.glade => main.glade} | 27 +- howdy-gtk/src/onboarding.glade | 470 +++++++++++++++++++++++++ howdy-gtk/src/onboarding.py | 256 ++++++++++++++ howdy-gtk/src/window.py | 13 +- src/dlib-data/install.sh | 1 + 5 files changed, 758 insertions(+), 9 deletions(-) rename howdy-gtk/src/{ui.glade => main.glade} (93%) create mode 100644 howdy-gtk/src/onboarding.glade create mode 100644 howdy-gtk/src/onboarding.py diff --git a/howdy-gtk/src/ui.glade b/howdy-gtk/src/main.glade similarity index 93% rename from howdy-gtk/src/ui.glade rename to howdy-gtk/src/main.glade index 9f8cf9c..8733770 100644 --- a/howdy-gtk/src/ui.glade +++ b/howdy-gtk/src/main.glade @@ -2,6 +2,21 @@ + + True + False + gtk-add + + + True + False + gtk-add + + + True + False + gtk-delete + False 5 @@ -57,7 +72,9 @@ True True True - bottom + iconadduser + none + True @@ -113,11 +130,11 @@ - gtk-add + Add True True True - True + iconadd 0.5899999737739563 True @@ -131,12 +148,12 @@ - gtk-delete + Delete True True True 5 - True + icondelete True diff --git a/howdy-gtk/src/onboarding.glade b/howdy-gtk/src/onboarding.glade new file mode 100644 index 0000000..e3b4b95 --- /dev/null +++ b/howdy-gtk/src/onboarding.glade @@ -0,0 +1,470 @@ + + + + + + True + False + 4 + gtk-cancel + + + True + False + 5 + gtk-apply + + + True + False + 4 + gtk-go-forward + + + True + False + 5 + gtk-media-play + + + 500 + 400 + False + Welcome to Howdy + center + logo.png + menu + center + + + True + False + immediate + vertical + + + False + True + vertical + + + True + False + 20 + Setup is done! + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + 10 + 20 + We're done! Howdy is now active on this computer. Try doing anything you would normally have to type your password for to authenticate, like running a command with sudo. + +You can open the Howdy Configurator later on to change more advanced settings or add additional models. Press Finish below to close this window. + center + True + + + False + True + 1 + + + + + False + True + 0 + + + + + False + True + vertical + + + True + False + 20 + Adding a face model + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + 10 + 20 + To authenticate you Howdy needs to save a model of your face to recognise you. Press the Scan button below to start the facial scan. + center + True + + + False + True + 1 + + + + + True + False + + + + + + Start face scan + True + True + True + True + True + 50 + 50 + iconscan + none + right + True + + + + True + True + 1 + + + + + + + + False + True + 2 + + + + + False + True + 1 + + + + + False + True + vertical + + + True + False + 20 + Configuring webcam + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + 10 + 20 + Howdy will search your system automatically for any available cameras, so make sure your webcam is connected. After detection a list of usable webcams will be shown. Pick the one you want to use and click Next. + center + True + + + False + True + 1 + + + + + True + False + 0.89000000000000001 + 10 + 10 + vertical + + + True + False + 15 + Testing your webcams, please wait... + + + + + + False + True + 0 + + + + + + + + False + True + 2 + + + + + False + True + 3 + + + + + False + True + 10 + vertical + + + True + False + 20 + Downloading data files + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + 10 + 20 + Howdy needs three pre trained facial recognition datasets to be able to recognise you, which will be downloaded now. You can see the download progress below. + center + True + + + False + True + 1 + + + + + True + False + + + True + False + 10 + 10 + 10 + Starting download... + center + + + + + + + + + True + True + 3 + + + + + True + True + 4 + + + + + True + False + vertical + + + False + 20 + 10 + 7 + 13 + logo_about.png + + + False + True + 0 + + + + + True + False + 5 + Welcome to Howdy! + + + + + + False + True + 1 + + + + + 100 + True + False + center + center + 20 + 20 + 10 + This wizard will walk you through the setup process and automatically configure Howdy for you. Press next to continue. + center + True + + + False + True + 2 + + + + + False + True + 6 + + + + + True + False + 10 + + + Cancel + True + True + True + 10 + iconcancel + True + + + + False + True + 0 + + + + + + + + Next + True + True + True + True + True + 10 + iconforward + none + right + True + + + + False + True + end + 2 + + + + + Finish setup + True + True + True + 10 + iconfinish + none + True + + + + False + True + end + 3 + + + + + False + True + end + 7 + + + + + + diff --git a/howdy-gtk/src/onboarding.py b/howdy-gtk/src/onboarding.py new file mode 100644 index 0000000..01340f5 --- /dev/null +++ b/howdy-gtk/src/onboarding.py @@ -0,0 +1,256 @@ +import sys +import os +import re +import time +import subprocess + +from i18n import _ + +from gi.repository import Gtk as gtk +from gi.repository import Gdk as gdk +from gi.repository import GObject as gobject +from gi.repository import Pango as pango + + +class OnboardingWindow(gtk.Window): + def __init__(self): + """Initialize the sticky window""" + print("create") + # Make the class a GTK window + gtk.Window.__init__(self) + + self.connect("destroy", self.exit) + self.connect("delete_event", self.exit) + + self.builder = gtk.Builder() + self.builder.add_from_file("./onboarding.glade") + self.builder.connect_signals(self) + + self.window = self.builder.get_object("onboardingwindow") + self.nextbutton = self.builder.get_object("nextbutton") + + self.slides = [ + self.builder.get_object("slide0"), + self.builder.get_object("slide1"), + self.builder.get_object("slide2"), + self.builder.get_object("slide3"), + self.builder.get_object("slide4") + ] + + self.window.show_all() + self.window.resize(500, 400) + + self.window.current_slide = 0 + + # Start GTK main loop + gtk.main() + + def go_next_slide(self, button=None): + self.nextbutton.set_sensitive(False) + + self.slides[self.window.current_slide].hide() + # self.window.current_slide += 1 + self.slides[self.window.current_slide + 1].show() + self.window.current_slide += 1 + + if self.window.current_slide == 1: + self.execute_slide1() + elif self.window.current_slide == 2: + gobject.timeout_add(10, self.execute_slide2) + elif self.window.current_slide == 3: + self.execute_slide3() + elif self.window.current_slide == 4: + self.execute_slide4() + + def execute_slide1(self): + self.downloadoutputlabel = self.builder.get_object("downloadoutputlabel") + eventbox = self.builder.get_object("downloadeventbox") + eventbox.modify_bg(gtk.StateType.NORMAL, gdk.Color(red=0, green=0, blue=0)) + + if os.path.exists("/lib/security/howdy/dlib-data/shape_predictor_5_face_landmarks.dat"): + self.downloadoutputlabel.set_text(_("Datafiles have already been downloaded!\nClick Next to continue")) + self.enable_next() + return + + self.proc = subprocess.Popen("./install.sh", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd="/lib/security/howdy/dlib-data") + + self.download_lines = [] + self.read_download_line() + + def read_download_line(self): + line = self.proc.stdout.readline() + print(line) + self.download_lines.append(line.decode("utf-8")) + + if len(self.download_lines) > 10: + self.download_lines.pop(0) + + self.downloadoutputlabel.set_text(" ".join(self.download_lines)) + + if line: + gobject.timeout_add(10, self.read_download_line) + return + + # Wait for the process to finish and check the status code + if self.proc.wait(5) != 0: + self.show_error(_("Error while downloading datafiles"), " ".join(self.download_lines)) + + self.downloadoutputlabel.set_text(_("Done!\nClick Next to continue")) + self.enable_next() + + def execute_slide2(self): + def is_gray(frame): + for row in frame: + for pixel in row: + if not pixel[0] == pixel[1] == pixel[2]: + return False + return True + + try: + import cv2 + except Exception: + self.show_error(_("Error while importing OpenCV2"), _("Try reinstalling cv2")) + + device_ids = os.listdir("/dev/v4l/by-path") + device_rows = [] + + if not device_ids: + self.show_error(_("No webcams found on system"), _("Please configure your camera yourself if you are sure a compatible camera is connected")) + + # Loop though all devices + for dev in device_ids: + time.sleep(.5) + + # The full path to the device is the default name + device_path = "/dev/v4l/by-path/" + dev + device_name = dev + + # Get the udevadm details to try to get a better name + udevadm = subprocess.check_output(["udevadm info -r --query=all -n " + device_path], shell=True).decode("utf-8") + + # Loop though udevadm to search for a better name + for line in udevadm.split("\n"): + # Match it and encase it in quotes + re_name = re.search('product.*=(.*)$', line, re.IGNORECASE) + if re_name: + device_name = re_name.group(1) + + try: + capture = cv2.VideoCapture(device_path) + capture.grab() + ret, frame = capture.read() + except Exception: + device_rows.append([device_name, device_path, -9, _("No, camera can't be opened")]) + continue + + if not is_gray(frame): + device_rows.append([device_name, device_path, -5, _("No, not an infrared camera")]) + continue + + time.sleep(.2) + + ret, frame = capture.read() + + if not is_gray(frame): + device_rows.append([device_name, device_path, -5, _("No, not an infrared camera")]) + continue + + device_rows.append([device_name, device_path, 5, _("Yes, compatible infrared camera")]) + + capture.release() + + device_rows = sorted(device_rows, key=lambda k: -k[2]) + + self.loadinglabel = self.builder.get_object("loadinglabel") + self.devicelistbox = self.builder.get_object("devicelistbox") + + self.treeview = gtk.TreeView() + self.treeview.set_vexpand(True) + + # Set the coloums + for i, column in enumerate([_("Camera identifier or path"), _("Recommended")]): + cell = gtk.CellRendererText() + cell.set_property("ellipsize", pango.EllipsizeMode.END) + col = gtk.TreeViewColumn(column, cell, text=i) + self.treeview.append_column(col) + + # Add the treeview + self.devicelistbox.add(self.treeview) + + # Create a datamodel + self.listmodel = gtk.ListStore(str, str, str) + + for device in device_rows: + self.listmodel.append([device[0], device[3], device[1]]) + + self.treeview.set_model(self.listmodel) + self.treeview.set_cursor(0) + + self.loadinglabel.hide() + self.treeview.show() + self.enable_next() + + def execute_slide3(self): + selection = self.treeview.get_selection() + (listmodel, rowlist) = selection.get_selected_rows() + + if len(rowlist) != 1: + self.show_error(_("Error selecting camera")) + + device_path = listmodel.get_value(listmodel.get_iter(rowlist[0]), 2) + self.proc = subprocess.Popen("howdy set device_path " + device_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) + + self.window.set_focus(self.builder.get_object("scanbutton")) + + def on_scanbutton_click(self, button): + status = self.proc.wait(2) + + if status != 0: + self.show_error(_("Error setting camera path"), _("Please set the camera path manually")) + + self.dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL) + self.dialog.set_title(_("Creating Model")) + self.dialog.props.text = _("Please look directly into the camera") + self.dialog.show_all() + + # Wait a bit to allow the user to read the dialog + gobject.timeout_add(600, self.run_add) + + def run_add(self): + status, output = subprocess.getstatusoutput(["howdy add -y"]) + print(output) + + self.dialog.destroy() + + if status != 0: + self.show_error(_("Can't save face model"), output) + + gobject.timeout_add(10, self.go_next_slide) + + def execute_slide4(self): + self.nextbutton.hide() + self.builder.get_object("cancelbutton").hide() + + finishbutton = self.builder.get_object("finishbutton") + finishbutton.show() + self.window.set_focus(finishbutton) + + def enable_next(self): + self.nextbutton.set_sensitive(True) + self.window.set_focus(self.nextbutton) + + def show_error(self, error, secon=""): + dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.ERROR, buttons=gtk.ButtonsType.CLOSE) + dialog.set_title(_("Howdy Error")) + dialog.props.text = error + dialog.format_secondary_text(secon) + + dialog.run() + + dialog.destroy() + self.exit() + + def exit(self, widget=None): + """Cleanly exit""" + gtk.main_quit() + sys.exit(0) diff --git a/howdy-gtk/src/window.py b/howdy-gtk/src/window.py index 65ed655..b92904a 100644 --- a/howdy-gtk/src/window.py +++ b/howdy-gtk/src/window.py @@ -28,7 +28,7 @@ class MainWindow(gtk.Window): self.connect("delete_event", self.exit) self.builder = gtk.Builder() - self.builder.add_from_file("./ui.glade") + self.builder.add_from_file("./main.glade") self.builder.connect_signals(self) self.window = self.builder.get_object("mainwindow") @@ -41,9 +41,7 @@ class MainWindow(gtk.Window): # Set the coloums for i, column in enumerate([_("ID"), _("Created"), _("Label")]): - cell = gtk.CellRendererText() - col = gtk.TreeViewColumn(column, cell, text=i) - + col = gtk.TreeViewColumn(column, gtk.CellRendererText(), text=i) self.treeview.append_column(col) # Add the treeview @@ -114,6 +112,13 @@ signal.signal(signal.SIGINT, signal.SIG_DFL) # Make sure we run as sudo elevate.elevate() +# If no models have been created yet, start the onboarding +if os.path.exists("/lib/security/howdy/models"): + import onboarding + onboarding.OnboardingWindow() + + sys.exit(0) + # Class is split so it isn't too long, import split functions import tab_models MainWindow.on_user_add = tab_models.on_user_add diff --git a/src/dlib-data/install.sh b/src/dlib-data/install.sh index 5698f01..f74a09f 100755 --- a/src/dlib-data/install.sh +++ b/src/dlib-data/install.sh @@ -21,5 +21,6 @@ else fi # Uncompress the data files and delete the original archive +echo " " echo "Unpacking..." bzip2 -d -f *.bz2