Files
Nilai_Clock_Client/doc/ANDROID_IMPLEMENTATION_DETAILS.md
2025-07-11 14:07:20 +08:00

20 KiB

Android Implementation Details - Nilai Clock Client

Overview

This document provides comprehensive details about the current Android implementation of the Nilai Clock Client, including native plugins, services, permissions, and platform-specific configurations.

Android Project Structure

Capacitor Android Project Layout

android/
├── app/
│   ├── src/main/
│   │   ├── java/com/ouji/factory/myapp/
│   │   │   ├── MainActivity.java
│   │   │   └── AppSecurity.java
│   │   ├── res/
│   │   │   ├── values/
│   │   │   ├── xml/
│   │   │   └── drawable/
│   │   └── AndroidManifest.xml
│   ├── build.gradle
│   └── capacitor.settings.gradle
├── build.gradle
├── gradle.properties
└── settings.gradle

Key Android Files

MainActivity.java

package com.ouji.factory.myapp;

import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.getcapacitor.BridgeActivity;
import com.getcapacitor.Plugin;

public class MainActivity extends BridgeActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        // Register custom AppSecurity plugin
        registerPlugin(AppSecurity.class);
        super.onCreate(savedInstanceState);
        
        // Configure native window behavior
        setupNativeWindow();
    }
    
    private void setupNativeWindow() {
        // Hide system UI for immersive experience
        getWindow().setFlags(
            WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
            WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
        );
        
        // Configure status bar
        WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
        WindowInsetsControllerCompat controller = 
            WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
        
        if (controller != null) {
            controller.setSystemBarsBehavior(
                WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
            );
        }
    }
}

AppSecurity.java (Custom Plugin)

package com.ouji.factory.myapp;

import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import org.json.JSONArray;
import java.util.List;

@CapacitorPlugin(name = "AppSecurity")
public class AppSecurity extends Plugin {

    @PluginMethod
    public void getInstalledApps(PluginCall call) {
        PackageManager pm = getContext().getPackageManager();
        List<ApplicationInfo> packages = pm.getInstalledApplications(PackageManager.GET_META_DATA);
        JSONArray appPackages = new JSONArray();
        
        for (ApplicationInfo packageInfo : packages) {
            appPackages.put(packageInfo.packageName);
        }
        
        JSObject ret = new JSObject();
        ret.put("packages", appPackages);
        call.resolve(ret);
    }
    
    @PluginMethod
    public void checkRootStatus(PluginCall call) {
        boolean isRooted = detectRoot();
        JSObject ret = new JSObject();
        ret.put("isRooted", isRooted);
        ret.put("riskLevel", isRooted ? "high" : "low");
        call.resolve(ret);
    }
    
    private boolean detectRoot() {
        // Check for common root indicators
        String[] rootPaths = {
            "/system/app/Superuser.apk",
            "/sbin/su",
            "/system/bin/su",
            "/system/xbin/su",
            "/data/local/xbin/su",
            "/data/local/bin/su",
            "/system/sd/xbin/su",
            "/system/bin/failsafe/su",
            "/data/local/su"
        };
        
        for (String path : rootPaths) {
            if (new java.io.File(path).exists()) {
                return true;
            }
        }
        
        return false;
    }
}

Android Permissions

AndroidManifest.xml Permissions

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />

<!-- Background location for Android 10+ -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

<!-- Prevent app from being killed -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

Permission Handling Strategy

Location Permissions

  1. ACCESS_FINE_LOCATION: Required for precise location
  2. ACCESS_BACKGROUND_LOCATION: Required for background tracking
  3. Runtime Permission Flow:
    • Request fine location first
    • If granted, request background location
    • Handle permission denial gracefully

Camera Permission

  • CAMERA: Required for QR code scanning
  • Runtime Request: Requested when QR scanner is first used
  • Fallback: File upload option if camera denied

Background Location Implementation

Android Background Geolocation Service

Service Configuration

// Background geolocation watcher configuration
const androidConfig = {
  requestPermissions: true,
  stale: false,
  distanceFilter: 50, // Minimum 50 meters movement
  backgroundMessage: "Tracking work location for attendance verification",
  backgroundTitle: "Nilai Clock - Location Tracking",
  // Android-specific options
  enableHighAccuracy: true,
  interval: 30000, // 30 seconds
  fastestInterval: 15000, // 15 seconds minimum
  maxWaitTime: 60000 // 1 minute maximum wait
}

