← Back to articles Intune

Intune App SDK Android Phase 7: App Participation Deep Dive

Intune App SDK for Android — Phase 7: App Participation Features

If you've wrestled with MAM policy enforcement in a custom Android app and wondered why a save-to-OneDrive button still works even after enabling App Protection Policy — this is the article you need. Phase 7 of the Intune App SDK integration is where the SDK stops doing the heavy lifting and hands the wheel to you, the developer. The SDK cannot infer your app's save destinations, notification content sensitivity, or backup strategy. You have to wire these up explicitly.

This guide is written for Android engineers who've already completed SDK Phases 1–6 and for Intune architects who need to know what to demand from dev teams during policy validation. We'll cover every App Participation Feature with production-ready Kotlin, a Graph API validation example, and a PowerShell script to audit your App Protection Policy configuration.

⚠ PrerequisitesPhases 1–6 must be complete — specifically Phase 5 (Multi-Identity with MAMPolicyManager) and Phase 6 (App Configuration). Without a valid MAM identity in context, every getPolicy() call returns a permissive default policy, silently allowing operations that should be blocked.

The Big Picture: Where Phase 7 Sits

INTUNE APP SDK — ANDROID INTEGRATION LAYERSPhase 1-2SDK + GradlePhase 3-4Identity/EnrollPhase 5-6Multi-ID/ConfigPhase 7App ParticipationPhase 8+Testing/PublishPhase 7 — What the SDK Delegates to Your CodeSave / Open PolicyCheck before dataleaves/enters appNotification PolicySanitize or blockorg-data notificationsBackup ProtectionMAM-compliantbackup agentsCA RegistrationApp ProtectionConditional AccessPOLICY ENGINEMAMPolicyManagergetPolicy(activity) · getPolicyForIdentityOID(oid)AppPolicy interface — all checks flow through here
Phase 7 sits on top of the SDK foundation. All four feature areas call into MAMPolicyManager — the app provides context the SDK can't infer on its own.

Retrieving an AppPolicy Instance

Every Phase 7 check starts with one of two MAMPolicyManager calls. Get this right first — everything downstream depends on obtaining the policy for the correct identity.

// Option A — activity-scoped (uses current foreground identity)
val policy: AppPolicy = MAMPolicyManager.getPolicy(currentActivity)

// Option B — explicit OID (preferred for multi-identity apps)
val policy: AppPolicy = MAMPolicyManager.getPolicyForIdentityOID(userOid)
🔴 Silent Failure ModeIf no MAM account is enrolled for the identity, getPolicy() returns a permissive stub — all checks return true (allowed). This means a misconfigured enrollment silently disables all your policy checks. Always verify enrollment state before relying on policy results in QA.

Save Location Policy Enforcement

This is the most commonly mis-implemented feature. The SDK has zero visibility into where your Save button sends data — SharePoint, a local path, Box — so you must query the policy before initiating any external save operation.

User taps SaveApp intercepts actiongetIsSaveToLocationAllowedForOID()SaveLocation enum + userOidfalse → BLOCKEDshowSharingBlockedDialog()true → ALLOWEDProceed with savefalsetrue
Save policy decision flow — your code calls the SDK before allowing data to leave the app, then either blocks with the built-in dialog or proceeds normally.
// In your save handler — e.g., a "Save to OneDrive" button click
fun onSaveToOneDriveClicked(activity: Activity, userOid: String) {
    val policy = MAMPolicyManager.getPolicy(activity)
    val isSaveAllowed = policy.getIsSaveToLocationAllowedForOID(
        SaveLocation.ONEDRIVE,
        userOid  // Entra OID of the signed-in user
    )
    if (!isSaveAllowed) {
        MAMUIHelper.showSharingBlockedDialog(activity)
        return
    }
    // Safe to proceed
    initiateOneDriveUpload()
}
SaveLocationUse CaseOID Required
ONEDRIVESave to OneDrive consumer or business✓ or null
SHAREPOINTSave to SharePoint document library✓ or null
BOXSave to Box cloud storage✓ or null
LOCALExternal device storage (not app-private)Always null
PHOTO_LIBRARYAndroid local photo storageAlways null
IMANAGESave to iManage DMS✓ or null
EGNYTESave to Egnyte✓ or null
ACCOUNT_DOCUMENTAccount-associated multi-identity location✓ required
OTHERUnlisted locationsAlways null
💡 App-private storage is always allowedFiles saved to the app's own internal storage directory that are necessary for app operation never require a policy check. Only data that crosses the app boundary needs to be gated.

