app.LoadPlugin("FaceAPI");
var lay, frame, cam, canvas, faceApi;
var txtEmpId, txtEmpName, empId = "", empName = "";
var detecting = false;
var useFront = true; // default to front camera; toggle removed
var FRONT_IS_MIRRORED = true;
var loc, lastLat = null, lastLon = null;
var STRICT_RECOGNITION = false; // when false, always log check-in/out; recognition optional
var PROFILE_DIR = app.GetPath() + "/Attenda";
var PROFILE_FILE = PROFILE_DIR + "/employee_profile.json";
var LOG_FILE = PROFILE_DIR + "/attendance_log.json";
var MODELS_ROOT = (app.GetAppPath ? (app.GetAppPath() + "/Assets/people") : (app.GetPath() + "/Assets/people"));
var detLoopId = null;
// Single-employee recognition state
var registeredLabel = ""; // folder label used by FaceAPI (e.g. empId_empName)
var recognitionReady = false; // true when faces are loaded for recognition
var REGISTRATION_SHOTS = 4; // number of images to capture during registration (keeps it fast)
var REGISTRATION_MIN_REQUIRED = 4; // minimum images required to enable recognition
var MIN_IMAGE_SIZE_BYTES = 4096; // basic sanity check size for captured images
function OnStart() {
lay = app.CreateLayout("Linear", "VCenter,FillXY");
var header = app.CreateText("📸 Attenda Face/GPS Attendance", 1, 0.09, "Bold,FillX,Center");
header.SetTextSize(20);
header.SetBackColor("#1565C0");
header.SetTextColor("#FFFFFF");
lay.AddChild(header);
frame = app.AddLayout(lay, "Frame");
BuildCamera();
AddEmployeeInputs();
AddButtonBar();
setTimeout(function(){ LoadEmployeeData(); }, 100);
app.AddLayout(lay);
app.ShowProgress("Loading FaceAPI…");
// Start in detection mode (no model needed); switch to recognition after registration.
faceApi = app.CreateFaceAPI("detection", function(){
app.HideProgress();
StartDetectionLoop();
});
loc = app.CreateLocator("GPS,Network");
loc.SetOnChange(OnLocation);
loc.Start();
app.ShowPopup("Locating…");
}
function BuildCamera(){
try{ if(cam){ cam.StopPreview(); frame.RemoveChild(cam); } }catch(e){}
try{ if(canvas){ frame.RemoveChild(canvas); } }catch(e){}
var flags = "VGA,UseBitmap" + (useFront ? ",Front" : ",Back");
cam = app.AddCameraView(frame, 0.96, 0.58, flags);
cam.SetOnReady(function(){ cam.StartPreview(); });
canvas = app.AddImage(frame, null, 0.96, 0.58, "alias");
canvas.SetAlpha(0.5);
canvas.SetPaintColor("#00C853");
canvas.SetPaintStyle("Line");
canvas.SetLineWidth(3);
canvas.SetAutoUpdate(false);
}
function StartDetectionLoop(){
try{ if(detLoopId) { clearInterval(detLoopId); detLoopId = null; } }catch(e){}
detLoopId = setInterval(function(){
try { faceApi.IdentifyFaces(cam, onResult); }
catch(e){ app.Debug("IdentifyFaces error: " + e); }
}, 800);
}
function onResult(data){
canvas.Clear();
if(!data || !data.length){
detecting = false;
canvas.SetPaintColor("#FF3B30");
canvas.DrawRectangle(0.05, 0.05, 0.95, 0.95);
canvas.Update();
canvas.SetPaintColor("#00C853");
return;
}
detecting = true;
canvas.SetPaintColor("#00C853");
var f = data[0];
var x1 = f.x / f.imageWidth;
var y1 = f.y / f.imageHeight;
var x2 = (f.x + f.width) / f.imageWidth;
var y2 = (f.y + f.height) / f.imageHeight;
if(useFront && FRONT_IS_MIRRORED){
var nx1 = 1 - x2, nx2 = 1 - x1; x1 = nx1; x2 = nx2;
}
canvas.DrawRectangle(x1, y1, x2, y2);
canvas.Update();
}
function AddEmployeeInputs(){
var inputLayout = app.CreateLayout("Linear", "Vertical,FillX");
inputLayout.SetPadding(0.02, 0.01, 0.02, 0.01);
inputLayout.SetBackColor("#F5F5F5");
var lblEmpId = app.CreateText("Employee ID:", 0.9, -1, "Left");
lblEmpId.SetTextSize(14);
lblEmpId.SetTextColor("#000000");
lblEmpId.SetMargins(0, 0.005, 0, 0.002);
inputLayout.AddChild(lblEmpId);
txtEmpId = app.CreateTextEdit(empId, 0.9, 0.04);
txtEmpId.SetTextSize(16);
txtEmpId.SetTextColor("#000000");
txtEmpId.SetBackColor("#FFFFFF");
txtEmpId.SetMargins(0, 0, 0, 0.005);
txtEmpId.SetOnChange(function(text){ empId = (text||"").trim(); SaveEmployeeData(); });
inputLayout.AddChild(txtEmpId);
var lblEmpName = app.CreateText("Employee Name:", 0.9, -1, "Left");
lblEmpName.SetTextSize(14);
lblEmpName.SetTextColor("#000000");
lblEmpName.SetMargins(0, 0.005, 0, 0.002);
inputLayout.AddChild(lblEmpName);
txtEmpName = app.CreateTextEdit(empName, 0.9, 0.04);
txtEmpName.SetTextSize(16);
txtEmpName.SetTextColor("#000000");
txtEmpName.SetBackColor("#FFFFFF");
txtEmpName.SetMargins(0, 0, 0, 0.005);
txtEmpName.SetOnChange(function(text){ empName = (text||"").trim(); SaveEmployeeData(); });
inputLayout.AddChild(txtEmpName);
lay.AddChild(inputLayout);
}
function AddButtonBar(){
var bar = app.CreateLayout("Linear", "Horizontal,FillX,Bottom");
bar.SetPadding(0.02, 0.016, 0.02, 0.02);
bar.SetBackColor("#F8F9FA");
function styleButton(btn, back, stroke){ btn.SetStyle(back, "#FFFFFF", 14, stroke, 2, 0.7); btn.SetTextSize(18); btn.SetMargins(0.005, 0, 0.005, 0); }
var btnReg = app.CreateButton("📝 Register", 0.31, -1);
styleButton(btnReg, "#2E7D32", "#1B5E20");
btnReg.SetOnTouch(function(){ DoRegister(); });
var btnIn = app.CreateButton("✅ Check-in", 0.31, -1);
styleButton(btnIn, "#1565C0", "#0D47A1");
btnIn.SetOnTouch(function(){ DoAction("checkin"); });
var btnOut = app.CreateButton("⏹️ Check-out", 0.31, -1);
styleButton(btnOut, "#EF6C00", "#E65100");
btnOut.SetOnTouch(function(){ DoAction("checkout"); });
bar.AddChild(btnReg);
bar.AddChild(btnIn);
bar.AddChild(btnOut);
lay.AddChild(bar);
}
function OnLocation(data){
lastLat = data.latitude;
lastLon = data.longitude;
}
function LoadEmployeeData(){
try{
if(app.FileExists(PROFILE_FILE)){
var data = app.ReadFile(PROFILE_FILE);
var profile = JSON.parse(data);
empId = profile.employeeId || "";
empName = profile.employeeName || "";
if(txtEmpId) txtEmpId.SetText(empId);
if(txtEmpName) txtEmpName.SetText(empName);
}
}catch(e){ app.Debug("LoadEmployeeData error: " + e); }
}
function SaveEmployeeData(){
try{
if(!app.FolderExists(PROFILE_DIR)) app.MakeFolder(PROFILE_DIR);
var profile = { employeeId: empId, employeeName: empName, lastUpdated: new Date().toISOString() };
app.WriteFile(PROFILE_FILE, JSON.stringify(profile, null, 2));
}catch(e){ app.Debug("SaveEmployeeData error: " + e); }
}
function AppendAttendanceLog(entry){
try{
if(!app.FolderExists(PROFILE_DIR)) app.MakeFolder(PROFILE_DIR);
var log = [];
if(app.FileExists(LOG_FILE)){
try{ log = JSON.parse(app.ReadFile(LOG_FILE)) || []; }catch(e){ log = []; }
}
log.push(entry);
app.WriteFile(LOG_FILE, JSON.stringify(log, null, 2));
}catch(e){ app.Debug("AppendAttendanceLog error: " + e); }
}
function DoAction(type){
var currentEmpId = txtEmpId ? (txtEmpId.GetText()||"").trim() : "";
var currentEmpName = txtEmpName ? (txtEmpName.GetText()||"").trim() : "";
empId = currentEmpId; empName = currentEmpName;
if(!empId || !empName){
app.Alert("Please enter both Employee ID and Employee Name.", "Missing Information");
return;
}
// For check-in/out, verify recognition against the registered label.
if(type === "checkin" || type === "checkout"){
// Only attempt recognition when a face is currently detected
if(!detecting){
var entryNoFace = {
ts: new Date().toISOString(),
action: type,
employee: { id: empId, name: empName },
recognized: false,
faceDetected: false,
gps: { lat: lastLat, lon: lastLon }
};
AppendAttendanceLog(entryNoFace);
app.Alert(JSON.stringify(entryNoFace, null, 2), "Recorded (no face)");
return;
}
if(STRICT_RECOGNITION){
if(!recognitionReady){
// No errors: show info and do not log
app.Alert("Recognition required. Please register first.");
return;
}
VerifyEmployee(function(ok){
if(!ok){ app.Alert("Face not recognized as registered employee."); return; }
var entryStrict = {
ts: new Date().toISOString(),
action: type,
employee: { id: empId, name: empName },
recognized: true,
faceDetected: !!detecting,
gps: { lat: lastLat, lon: lastLon }
};
AppendAttendanceLog(entryStrict);
app.Alert(JSON.stringify(entryStrict, null, 2), "Recorded");
});
} else {
if(!recognitionReady){
var entryNoRec = {
ts: new Date().toISOString(),
action: type,
employee: { id: empId, name: empName },
recognized: false,
faceDetected: !!detecting,
gps: { lat: lastLat, lon: lastLon }
};
AppendAttendanceLog(entryNoRec);
app.Alert(JSON.stringify(entryNoRec, null, 2), "Recorded (no recognition)");
return;
}
VerifyEmployee(function(ok){
var entry = {
ts: new Date().toISOString(),
action: type,
employee: { id: empId, name: empName },
recognized: !!ok,
faceDetected: !!detecting,
gps: { lat: lastLat, lon: lastLon }
};
AppendAttendanceLog(entry);
app.Alert(JSON.stringify(entry, null, 2), ok ? "Recorded" : "Recorded (not recognized)");
});
}
return;
}
}
// -------- Registration & Recognition Setup ---------
function DoRegister(){
var id = txtEmpId ? (txtEmpId.GetText()||"").trim() : "";
var name = txtEmpName ? (txtEmpName.GetText()||"").trim() : "";
if(!id || !name){ app.Alert("Enter Employee ID and Name before registering."); return; }
empId = id; empName = name;
SaveEmployeeData();
registeredLabel = (empId + "_" + empName).replace(/\s+/g, "_");
EnsureModelsFolder(registeredLabel);
app.Debug("Capture dir: " + (MODELS_ROOT + "/" + registeredLabel));
app.ShowProgress("Capturing multiple face images ("+REGISTRATION_SHOTS+") for robustness…");
CaptureEmployeeFaces(registeredLabel, REGISTRATION_SHOTS, function(){
app.HideProgress();
var saved = CountSavedShots(registeredLabel);
app.Debug("Saved shots count for '"+registeredLabel+"': " + saved);
try{
var labelDir = MODELS_ROOT + "/" + registeredLabel;
var files = app.ListFolder(labelDir) || [];
app.Debug("Files under: " + labelDir + " => " + JSON.stringify(files));
if(files.length){
app.Alert("Saved to:\n" + labelDir + "\nFiles: " + files.join(", "));
} else {
app.Alert("No files found under:\n" + labelDir + "\nCapture may have failed. Check storage permission.");
}
}catch(eList){ app.Debug("List saved files error: " + eList); }
// Verify image sizes to avoid zero-byte or tiny invalid files
var validCount = VerifyImageFiles(registeredLabel);
app.Debug("Valid image files (>"+MIN_IMAGE_SIZE_BYTES+" bytes): " + validCount);
if(validCount >= REGISTRATION_MIN_REQUIRED){
// Longer delay to allow filesystem to flush completely
setTimeout(function(){
InitRecognition(function(ok){
recognitionReady = !!ok;
if(recognitionReady) app.Alert("Registration complete. Recognition enabled for: " + empName + " ("+validCount+" images)");
else app.Alert("Registration saved, but recognition not ready. Try capturing more images.");
});
}, 1500);
} else {
recognitionReady = false;
app.Alert("Captured only " + validCount + " valid image(s). Need at least " + REGISTRATION_MIN_REQUIRED + ". Please register again under steady light and varied angles.");
}
});
}
function EnsureModelsFolder(label){
try{
var appRoot = app.GetAppPath ? app.GetAppPath() : app.GetPath();
var assetsDir = appRoot + "/Assets";
var peopleDir = assetsDir + "/people";
// Ensure Assets and Assets/people exist
if(!app.FolderExists(assetsDir)) app.MakeFolder(assetsDir);
if(!app.FolderExists(peopleDir)) app.MakeFolder(peopleDir);
var dir = MODELS_ROOT + "/" + label;
if(!app.FolderExists(MODELS_ROOT)) app.MakeFolder(MODELS_ROOT);
if(!app.FolderExists(dir)) app.MakeFolder(dir);
}catch(e){ app.Debug("EnsureModelsFolder error: " + e); }
}
function CaptureEmployeeFaces(label, shots, done){
var dir = MODELS_ROOT + "/" + label;
var i = 1;
var start = Date.now();
var maxMs = Math.max(7000, shots * 3000); // safety timeout to avoid endless spinning
function snap(){
if(i > shots){ if(done) done(); return; }
// Only capture when a face is actively detected to improve image quality
if(!detecting){
if(Date.now() - start > maxMs){ if(done) done(); return; }
setTimeout(snap, 300);
return;
}
var path = dir + "/shot"+i+".jpg";
try{
// Many DroidScript builds don't support a callback for TakePicture; pace with a timer.
if(typeof cam.TakePicture === "function"){
cam.TakePicture(path);
} else {
// Fallback: trigger a detection so user sees the face box, even if no file saved.
try{ faceApi.IdentifyFaces(cam, function(){}); }catch(e2){ app.Debug("IdentifyFaces fallback error: " + e2); }
}
}catch(e){ app.Debug("TakePicture error: " + e); }
i++;
// Stop if we've been trying for too long.
if(Date.now() - start > maxMs){ if(done) done(); return; }
setTimeout(snap, 1200);
}
// Optional: if the environment supports picture event, use it to pace, otherwise the timer handles it.
try{
if(typeof cam.SetOnPicture === "function"){
cam.SetOnPicture(function(){ /* event received; timer already pacing next snap */ });
}
}catch(e){ /* ignore */ }
snap();
}
function VerifyImageFiles(label){
try{
var dir = MODELS_ROOT + "/" + label;
var files = app.ListFolder(dir) || [];
var good = 0;
for(var j=0;j<files.length;j++){
var f = files[j];
var low = (f||"").toLowerCase();
if(low.endsWith(".jpg") || low.endsWith(".jpeg") || low.endsWith(".png")){
var size = 0;
try{ size = app.GetFileSize(dir+"/"+f) || 0; }catch(e){ size = 0; }
app.Debug("File '"+f+"' size="+size);
if(size >= MIN_IMAGE_SIZE_BYTES) good++;
}
}
return good;
}catch(e){ app.Debug("VerifyImageFiles error: " + e); return 0; }
}
function CountSavedShots(label){
try{
var dir = MODELS_ROOT + "/" + label;
var files = app.ListFolder(dir) || [];
var count = 0;
for(var j=0;j<files.length;j++){
var f = (files[j]||"").toLowerCase();
if(f.endsWith(".jpg") || f.endsWith(".jpeg") || f.endsWith(".png")) count++;
}
return count;
}catch(e){ app.Debug("CountSavedShots error: " + e); return 0; }
}
function HasAtLeastOneModelImage(){
try{
var labels = app.ListFolder(MODELS_ROOT) || [];
for(var i=0;i<labels.length;i++){
var label = labels[i];
var dir = MODELS_ROOT + "/" + label;
if(app.FolderExists(dir)){
var files = app.ListFolder(dir) || [];
for(var j=0;j<files.length;j++){
var f = (files[j]||"").toLowerCase();
if(f.endsWith(".jpg") || f.endsWith(".jpeg") || f.endsWith(".png")) return true;
}
}
}
}catch(e){ app.Debug("HasAtLeastOneModelImage error: " + e); }
return false;
}
function InitRecognition(done){
try{
// Debug dataset presence
try{
var root = MODELS_ROOT;
var rootFiles = app.ListFolder(root) || [];
app.Debug("Assets/people labels: " + JSON.stringify(rootFiles));
var labelDir = MODELS_ROOT + "/" + registeredLabel;
var labelFiles = app.ListFolder(labelDir) || [];
app.Debug("Label '"+registeredLabel+"' files: " + JSON.stringify(labelFiles));
}catch(eDbg){ app.Debug("Pre-init dataset debug error: " + eDbg); }
if(!HasAtLeastOneModelImage()){
app.Alert("No model images found in '" + MODELS_ROOT + "'. Please re-register with face visible and storage permission enabled.");
if(done) done(false);
return;
}
try{ if(detLoopId) { clearInterval(detLoopId); detLoopId = null; } }catch(e){}
app.ShowProgress("Initializing recognition…");
faceApi = app.CreateFaceAPI("recognition", function(){
var ok = false;
try{
// Try relative Assets path first
var r = faceApi.SetFaces("people");
ok = (r === undefined ? true : !!r);
// Fallbacks to absolute (implementation-dependent)
if(!ok){
try{ r = faceApi.SetFaces(MODELS_ROOT); ok = (r === undefined ? true : !!r); }catch(e1){ app.Debug("SetFaces(MODELS_ROOT) error: " + e1); }
}
if(!ok){
try{ r = faceApi.SetFaces((app.GetAppPath? app.GetAppPath() : app.GetPath())+"/Assets/people"); ok = (r === undefined ? true : !!r); }catch(e2){ app.Debug("SetFaces(app Assets/people) error: " + e2); }
}
if(!ok){
try{ r = faceApi.SetFaces(app.GetPath()+"/Assets/people"); ok = (r === undefined ? true : !!r); }catch(e3){ app.Debug("SetFaces(app.GetPath Assets/people) error: " + e3); }
}
recognitionReady = !!ok;
}catch(eSet){ app.Debug("SetFaces('people') error: " + eSet); ok = false; recognitionReady = false; }
app.HideProgress();
StartDetectionLoop();
if(done) done(ok);
});
}catch(e){ app.Debug("InitRecognition error: " + e); if(done) done(false); }
}
function TestDatasetSetup(){
try{
app.Debug("=== DATASET TEST ===");
app.Debug("MODELS_ROOT: " + MODELS_ROOT);
app.Debug("Registered label: " + registeredLabel);
var labelDir = MODELS_ROOT + "/" + registeredLabel;
app.Debug("Full path: " + labelDir);
app.Debug("Folder exists: " + app.FolderExists(labelDir));
if(app.FolderExists(labelDir)){
var files = app.ListFolder(labelDir) || [];
app.Debug("Files found: " + files.length);
for(var i=0;i<files.length;i++){
var path = labelDir + "/" + files[i];
var size = 0;
try{ size = app.GetFileSize(path) || 0; }catch(e){ size = 0; }
app.Debug(" - " + files[i] + ": " + size + " bytes");
}
}
}catch(e){ app.Debug("TestDatasetSetup error: " + e); }
}
function VerifyEmployee(cb){
try{
faceApi.IdentifyFaces(cam, function(data){
var ok = false;
if(data && data.length){
// Expect FaceAPI to label recognized face with folder name
var name = data[0].name || "";
app.Debug("Recognized name: '"+name+"' vs registeredLabel: '"+registeredLabel+"'");
if(name === registeredLabel) ok = true;
}
cb(ok);
});
}catch(e){ app.Debug("VerifyEmployee error: " + e); cb(false); }
}