Foreground Service Notification

<!-- Required for Android 8+ background location -->
<service android:name="com.transistorsoft.capacitor.backgroundgeolocation.BackgroundGeolocationService"
         android:foregroundServiceType="location"
         android:enabled="true"
         android:exported="false" />

Location Update Handling

// Handle location updates from background service
const handleLocationUpdate = (location) => {
  console.log('Android location update:', {
    latitude: location.coords.latitude,
    longitude: location.coords.longitude,
    accuracy: location.coords.accuracy,
    timestamp: location.timestamp,
    source: 'background'
  })
  
  // Send to server if user is clocked in
  if (this.isClockedIn) {
    this.sendLocationToServer(location, { source: 'background' })
  }
}

Battery Optimization Handling

Request Battery Optimization Exemption

// In MainActivity.java - Request to ignore battery optimization
private void requestBatteryOptimizationExemption() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        Intent intent = new Intent();
        String packageName = getPackageName();
        PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
        
        if (!pm.isIgnoringBatteryOptimizations(packageName)) {
            intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
            intent.setData(Uri.parse("package:" + packageName));
            startActivity(intent);
        }
    }
}

Network Security Configuration

network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <!-- Allow cleartext traffic for development -->
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">localhost</domain>
        <domain includeSubdomains="true">10.0.2.2</domain>
        <domain includeSubdomains="true">198.18.0.1</domain>
        <domain includeSubdomains="true">192.168.36.54</domain>
        <trust-anchors>
            <certificates src="user" />
        </trust-anchors>
    </domain-config>
    
    <!-- Debug overrides for development -->
    <debug-overrides>
        <trust-anchors>
            <certificates src="user"/>
            <certificates src="system"/>
        </trust-anchors>
    </debug-overrides>
    
    <!-- Production configuration -->
    <domain-config>
        <domain includeSubdomains="true">myapp.ouji.com</domain>
        <trust-anchors>
            <certificates src="system"/>
        </trust-anchors>
    </domain-config>
</network-security-config>

Application Security Configuration

<!-- In AndroidManifest.xml -->
<application
    android:networkSecurityConfig="@xml/network_security_config"
    android:usesCleartextTraffic="true"
    android:allowBackup="false"
    android:extractNativeLibs="false">

Device UUID Generation

Android-Specific Implementation

// Android device UUID generation strategy
async generateAndroidDeviceUuid() {
  try {
    const deviceInfo = await Device.getInfo()
    const deviceId = await Device.getId()
    
    // Create unique fingerprint from device characteristics
    const baseString = [
      deviceInfo.platform,        // "android"
      deviceInfo.model,          // "Pixel 6"
      deviceInfo.manufacturer,   // "Google"
      deviceId.identifier,       // Android ID or UUID
      deviceInfo.osVersion,      // "13"
      await this.getAndroidSpecificId()
    ].join('-')
    
    // Hash the base string for consistent UUID
    return await this.hashString(baseString)
  } catch (error) {
    console.error('Android UUID generation failed:', error)
    return this.generateFallbackUuid()
  }
}

async getAndroidSpecificId() {
  try {
    // Try to get Android ID or generate persistent identifier
    const { value } = await Preferences.get({ key: 'android_persistent_id' })
    
    if (value) return value
    
    // Generate new persistent ID for this installation
    const persistentId = this.generateRandomId(32)
    await Preferences.set({ 
      key: 'android_persistent_id', 
      value: persistentId 
    })
    
    return persistentId
  } catch (error) {
    return `android-fallback-${Date.now()}`
  }
}

Anti-Spoofing Implementation

GPS Spoofing Detection