Open Location Policy Enforcement

The inverse of Save policy — call this before importing or opening a file from an external source. The OpenLocation enum mirrors SaveLocation but with some different members, including the handy convenience method for local files with embedded identity metadata.

// Explicit OID check for SharePoint import
val isOpenAllowed = MAMPolicyManager.getPolicy(activity)
    .getIsOpenFromLocationAllowedForOID(OpenLocation.SHAREPOINT, userOid)

// Convenience method — parses file owner OID from file metadata automatically
val isLocalFileAllowed = policy.isOpenFromLocalStorageAllowed(file)
// Equivalent to: getIsOpenFromLocationAllowedForOID(OpenLocation.LOCAL, fileOwnerOid)

Notification Restriction Policy

This one catches developers off guard because the failure is invisible to end users — a notification simply never shows. The SDK will attempt a best-effort block for single-identity apps if you skip this, but BLOCK_ORG_DATA gets silently upgraded to a full block, meaning your users lose the sanitized notification entirely.

getNotificationRestriction()BLOCKEDDo NOT showthe notificationBLOCK_ORG_DATAShow sanitizedgeneric bodyUNRESTRICTEDShow fullnotification⚠ SDK Fallback (Skip the check at your peril)Single-identity apps only — BLOCK_ORG_DATA is treated as BLOCKED.Multi-identity apps get NO automatic fallback — notifications leak.
Three notification restriction states. The SDK fallback only covers single-identity apps and silently converts BLOCK_ORG_DATA into a full block — implement this yourself.
// In your notification dispatch path
fun sendOrgNotification(notificationIdentityOid: String, content: NotificationContent) {
    val restriction = MAMPolicyManager
        .getPolicyForIdentityOID(notificationIdentityOid)
        .getNotificationRestriction()

    when (restriction) {
        NotificationRestriction.BLOCKED -> {
            // Swallow entirely — no notification posted
            return
        }
        NotificationRestriction.BLOCK_ORG_DATA -> {
            // Replace body with sanitized placeholder
            val sanitized = content.copy(
                title = getString(R.string.notification_sanitized_title),
                body  = getString(R.string.notification_sanitized_body)
            )
            postNotification(sanitized)
        }
        NotificationRestriction.UNRESTRICTED -> {
            postNotification(content)
        }
    }
}
🔴 Multi-Identity Notification GapFor multi-identity apps the SDK provides zero automatic notification enforcement. If you don't call getNotificationRestriction(), org data will appear in notifications regardless of policy. This is the most commonly missed Phase 7 requirement in app reviews.

Backup Data Protection

Android's Auto Backup can silently exfiltrate MAM-managed data to a user's personal Google account. The SDK provides two compliant paths — use MAMDefaultBackupAgent for most apps, or extend MAMBackupAgentHelper for custom backup logic.

<!-- AndroidManifest.xml — MAM-compliant Auto Backup -->
<application
    android:allowBackup="true"
    android:fullBackupOnly="true"
    android:backupAgent="com.microsoft.intune.mam.client.app.backup.MAMDefaultBackupAgent"
    ... />
// Custom backup agent — extend MAMBackupAgentHelper
class MyBackupAgent : MAMBackupAgentHelper() {
    override fun onCreate() {
        val helper = FileBackupHelper(this, "my_data_file.db")
        addHelper("file_backup_key", helper)
    }
}

Screen Capture Restriction (Custom Workflows)

The SDK automatically sets FLAG_SECURE on MAM-managed activities, which blocks the system screenshot mechanism. But if your app has a custom screen capture workflow — in-app recording, watermarked screenshot export, etc. — you must check the policy explicitly.

