diff --git a/app/build.gradle b/app/build.gradle
index 8b8276c..0998e97 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -15,17 +15,16 @@
plugins {
id 'com.android.application'
id 'com.google.android.gms.strict-version-matcher-plugin'
- id 'kotlin-android'
id 'org.jetbrains.kotlin.plugin.compose' version "$kotlin_version"
}
-apply plugin: 'com.google.android.gms.oss-licenses-plugin'
+apply plugin: 'com.google.android.gms.oss-licenses-plugin'
android {
- compileSdkVersion 36
- namespace 'com.amapi.extensibility.demo'
+ compileSdkVersion = 36
+ namespace = 'com.amapi.extensibility.demo'
buildFeatures {
- compose true
+ compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.15"
@@ -36,43 +35,46 @@ android {
}
defaultConfig {
- applicationId "com.amapi.extensibility.demo"
- minSdkVersion 23
- targetSdkVersion 35
- versionCode 1
- versionName "1.0"
+ applicationId = "com.amapi.extensibility.demo"
+ minSdkVersion = 24
+ targetSdkVersion = 35
+ versionCode = 1
+ versionName = "1.0"
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
- minifyEnabled true
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ minifyEnabled = true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
}
debug {
- minifyEnabled false
- proguardFiles 'proguard-rules.pro'
+ minifyEnabled = false
}
}
compileOptions {
// Flag to enable support for the new language APIs
- coreLibraryDesugaringEnabled true
+ coreLibraryDesugaringEnabled = true
- sourceCompatibility JavaVersion.VERSION_17
- targetCompatibility JavaVersion.VERSION_17
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
- kotlinOptions {
- freeCompilerArgs = ['-Xjvm-default=all-compatibility']
- jvmTarget = '17'
+}
+
+kotlin {
+ compilerOptions {
+ freeCompilerArgs.add('-Xjvm-default=all-compatibility')
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
+
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
- implementation 'com.google.android.libraries.enterprise.amapi:amapi:1.7.0-rc01'
+ implementation 'com.google.android.libraries.enterprise.amapi:amapi:1.8.4'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.17.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
@@ -82,26 +84,17 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'com.google.guava:guava:33.5.0-android'
implementation 'com.google.android.gms:play-services-oss-licenses:17.3.0'
- implementation 'androidx.test.espresso:espresso-idling-resource:3.7.0'
// Compose dependencies
- def composeBom = platform('androidx.compose:compose-bom:2025.09.01')
+ def composeBom = platform('androidx.compose:compose-bom:2026.01.01')
implementation composeBom
- androidTestImplementation composeBom
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.foundation:foundation'
implementation 'androidx.compose.runtime:runtime'
implementation 'androidx.compose.ui:ui'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.4'
implementation 'androidx.activity:activity-compose'
+ implementation 'androidx.navigation:navigation-compose:2.9.7'
implementation 'androidx.compose.ui:ui-tooling-preview'
debugImplementation 'androidx.compose.ui:ui-tooling'
-
- testImplementation 'junit:junit:4.13.2'
-
- androidTestImplementation 'androidx.test:rules:1.7.0'
- androidTestImplementation 'androidx.test.ext:junit:1.3.0'
- androidTestImplementation 'androidx.test:runner:1.7.0'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
- androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 036908b..9fd6483 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -17,6 +17,8 @@
package="com.amapi.extensibility.demo" >
+
+
@@ -31,29 +33,64 @@
android:taskAffinity=""
android:theme="@style/AppTheme" >
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+ AccountSetupScreenContent(accountSetupResult, Modifier.padding(innerPadding))
+ }
+ }
+
+ @Composable
+ private fun AccountSetupScreenContent(
+ accountSetupResult: String?,
+ modifier: Modifier = Modifier,
+ ) {
+ val scrollState = rememberScrollState()
+
+ Column(
+ modifier = modifier.then(Modifier.fillMaxSize().padding(16.dp).verticalScroll(scrollState)),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ val buttonModifier = Modifier.fillMaxWidth()
+
+ Text(
+ text = accountSetupResult ?: stringResource(R.string.account_setup_result),
+ modifier = Modifier.padding(bottom = 16.dp),
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+
+ var enrollmentToken by remember { mutableStateOf("TOKEN") }
+
+ TextField(
+ value = enrollmentToken,
+ onValueChange = { enrollmentToken = it },
+ label = { Text("Enrollment Token") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+
+ Button(
+ onClick = { viewModel.startAccountSetup(enrollmentToken) },
+ modifier = buttonModifier,
+ ) {
+ Text(text = stringResource(R.string.start_account_setup_button))
+ }
+
+ Button(onClick = viewModel::launchAuthenticationActivity, modifier = buttonModifier) {
+ Text(text = stringResource(R.string.launch_auth_activity_button))
+ }
+
+ Button(onClick = viewModel::listAccountSetupAttempts, modifier = buttonModifier) {
+ Text(text = stringResource(R.string.list_account_setup_attempts_button))
+ }
+
+ Button(onClick = viewModel::cancelAccountSetupAttempt, modifier = buttonModifier) {
+ Text(text = stringResource(R.string.cancel_account_setup_attempt_button))
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/amapi/extensibility/demo/accountsetup/AccountSetupViewModel.kt b/app/src/main/java/com/amapi/extensibility/demo/accountsetup/AccountSetupViewModel.kt
new file mode 100644
index 0000000..d15810a
--- /dev/null
+++ b/app/src/main/java/com/amapi/extensibility/demo/accountsetup/AccountSetupViewModel.kt
@@ -0,0 +1,123 @@
+/* Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.amapi.extensibility.demo.accountsetup
+
+import android.app.Application
+import android.content.ComponentName
+import android.util.Log
+import androidx.activity.result.ActivityResultRegistry
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.amapi.extensibility.demo.notification.NotificationReceiverService
+import com.amapi.extensibility.demo.receiver.DeviceAdminReceiver
+import com.amapi.extensibility.demo.util.TAG
+import com.google.android.managementapi.accountsetup.AccountSetupClient
+import com.google.android.managementapi.accountsetup.AccountSetupClientFactory
+import com.google.android.managementapi.accountsetup.model.CancelAccountSetupAttemptRequest
+import com.google.android.managementapi.accountsetup.model.LaunchAuthenticationActivityRequest
+import com.google.android.managementapi.accountsetup.model.StartAccountSetupRequest
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+/** A ViewModel used to handle account setup related operations. */
+class AccountSetupViewModel(
+ application: Application,
+ activityResultRegistry: ActivityResultRegistry,
+) : AndroidViewModel(application) {
+
+ private val _accountSetupResult = MutableStateFlow(null)
+ val accountSetupResult: StateFlow = _accountSetupResult.asStateFlow()
+
+ private val accountSetupClient: AccountSetupClient =
+ AccountSetupClientFactory.create(getApplication(), activityResultRegistry)
+
+ fun startAccountSetup(enrollmentToken: String) {
+ val request =
+ StartAccountSetupRequest.builder()
+ .setEnrollmentToken(enrollmentToken)
+ .setAdminComponentName(ComponentName(getApplication(), DeviceAdminReceiver::class.java))
+ .setNotificationReceiverServiceComponentName(
+ ComponentName(getApplication(), NotificationReceiverService::class.java)
+ )
+ .build()
+
+ viewModelScope.launch {
+ try {
+ val result = accountSetupClient.startAccountSetup(request)
+ Log.i(TAG, "Successfully started account setup: $result")
+ _accountSetupResult.value = result.toString()
+ } catch (t: Throwable) {
+ Log.e(TAG, "Failed to start account setup", t)
+ _accountSetupResult.value = t.message
+ }
+ }
+ }
+
+ fun launchAuthenticationActivity() {
+ val request = LaunchAuthenticationActivityRequest.getDefaultInstance()
+
+ viewModelScope.launch {
+ try {
+ val result = accountSetupClient.launchAuthenticationActivity(request)
+ Log.i(TAG, "Successfully launched auth activity: $result")
+ _accountSetupResult.value = result.toString()
+ } catch (t: Throwable) {
+ Log.e(TAG, "Failed to launch auth activity", t)
+ _accountSetupResult.value = t.message
+ }
+ }
+ }
+
+ fun listAccountSetupAttempts() {
+ viewModelScope.launch {
+ try {
+ val result = accountSetupClient.listAccountSetupAttempts()
+ Log.i(TAG, "Successfully listed account setup attempts: $result")
+ _accountSetupResult.value = result.toString()
+ } catch (t: Throwable) {
+ Log.e(TAG, "Failed to list account setup attempts", t)
+ _accountSetupResult.value = t.message
+ }
+ }
+ }
+
+ fun cancelAccountSetupAttempt() {
+ val request = CancelAccountSetupAttemptRequest.getDefaultInstance()
+ viewModelScope.launch {
+ try {
+ val result = accountSetupClient.cancelAccountSetupAttempt(request)
+ Log.i(TAG, "Successfully canceled account setup attempt: $result")
+ _accountSetupResult.value = result.toString()
+ } catch (t: Throwable) {
+ Log.e(TAG, "Failed to cancel account setup attempt", t)
+ _accountSetupResult.value = t.message
+ }
+ }
+ }
+
+ /** Factory for creating AccountSetupViewModel instances. */
+ class AccountSetupViewModelFactory(
+ private val application: Application,
+ private val activityResultRegistry: ActivityResultRegistry,
+ ) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T =
+ AccountSetupViewModel(application, activityResultRegistry) as? T
+ ?: throw IllegalArgumentException("Unknown ViewModel class")
+ }
+}
diff --git a/app/src/main/java/com/amapi/extensibility/demo/approles/AppRolesActivity.kt b/app/src/main/java/com/amapi/extensibility/demo/approles/AppRolesActivity.kt
new file mode 100644
index 0000000..149a2f2
--- /dev/null
+++ b/app/src/main/java/com/amapi/extensibility/demo/approles/AppRolesActivity.kt
@@ -0,0 +1,90 @@
+/* Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.amapi.extensibility.demo.approles
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.ViewModel
+import com.amapi.extensibility.demo.R
+import com.google.android.managementapi.approles.model.AppRolesSetRequest
+import kotlinx.coroutines.flow.StateFlow
+
+class AppRolesViewModel : ViewModel() {
+ val appRolesSetRequest: StateFlow =
+ InMemoryAppRolesRepository.appRolesSetRequest
+}
+
+class AppRolesActivity : ComponentActivity() {
+ private val viewModel: AppRolesViewModel by viewModels()
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ MaterialTheme {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ colors =
+ TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ titleContentColor = MaterialTheme.colorScheme.primary,
+ ),
+ title = { Text(stringResource(R.string.app_roles_title)) },
+ )
+ }
+ ) { innerPadding ->
+ AppRolesScreen(
+ modifier = Modifier.fillMaxSize().padding(innerPadding),
+ viewModel = viewModel,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun AppRolesScreen(modifier: Modifier = Modifier, viewModel: AppRolesViewModel) {
+ val request by viewModel.appRolesSetRequest.collectAsState()
+
+ Surface(modifier = modifier, color = MaterialTheme.colorScheme.background) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Text(text = "App Roles and Capabilities:")
+ Text(
+ modifier = Modifier.padding(top = 10.dp),
+ text = request?.roles?.toString() ?: "No roles received yet.",
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/amapi/extensibility/demo/util/AppIdlingResource.kt b/app/src/main/java/com/amapi/extensibility/demo/approles/InMemoryAppRolesRepository.kt
similarity index 54%
rename from app/src/main/java/com/amapi/extensibility/demo/util/AppIdlingResource.kt
rename to app/src/main/java/com/amapi/extensibility/demo/approles/InMemoryAppRolesRepository.kt
index e0d985b..95aefec 100644
--- a/app/src/main/java/com/amapi/extensibility/demo/util/AppIdlingResource.kt
+++ b/app/src/main/java/com/amapi/extensibility/demo/approles/InMemoryAppRolesRepository.kt
@@ -12,23 +12,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.amapi.extensibility.demo.util
+package com.amapi.extensibility.demo.approles
-import androidx.test.espresso.idling.CountingIdlingResource
+import com.google.android.managementapi.approles.model.AppRolesSetRequest
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
-object AppIdlingResource {
- private const val RESOURCE = "GENERAL_RESOURCE"
- private val countingIdlingResource: CountingIdlingResource = CountingIdlingResource(RESOURCE)
+object InMemoryAppRolesRepository {
+ private val _appRolesSetRequest = MutableStateFlow(null)
+ val appRolesSetRequest: StateFlow = _appRolesSetRequest
- fun getIdlingResource() = countingIdlingResource
-
- fun increment() {
- countingIdlingResource.increment()
- }
-
- fun decrement() {
- if (!countingIdlingResource.isIdleNow) {
- countingIdlingResource.decrement()
- }
+ fun onAppRolesSet(request: AppRolesSetRequest) {
+ _appRolesSetRequest.value = request
}
}
diff --git a/app/src/main/java/com/amapi/extensibility/demo/commands/CommandActivity.kt b/app/src/main/java/com/amapi/extensibility/demo/commands/CommandActivity.kt
index c7348b6..afb459d 100644
--- a/app/src/main/java/com/amapi/extensibility/demo/commands/CommandActivity.kt
+++ b/app/src/main/java/com/amapi/extensibility/demo/commands/CommandActivity.kt
@@ -14,38 +14,83 @@
*/
package com.amapi.extensibility.demo.commands
-import android.content.Intent
import android.os.Bundle
-import android.widget.Button
-import android.widget.EditText
-import android.widget.TextView
-import androidx.appcompat.app.AppCompatActivity
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider
import com.amapi.extensibility.demo.R
-import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
-class CommandActivity : AppCompatActivity() {
+class CommandActivity : ComponentActivity() {
private lateinit var commandViewModel: CommandViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_command)
commandViewModel = ViewModelProvider(this).get(CommandViewModel::class.java)
- commandViewModel.commandResultLiveData.observe(this) { results ->
- findViewById(R.id.command_result_textview).apply {
- text = results
+
+ setContent {
+ Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
+ val commandResult by commandViewModel.commandResult.collectAsState()
+ CommandScreen(commandResult)
}
}
- findViewById