// Android-specific anti-spoofing checks
async performAndroidSecurityCheck() {
  try {
    // Get installed applications
    const installedApps = await AppSecurity.getInstalledApps()
    
    // Check against blacklist
    const blacklist = await this.fetchBlacklistFromServer()
    const suspiciousApps = installedApps.packages.filter(pkg => 
      blacklist.includes(pkg)
    )
    
    // Check for root access
    const rootStatus = await AppSecurity.checkRootStatus()
    
    // Determine risk level
    let riskLevel = 'low'
    let riskFactors = []
    
    if (suspiciousApps.length > 0) {
      riskLevel = 'high'
      riskFactors.push('gps_spoofing_apps_detected')
    }
    
    if (rootStatus.isRooted) {
      riskLevel = 'high'
      riskFactors.push('device_rooted')
    }
    
    const result = {
      platform: 'android',
      riskLevel,
      riskFactors,
      suspiciousApps,
      isRooted: rootStatus.isRooted,
      timestamp: new Date().toISOString()
    }
    
    // Send to server
    await this.sendSecurityCheckToServer(result)
    
    return result
  } catch (error) {
    console.error('Android security check failed:', error)
    return { riskLevel: 'unknown', error: error.message }
  }
}

Known GPS Spoofing Apps Blacklist

const gpsSpooferBlacklist = [
  // Popular GPS spoofing apps
  'com.lexa.fakegps',
  'com.incorporateapps.fakegps.fre',
  'com.blogspot.newapphorizons.fakegps',
  'com.theappninjas.gpsjoystick',
  'com.fakegps.mock',
  'com.mock.location.app',
  'com.gpsemulator',
  'com.locationspoofer',
  'com.fakegps.location',
  'com.mock.gps.location',
  
  // Developer tools that can spoof location
  'com.android.development',
  'com.android.development_settings',
  'com.mock.location.developer',
  
  // Xposed modules
  'de.robv.android.xposed.installer',
  'com.solohsu.android.edxp.manager',
  
  // Root management apps
  'com.noshufou.android.su',
  'com.noshufou.android.su.elite',
  'eu.chainfire.supersu',
  'com.koushikdutta.superuser',
  'com.thirdparty.superuser',
  'com.yellowes.su'
]

QR Code Scanning Implementation

Camera Integration

// Android camera configuration for QR scanning
const androidCameraConfig = {
  fps: 10,
  qrbox: { width: 250, height: 250 },
  aspectRatio: 1.0,
  disableFlip: false,
  videoConstraints: {
    facingMode: 'environment', // Back camera
    advanced: [{
      focusMode: 'continuous',
      zoom: 1.0
    }]
  }
}

// Initialize QR scanner
const initializeAndroidQRScanner = () => {
  html5QrCode = new Html5Qrcode('qr-reader')
  
  html5QrCode.start(
    { facingMode: 'environment' },
    androidCameraConfig,
    onScanSuccess,
    onScanFailure
  ).catch(err => {
    console.error('Android QR scanner failed to start:', err)
    // Fallback to file upload
    showFileUploadOption()
  })
}

Camera Permission Handling

// Check and request camera permission
const checkAndroidCameraPermission = async () => {
  try {
    const permissions = await Camera.checkPermissions()
    
    if (permissions.camera === 'granted') {
      return true
    }
    
    if (permissions.camera === 'denied') {
      // Guide user to settings
      showPermissionDeniedAlert()
      return false
    }
    
    // Request permission
    const result = await Camera.requestPermissions()
    return result.camera === 'granted'
  } catch (error) {
    console.error('Camera permission check failed:', error)
    return false
  }
}

Local Storage and Preferences

Android Secure Storage

// Android uses EncryptedSharedPreferences
class AndroidSecureStorage {
  async setItem(key, value) {
    try {
      await Preferences.set({
        key: key,
        value: value
      })
      return true
    } catch (error) {
      console.error('Android secure storage set failed:', error)
      return false
    }
  }
  
  async getItem(key) {
    try {
      const { value } = await Preferences.get({ key })
      return value
    } catch (error) {
      console.error('Android secure storage get failed:', error)
      return null
    }
  }
  
  async removeItem(key) {
    try {
      await Preferences.remove({ key })
      return true
    } catch (error) {
      console.error('Android secure storage remove failed:', error)
      return false
    }
  }
}

Performance Optimizations

Memory Management