// Hide custom screenshot capability if policy prohibits it
val isScreenCaptureAllowed = MAMPolicyManager
    .getPolicy(currentActivity)
    .getIsScreenCaptureAllowed()

screenshotButton.visibility = if (isScreenCaptureAllowed) View.VISIBLE else View.GONE

App Protection Conditional Access Registration

If your tenant uses App Protection CA policies (requiring MAM-compliant apps as a condition of access), the app must explicitly register each account with the SDK enrollment manager. This call belongs in Application.onCreate() or immediately after MSAL token acquisition.

// Register after successful MSAL sign-in
MAMEnrollmentManager.getInstance(context).registerAccountForMAM(
    upn,       // e.g. alice@contoso.com
    aadId,     // Entra Object ID (OID)
    tenantId,  // Entra Tenant ID
    authority  // e.g. https://login.microsoftonline.com/<tenantId>
)

// SDK Notification — listen for enrollment result
MAMNotificationReceiverRegistry.getInstance(context)
    .registerReceiver(
        { notification ->
            when (notification.type) {
                MAMNotificationType.MAM_ENROLLMENT_RESULT  -> { handleEnrollmentResult(notification); true }
                MAMNotificationType.WIPE_USER_DATA         -> { handleWipe(); true }
                MAMNotificationType.COMPLIANCE_STATUS      -> { handleCompliance(notification); true }
                else -> true
            }
        },
        MAMNotificationType.MAM_ENROLLMENT_RESULT,
        MAMNotificationType.WIPE_USER_DATA,
        MAMNotificationType.COMPLIANCE_STATUS
    )

Validating Policy Configuration via Graph API

Before testing your app, confirm the App Protection Policy is actually targeting your app and has the expected settings. Use the Intune Graph API endpoint for managed app policies.

GET https://graph.microsoft.com/beta/deviceAppManagement/androidManagedAppProtections
// Required scope: DeviceManagementApps.Read.All

// Sample response fragment
{
  "value": [
    {
      "id": "T_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "displayName": "Contoso Android MAM Policy",
      "allowedDataStorageLocations": ["oneDriveForBusiness", "sharePoint"],
      "notificationRestriction": "blockOrganizationalData",
      "screenCaptureBlocked": true,
      "isAssigned": true
    }
  ]
}
💡 Pro Tip: Get app-specific policy assignmentsAppend /deviceAppManagement/androidManagedAppProtections/{id}/apps to verify your app's bundle ID is in the target list before running device tests. Saves hours of debugging a policy that was never assigned to the right app.

PowerShell: Audit App Protection Policy for Phase 7 Coverage

This script pulls your Android MAM policies and flags any settings that Phase 7 features depend on, so you can catch misconfigurations in CI or during policy reviews.

#Requires -Modules Microsoft.Graph.DeviceManagement.Apps
# Audit-MAMAndroidPolicies.ps1
# Checks Phase 7-relevant settings across all Android MAM policies

Connect-MgGraph -Scopes "DeviceManagementApps.Read.All"

$policies = Invoke-MgGraphRequest -Method "GET" `
    -Uri "https://graph.microsoft.com/beta/deviceAppManagement/androidManagedAppProtections"

if (-not $policies.value) {
    Write-Warning "No Android MAM policies found."
    return
}

$report = foreach ($policy in $policies.value) {
    # Fetch detailed policy to get all properties
    $detail = Invoke-MgGraphRequest -Method "GET" `
        -Uri "https://graph.microsoft.com/beta/deviceAppManagement/androidManagedAppProtections/$($policy.id)"

    [PSCustomObject]@{
        PolicyName                  = $detail.displayName
        IsAssigned                  = $detail.isAssigned
        SaveLocations               = ($detail.allowedDataStorageLocations -join ", ")
        OpenLocations               = ($detail.allowedInboundDataTransferSources -join ", ")
        NotificationRestriction     = $detail.notificationRestriction
        ScreenCaptureBlocked        = $detail.screenCaptureBlocked
        BackupBlocked               = $detail.backupBlocked
        Phase7SavePolicyConfigured  = ($null -ne $detail.allowedDataStorageLocations)
        Phase7NotifPolicyConfigured = ($detail.notificationRestriction -ne "unrestricted")
    }
}

