Spaces:
Sleeping
Sleeping
| """ | |
| AMAPI (Android Management API) Endpoints | |
| Provides QR-code based provisioning that uses Google's trusted Cloud DPC | |
| instead of directly downloading our APK. This completely bypasses Play Protect. | |
| Setup flow: | |
| 1. POST /amapi/setup - Create enterprise (one-time) | |
| 2. POST /amapi/policy - Create/update device policy | |
| 3. POST /amapi/enrollment-token - Generate enrollment token | |
| 4. GET /amapi/qr - Get QR code data for device provisioning | |
| """ | |
| import json | |
| import logging | |
| from typing import Optional | |
| from fastapi import APIRouter, HTTPException | |
| from pydantic import BaseModel, Field | |
| from app.services import amapi_service | |
| logger = logging.getLogger(__name__) | |
| router = APIRouter(prefix="/amapi", tags=["Android Management API"]) | |
| # =============== Schemas =============== | |
| class SetupResponse(BaseModel): | |
| configured: bool | |
| enterprise_name: Optional[str] = None | |
| message: str | |
| class EnrollmentTokenResponse(BaseModel): | |
| token_value: str | |
| token_name: str | |
| qr_string: str | |
| expiry: Optional[str] = None | |
| class PolicyResponse(BaseModel): | |
| policy_name: str | |
| message: str | |
| # =============== Endpoints =============== | |
| async def amapi_status(): | |
| """Check if AMAPI is configured and ready.""" | |
| configured = amapi_service.is_configured() | |
| enterprise = amapi_service.get_enterprise_name() | |
| return { | |
| "configured": configured, | |
| "has_enterprise": bool(enterprise), | |
| "enterprise_name": enterprise, | |
| "message": "AMAPI ready" if (configured and enterprise) else | |
| "AMAPI configured but no enterprise" if configured else | |
| "AMAPI not configured - set AMAPI_SERVICE_ACCOUNT_JSON and AMAPI_PROJECT_ID" | |
| } | |
| async def setup_enterprise(): | |
| """Create a new Android Enterprise (one-time setup). | |
| After calling this, save the enterprise_name as AMAPI_ENTERPRISE_NAME env var. | |
| """ | |
| if not amapi_service.is_configured(): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="AMAPI not configured. Set AMAPI_SERVICE_ACCOUNT_JSON and AMAPI_PROJECT_ID environment variables." | |
| ) | |
| try: | |
| result = amapi_service.create_enterprise() | |
| return SetupResponse( | |
| configured=True, | |
| enterprise_name=result["enterprise_name"], | |
| message=f"Enterprise created! Save this as AMAPI_ENTERPRISE_NAME: {result['enterprise_name']}" | |
| ) | |
| except Exception as e: | |
| logger.error(f"Enterprise creation failed: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def create_or_update_policy(): | |
| """Create or update the device management policy. | |
| This policy tells Google's DPC to force-install the EMI Locker app | |
| and apply security restrictions (no factory reset, no USB, etc.) | |
| """ | |
| enterprise = amapi_service.get_enterprise_name() | |
| if not enterprise: | |
| raise HTTPException(status_code=400, detail="No enterprise configured. Run /amapi/setup first.") | |
| try: | |
| result = amapi_service.create_policy(enterprise) | |
| return PolicyResponse( | |
| policy_name=result.get("name", ""), | |
| message="Policy created/updated. EMI Locker will be force-installed on enrolled devices." | |
| ) | |
| except Exception as e: | |
| logger.error(f"Policy creation failed: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def create_enrollment_token(): | |
| """Generate a reusable enrollment token for QR provisioning. | |
| The QR code generated from this token uses Google's trusted Cloud DPC, | |
| which downloads from Play Store - completely bypassing Play Protect. | |
| """ | |
| enterprise = amapi_service.get_enterprise_name() | |
| if not enterprise: | |
| raise HTTPException(status_code=400, detail="No enterprise configured. Run /amapi/setup first.") | |
| try: | |
| token = amapi_service.create_enrollment_token(enterprise) | |
| token_value = token.get("value", "") | |
| # Prefer Google's pre-built qrCode | |
| google_qr = token.get("qrCode", "") | |
| qr_string = google_qr if google_qr else amapi_service.generate_qr_string(token_value) | |
| return EnrollmentTokenResponse( | |
| token_value=token_value, | |
| token_name=token.get("name", ""), | |
| qr_string=qr_string, | |
| expiry=token.get("expirationTimestamp") | |
| ) | |
| except Exception as e: | |
| logger.error(f"Enrollment token creation failed: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_provisioning_qr(): | |
| """DEPRECATED: AMAPI QR provisioning is blocked by Google for EMI use cases. | |
| Google's Permissible Usage Policy explicitly bans EMI/device financing. | |
| Enterprise quota = 0 (denied). This endpoint will ALWAYS fail. | |
| Use instead: | |
| - /zte/provisioning-qr or /zte/provisioning-data (Custom DPC QR - no Google limits) | |
| - USB/ADB setup (most reliable) | |
| """ | |
| raise HTTPException( | |
| status_code=410, | |
| detail={ | |
| "error": "AMAPI QR is permanently disabled for EMI use cases", | |
| "reason": "Google's Permissible Usage Policy bans EMI/device financing from using Android Management API. Enterprise quota = 0.", | |
| "solution": "Use Custom DPC QR from dashboard (Device Setup → QR Code tab) or USB/ADB method. These have NO Google limits.", | |
| "custom_dpc_qr": "/api/v1/zte/provisioning-qr", | |
| "custom_dpc_data": "/api/v1/zte/provisioning-data", | |
| } | |
| ) | |
| async def list_enrolled_devices(): | |
| """List all devices enrolled via AMAPI.""" | |
| enterprise = amapi_service.get_enterprise_name() | |
| if not enterprise: | |
| raise HTTPException(status_code=400, detail="No enterprise configured.") | |
| try: | |
| devices = amapi_service.list_devices(enterprise) | |
| return {"devices": devices, "count": len(devices)} | |
| except Exception as e: | |
| logger.error(f"Device listing failed: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def delete_device(device_id: str): | |
| """Delete a specific AMAPI-enrolled device to free up quota.""" | |
| try: | |
| success = amapi_service.delete_device(device_id) | |
| if success: | |
| return {"status": "deleted", "device": device_id} | |
| raise HTTPException(status_code=500, detail=f"Failed to delete device: {device_id}") | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Device deletion failed: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def cleanup_devices(): | |
| """Delete ALL AMAPI-enrolled devices to free up quota. | |
| Fix for: 'organization reached its usage limit' error. | |
| Google limits the number of devices per enterprise. | |
| This deletes all old enrolled devices to free up slots. | |
| """ | |
| enterprise = amapi_service.get_enterprise_name() | |
| if not enterprise: | |
| raise HTTPException(status_code=400, detail="No enterprise configured.") | |
| try: | |
| result = amapi_service.cleanup_all_devices(enterprise) | |
| return { | |
| "status": "cleanup_complete", | |
| "total_devices": result["total"], | |
| "deleted": result["deleted"], | |
| "failed": result["failed"], | |
| "message": f"Deleted {result['deleted']}/{result['total']} devices. You can now enroll new devices." | |
| } | |
| except Exception as e: | |
| logger.error(f"Cleanup failed: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def create_new_enterprise(): | |
| """Create a brand new enterprise to bypass device limits. | |
| Nuclear option when cleanup isn't enough. Creates a fresh enterprise | |
| with zero device count, resets everything. | |
| """ | |
| if not amapi_service.is_configured(): | |
| raise HTTPException(status_code=400, detail="AMAPI not configured.") | |
| try: | |
| result = amapi_service.create_new_enterprise() | |
| return { | |
| "status": "new_enterprise_created", | |
| "enterprise_name": result["enterprise_name"], | |
| "message": f"New enterprise created: {result['enterprise_name']}. " | |
| f"Update AMAPI_ENTERPRISE_NAME env var to persist across restarts." | |
| } | |
| except Exception as e: | |
| logger.error(f"New enterprise creation failed: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def enterprise_callback(enterpriseToken: str = ""): | |
| """Callback URL for enterprise signup flow. | |
| Used internally by Google during enterprise creation. | |
| """ | |
| return {"status": "ok", "token": enterpriseToken} | |