// Android-specific memory optimization
const optimizeAndroidPerformance = () => {
  // Limit location update frequency
  const locationThrottle = 30000 // 30 seconds
  
  // Clean up unused resources
  const cleanupResources = () => {
    if (html5QrCode && !html5QrCode.isScanning) {
      html5QrCode.clear()
    }
    
    // Clear old location data
    if (locationHistory.length > 100) {
      locationHistory.splice(0, locationHistory.length - 50)
    }
  }
  
  // Set up periodic cleanup
  setInterval(cleanupResources, 300000) // 5 minutes
}

Battery Usage Optimization

// Optimize for Android battery usage
const optimizeAndroidBattery = () => {
  // Reduce location accuracy when not critical
  const adaptiveLocationConfig = {
    enableHighAccuracy: false, // Use network location when possible
    timeout: 30000,
    maximumAge: 60000 // Accept 1-minute old location
  }
  
  // Pause non-critical background tasks
  const pauseNonCriticalTasks = () => {
    // Stop security checks when app is backgrounded
    if (document.hidden) {
      clearInterval(securityCheckInterval)
    }
  }
  
  document.addEventListener('visibilitychange', pauseNonCriticalTasks)
}

Build Configuration

Android Build Variants

// app/build.gradle
android {
    buildTypes {
        debug {
            debuggable true
            minifyEnabled false
            applicationIdSuffix ".debug"
        }
        release {
            debuggable false
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    
    flavorDimensions "environment"
    productFlavors {
        development {
            dimension "environment"
            applicationIdSuffix ".dev"
            buildConfigField "String", "API_BASE_URL", '"http://192.168.36.54:3000"'
        }
        production {
            dimension "environment"
            buildConfigField "String", "API_BASE_URL", '"https://myapp.ouji.com/nilai_clock_api"'
        }
    }
}

ProGuard Configuration

# proguard-rules.pro
-keep class com.ouji.factory.myapp.** { *; }
-keep class com.getcapacitor.** { *; }
-keep class com.transistorsoft.** { *; }

# Keep location service classes
-keep class * extends android.app.Service
-keep class * implements android.location.LocationListener

# Keep JSON serialization
-keepattributes Signature
-keepattributes *Annotation*
-keep class com.google.gson.** { *; }

Debugging and Logging

Android Debug Configuration

// Android-specific debug logging
const AndroidDebugger = {
  enableLocationLogging: () => {
    console.log('Android location debugging enabled')
    // Log all location updates
    window.addEventListener('location-update', (event) => {
      console.log('Android location:', event.detail)
    })
  },
  
  enableSecurityLogging: () => {
    console.log('Android security debugging enabled')
    // Log security check results
    window.addEventListener('security-check', (event) => {
      console.log('Android security:', event.detail)
    })
  },
  
  logDeviceInfo: async () => {
    const deviceInfo = await Device.getInfo()
    console.log('Android device info:', deviceInfo)
  }
}

Performance Monitoring

// Monitor Android app performance
const AndroidPerformanceMonitor = {
  trackMemoryUsage: () => {
    if (performance.memory) {
      console.log('Memory usage:', {
        used: performance.memory.usedJSHeapSize,
        total: performance.memory.totalJSHeapSize,
        limit: performance.memory.jsHeapSizeLimit
      })
    }
  },
  
  trackLocationPerformance: () => {
    const startTime = performance.now()
    
    return {
      end: () => {
        const endTime = performance.now()
        console.log(`Location operation took ${endTime - startTime} ms`)
      }
    }
  }
}

Troubleshooting Common Android Issues

Location Issues

  1. Background location stops: Check battery optimization settings
  2. Inaccurate location: Verify GPS is enabled, check for spoofing apps
  3. Permission denied: Guide user through settings, explain necessity

Camera Issues

  1. Camera won't open: Check permission, verify camera availability
  2. QR scan fails: Improve lighting, check QR code quality
  3. App crashes on scan: Handle camera resource conflicts

Performance Issues

  1. High battery usage: Optimize location frequency, reduce background tasks
  2. Memory leaks: Properly dispose of resources, limit data retention
  3. Slow startup: Optimize initialization sequence, lazy load components

Security Issues

  1. False positives: Refine blacklist, improve detection algorithms
  2. Bypass attempts: Enhance root detection, monitor for new spoofing methods
  3. Data security: Ensure proper encryption, secure API communication