$report | Format-Table -AutoSize

# Flag any policies where notification restriction is default (unrestricted)
$unrestrictedNotif = $report | Where-Object { $_.NotificationRestriction -eq "unrestricted" }
if ($unrestrictedNotif) {
    Write-Warning "⚠ Policies with unrestricted notifications (Phase 7 not enforced):"
    $unrestrictedNotif | Select-Object PolicyName, NotificationRestriction | Format-Table
}

# Flag unassigned policies
$unassigned = $report | Where-Object { -not $_.IsAssigned }
if ($unassigned) {
    Write-Warning "⚠ Unassigned policies (no users/groups targeted):"
    $unassigned | Select-Object PolicyName | Format-Table
}

Write-Host "Audit complete. Review flagged items before releasing the app." -ForegroundColor Cyan

Common Gotchas and Production Lessons

IssueRoot CauseResolution
Policy checks always return true (allowed)No MAM account enrolled — stub policy returnedVerify MAMEnrollmentManager registration succeeded before calling policy checks
BLOCK_ORG_DATA notifications fully suppressedApp skipped getNotificationRestriction() — SDK fallback treats it as BLOCKEDImplement the when block in all notification dispatch paths
Save to LOCAL is blocked despite policy allowing itPassing a non-null OID for SaveLocation.LOCALAlways pass null as OID for LOCAL and PHOTO_LIBRARY
Screenshot button still visible after policy changeChecking policy at Activity start only, not on resumeRe-check getIsScreenCaptureAllowed() in onResume()
Backup exfiltrates org dataUsing default BackupAgent instead of MAM variantSwitch manifest to MAMDefaultBackupAgent or extend MAMBackupAgentHelper
App Protection CA fails silentlyregisterAccountForMAM() not called after MSAL authRegister immediately post-token acquisition; listen for MAM_ENROLLMENT_RESULT
✅ Quick Validation ChecklistBefore submitting your app for Intune policy testing: (1) All save/open actions call the appropriate location policy check. (2) All notification dispatch paths call getNotificationRestriction(). (3) Manifest uses MAMDefaultBackupAgent or a custom MAMBackupAgentHelper. (4) registerAccountForMAM() is called immediately after MSAL sign-in. (5) Custom screen capture UI is gated on getIsScreenCaptureAllowed().

Wrapping Up

Phase 7 is where most MAM integration projects fail audit — not because the SDK is hard to use, but because these checks are easy to skip and easy to miss in testing when your test account happens to have a permissive policy. The golden rule: if data crosses an app boundary in either direction, there is a policy check that belongs there, and it's your code, not the SDK's, that has to make that call.

Run the PowerShell audit script against your tenant policies before every QA cycle, validate your Graph API responses match what your app expects, and don't trust the SDK's fallback behavior for notification policy in production — implement it explicitly every time.

Was this article helpful?

🎓 Ready to go deeper?

Practice real MD-102 exam questions, get AI feedback on your weak areas, and fast-track your Intune certification.

Start Free Practice → Book a Session
Souhaiel Morhag
Souhaiel Morhag
Microsoft Endpoint & Modern Workplace Engineer

Souhaiel Morhag is a Microsoft Intune and endpoint management specialist with hands-on experience deploying and securing enterprise environments across Microsoft 365. He founded MSEndpoint.com to share practical, real-world guides for IT admins navigating Microsoft technologies — and built the MSEndpoint Academy at app.msendpoint.com/academy, a dedicated learning platform for professionals preparing for the MD-102 (Microsoft 365 Endpoint Administrator) certification. Through in-depth articles and AI-powered practice exams, Souhaiel helps IT teams move faster and certify with confidence.

Related Articles

Popular on MSEndpoint