From d67698f604f58db46460ef11d2d4962f16078915 Mon Sep 17 00:00:00 2001 From: Jonathan Scott Date: Tue, 4 Jun 2019 11:00:04 +0100 Subject: [PATCH 01/73] Bump version for release. Test: N/A Bug: 130713895 Change-Id: I0608e2fc7f18056ad1d131c76cd3d8e23509141b --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 65180e3f..766112b3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ ext { // exactly 1 digit versionMinor = 1 // exactly 2 digits - versionBuild = 00 + versionBuild = 02 } android { From 1dc24e6bb80f0091cccbfd946dc7e741844cb81c Mon Sep 17 00:00:00 2001 From: Jonathan Scott Date: Wed, 29 May 2019 11:49:21 +0100 Subject: [PATCH 02/73] Add tests for AppStatesService. Test: ./gradlew test Change-Id: I8729174b071446e26ce653e3f728acbc1ba5c7c4 --- app/build.gradle | 10 +- .../testdpc/feedback/AppStatesService.java | 43 ++- .../feedback/AppStatesServiceTest.java | 274 ++++++++++++++++++ 3 files changed, 318 insertions(+), 9 deletions(-) create mode 100644 app/src/test/java/com/afwsamples/testdpc/feedback/AppStatesServiceTest.java diff --git a/app/build.gradle b/app/build.gradle index 766112b3..5e94cc7a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,6 +51,12 @@ android { textOutput "stdout" } + testOptions { + unitTests { + includeAndroidResources = true + } + } + signingConfigs { debug { storeFile file("$projectDir/debug.keystore") @@ -137,6 +143,8 @@ dependencies { implementation 'org.bouncycastle:bcpkix-jdk15on:1.56' implementation 'org.bouncycastle:bcprov-jdk15on:1.56' implementation 'com.google.guava:guava:23.6-android' - + testImplementation 'org.robolectric:robolectric:4.2' + testImplementation "com.google.truth:truth:0.44" + testImplementation 'junit:junit:4.5' annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" } diff --git a/app/src/main/java/com/afwsamples/testdpc/feedback/AppStatesService.java b/app/src/main/java/com/afwsamples/testdpc/feedback/AppStatesService.java index 9c7aa6fe..577d6579 100644 --- a/app/src/main/java/com/afwsamples/testdpc/feedback/AppStatesService.java +++ b/app/src/main/java/com/afwsamples/testdpc/feedback/AppStatesService.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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 + * + * http://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.afwsamples.testdpc.feedback; import android.app.NotificationChannel; @@ -7,6 +22,7 @@ import androidx.core.app.NotificationManagerCompat; import androidx.preference.PreferenceManager; import android.util.Log; +import androidx.annotation.VisibleForTesting; import androidx.enterprise.feedback.KeyedAppState; import androidx.enterprise.feedback.KeyedAppStatesService; import androidx.enterprise.feedback.ReceivedKeyedAppState; @@ -26,6 +42,9 @@ public class AppStatesService extends KeyedAppStatesService { private static final String CHANNEL_ID = "KeyedAppStates"; private static final String CHANNEL_NAME = "Keyed App States"; + @VisibleForTesting + static final String TAG = "KeyedAppStates"; + private int nextNotificationId = 0; private Map idMapping = new HashMap<>(); @@ -47,17 +66,25 @@ public void onReceive(Collection states, boolean requestS } private void showNotification(ReceivedKeyedAppState state, boolean requestSync) { - Log.i("KeyedAppStates", - state.timestamp() + " " + - state.packageName() + ":" + - state.key() + "=" + - state.data() + " (" + - state.message() + ")" + (requestSync ? " - SYNC REQUESTED" : "")); + final String logMessage = state.timestamp() + " " + + state.packageName() + ":" + + state.key() + "=" + + state.data() + " (" + + state.message() + ")" + (requestSync ? " - SYNC REQUESTED" : ""); + + if (state.severity() == KeyedAppState.SEVERITY_ERROR) { + Log.e(TAG, logMessage); + } else { + Log.i(TAG, logMessage); + } + + final String severity = (state.severity() == KeyedAppState.SEVERITY_ERROR) ? "ERROR" : + (state.severity() == KeyedAppState.SEVERITY_INFO) ? "INFO" : "UNKNOWN"; NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.arrow_down) - .setContentTitle(state.packageName() + ":" + state.key()) + .setContentTitle(state.packageName() + ":" + state.key() + " " + severity) .setContentText(state.timestamp() + " " + state.data() + " (" + state.message() +")" + @@ -83,4 +110,4 @@ private int getIdForState(ReceivedKeyedAppState state) { } return idMapping.get(key); } -} +} \ No newline at end of file diff --git a/app/src/test/java/com/afwsamples/testdpc/feedback/AppStatesServiceTest.java b/app/src/test/java/com/afwsamples/testdpc/feedback/AppStatesServiceTest.java new file mode 100644 index 00000000..82ce40c9 --- /dev/null +++ b/app/src/test/java/com/afwsamples/testdpc/feedback/AppStatesServiceTest.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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 + * + * http://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.afwsamples.testdpc.feedback; + +import static android.util.Log.ERROR; +import static android.util.Log.INFO; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build.VERSION_CODES; +import android.support.v7.preference.PreferenceManager; +import androidx.enterprise.feedback.KeyedAppState; +import androidx.enterprise.feedback.ReceivedKeyedAppState; +import com.afwsamples.testdpc.R; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLog; +import org.robolectric.shadows.ShadowNotification; +import org.robolectric.shadows.ShadowNotificationManager; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk=VERSION_CODES.N) // Feedback channel is supported from N onwards +public class AppStatesServiceTest { + + private static final String REQUEST_SYNC_LOG_TEXT = "SYNC REQUESTED"; + + private static final ReceivedKeyedAppState STATE1 = + ReceivedKeyedAppState.builder() + .setPackageName("test.package") + .setTimestamp(123L) + .setSeverity(KeyedAppState.SEVERITY_INFO) + .setKey("key1") + .setMessage("message1") + .setData("data1") + .build(); + + private static final ReceivedKeyedAppState STATE1_DIFFERENT_MESSAGE = + ReceivedKeyedAppState.builder() + .setPackageName("test.package") + .setTimestamp(123L) + .setSeverity(KeyedAppState.SEVERITY_INFO) + .setKey("key1") + .setMessage("different message1") + .setData("data1") + .build(); + + private static final ReceivedKeyedAppState STATE2 = + ReceivedKeyedAppState.builder() + .setPackageName("test.package") + .setTimestamp(123L) + .setSeverity(KeyedAppState.SEVERITY_ERROR) + .setKey("key2") + .setMessage("message2") + .setData("data2") + .build(); + + + private static final ReceivedKeyedAppState INFO_STATE = STATE1; + private static final ReceivedKeyedAppState ERROR_STATE = STATE2; + + private final Context context = RuntimeEnvironment.application; + private final SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(context); + private final NotificationManager notificationManager = + context.getSystemService(NotificationManager.class); + private final AppStatesService service = + Robolectric.buildService(AppStatesService.class).get(); + + @Test + public void onReceive_shouldNotNotify_noNotification() { + setNotificationPreference(false); + + service.onReceive(Arrays.asList(STATE1), /* requestSync= */ false); + + assertThat(shadowOf(notificationManager).getActiveNotifications()).isEmpty(); + } + + @Test + public void onReceive_shouldNotNotify_noLogs() { + setNotificationPreference(false); + + service.onReceive(Arrays.asList(STATE1), /* requestSync= */ false); + + assertThat(ShadowLog.getLogsForTag(AppStatesService.TAG)).isEmpty(); + } + + @Test + public void onReceive_shouldNotify_logContainsRequiredInformation() { + setNotificationPreference(true); + + service.onReceive(Arrays.asList(STATE1), /* requestSync= */ false); + + assertThatLogContainsRequiredInformation( + ShadowLog.getLogsForTag(AppStatesService.TAG).get(0), STATE1); + } + + private void assertThatLogContainsRequiredInformation( + ShadowLog.LogItem logItem, ReceivedKeyedAppState state) { + assertThat(logItem.msg).contains(Long.toString(state.timestamp())); + assertThat(logItem.msg).contains(state.packageName()); + assertThat(logItem.msg).contains(state.key()); + assertThat(logItem.msg).contains(state.data()); + assertThat(logItem.msg).contains(state.message()); + } + + @Test + public void onReceive_infoLog_shouldNotify_logIsInfoLevel() { + setNotificationPreference(true); + + service.onReceive(Arrays.asList(INFO_STATE), /* requestSync= */ false); + + assertThat(ShadowLog.getLogsForTag(AppStatesService.TAG).get(0).type).isEqualTo(INFO); + } + + @Test + public void onReceive_errorLog_shouldNotify_logIsErrorLevel() { + setNotificationPreference(true); + + service.onReceive(Arrays.asList(ERROR_STATE), /* requestSync= */ false); + + assertThat(ShadowLog.getLogsForTag(AppStatesService.TAG).get(0).type).isEqualTo(ERROR); + } + + @Test + public void onReceive_shouldNotify_noRequestSync_logDoesNotContainRequestSync() { + setNotificationPreference(true); + + service.onReceive(Arrays.asList(STATE1), /* requestSync= */ false); + + ShadowLog.LogItem logItem = ShadowLog.getLogsForTag(AppStatesService.TAG).get(0); + assertThat(logItem.msg).doesNotContain(REQUEST_SYNC_LOG_TEXT); + } + + @Test + public void onReceive_shouldNotify_requestSync_logContainsRequestSync() { + setNotificationPreference(true); + + service.onReceive(Arrays.asList(STATE1), /* requestSync= */ true); + + ShadowLog.LogItem logItem = ShadowLog.getLogsForTag(AppStatesService.TAG).get(0); + assertThat(logItem.msg).contains(REQUEST_SYNC_LOG_TEXT); + } + + @Test + public void onReceive_shouldNotify_oneLogPerState() { + setNotificationPreference(true); + + service.onReceive(Arrays.asList(STATE1, STATE2), /* requestSync= */ false); + + assertThat(ShadowLog.getLogsForTag(AppStatesService.TAG)).hasSize(2); + } + + @Test + public void onReceive_shouldNotify_notificationContainsRequiredInformation() { + setNotificationPreference(true); + + service.onReceive(Arrays.asList(STATE1), /* requestSync= */ false); + + assertThatNotificationContainsRequiredInformation( + shadowOf(notificationManager).getAllNotifications().get(0), + STATE1 + ); + } + + private void assertThatNotificationContainsRequiredInformation( + Notification notification, ReceivedKeyedAppState state) { + assertThat(shadowOf(notification).getContentTitle().toString()).contains(state.packageName()); + assertThat(shadowOf(notification).getContentTitle().toString()).contains(state.key()); + assertThat(shadowOf(notification).getContentText().toString()) + .contains(Long.toString(state.timestamp())); + assertThat(shadowOf(notification).getContentText().toString()).contains(state.data()); + assertThat(shadowOf(notification).getContentText().toString()).contains(state.message()); + } + + @Test + public void onReceive_infoLog_shouldNotify_notificationTitleIncludesInfo() { + setNotificationPreference(true); + + service.onReceive(Arrays.asList(INFO_STATE), /* requestSync= */ false); + + final Notification notification = shadowOf(notificationManager).getAllNotifications().get(0); + assertThat( + shadowOf(notification).getContentTitle().toString()).contains("INFO"); + } + + @Test + public void onReceive_errorLog_shouldNotify_notificationTitleIncludesError() { + setNotificationPreference(true); + + service.onReceive(Arrays.asList(ERROR_STATE), /* requestSync= */ false); + + final Notification notification = shadowOf(notificationManager).getAllNotifications().get(0); + assertThat( + shadowOf(notification).getContentTitle().toString()).contains("ERROR"); + } + + @Test + public void onReceive_shouldNotify_noRequestSync_notificationDoesNotContainRequestSync() { + setNotificationPreference(true); + + service.onReceive(Arrays.asList(STATE1), /* requestSync= */ false); + + final Notification notification = shadowOf(notificationManager).getAllNotifications().get(0); + assertThat( + shadowOf(notification).getContentText().toString()).doesNotContain(REQUEST_SYNC_LOG_TEXT); + } + + @Test + public void onReceive_shouldNotify_requestSync_notificationContainsRequestSync() { + setNotificationPreference(true); + + service.onReceive(Arrays.asList(STATE1), /* requestSync= */ true); + + final Notification notification = shadowOf(notificationManager).getAllNotifications().get(0); + assertThat(shadowOf(notification).getContentText().toString()).contains(REQUEST_SYNC_LOG_TEXT); + } + + @Test + public void onReceive_shouldNotify_oneNotificationPerKey() { + setNotificationPreference(true); + + service.onReceive(Arrays.asList(STATE1, STATE2), /* requestSync= */ false); + + assertThat(shadowOf(notificationManager).getAllNotifications()).hasSize(2); + } + + @Test + public void onReceive_multiple_shouldNotify_oneNotificationPerKey() { + setNotificationPreference(true); + service.onReceive(Arrays.asList(STATE1), /* requestSync= */ false); + + service.onReceive(Arrays.asList(STATE2), /* requestSync= */ false); + + assertThat(shadowOf(notificationManager).getAllNotifications()).hasSize(2); + } + + @Test + public void onReceive_shouldNotify_sameKeyUpdatesNotification() { + setNotificationPreference(true); + service.onReceive(Arrays.asList(STATE1), /* requestSync= */ false); + + service.onReceive(Arrays.asList(STATE1_DIFFERENT_MESSAGE), /* requestSync= */ false); + + assertThat(shadowOf(notificationManager).getAllNotifications()).hasSize(1); + } + + private void setNotificationPreference(boolean shouldNotify) { + preferences.edit() + .putBoolean(context.getString(R.string.app_feedback_notifications), shouldNotify) + .commit(); + } +} \ No newline at end of file From e742ebc5e05bc5c1f3851ccbc975ec69082b2f24 Mon Sep 17 00:00:00 2001 From: kholoud mohamed Date: Tue, 4 Jun 2019 18:00:17 +0100 Subject: [PATCH 03/73] remove login screen from admin-integrated flow Bug: 133139326 Test: N/A Change-Id: I6fa4644642e29842951fa03cd8c91698c37f6e58 --- .../testdpc/provision/DpcLoginActivity.java | 41 +------------------ 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/afwsamples/testdpc/provision/DpcLoginActivity.java b/app/src/main/java/com/afwsamples/testdpc/provision/DpcLoginActivity.java index 5ace02e0..60f02f73 100644 --- a/app/src/main/java/com/afwsamples/testdpc/provision/DpcLoginActivity.java +++ b/app/src/main/java/com/afwsamples/testdpc/provision/DpcLoginActivity.java @@ -16,23 +16,16 @@ package com.afwsamples.testdpc.provision; -import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE; -import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_MODE; import static android.app.admin.DevicePolicyManager.PROVISIONING_MODE_FULLY_MANAGED_DEVICE; import static android.app.admin.DevicePolicyManager.PROVISIONING_MODE_MANAGED_PROFILE; -import android.accounts.Account; import android.app.Activity; import android.content.Intent; import android.os.Bundle; -import android.os.PersistableBundle; -import android.util.Log; import android.view.View; import android.widget.RadioGroup; -import com.afwsamples.testdpc.AddAccountActivity; import com.afwsamples.testdpc.R; -import com.afwsamples.testdpc.common.LaunchIntentUtil; import com.android.setupwizardlib.GlifLayout; /** @@ -41,9 +34,6 @@ */ public class DpcLoginActivity extends Activity { - private static final String LOG_TAG = "DpcLoginActivity"; - private static final int ADD_ACCOUNT_REQUEST_CODE = 1; - @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -58,32 +48,6 @@ public void onBackPressed() { super.onBackPressed(); } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case ADD_ACCOUNT_REQUEST_CODE: - finishWithIntent(createResultIntentFromData(data)); - break; - default: - Log.d(LOG_TAG, "Unknown result code: " + resultCode); - break; - } - } - - private Intent createResultIntentFromData(Intent data) { - final Intent resultIntent = new Intent(); - resultIntent.putExtra(EXTRA_PROVISIONING_MODE, PROVISIONING_MODE_MANAGED_PROFILE); - if (data != null && data.hasExtra(EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE)) { - final Account accountToMigrate = data.getParcelableExtra( - EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE); - resultIntent.putExtra(EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE, accountToMigrate); - final PersistableBundle bundle = new PersistableBundle(); - bundle.putString(LaunchIntentUtil.EXTRA_ACCOUNT_NAME, accountToMigrate.name); - resultIntent.putExtra(EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE, bundle); - } - return resultIntent; - } - private void onNavigateNext(View nextButton) { final Intent intent = new Intent(); RadioGroup dpcLoginOptions = findViewById(R.id.dpc_login_options); @@ -93,9 +57,8 @@ private void onNavigateNext(View nextButton) { finishWithIntent(intent); return; case R.id.dpc_login_po: - startActivityForResult( - new Intent(getApplicationContext(), AddAccountActivity.class), - ADD_ACCOUNT_REQUEST_CODE); + intent.putExtra(EXTRA_PROVISIONING_MODE, PROVISIONING_MODE_MANAGED_PROFILE); + finishWithIntent(intent); return; default: finish(); From f751b1f5e6070aaaf3d14b679f528a4fb373c501 Mon Sep 17 00:00:00 2001 From: kholoud mohamed Date: Wed, 5 Jun 2019 13:23:26 +0100 Subject: [PATCH 04/73] fix error in AppStatesServiceTest Test: ./gradlew test Change-Id: Icfa9e7aa5ce62bd2d355b0a2cb5b66b2da1df9b4 --- .../com/afwsamples/testdpc/feedback/AppStatesServiceTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/test/java/com/afwsamples/testdpc/feedback/AppStatesServiceTest.java b/app/src/test/java/com/afwsamples/testdpc/feedback/AppStatesServiceTest.java index 82ce40c9..ced7820f 100644 --- a/app/src/test/java/com/afwsamples/testdpc/feedback/AppStatesServiceTest.java +++ b/app/src/test/java/com/afwsamples/testdpc/feedback/AppStatesServiceTest.java @@ -25,9 +25,9 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Build.VERSION_CODES; -import android.support.v7.preference.PreferenceManager; import androidx.enterprise.feedback.KeyedAppState; import androidx.enterprise.feedback.ReceivedKeyedAppState; +import androidx.preference.PreferenceManager; import com.afwsamples.testdpc.R; import java.util.Arrays; import org.junit.Test; @@ -37,8 +37,6 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLog; -import org.robolectric.shadows.ShadowNotification; -import org.robolectric.shadows.ShadowNotificationManager; @RunWith(RobolectricTestRunner.class) @Config(minSdk=VERSION_CODES.N) // Feedback channel is supported from N onwards From d8250e62c0f3348b1dd0ff73b77916e22fe3a3da Mon Sep 17 00:00:00 2001 From: kholoud mohamed Date: Wed, 5 Jun 2019 15:57:16 +0100 Subject: [PATCH 05/73] add tests for DpcLoginActivity Fixes:133139326 Test: ./gradlew test Change-Id: I77c5a3de168daa5e16e81238cc889470c6f70ed6 --- app/build.gradle | 1 + .../provision/DpcLoginActivityTest.java | 78 +++++++++++++++++++ build.gradle | 2 +- 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 app/src/test/java/com/afwsamples/testdpc/provision/DpcLoginActivityTest.java diff --git a/app/build.gradle b/app/build.gradle index 5e94cc7a..61ff4efc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -146,5 +146,6 @@ dependencies { testImplementation 'org.robolectric:robolectric:4.2' testImplementation "com.google.truth:truth:0.44" testImplementation 'junit:junit:4.5' + testImplementation 'androidx.test:core:1.0.0' annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" } diff --git a/app/src/test/java/com/afwsamples/testdpc/provision/DpcLoginActivityTest.java b/app/src/test/java/com/afwsamples/testdpc/provision/DpcLoginActivityTest.java new file mode 100644 index 00000000..c11e24df --- /dev/null +++ b/app/src/test/java/com/afwsamples/testdpc/provision/DpcLoginActivityTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * 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 + * + * http://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.afwsamples.testdpc.provision; + +import static android.app.Activity.RESULT_CANCELED; +import static android.app.Activity.RESULT_OK; +import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_MODE; +import static android.app.admin.DevicePolicyManager.PROVISIONING_MODE_FULLY_MANAGED_DEVICE; +import static android.app.admin.DevicePolicyManager.PROVISIONING_MODE_MANAGED_PROFILE; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.Shadows.shadowOf; + +import android.os.Build.VERSION_CODES; +import android.widget.RadioButton; +import androidx.test.core.app.ActivityScenario; +import com.afwsamples.testdpc.R; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +// TODO: update to Q when Q is testable +@Config(minSdk = VERSION_CODES.P) +public class DpcLoginActivityTest { + + @Test + public void onNavigateNext_poRadioBoxSelected_shouldFinishWithCorrectIntent() { + ActivityScenario.launch(DpcLoginActivity.class).onActivity(activity -> { + RadioButton dpcLoginPo = activity.findViewById(R.id.dpc_login_po); + dpcLoginPo.setChecked(true); + + activity.findViewById(R.id.suw_navbar_next).performClick(); + + assertThat(shadowOf(activity).getResultCode()).isEqualTo(RESULT_OK); + assertThat(shadowOf(activity).getResultIntent().getIntExtra(EXTRA_PROVISIONING_MODE, -1)) + .isEqualTo(PROVISIONING_MODE_MANAGED_PROFILE); + }); + } + + @Test + public void onNavigateNext_doRadioBoxSelected_shouldFinishWithCorrectIntent() { + ActivityScenario.launch(DpcLoginActivity.class).onActivity(activity -> { + RadioButton dpcLoginDo = activity.findViewById(R.id.dpc_login_do); + dpcLoginDo.setChecked(true); + + activity.findViewById(R.id.suw_navbar_next).performClick(); + + assertThat(shadowOf(activity).getResultCode()).isEqualTo(RESULT_OK); + assertThat(shadowOf(activity).getResultIntent().getIntExtra(EXTRA_PROVISIONING_MODE, -1)) + .isEqualTo(PROVISIONING_MODE_FULLY_MANAGED_DEVICE); + }); + } + + @Test + public void onBackPressed_shouldSetResultToCancelled() { + ActivityScenario.launch(DpcLoginActivity.class).onActivity(activity -> { + + activity.onBackPressed(); + + assertThat(shadowOf(activity).getResultCode()).isEqualTo(RESULT_CANCELED); + }); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index c77c4e30..72d6d248 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { maven {url "$rootDir/../../../../prebuilts/tools/common/m2/repository"} } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.android.tools.build:gradle:3.4.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From eba130286d4e53f5d85bd4da9d8256b5ae30ea03 Mon Sep 17 00:00:00 2001 From: Jonathan Scott Date: Thu, 6 Jun 2019 11:38:39 +0100 Subject: [PATCH 06/73] Bump version of enterprise-feedback. Test: ./gradlew test Change-Id: Ie3016698e59f984e94d65e5a8db2328e112dd56d --- app/build.gradle | 2 +- .../testdpc/feedback/AppStatesService.java | 26 +++++++++---------- .../feedback/AppStatesServiceTest.java | 20 +++++++------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 61ff4efc..793e8532 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -133,7 +133,7 @@ private void stripTestOnlyForBuild(flavor, buildType) { dependencies { def lifecycle_version = "2.0.0" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" - implementation 'androidx.enterprise:enterprise-feedback:1.0.0-alpha01' + implementation 'androidx.enterprise:enterprise-feedback:1.0.0-alpha02' implementation 'androidx.multidex:multidex:2.0.0' implementation 'androidx.legacy:legacy-preference-v14:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.0.0' diff --git a/app/src/main/java/com/afwsamples/testdpc/feedback/AppStatesService.java b/app/src/main/java/com/afwsamples/testdpc/feedback/AppStatesService.java index 577d6579..7b678f6c 100644 --- a/app/src/main/java/com/afwsamples/testdpc/feedback/AppStatesService.java +++ b/app/src/main/java/com/afwsamples/testdpc/feedback/AppStatesService.java @@ -66,28 +66,28 @@ public void onReceive(Collection states, boolean requestS } private void showNotification(ReceivedKeyedAppState state, boolean requestSync) { - final String logMessage = state.timestamp() + " " + - state.packageName() + ":" + - state.key() + "=" + - state.data() + " (" + - state.message() + ")" + (requestSync ? " - SYNC REQUESTED" : ""); + final String logMessage = state.getTimestamp() + " " + + state.getPackageName() + ":" + + state.getKey() + "=" + + state.getData() + " (" + + state.getMessage() + ")" + (requestSync ? " - SYNC REQUESTED" : ""); - if (state.severity() == KeyedAppState.SEVERITY_ERROR) { + if (state.getSeverity() == KeyedAppState.SEVERITY_ERROR) { Log.e(TAG, logMessage); } else { Log.i(TAG, logMessage); } - final String severity = (state.severity() == KeyedAppState.SEVERITY_ERROR) ? "ERROR" : - (state.severity() == KeyedAppState.SEVERITY_INFO) ? "INFO" : "UNKNOWN"; + final String severity = (state.getSeverity() == KeyedAppState.SEVERITY_ERROR) ? "ERROR" : + (state.getSeverity() == KeyedAppState.SEVERITY_INFO) ? "INFO" : "UNKNOWN"; NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.arrow_down) - .setContentTitle(state.packageName() + ":" + state.key() + " " + severity) - .setContentText(state.timestamp() + " " + - state.data() + - " (" + state.message() +")" + + .setContentTitle(state.getPackageName() + ":" + state.getKey() + " " + severity) + .setContentText(state.getTimestamp() + " " + + state.getData() + + " (" + state.getMessage() +")" + (requestSync ? "\nSYNC REQUESTED" : "")); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); notificationManager.notify(getIdForState(state), notificationBuilder.build()); @@ -103,7 +103,7 @@ private void createNotificationChannel() { } private int getIdForState(ReceivedKeyedAppState state) { - String key = state.packageName() + ":" + state.key(); + String key = state.getPackageName() + ":" + state.getKey(); if (!idMapping.containsKey(key)) { idMapping.put(key, nextNotificationId++); diff --git a/app/src/test/java/com/afwsamples/testdpc/feedback/AppStatesServiceTest.java b/app/src/test/java/com/afwsamples/testdpc/feedback/AppStatesServiceTest.java index ced7820f..da9cdd28 100644 --- a/app/src/test/java/com/afwsamples/testdpc/feedback/AppStatesServiceTest.java +++ b/app/src/test/java/com/afwsamples/testdpc/feedback/AppStatesServiceTest.java @@ -116,11 +116,11 @@ public void onReceive_shouldNotify_logContainsRequiredInformation() { private void assertThatLogContainsRequiredInformation( ShadowLog.LogItem logItem, ReceivedKeyedAppState state) { - assertThat(logItem.msg).contains(Long.toString(state.timestamp())); - assertThat(logItem.msg).contains(state.packageName()); - assertThat(logItem.msg).contains(state.key()); - assertThat(logItem.msg).contains(state.data()); - assertThat(logItem.msg).contains(state.message()); + assertThat(logItem.msg).contains(Long.toString(state.getTimestamp())); + assertThat(logItem.msg).contains(state.getPackageName()); + assertThat(logItem.msg).contains(state.getKey()); + assertThat(logItem.msg).contains(state.getData()); + assertThat(logItem.msg).contains(state.getMessage()); } @Test @@ -184,12 +184,12 @@ public void onReceive_shouldNotify_notificationContainsRequiredInformation() { private void assertThatNotificationContainsRequiredInformation( Notification notification, ReceivedKeyedAppState state) { - assertThat(shadowOf(notification).getContentTitle().toString()).contains(state.packageName()); - assertThat(shadowOf(notification).getContentTitle().toString()).contains(state.key()); + assertThat(shadowOf(notification).getContentTitle().toString()).contains(state.getPackageName()); + assertThat(shadowOf(notification).getContentTitle().toString()).contains(state.getKey()); assertThat(shadowOf(notification).getContentText().toString()) - .contains(Long.toString(state.timestamp())); - assertThat(shadowOf(notification).getContentText().toString()).contains(state.data()); - assertThat(shadowOf(notification).getContentText().toString()).contains(state.message()); + .contains(Long.toString(state.getTimestamp())); + assertThat(shadowOf(notification).getContentText().toString()).contains(state.getData()); + assertThat(shadowOf(notification).getContentText().toString()).contains(state.getMessage()); } @Test From 7e529a799ec0286a70b1a3aedeec8cf57aed30fc Mon Sep 17 00:00:00 2001 From: Jonathan Scott Date: Thu, 13 Jun 2019 17:31:50 +0100 Subject: [PATCH 07/73] Fix crash on L. Test: Manual Change-Id: I1608c85dc23cca7af42216a94fe981fb65e0f43a --- .../testdpc/policy/PolicyManagementFragment.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/afwsamples/testdpc/policy/PolicyManagementFragment.java b/app/src/main/java/com/afwsamples/testdpc/policy/PolicyManagementFragment.java index 0c26436e..9036ff53 100644 --- a/app/src/main/java/com/afwsamples/testdpc/policy/PolicyManagementFragment.java +++ b/app/src/main/java/com/afwsamples/testdpc/policy/PolicyManagementFragment.java @@ -2102,8 +2102,13 @@ public void onClick(DialogInterface dialogInterface, int i) { .show(); } - @TargetApi(VERSION_CODES.M) + @TargetApi(VERSION_CODES.N) private void loadAppFeedbackNotifications() { + if (Util.SDK_INT < VERSION_CODES.N) { + // This toggle is only available >= N due to device_policy_header.xml + // so this code not executing will not be noticed + return; + } mEnableAppFeedbackNotificationsPreference.setChecked( PreferenceManager.getDefaultSharedPreferences(getContext()) .getBoolean(getString(R.string.app_feedback_notifications), false)); From 84df07edac68dd07b4fe73d8c207b10839abed42 Mon Sep 17 00:00:00 2001 From: kholoud mohamed Date: Fri, 7 Jun 2019 17:17:00 +0100 Subject: [PATCH 08/73] grant required permissions for wifi configuration Fixes: 120591423 Test: ./gradlew test Change-Id: Ic74ed576c0719bf85c9e3a4ab1aaf82890afb196 --- app/src/main/AndroidManifest.xml | 1 + .../testdpc/common/PermissionsHelper.java | 118 +++++++++++ .../WifiConfigCreationDialog.java | 4 +- .../policy/wifimanagement/WifiConfigUtil.java | 57 +++--- .../WifiModificationFragment.java | 23 ++- .../testdpc/common/PermissionsHelperTest.java | 190 ++++++++++++++++++ 6 files changed, 352 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/com/afwsamples/testdpc/common/PermissionsHelper.java create mode 100644 app/src/test/java/com/afwsamples/testdpc/common/PermissionsHelperTest.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 498dfb0a..0f99cfd5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,6 +30,7 @@ + manifestPermissions = Arrays.asList(packageInfo.requestedPermissions); + for (String expectedPermission : requiredPermissions) { + if (!manifestPermissions.contains(expectedPermission)) { + Log.e(TAG, "Missing required permission from manifest: " + expectedPermission); + return false; + } + if (!maybeGrantDangerousPermission(expectedPermission, admin, context)){ + return false; + } + } + return true; + } + + /** + * Attempts to grant a permission automatically if it is considered dangerous - this only happens + * for PO/DO devices. + */ + @RequiresApi(VERSION_CODES.M) + private static boolean maybeGrantDangerousPermission(String permission, ComponentName admin, + Context context) { + if (!isPermissionDangerous(permission, context)) { + return true; + } + if (!ProvisioningStateUtil.isManagedByTestDPC(context)) { + return false; + } + if (hasPermissionGranted(admin, context, permission)) { + return true; + } + DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context + .getSystemService(Context.DEVICE_POLICY_SERVICE); + return devicePolicyManager.setPermissionGrantState( + admin, + context.getPackageName(), + permission, + DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED); + } + + + // Min API version required for DevicePolicyManager.getPermissionGrantState + @RequiresApi(VERSION_CODES.M) + private static boolean hasPermissionGranted( + ComponentName componentName, Context context, String permission) { + DevicePolicyManager devicePolicyManager = context.getSystemService(DevicePolicyManager.class); + return devicePolicyManager + .getPermissionGrantState(componentName, context.getPackageName(), permission) + == DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED; + } + + private static boolean isPermissionDangerous(String permission, Context context) { + PermissionInfo permissionInfo; + try { + permissionInfo = context.getPackageManager().getPermissionInfo(permission, 0); + } catch (NameNotFoundException e) { + Log.e(TAG, "Failed to look up permission.", e); + return false; + } + return permissionInfo != null + && (permissionInfo.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE) + == PermissionInfo.PROTECTION_DANGEROUS; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiConfigCreationDialog.java b/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiConfigCreationDialog.java index d70c9426..a7629d7e 100644 --- a/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiConfigCreationDialog.java +++ b/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiConfigCreationDialog.java @@ -19,10 +19,8 @@ import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; -import android.content.Context; import android.content.DialogInterface; import android.net.wifi.WifiConfiguration; -import android.net.wifi.WifiManager; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; @@ -191,7 +189,7 @@ private void updateConfigurationSecurity(WifiConfiguration config) { String password = mPasswordText.getText().toString(); // WEP-40, WEP-104, and 256-bit WEP (WEP-232?) if ((length == 10 || length == 26 || length == 58) - && password.matches("[0-9A-Fa-f]*")) { + && password.matches("[0-9A-Fa-f]*")) { config.wepKeys[0] = password; } else { config.wepKeys[0] = getQuotedString(password); diff --git a/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiConfigUtil.java b/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiConfigUtil.java index d17374d9..f9010158 100644 --- a/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiConfigUtil.java +++ b/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiConfigUtil.java @@ -22,39 +22,30 @@ public class WifiConfigUtil { - /** - * Save or replace the wifi configuration. - * - * @param context - * @param wifiConfiguration - * @return success to add/replace the wifi configuration - */ - public static boolean saveWifiConfiguration(Context context, WifiConfiguration - wifiConfiguration) { - WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); - final int networkId; - - if (wifiConfiguration.networkId == -1) { - // new wifi configuration, add it and then save it. - networkId = wifiManager.addNetwork(wifiConfiguration); - } else { - // existing wifi configuration, update it and then save it. - networkId = wifiManager.updateNetwork(wifiConfiguration); - } - - if (networkId == -1) { - return false; - } - - // Added successfully, try to save it now. - wifiManager.enableNetwork(networkId, /* disableOthers */ false); - if (wifiManager.saveConfiguration()) { - return true; - } else { - // Remove the added network that fail to save. - wifiManager.removeNetwork(networkId); - return false; - } + /** + * Save or replace the wifi configuration. + * + * @return success to add/replace the wifi configuration + */ + public static boolean saveWifiConfiguration(Context context, WifiConfiguration + wifiConfiguration) { + WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + final int networkId; + + // WifiManager deprecated APIs including #addNetwork, #updateNetwork, #enableNetwork are restricted to system apps and DPCs + // https://developer.android.com/preview/privacy/camera-connectivity#wifi-network-config-restrictions + if (wifiConfiguration.networkId == -1) { + // new wifi configuration, add it and then save it. + networkId = wifiManager.addNetwork(wifiConfiguration); + } else { + // existing wifi configuration, update it and then save it. + networkId = wifiManager.updateNetwork(wifiConfiguration); } + if (networkId == -1) { + return false; + } + wifiManager.enableNetwork(networkId, /* disableOthers= */ false); + return true; + } } diff --git a/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiModificationFragment.java b/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiModificationFragment.java index b8d9c498..c3f94c1e 100644 --- a/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiModificationFragment.java +++ b/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiModificationFragment.java @@ -16,12 +16,16 @@ package com.afwsamples.testdpc.policy.wifimanagement; +import static android.net.wifi.WifiEnterpriseConfig.Eap; + +import android.Manifest.permission; import android.app.AlertDialog; import android.app.DialogFragment; import android.app.Fragment; import android.content.Context; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -31,14 +35,14 @@ import android.widget.CheckedTextView; import android.widget.ListView; import android.widget.TextView; - +import androidx.annotation.RequiresApi; +import com.afwsamples.testdpc.DeviceAdminReceiver; import com.afwsamples.testdpc.R; - +import com.afwsamples.testdpc.common.PermissionsHelper; +import com.afwsamples.testdpc.common.Util; import java.util.ArrayList; import java.util.List; -import static android.net.wifi.WifiEnterpriseConfig.Eap; - /** * Fragment for WiFi configuration editing. */ @@ -46,7 +50,6 @@ public class WifiModificationFragment extends Fragment implements WifiConfigCreationDialog.Listener { private static final String TAG_WIFI_CONFIG_MODIFICATION = "wifi_config_modification"; - private TextView mText; private ListView mConfigsList; private ConfigsAdapter mConfigsAdapter; private List mConfiguredNetworks = new ArrayList<>(); @@ -54,6 +57,8 @@ public class WifiModificationFragment extends Fragment private void updateConfigsList() { mConfiguredNetworks.clear(); + // WifiManager deprecated APIs including #getConfiguredNetworks are restricted to system apps and DPCs + // https://developer.android.com/preview/privacy/camera-connectivity#wifi-network-config-restrictions List configuredNetworks = mWifiManager.getConfiguredNetworks(); if (configuredNetworks != null) { mConfiguredNetworks.addAll(configuredNetworks); @@ -110,6 +115,7 @@ public void onResume() { updateConfigsList(); } + @RequiresApi(api = VERSION_CODES.M) @Override public View onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState) { @@ -125,6 +131,13 @@ public View onCreateView(LayoutInflater inflater, final ViewGroup container, mConfigsAdapter = new ConfigsAdapter(); mConfigsList.setAdapter(mConfigsAdapter); mConfigsList.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + + if (Util.SDK_INT >= VERSION_CODES.M) { + PermissionsHelper + .ensureRequiredPermissions(new String[]{permission.ACCESS_FINE_LOCATION}, + DeviceAdminReceiver.getComponentName(this.getActivity()), this.getContext()); + } + updateConfigsList(); Button updateConfigButton = (Button) view.findViewById(R.id.updateSelectedConfig); diff --git a/app/src/test/java/com/afwsamples/testdpc/common/PermissionsHelperTest.java b/app/src/test/java/com/afwsamples/testdpc/common/PermissionsHelperTest.java new file mode 100644 index 00000000..039823b5 --- /dev/null +++ b/app/src/test/java/com/afwsamples/testdpc/common/PermissionsHelperTest.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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 + * + * http://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.afwsamples.testdpc.common; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.robolectric.Shadows.shadowOf; + +import android.Manifest.permission; +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PermissionInfo; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLog; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.M) +public class PermissionsHelperTest { + + private final Context context = RuntimeEnvironment.application; + private final DevicePolicyManager devicePolicyManager = + (DevicePolicyManager) ApplicationProvider.getApplicationContext() + .getSystemService(Context.DEVICE_POLICY_SERVICE); + private final ComponentName testDpcAdmin = + new ComponentName("com.afwsamples.testdpc", "TestCls"); + private final ComponentName nonTestDpcAdmin = new ComponentName("TestPkg", "TestCls"); + + // Permission protection levels should be defined by the framework/shadows and should not be set + // by the tests, however this is not the case now + private static final String MISSING_PERMISSION = "permission"; + private static final String DANGEROUS_PERMISSION = permission.ACCESS_FINE_LOCATION; + private static final String NORMAL_PERMISSION = permission.ACCESS_WIFI_STATE; + private static final String MISSING_INFO_PERMISSION = permission.CHANGE_WIFI_STATE; + + @Test + public void ensureRequiredPermissions_ifPermissionMissingFromManifest_shouldReturnFalseAndLogError() { + boolean requiredPermissionsGranted = PermissionsHelper + .ensureRequiredPermissions(new String[]{MISSING_PERMISSION}, nonTestDpcAdmin, context); + + assertFalse(requiredPermissionsGranted); + assertTrue(ShadowLog.getLogsForTag(PermissionsHelper.TAG).get(0).msg + .contains("Missing required permission from manifest: " + MISSING_PERMISSION)); + } + + @Test + public void ensureRequiredPermissions_ifPermissionIsDangerousAndDpcIsProfileOwner_shouldReturnTrueAndSetPermissionGrantState() { + addPermissionInfo(DANGEROUS_PERMISSION, PermissionInfo.PROTECTION_DANGEROUS); + shadowOf(devicePolicyManager).setProfileOwner(testDpcAdmin); + + boolean requiredPermissionsGranted = PermissionsHelper + .ensureRequiredPermissions(new String[]{DANGEROUS_PERMISSION}, testDpcAdmin, context); + + assertTrue(requiredPermissionsGranted); + assertThat(devicePolicyManager + .getPermissionGrantState(testDpcAdmin, context.getPackageName(), DANGEROUS_PERMISSION)) + .isEqualTo(DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED); + } + + @Test + public void ensureRequiredPermissions_ifPermissionIsDangerousAndDpcIsDeviceOwner_shouldReturnTrueAndSetPermissionGrantState() { + addPermissionInfo(DANGEROUS_PERMISSION, PermissionInfo.PROTECTION_DANGEROUS); + shadowOf(devicePolicyManager).setDeviceOwner(testDpcAdmin); + + boolean requiredPermissionsGranted = PermissionsHelper + .ensureRequiredPermissions(new String[]{DANGEROUS_PERMISSION}, testDpcAdmin, context); + + assertTrue(requiredPermissionsGranted); + assertThat(devicePolicyManager + .getPermissionGrantState(testDpcAdmin, context.getPackageName(), DANGEROUS_PERMISSION)) + .isEqualTo(DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED); + } + + @Test + public void ensureRequiredPermissions_ifPermissionIsDangerousAndPermissionGrantStateIsAlreadySet_shouldReturnTrue() { + addPermissionInfo(DANGEROUS_PERMISSION, PermissionInfo.PROTECTION_DANGEROUS); + shadowOf(devicePolicyManager).setProfileOwner(testDpcAdmin); + devicePolicyManager.setPermissionGrantState( + testDpcAdmin, + context.getPackageName(), + DANGEROUS_PERMISSION, + DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED); + + boolean requiredPermissionsGranted = PermissionsHelper + .ensureRequiredPermissions(new String[]{DANGEROUS_PERMISSION}, testDpcAdmin, context); + + assertTrue(requiredPermissionsGranted); + assertThat(devicePolicyManager + .getPermissionGrantState(testDpcAdmin, context.getPackageName(), DANGEROUS_PERMISSION)) + .isEqualTo(DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED); + } + + @Test + public void ensureRequiredPermissions_ifPermissionIsDangerousAndDpcIsNotOwner_shouldReturnFalse() { + addPermissionInfo(DANGEROUS_PERMISSION, PermissionInfo.PROTECTION_DANGEROUS); + shadowOf(devicePolicyManager).setProfileOwner(nonTestDpcAdmin); + + boolean requiredPermissionsGranted = PermissionsHelper + .ensureRequiredPermissions(new String[]{DANGEROUS_PERMISSION}, nonTestDpcAdmin, context); + + assertFalse(requiredPermissionsGranted); + } + + @Test + public void ensureRequiredPermissions_ifPermissionInfoNotFound_shouldReturnTrueAndLogError() { + shadowOf(devicePolicyManager).setProfileOwner(testDpcAdmin); + + boolean requiredPermissionsGranted = PermissionsHelper + .ensureRequiredPermissions(new String[]{MISSING_INFO_PERMISSION}, testDpcAdmin, context); + + assertTrue(requiredPermissionsGranted); + assertTrue(ShadowLog.getLogsForTag(PermissionsHelper.TAG).get(0).msg + .contains("Failed to look up permission.")); + } + + @Test + public void ensureRequiredPermissions_ifPermissionIsNormal_shouldReturnTrueAndNotSetPermissionGrantState() { + addPermissionInfo(NORMAL_PERMISSION, PermissionInfo.PROTECTION_NORMAL); + shadowOf(devicePolicyManager).setProfileOwner(testDpcAdmin); + + boolean requiredPermissionsGranted = PermissionsHelper + .ensureRequiredPermissions(new String[]{NORMAL_PERMISSION}, testDpcAdmin, context); + + assertTrue(requiredPermissionsGranted); + assertThat(devicePolicyManager + .getPermissionGrantState(testDpcAdmin, context.getPackageName(), NORMAL_PERMISSION)) + .isEqualTo(DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT); + } + + @Test + public void ensureRequiredPermissions_ifAllPermissionsAreGranted_shouldReturnTrue() { + addPermissionInfo(NORMAL_PERMISSION, PermissionInfo.PROTECTION_NORMAL); + addPermissionInfo(DANGEROUS_PERMISSION, PermissionInfo.PROTECTION_DANGEROUS); + shadowOf(devicePolicyManager).setProfileOwner(testDpcAdmin); + + boolean requiredPermissionsGranted = PermissionsHelper + .ensureRequiredPermissions(new String[]{NORMAL_PERMISSION, DANGEROUS_PERMISSION}, + testDpcAdmin, context); + + assertTrue(requiredPermissionsGranted); + assertThat(devicePolicyManager + .getPermissionGrantState(testDpcAdmin, context.getPackageName(), NORMAL_PERMISSION)) + .isEqualTo(DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT); + assertThat(devicePolicyManager + .getPermissionGrantState(testDpcAdmin, context.getPackageName(), DANGEROUS_PERMISSION)) + .isEqualTo(DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED); + } + + @Test + public void ensureRequiredPermissions_ifAtLeastOnePermissionNotGranted_shouldReturnFalse() { + addPermissionInfo(NORMAL_PERMISSION, PermissionInfo.PROTECTION_NORMAL); + addPermissionInfo(DANGEROUS_PERMISSION, PermissionInfo.PROTECTION_DANGEROUS); + shadowOf(devicePolicyManager).setProfileOwner(nonTestDpcAdmin); + + boolean requiredPermissionsGranted = PermissionsHelper + .ensureRequiredPermissions(new String[]{NORMAL_PERMISSION, DANGEROUS_PERMISSION}, + nonTestDpcAdmin, context); + + assertFalse(requiredPermissionsGranted); + } + + private void addPermissionInfo(String permission, int protectionLevel) { + PermissionInfo permissionInfo = new PermissionInfo(); + permissionInfo.name = permission; + permissionInfo.protectionLevel = protectionLevel; + shadowOf(context.getPackageManager()).addPermissionInfo(permissionInfo); + } + +} \ No newline at end of file From 449e4658e9841e80106b0b539669e345009aef37 Mon Sep 17 00:00:00 2001 From: kholoud mohamed Date: Tue, 18 Jun 2019 12:08:37 +0100 Subject: [PATCH 09/73] change disambig screen ui Fixes: 123523949 Test: ./gradlew test Change-Id: If826a8244f201073f9e2d0a579015fa6d538e8c9 --- app/src/main/AndroidManifest.xml | 3 +- .../testdpc/provision/DpcLoginActivity.java | 65 +++---- .../res/drawable/managed_device_setup.xml | 47 +++++ .../res/drawable/managed_profile_setup.xml | 42 ++++ .../main/res/layout/activity_dpc_login.xml | 179 +++++++++++++++--- app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/dimens.xml | 14 ++ app/src/main/res/values/strings.xml | 43 +++++ app/src/main/res/values/styles.xml | 23 +++ .../provision/DpcLoginActivityTest.java | 15 +- 10 files changed, 358 insertions(+), 74 deletions(-) create mode 100644 app/src/main/res/drawable/managed_device_setup.xml create mode 100644 app/src/main/res/drawable/managed_profile_setup.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0f99cfd5..0aed44e1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -100,7 +100,8 @@ + android:permission="android.permission.BIND_DEVICE_ADMIN" + android:theme="@style/DpcLoginTheme"> diff --git a/app/src/main/java/com/afwsamples/testdpc/provision/DpcLoginActivity.java b/app/src/main/java/com/afwsamples/testdpc/provision/DpcLoginActivity.java index 60f02f73..ed408bb9 100644 --- a/app/src/main/java/com/afwsamples/testdpc/provision/DpcLoginActivity.java +++ b/app/src/main/java/com/afwsamples/testdpc/provision/DpcLoginActivity.java @@ -24,9 +24,8 @@ import android.content.Intent; import android.os.Bundle; import android.view.View; -import android.widget.RadioGroup; +import android.widget.LinearLayout; import com.afwsamples.testdpc.R; -import com.android.setupwizardlib.GlifLayout; /** * Activity that gets launched by the @@ -34,39 +33,37 @@ */ public class DpcLoginActivity extends Activity { - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - setContentView(R.layout.activity_dpc_login); - GlifLayout layout = findViewById(R.id.dpc_login); - layout.findViewById(R.id.suw_navbar_next).setOnClickListener(this::onNavigateNext); - } + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.activity_dpc_login); + final LinearLayout layout = findViewById(R.id.dpc_login); + layout.findViewById(R.id.do_selection_button).setOnClickListener(this::onDoButtonClick); + layout.findViewById(R.id.po_selection_button).setOnClickListener(this::onPoButtonClick); + } - @Override - public void onBackPressed() { - setResult(RESULT_CANCELED); - super.onBackPressed(); - } + @Override + public void onBackPressed() { + setResult(RESULT_CANCELED); + super.onBackPressed(); + } - private void onNavigateNext(View nextButton) { - final Intent intent = new Intent(); - RadioGroup dpcLoginOptions = findViewById(R.id.dpc_login_options); - switch (dpcLoginOptions.getCheckedRadioButtonId()) { - case R.id.dpc_login_do: - intent.putExtra(EXTRA_PROVISIONING_MODE, PROVISIONING_MODE_FULLY_MANAGED_DEVICE); - finishWithIntent(intent); - return; - case R.id.dpc_login_po: - intent.putExtra(EXTRA_PROVISIONING_MODE, PROVISIONING_MODE_MANAGED_PROFILE); - finishWithIntent(intent); - return; - default: - finish(); - } - } + private void onDoButtonClick(View button) { + final Intent intent = new Intent(); + intent.putExtra(EXTRA_PROVISIONING_MODE, PROVISIONING_MODE_FULLY_MANAGED_DEVICE); + finishWithIntent(intent); + return; + } - private void finishWithIntent(Intent intent) { - setResult(RESULT_OK, intent); - finish(); - } + private void onPoButtonClick(View button) { + final Intent intent = new Intent(); + intent.putExtra(EXTRA_PROVISIONING_MODE, PROVISIONING_MODE_MANAGED_PROFILE); + finishWithIntent(intent); + return; + } + + private void finishWithIntent(Intent intent) { + setResult(RESULT_OK, intent); + finish(); + } } diff --git a/app/src/main/res/drawable/managed_device_setup.xml b/app/src/main/res/drawable/managed_device_setup.xml new file mode 100644 index 00000000..1e8e1a08 --- /dev/null +++ b/app/src/main/res/drawable/managed_device_setup.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/managed_profile_setup.xml b/app/src/main/res/drawable/managed_profile_setup.xml new file mode 100644 index 00000000..96189073 --- /dev/null +++ b/app/src/main/res/drawable/managed_profile_setup.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_dpc_login.xml b/app/src/main/res/layout/activity_dpc_login.xml index 0e378cca..eb386dba 100644 --- a/app/src/main/res/layout/activity_dpc_login.xml +++ b/app/src/main/res/layout/activity_dpc_login.xml @@ -14,48 +14,171 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + android:layout_width="match_parent"> + android:layout_marginTop="@dimen/selection_title_top_margin" + android:gravity="center_horizontal" + android:text="@string/selector_title" + android:textAlignment="center" + android:textAppearance="@style/TextAppearance.AppCompat.Large" + android:textSize="@dimen/selection_title_text_size" + custom:layout_constraintEnd_toEndOf="parent" + custom:layout_constraintStart_toStartOf="parent"/> - + android:layout_marginTop="@dimen/selection_content_padding" + android:layout_marginBottom="@dimen/selection_content_padding" + android:layout_marginLeft="@dimen/constraint_layout_margin" + android:layout_marginRight="@dimen/constraint_layout_margin"> - + android:paddingStart="@dimen/selection_guideline_padding" + android:paddingEnd="@dimen/selection_guideline_padding" + android:orientation="vertical" + custom:layout_constraintGuide_percent="0.368"/> - + + - - + android:text="@string/do_selection_subtitle" + custom:layout_constraintBottom_toTopOf="@+id/do_selection_detailed_text" + custom:layout_constraintEnd_toEndOf="parent" + custom:layout_constraintHorizontal_bias="0.0" + custom:layout_constraintStart_toEndOf="@+id/guideline_vertical_left"/> + + + +