diff --git a/app/build.gradle b/app/build.gradle
index 1f3d89c1..65eda47f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -7,17 +7,17 @@ ext {
// exactly 1 digit
versionMinor = 0
// exactly 2 digits
- versionBuild = 3
+ versionBuild = 8
}
android {
- compileSdkVersion 'android-N'
- buildToolsVersion '23.0.2'
+ compileSdkVersion 24
+ buildToolsVersion '24.0.0'
defaultConfig {
applicationId "com.afwsamples.testdpc"
minSdkVersion 21
- targetSdkVersion 23
+ targetSdkVersion 24
versionCode versionMajor * 1000 + versionMinor * 100 + versionBuild
versionName "${versionMajor}.${versionMinor}.${versionBuild}"
}
@@ -48,38 +48,6 @@ android {
}
}
- productFlavors {
- standard {
- minSdkVersion 21
- targetSdkVersion 23
- }
-
- N {
- minSdkVersion 'N'
- targetSdkVersion 'N'
- }
- }
-
- /* TODO: Remove once release version of N SDK is released. */
- applicationVariants.all { variant ->
- variant.outputs.each { output ->
- output.processManifest.doLast {
- // minSdkVersion and targetSdkVersion are overrided if we build against preview
- // SDK, let us override them again here.
- minSdkVersion = variant.getMergedFlavor().minSdkVersion.getApiString();
- targetSdkVersion = variant.getMergedFlavor().targetSdkVersion.getApiString();
-
- def manifestOutFile = output.processManifest.manifestOutputFile
- def newFileContents = manifestOutFile.getText('UTF-8').
- replace('android:minSdkVersion="N"',
- 'android:minSdkVersion="' + minSdkVersion + '"')
- newFileContents = newFileContents.replace('android:targetSdkVersion="N"',
- 'android:targetSdkVersion="' + targetSdkVersion + '"')
- manifestOutFile.write(newFileContents, 'UTF-8')
- }
- }
- }
-
// Enable lint checking in all build variants.
applicationVariants.all { variant ->
variant.outputs.each { output ->
@@ -90,8 +58,9 @@ android {
}
dependencies {
- compile 'com.android.support:appcompat-v7:24.+'
+ compile 'com.android.support:preference-v14:24+'
compile 'com.android.support:recyclerview-v7:24.+'
compile "com.android.support:support-v13:24.+"
+ compile 'com.google.android.gms:play-services-safetynet:+'
compile(name:'setup-wizard-lib-platform-release', ext:'aar')
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c395397c..2e9604fa 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -25,6 +25,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
0);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ getFragmentManager().removeOnBackStackChangedListener(this);
+ }
+
}
diff --git a/app/src/main/java/com/afwsamples/testdpc/SetupManagementActivity.java b/app/src/main/java/com/afwsamples/testdpc/SetupManagementActivity.java
new file mode 100644
index 00000000..0c98f9f9
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/SetupManagementActivity.java
@@ -0,0 +1,18 @@
+package com.afwsamples.testdpc;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class SetupManagementActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ if (savedInstanceState == null) {
+ getFragmentManager().beginTransaction().add(R.id.container,
+ new SetupManagementFragment(),
+ SetupManagementFragment.FRAGMENT_TAG).commit();
+ }
+ }
+}
diff --git a/app/src/main/java/com/afwsamples/testdpc/common/AppInfoArrayAdapter.java b/app/src/main/java/com/afwsamples/testdpc/common/AppInfoArrayAdapter.java
index b766e053..e5a93747 100644
--- a/app/src/main/java/com/afwsamples/testdpc/common/AppInfoArrayAdapter.java
+++ b/app/src/main/java/com/afwsamples/testdpc/common/AppInfoArrayAdapter.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -37,6 +38,7 @@
public class AppInfoArrayAdapter extends ArrayAdapter {
private PackageManager mPackageManager;
private int mAppInfoFlags = 0;
+ private static final String TAG = "AppInfoArrayAdapter";
public AppInfoArrayAdapter(Context context, int resource, List pkgNameList,
boolean includeDisabledApps) {
@@ -66,7 +68,7 @@ public View getView(int position, View convertView, ViewGroup parent) {
TextView pkgNameTextView = (TextView) convertView.findViewById(R.id.pkg_name);
pkgNameTextView.setText(mPackageManager.getApplicationLabel(applicationInfo));
} catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
+ Log.e(TAG, "Package not found ", e);
// Returns an empty view in case the package is not found.
return new View(getContext());
}
diff --git a/app/src/main/java/com/afwsamples/testdpc/common/BaseSearchablePolicyPreferenceFragment.java b/app/src/main/java/com/afwsamples/testdpc/common/BaseSearchablePolicyPreferenceFragment.java
new file mode 100644
index 00000000..d00fd86f
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/common/BaseSearchablePolicyPreferenceFragment.java
@@ -0,0 +1,172 @@
+package com.afwsamples.testdpc.common;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v14.preference.PreferenceFragment;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceGroup;
+import android.support.v7.preference.PreferenceGroupAdapter;
+import android.support.v7.preference.PreferenceScreen;
+import android.support.v7.preference.PreferenceViewHolder;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+
+import com.afwsamples.testdpc.R;
+
+/**
+ * Base class of searchable policy preference fragment. With specified preference key,
+ * it will scroll to the corresponding preference and highlight it.
+ */
+public abstract class BaseSearchablePolicyPreferenceFragment extends PreferenceFragment {
+ protected LinearLayoutManager mLayoutManager;
+ private HighlightablePreferenceGroupAdapter mAdapter;
+ private String mPreferenceKey;
+ private boolean mPreferenceHighlighted = false;
+ public static final String EXTRA_PREFERENCE_KEY = "preference_key";
+ private static final String SAVE_HIGHLIGHTED_KEY = "preference_highlighted";
+ private static final int DELAY_HIGHLIGHT_DURATION_MILLIS = 500;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ mPreferenceHighlighted = savedInstanceState.getBoolean(SAVE_HIGHLIGHTED_KEY);
+ }
+ final Bundle args = getArguments();
+ if (args != null) {
+ mPreferenceKey = args.getString(EXTRA_PREFERENCE_KEY);
+ }
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ // Hide the search icon when we are showing search result.
+ MenuItem showSearchItem = menu.findItem(R.id.action_show_search);
+ if (showSearchItem != null) {
+ boolean isShowingSearchResult = !TextUtils.isEmpty(mPreferenceKey);
+ showSearchItem.setVisible(!isShowingSearchResult);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ highlightPreferenceIfNeeded();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(SAVE_HIGHLIGHTED_KEY, mPreferenceHighlighted);
+ }
+
+ @Override
+ public RecyclerView.LayoutManager onCreateLayoutManager() {
+ mLayoutManager = new LinearLayoutManager(getActivity());
+ return mLayoutManager;
+ }
+
+ @Override
+ protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
+ mAdapter = new HighlightablePreferenceGroupAdapter(preferenceScreen);
+ return mAdapter;
+ }
+
+ private void highlightPreferenceIfNeeded() {
+ if (isAdded() && !mPreferenceHighlighted && !TextUtils.isEmpty(mPreferenceKey)) {
+ highlightPreference(mPreferenceKey);
+ }
+ }
+
+ private void highlightPreference(String key) {
+ final int position = canUseListViewForHighLighting(key);
+ if (position >= 0) {
+ mPreferenceHighlighted = true;
+ mLayoutManager.scrollToPosition(position);
+ getView().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mAdapter.highlight(position);
+ }
+ }, DELAY_HIGHLIGHT_DURATION_MILLIS);
+ }
+ }
+
+ /**
+ * Return a valid ListView position or -1 if none is found
+ */
+ private int canUseListViewForHighLighting(String key) {
+ if (getListView() == null) {
+ return -1;
+ }
+
+ RecyclerView listView = getListView();
+ RecyclerView.Adapter adapter = listView.getAdapter();
+
+ if (adapter != null && adapter instanceof PreferenceGroupAdapter) {
+ return findListPositionFromKey((PreferenceGroupAdapter) adapter, key);
+ }
+
+ return -1;
+ }
+
+ private int findListPositionFromKey(PreferenceGroupAdapter adapter, String key) {
+ final int count = adapter.getItemCount();
+ for (int n = 0; n < count; n++) {
+ final Preference preference = adapter.getItem(n);
+ final String preferenceKey = preference.getKey();
+ if (preferenceKey != null && preferenceKey.equals(key)) {
+ return n;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Highlight a specific preference by showing a ripple.
+ */
+ public static class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter {
+ private int mHighlightPosition = -1;
+
+ public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup) {
+ super(preferenceGroup);
+ }
+
+ public void highlight(int position) {
+ mHighlightPosition = position;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder, int position) {
+ super.onBindViewHolder(holder, position);
+ if (position == mHighlightPosition) {
+ View v = holder.itemView;
+ if (v.getBackground() != null) {
+ final int centerX = v.getWidth() / 2;
+ final int centerY = v.getHeight() / 2;
+ v.getBackground().setHotspot(centerX, centerY);
+ }
+ v.setPressed(true);
+ v.setPressed(false);
+ mHighlightPosition = -1;
+ }
+ }
+ }
+
+ public abstract int getPreferenceXml();
+
+ /**
+ * The implementation must not use any variable that only initialzied in life-cycle callback.
+ * @return whether the preference fragment is available.
+ */
+ public abstract boolean isAvailable(Context context);
+}
diff --git a/app/src/main/java/com/afwsamples/testdpc/common/CertificateUtil.java b/app/src/main/java/com/afwsamples/testdpc/common/CertificateUtil.java
new file mode 100644
index 00000000..94657bcf
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/common/CertificateUtil.java
@@ -0,0 +1,89 @@
+/*
+ * 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 android.content.ContentResolver;
+import android.net.Uri;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.Enumeration;
+
+public class CertificateUtil {
+ private static final String TAG = "CertificateUtil";
+
+ /**
+ * By enumerating the entries in a pkcs12 cert, find out the first entry that contain both
+ * private key and certificate.
+ *
+ * @param contentResolver
+ * @param uri uri of pkcs12 cert
+ * @param password cert password
+ * @return {@link PKCS12ParseInfo} which contains alias, x509 cert and private key, null if
+ * no such an entry.
+ * @throws KeyStoreException
+ * @throws NoSuchAlgorithmException
+ * @throws IOException
+ * @throws CertificateException
+ * @throws UnrecoverableKeyException
+ */
+ public static PKCS12ParseInfo parsePKCS12Certificate(
+ ContentResolver contentResolver, Uri uri, String password)
+ throws KeyStoreException, NoSuchAlgorithmException, IOException, CertificateException,
+ UnrecoverableKeyException {
+ InputStream inputStream = contentResolver.openInputStream(uri);
+ KeyStore keystore = KeyStore.getInstance("PKCS12");
+ keystore.load(inputStream, password.toCharArray());
+ Enumeration aliases = keystore.aliases();
+ // Find an entry contains both private key and user cert.
+ for (String alias : Collections.list(aliases)) {
+ PrivateKey privateKey = (PrivateKey) keystore.getKey(alias, "".toCharArray());
+ if (privateKey == null) {
+ continue;
+ }
+ X509Certificate clientCertificate =
+ (X509Certificate) keystore.getCertificate(alias);
+ if (clientCertificate == null) {
+ continue;
+ }
+ Log.d(TAG, "parsePKCS12Certificate: " + alias + " is selected");
+ return new PKCS12ParseInfo(alias, clientCertificate, privateKey);
+ }
+ return null;
+ }
+
+ public static class PKCS12ParseInfo {
+ public String alias;
+ public X509Certificate certificate;
+ public PrivateKey privateKey;
+
+ public PKCS12ParseInfo(String alias, X509Certificate certificate, PrivateKey privateKey) {
+ this.alias = alias;
+ this.certificate = certificate;
+ this.privateKey = privateKey;
+ }
+ }
+}
diff --git a/app/src/main/java/com/afwsamples/testdpc/common/LaunchIntentUtil.java b/app/src/main/java/com/afwsamples/testdpc/common/LaunchIntentUtil.java
index 8ef868ac..157bc310 100644
--- a/app/src/main/java/com/afwsamples/testdpc/common/LaunchIntentUtil.java
+++ b/app/src/main/java/com/afwsamples/testdpc/common/LaunchIntentUtil.java
@@ -26,7 +26,6 @@
* Common utility functions used for retrieving information from the intent that launched TestDPC.
*/
public class LaunchIntentUtil {
-
public static final String EXTRA_ACCOUNT_NAME = "account_name";
private static final String EXTRA_IS_SETUP_WIZARD = "is_setup_wizard";
diff --git a/app/src/main/java/com/afwsamples/testdpc/common/ProfileOrParentFragment.java b/app/src/main/java/com/afwsamples/testdpc/common/ProfileOrParentFragment.java
index 42b245df..c93b30a5 100644
--- a/app/src/main/java/com/afwsamples/testdpc/common/ProfileOrParentFragment.java
+++ b/app/src/main/java/com/afwsamples/testdpc/common/ProfileOrParentFragment.java
@@ -21,18 +21,15 @@
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
-import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.UserManager;
-import android.preference.PreferenceFragment;
-import android.preference.PreferenceManager;
import android.support.v13.app.FragmentTabHost;
+import android.support.v7.preference.PreferenceManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.TabHost;
import com.afwsamples.testdpc.DeviceAdminReceiver;
import com.afwsamples.testdpc.R;
@@ -47,7 +44,7 @@
* Please notice that all subclasses of this fragment only support N or above.
*/
@TargetApi(VERSION_CODES.N)
-public abstract class ProfileOrParentFragment extends PreferenceFragment {
+public abstract class ProfileOrParentFragment extends BaseSearchablePolicyPreferenceFragment {
private static final String LOG_TAG = "ProfileOrParentFragment";
private static final String EXTRA_PARENT_PROFILE = "com.afwsamples.testdpc.extra.PARENT";
@@ -156,8 +153,6 @@ protected boolean isProfileOwner() {
@Override
public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
// Check arguments- see whether we're supposed to run on behalf of the parent profile.
final Bundle arguments = getArguments();
if (arguments != null) {
@@ -172,13 +167,17 @@ public void onCreate(Bundle savedInstanceState) {
// Store whether we are the profile owner for faster lookup.
mProfileOwner = mDevicePolicyManager.isProfileOwnerApp(getActivity().getPackageName());
-
mDeviceOwner = mDevicePolicyManager.isDeviceOwnerApp(getActivity().getPackageName());
+ // Put at last to make sure all initializations above are done before subclass's
+ // onCreatePreferences is called.
+ super.onCreate(savedInstanceState);
+
// Switch to parent profile if we are running on their behalf.
+ // This needs to be called after super.onCreate because preference manager is set up
+ // inside super.onCreate.
if (mParentInstance) {
mDevicePolicyManager = mDevicePolicyManager.getParentProfileInstance(mAdminComponent);
-
final PreferenceManager pm = getPreferenceManager();
pm.setSharedPreferencesName(pm.getSharedPreferencesName() + TAG_PARENT);
}
diff --git a/app/src/main/java/com/afwsamples/testdpc/common/Util.java b/app/src/main/java/com/afwsamples/testdpc/common/Util.java
index 20beb7a5..006cb702 100644
--- a/app/src/main/java/com/afwsamples/testdpc/common/Util.java
+++ b/app/src/main/java/com/afwsamples/testdpc/common/Util.java
@@ -16,13 +16,17 @@
package com.afwsamples.testdpc.common;
+import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION_CODES;
+import android.os.UserManager;
import android.text.format.DateUtils;
import android.widget.ImageView;
import android.widget.Toast;
@@ -100,7 +104,21 @@ public static boolean isBeforeM() {
}
public static boolean isBeforeN() {
- // STOPSHIP Change to SDK_INT.
- return isBeforeM() || !Build.VERSION.CODENAME.startsWith("N");
+ return Build.VERSION.SDK_INT < VERSION_CODES.N;
+ }
+
+ @TargetApi(VERSION_CODES.N)
+ public static boolean isManagedProfile(Context context, ComponentName admin) {
+ if (isBeforeN()) {
+ // If user has more than one profile, then we deal with managed profile.
+ // Unfortunately there is no public API available to distinguish user profile owner
+ // and managed profile owner. Thus using this hack.
+ UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+ return userManager.getUserProfiles().size() > 1;
+ } else {
+ DevicePolicyManager devicePolicyManager =
+ (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
+ return devicePolicyManager.isManagedProfile(admin);
+ }
}
}
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 57673a14..41b0f461 100644
--- a/app/src/main/java/com/afwsamples/testdpc/policy/PolicyManagementFragment.java
+++ b/app/src/main/java/com/afwsamples/testdpc/policy/PolicyManagementFragment.java
@@ -16,13 +16,12 @@
package com.afwsamples.testdpc.policy;
-import static android.os.UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES;
-
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
+import android.app.DialogFragment;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.admin.DevicePolicyManager;
@@ -41,16 +40,14 @@
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
-import android.preference.EditTextPreference;
-import android.preference.Preference;
-import android.preference.PreferenceCategory;
-import android.preference.PreferenceFragment;
-import android.preference.SwitchPreference;
import android.provider.MediaStore;
import android.provider.Settings;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
+import android.support.v14.preference.SwitchPreference;
import android.support.v4.content.FileProvider;
+import android.support.v7.preference.EditTextPreference;
+import android.support.v7.preference.Preference;
import android.telephony.TelephonyManager;
import android.text.InputType;
import android.text.TextUtils;
@@ -73,7 +70,9 @@
import com.afwsamples.testdpc.DeviceAdminReceiver;
import com.afwsamples.testdpc.R;
import com.afwsamples.testdpc.common.AppInfoArrayAdapter;
+import com.afwsamples.testdpc.common.CertificateUtil;
import com.afwsamples.testdpc.common.MediaDisplayFragment;
+import com.afwsamples.testdpc.common.BaseSearchablePolicyPreferenceFragment;
import com.afwsamples.testdpc.common.Util;
import com.afwsamples.testdpc.policy.blockuninstallation.BlockUninstallationInfoArrayAdapter;
import com.afwsamples.testdpc.policy.certificate.DelegatedCertInstallerFragment;
@@ -85,12 +84,14 @@
import com.afwsamples.testdpc.policy.networking.NetworkUsageStatsFragment;
import com.afwsamples.testdpc.policy.systemupdatepolicy.SystemUpdatePolicyFragment;
import com.afwsamples.testdpc.policy.wifimanagement.WifiConfigCreationDialog;
+import com.afwsamples.testdpc.policy.wifimanagement.WifiEapTlsCreateDialogFragment;
import com.afwsamples.testdpc.policy.wifimanagement.WifiModificationFragment;
import com.afwsamples.testdpc.profilepolicy.ProfilePolicyManagementFragment;
import com.afwsamples.testdpc.profilepolicy.addsystemapps.EnableSystemAppsByIntentFragment;
import com.afwsamples.testdpc.profilepolicy.apprestrictions.AppRestrictionsManagingPackageFragment;
import com.afwsamples.testdpc.profilepolicy.apprestrictions.ManageAppRestrictionsFragment;
import com.afwsamples.testdpc.profilepolicy.permission.ManageAppPermissionsFragment;
+import com.afwsamples.testdpc.safetynet.SafetyNetFragment;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -98,7 +99,6 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
@@ -110,11 +110,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import static android.os.UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES;
+
/**
* Provides several device management functions.
*
@@ -177,7 +178,7 @@
*
{@link UserManager#DISALLOW_CONFIG_WIFI}
*
*/
-public class PolicyManagementFragment extends PreferenceFragment implements
+public class PolicyManagementFragment extends BaseSearchablePolicyPreferenceFragment implements
Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
// Tag for creating this fragment. This tag can be used to retrieve this fragment.
public static final String FRAGMENT_TAG = "PolicyManagementFragment";
@@ -189,7 +190,7 @@ public class PolicyManagementFragment extends PreferenceFragment implements
public static final int DEFAULT_BUFFER_SIZE = 4096;
public static final String X509_CERT_TYPE = "X.509";
- public static final String TAG = "PolicyManagementFragment";
+ public static final String TAG = "PolicyManagement";
public static final String OVERRIDE_KEY_SELECTION_KEY = "override_key_selection";
@@ -260,13 +261,14 @@ public class PolicyManagementFragment extends PreferenceFragment implements
private static final String UNSUSPEND_APPS_KEY = "unsuspend_apps";
private static final String WIPE_DATA_KEY = "wipe_data";
private static final String CREATE_WIFI_CONFIGURATION_KEY = "create_wifi_configuration";
+ private static final String CREATE_EAP_TLS_WIFI_CONFIGURATION_KEY
+ = "create_eap_tls_wifi_configuration";
private static final String WIFI_CONFIG_LOCKDOWN_ENABLE_KEY = "enable_wifi_config_lockdown";
private static final String MODIFY_WIFI_CONFIGURATION_KEY = "modify_wifi_configuration";
private static final String TAG_WIFI_CONFIG_CREATION = "wifi_config_creation";
private static final String WIFI_CONFIG_LOCKDOWN_ON = "1";
private static final String WIFI_CONFIG_LOCKDOWN_OFF = "0";
-
- private static final long MS_PER_SECOND = 1000;
+ private static final String SAFETYNET_ATTEST = "safetynet_attest";
private static final String BATTERY_PLUGGED_ANY = Integer.toString(
BatteryManager.BATTERY_PLUGGED_AC |
@@ -302,7 +304,7 @@ public class PolicyManagementFragment extends PreferenceFragment implements
* Preferences that are allowed only in NYC+ if it is profile owner. This does not restrict
* device owner.
*/
- private static String[] MANAGED_PROFILE_NYC_PLUS_PREFERENCES = {
+ private static String[] PROFILE_OWNER_NYC_PLUS_PREFERENCES = {
RESET_PASSWORD_KEY
};
@@ -336,8 +338,6 @@ public class PolicyManagementFragment extends PreferenceFragment implements
@Override
public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
mAdminComponentName = DeviceAdminReceiver.getComponentName(getActivity());
mDevicePolicyManager = (DevicePolicyManager) getActivity().getSystemService(
Context.DEVICE_POLICY_SERVICE);
@@ -349,8 +349,12 @@ public void onCreate(Bundle savedInstanceState) {
mImageUri = getStorageUri("image.jpg");
mVideoUri = getStorageUri("video.mp4");
+ super.onCreate(savedInstanceState);
+ }
- addPreferencesFromResource(R.xml.device_policy_header);
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ addPreferencesFromResource(getPreferenceXml());
EditTextPreference overrideKeySelectionPreference =
(EditTextPreference) findPreference(OVERRIDE_KEY_SELECTION_KEY);
@@ -419,6 +423,7 @@ public void onCreate(Bundle savedInstanceState) {
findPreference(SET_PERMISSION_POLICY_KEY).setOnPreferenceClickListener(this);
findPreference(MANAGE_APP_PERMISSIONS_KEY).setOnPreferenceClickListener(this);
findPreference(CREATE_WIFI_CONFIGURATION_KEY).setOnPreferenceClickListener(this);
+ findPreference(CREATE_EAP_TLS_WIFI_CONFIGURATION_KEY).setOnPreferenceClickListener(this);
findPreference(WIFI_CONFIG_LOCKDOWN_ENABLE_KEY).setOnPreferenceChangeListener(this);
findPreference(MODIFY_WIFI_CONFIGURATION_KEY).setOnPreferenceClickListener(this);
findPreference(SHOW_WIFI_MAC_ADDRESS_KEY).setOnPreferenceClickListener(this);
@@ -429,6 +434,7 @@ public void onCreate(Bundle savedInstanceState) {
findPreference(REBOOT_KEY).setOnPreferenceClickListener(this);
findPreference(SET_SHORT_SUPPORT_MESSAGE_KEY).setOnPreferenceClickListener(this);
findPreference(SET_LONG_SUPPORT_MESSAGE_KEY).setOnPreferenceClickListener(this);
+ findPreference(SAFETYNET_ATTEST).setOnPreferenceClickListener(this);
mSetAutoTimeRequiredPreference = (SwitchPreference) findPreference(
SET_AUTO_TIME_REQUIRED_KEY);
mSetAutoTimeRequiredPreference.setOnPreferenceChangeListener(this);
@@ -443,14 +449,25 @@ public void onCreate(Bundle savedInstanceState) {
reloadSetAutoTimeRequiredUi();
}
+ @Override
+ public int getPreferenceXml() {
+ return R.xml.device_policy_header;
+ }
+
+ @Override
+ public boolean isAvailable(Context context) {
+ DevicePolicyManager dpm =
+ (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
+ String packageName = context.getPackageName();
+ return dpm.isProfileOwnerApp(packageName) || dpm.isDeviceOwnerApp(packageName);
+ }
+
@Override
public void onResume() {
super.onResume();
getActivity().getActionBar().setTitle(R.string.policies_management);
- boolean isDeviceOwner = mDevicePolicyManager.isDeviceOwnerApp(mPackageName);
- boolean isProfileOwner = mDevicePolicyManager.isProfileOwnerApp(mPackageName);
- if (!isDeviceOwner && !isProfileOwner) {
+ if (!isAvailable(getActivity())) {
showToast(R.string.this_is_not_a_device_owner);
getActivity().finish();
}
@@ -647,6 +664,9 @@ public void onPositiveButtonClicked(String[] lockTaskArray) {
case CREATE_WIFI_CONFIGURATION_KEY:
showWifiConfigCreationDialog();
return true;
+ case CREATE_EAP_TLS_WIFI_CONFIGURATION_KEY:
+ showEapTlsWifiConfigCreationDialog();
+ return true;
case MODIFY_WIFI_CONFIGURATION_KEY:
showFragment(new WifiModificationFragment());
return true;
@@ -667,6 +687,10 @@ public void onPositiveButtonClicked(String[] lockTaskArray) {
showFragment(SetSupportMessageFragment.newInstance(
SetSupportMessageFragment.TYPE_LONG));
return true;
+ case SAFETYNET_ATTEST:
+ DialogFragment safetynetFragment = new SafetyNetFragment();
+ safetynetFragment.show(getFragmentManager(), SafetyNetFragment.class.getName());
+ return true;
}
return false;
}
@@ -1075,22 +1099,25 @@ private void disableIncompatibleManagementOptionsInCurrentProfile() {
findPreference(preference).setEnabled(false);
}
if (Util.isBeforeN()) {
- for (String preference : MANAGED_PROFILE_NYC_PLUS_PREFERENCES) {
+ for (String preference : PROFILE_OWNER_NYC_PLUS_PREFERENCES) {
findPreference(preference).setEnabled(false);
}
}
- deviceOwnerStatusStringId = R.string.this_is_a_managed_profile_owner;
+ deviceOwnerStatusStringId = R.string.this_is_a_profile_owner;
} else if (isDeviceOwner) {
// If it's a device owner and running in the primary profile.
deviceOwnerStatusStringId = R.string.this_is_a_device_owner;
- for (String managedProfileSpecificOption : MANAGED_PROFILE_SPECIFIC_OPTIONS) {
- findPreference(managedProfileSpecificOption).setEnabled(false);
- }
}
findPreference(DEVICE_OWNER_STATUS_KEY).setSummary(deviceOwnerStatusStringId);
if (!isDeviceOwner) {
findPreference(WIFI_CONFIG_LOCKDOWN_ENABLE_KEY).setEnabled(false);
}
+ // Disable managed profile specific options if we are not running in managed profile.
+ if (!Util.isManagedProfile(getActivity(), mAdminComponentName)) {
+ for (String managedProfileSpecificOption : MANAGED_PROFILE_SPECIFIC_OPTIONS) {
+ findPreference(managedProfileSpecificOption).setEnabled(false);
+ }
+ }
}
private void disableIncompatibleManagementOptionsByApiLevel() {
@@ -1442,7 +1469,7 @@ private void showFileViewerForImportingCertificate(int requestCode) {
try {
startActivityForResult(certIntent, requestCode);
} catch (ActivityNotFoundException e) {
- e.printStackTrace();
+ Log.e(TAG, "showFileViewerForImportingCertificate: ", e);
}
}
@@ -1476,23 +1503,13 @@ private void importKeyCertificateFromIntent(Intent intent, String password, int
if (password == null) {
password = "";
}
- InputStream certificateInputStream;
try {
- certificateInputStream = getActivity().getContentResolver().openInputStream(data);
- KeyStore keyStore = KeyStore.getInstance(KeyChain.EXTRA_PKCS12);
- keyStore.load(certificateInputStream, password.toCharArray());
- Enumeration aliases = keyStore.aliases();
- while (aliases.hasMoreElements()) {
- String alias = aliases.nextElement();
- if (!TextUtils.isEmpty(alias)) {
- Certificate certificate = keyStore.getCertificate(alias);
- PrivateKey privateKey = (PrivateKey) keyStore
- .getKey(alias, "".toCharArray());
- showPromptForKeyCertificateAlias(privateKey, certificate, alias);
- }
- }
- } catch (KeyStoreException | FileNotFoundException | CertificateException
- | UnrecoverableKeyException | NoSuchAlgorithmException e) {
+ CertificateUtil.PKCS12ParseInfo parseInfo = CertificateUtil
+ .parsePKCS12Certificate(getActivity().getContentResolver(), data, password);
+ showPromptForKeyCertificateAlias(parseInfo.privateKey, parseInfo.certificate,
+ parseInfo.alias);
+ } catch (KeyStoreException | FileNotFoundException | CertificateException |
+ UnrecoverableKeyException | NoSuchAlgorithmException e) {
Log.e(TAG, "Unable to load key", e);
} catch (IOException e) {
showPromptForCertificatePassword(intent, ++attempts);
@@ -1644,10 +1661,8 @@ private void importCaCertificateFromIntent(Intent intent) {
isCaInstalled = mDevicePolicyManager.installCaCert(mAdminComponentName,
byteBuffer.toByteArray());
}
- } catch (FileNotFoundException e) {
- e.printStackTrace();
} catch (IOException e) {
- e.printStackTrace();
+ Log.e(TAG, "importCaCertificateFromIntent: ", e);
}
showToast(isCaInstalled ? R.string.install_ca_successfully : R.string.install_ca_fail);
}
@@ -2085,7 +2100,7 @@ private String[] getCaCertificateSubjectDnList() {
new ByteArrayInputStream(installedCaCert));
caSubjectDnList[i++] = certificate.getSubjectDN().getName();
} catch (CertificateException e) {
- e.printStackTrace();
+ Log.e(TAG, "getCaCertificateSubjectDnList: ", e);
}
}
}
@@ -2123,6 +2138,11 @@ private void showWifiConfigCreationDialog() {
dialog.show(getFragmentManager(), TAG_WIFI_CONFIG_CREATION);
}
+ private void showEapTlsWifiConfigCreationDialog() {
+ DialogFragment fragment = WifiEapTlsCreateDialogFragment.newInstance(null);
+ fragment.show(getFragmentManager(), WifiEapTlsCreateDialogFragment.class.getName());
+ }
+
@TargetApi(Build.VERSION_CODES.N)
private void reboot() {
if (mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE) {
diff --git a/app/src/main/java/com/afwsamples/testdpc/policy/UserRestrictionsDisplayFragment.java b/app/src/main/java/com/afwsamples/testdpc/policy/UserRestrictionsDisplayFragment.java
index da9c511b..288396ee 100644
--- a/app/src/main/java/com/afwsamples/testdpc/policy/UserRestrictionsDisplayFragment.java
+++ b/app/src/main/java/com/afwsamples/testdpc/policy/UserRestrictionsDisplayFragment.java
@@ -275,20 +275,13 @@ private void disableIncompatibleRestrictionsByUserType() {
}
}
- if (isManagedProfile()) {
+ if (Util.isManagedProfile(getActivity(), mAdminComponentName)) {
for (String restriction : NON_MANAGED_PROFILE_RESTRICTIONS) {
findPreference(restriction).setEnabled(false);
}
}
}
- private boolean isManagedProfile() {
- // If user has more than one profile, then we deal with managed profile.
- // Unfortunately there is no public API available to distinguish user profile owner
- // and managed profile owner. Thus using this hack.
- return mUserManager.getUserProfiles().size() > 1;
- }
-
private static class UserRestriction {
String key;
int titleResId;
diff --git a/app/src/main/java/com/afwsamples/testdpc/policy/blockuninstallation/BlockUninstallationInfoArrayAdapter.java b/app/src/main/java/com/afwsamples/testdpc/policy/blockuninstallation/BlockUninstallationInfoArrayAdapter.java
index 1fc22997..2d0a076c 100644
--- a/app/src/main/java/com/afwsamples/testdpc/policy/blockuninstallation/BlockUninstallationInfoArrayAdapter.java
+++ b/app/src/main/java/com/afwsamples/testdpc/policy/blockuninstallation/BlockUninstallationInfoArrayAdapter.java
@@ -20,6 +20,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
@@ -73,7 +74,7 @@ protected ApplicationInfo getApplicationInfo(int position) {
return mPackageManager.getApplicationInfo(getItem(position).resolvePackageName,
0 /* Default flags */);
} catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
+ Log.e(TAG, "getApplicationInfo: ", e);
}
return null;
}
diff --git a/app/src/main/java/com/afwsamples/testdpc/policy/keyguard/LockScreenPolicyFragment.java b/app/src/main/java/com/afwsamples/testdpc/policy/keyguard/LockScreenPolicyFragment.java
index c5c04611..a5af594e 100644
--- a/app/src/main/java/com/afwsamples/testdpc/policy/keyguard/LockScreenPolicyFragment.java
+++ b/app/src/main/java/com/afwsamples/testdpc/policy/keyguard/LockScreenPolicyFragment.java
@@ -19,12 +19,13 @@
import android.annotation.TargetApi;
import android.app.Fragment;
import android.app.admin.DevicePolicyManager;
+import android.content.Context;
import android.os.Build;
import android.os.Bundle;
-import android.preference.EditTextPreference;
-import android.preference.Preference;
-import android.preference.TwoStatePreference;
import android.support.v4.os.BuildCompat;
+import android.support.v7.preference.EditTextPreference;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.TwoStatePreference;
import android.util.ArrayMap;
import android.widget.Toast;
@@ -66,13 +67,13 @@ abstract static class Keys {
static final String KEYGUARD_FEATURES_CATEGORY = "keyguard_features";
static final String KEYGUARD_DISABLE_FINGERPRINT = "keyguard_disable_fingerprint";
+ static final String KEYGUARD_DISABLE_REMOTE_INPUT = "keyguard_disable_remote_input";
static final String KEYGUARD_DISABLE_SECURE_CAMERA = "keyguard_disable_secure_camera";
static final String KEYGUARD_DISABLE_SECURE_NOTIFICATIONS
= "keyguard_disable_secure_notifications";
static final String KEYGUARD_DISABLE_TRUST_AGENTS = "keyguard_disable_trust_agents";
static final String KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS
= "keyguard_disable_unredacted_notifications";
- static final String KEYGUARD_DISABLE_WIDGETS = "keyguard_disable_widgets";
static final String SET_TRUST_AGENT_CONFIG = "key_set_trust_agent_config";
static final Set NOT_APPLICABLE_TO_PARENT
@@ -127,13 +128,20 @@ abstract static class Keys {
KEYGUARD_FEATURES.put(Keys.KEYGUARD_DISABLE_FINGERPRINT,
DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT);
+
+ KEYGUARD_FEATURES.put(Keys.KEYGUARD_DISABLE_REMOTE_INPUT,
+ DevicePolicyManager.KEYGUARD_DISABLE_REMOTE_INPUT);
}
@Override
public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
getActivity().getActionBar().setTitle(R.string.lock_screen_policy);
- addPreferencesFromResource(R.xml.lock_screen_preferences);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onCreatePreferences(Bundle bundle, String rootKey) {
+ addPreferencesFromResource(getPreferenceXml());
setupAll();
disableIncompatibleManagementOptionsInCurrentProfile();
final int disabledFeatures = getDpm().getKeyguardDisabledFeatures(getAdmin());
@@ -142,6 +150,16 @@ public void onCreate(Bundle savedInstanceState) {
}
}
+ @Override
+ public int getPreferenceXml() {
+ return R.xml.lock_screen_preferences;
+ }
+
+ @Override
+ public boolean isAvailable(Context context) {
+ return true;
+ }
+
@Override
public void onResume() {
super.onResume();
diff --git a/app/src/main/java/com/afwsamples/testdpc/policy/keyguard/PasswordConstraintsFragment.java b/app/src/main/java/com/afwsamples/testdpc/policy/keyguard/PasswordConstraintsFragment.java
index 4bd61c5a..3b01d6b5 100644
--- a/app/src/main/java/com/afwsamples/testdpc/policy/keyguard/PasswordConstraintsFragment.java
+++ b/app/src/main/java/com/afwsamples/testdpc/policy/keyguard/PasswordConstraintsFragment.java
@@ -16,15 +16,14 @@
package com.afwsamples.testdpc.policy.keyguard;
-import android.app.Fragment;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
-import android.preference.EditTextPreference;
-import android.preference.ListPreference;
-import android.preference.Preference;
+import android.support.v7.preference.EditTextPreference;
+import android.support.v7.preference.ListPreference;
+import android.support.v7.preference.Preference;
import android.widget.Toast;
import com.afwsamples.testdpc.DeviceAdminReceiver;
@@ -109,14 +108,27 @@ abstract static class Keys {
for (int i = 0; i < policyIds.length; i++) {
PASSWORD_QUALITIES.put(policyIds[i], policyNames[i]);
}
- };
+ }
+
+ @Override
+ public int getPreferenceXml() {
+ return R.xml.password_constraint_preferences;
+ }
+
+ @Override
+ public boolean isAvailable(Context context) {
+ return true;
+ }
@Override
public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
getActivity().getActionBar().setTitle(R.string.password_constraints);
+ super.onCreate(savedInstanceState);
+ }
- addPreferencesFromResource(R.xml.password_constraint_preferences);
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ addPreferencesFromResource(getPreferenceXml());
// Populate password quality settings - messy because the only API for this requires two
// separate String[]s.
diff --git a/app/src/main/java/com/afwsamples/testdpc/policy/networking/NetworkUsageStatsFragment.java b/app/src/main/java/com/afwsamples/testdpc/policy/networking/NetworkUsageStatsFragment.java
index c57875cb..85f56a24 100644
--- a/app/src/main/java/com/afwsamples/testdpc/policy/networking/NetworkUsageStatsFragment.java
+++ b/app/src/main/java/com/afwsamples/testdpc/policy/networking/NetworkUsageStatsFragment.java
@@ -94,7 +94,7 @@ public class NetworkUsageStatsFragment extends ListFragment implements View.OnCl
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- mDateStringFormat = new SimpleDateFormat("*dd/MM/YYYY*");
+ mDateStringFormat = new SimpleDateFormat("*dd/MM/yyyy*");
mHourMinuteDateFormat = new SimpleDateFormat("kk:mm");
}
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 13f95d05..372fee15 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
@@ -135,17 +135,10 @@ public void onClick(View view) {
WifiConfiguration config = new WifiConfiguration();
config.SSID = getQuotedString(mSsidText.getText().toString());
updateConfigurationSecurity(config);
- WifiManager wifiManager = (WifiManager) getActivity().getSystemService(
- Context.WIFI_SERVICE);
- boolean success = false;
- if (mOldConfig == null) {
- success = ((wifiManager.addNetwork(config) != -1) &&
- wifiManager.saveConfiguration());
- } else {
+ if (mOldConfig != null) {
config.networkId = mOldConfig.networkId;
- success = ((wifiManager.updateNetwork(config) != -1) &&
- wifiManager.saveConfiguration());
}
+ boolean success = WifiConfigUtil.saveWifiConfiguration(getActivity(), config);
showToast(success ? R.string.wifi_config_success : R.string.wifi_config_fail);
}
dismiss();
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
new file mode 100644
index 00000000..9fa2290b
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiConfigUtil.java
@@ -0,0 +1,59 @@
+/*
+ * 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.policy.wifimanagement;
+
+import android.content.Context;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+
+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);
+ if (wifiConfiguration.networkId == -1) {
+ // new wifi configuration, add it and then save it.
+ int networkId = wifiManager.addNetwork(wifiConfiguration);
+ if (networkId != -1) {
+ // Added successfully, try to save it now.
+ if (wifiManager.saveConfiguration()) {
+ return true;
+ } else {
+ // Remove the added network that fail to save.
+ wifiManager.removeNetwork(networkId);
+ }
+ }
+ } else {
+ // existing wifi configuration, update it and then save it.
+ int networkId = wifiManager.updateNetwork(wifiConfiguration);
+ if (networkId != -1) {
+ if (wifiManager.saveConfiguration()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiEapTlsCreateDialogFragment.java b/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiEapTlsCreateDialogFragment.java
new file mode 100644
index 00000000..5dbf9c3c
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/policy/wifimanagement/WifiEapTlsCreateDialogFragment.java
@@ -0,0 +1,264 @@
+package com.afwsamples.testdpc.policy.wifimanagement;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.ActivityNotFoundException;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiEnterpriseConfig;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.afwsamples.testdpc.R;
+import com.afwsamples.testdpc.common.CertificateUtil;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+/**
+ * Dialog for adding/editing EAP-TLS Wifi.
+ * We currently only support CA cert in x.509 format and client cert in PKCS12 format.
+ */
+public class WifiEapTlsCreateDialogFragment extends DialogFragment {
+
+ private final static int REQUEST_CA_CERT = 1;
+ private final static int REQUEST_USER_CERT = 2;
+ private final static String ARG_CONFIG = "config";
+ private static final String TAG = "wifi_eap_tls";
+
+ private WifiConfiguration mWifiConfiguration;
+ private Uri mCaCertUri;
+ private Uri mUserCertUri;
+
+ private EditText mSsidEditText;
+ private TextView mCaCertTextView;
+ private TextView mUserCertTextView;
+ private EditText mCertPasswordEditText;
+ private EditText mIdentityEditText;
+
+ public static WifiEapTlsCreateDialogFragment newInstance(WifiConfiguration config) {
+ Bundle arguments = new Bundle();
+ arguments.putParcelable(ARG_CONFIG, config);
+ WifiEapTlsCreateDialogFragment fragment = new WifiEapTlsCreateDialogFragment();
+ fragment.setArguments(arguments);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mWifiConfiguration = getArguments().getParcelable(ARG_CONFIG);
+ if (mWifiConfiguration == null) {
+ mWifiConfiguration = new WifiConfiguration();
+ }
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ LayoutInflater inflater = LayoutInflater.from(getActivity());
+ View rootView = inflater.inflate(R.layout.eap_tls_wifi_config_dialog, null);
+ rootView.findViewById(R.id.import_ca_cert).setOnClickListener(
+ new ImportButtonOnClickListener(REQUEST_CA_CERT, "application/x-x509-ca-cert"));
+ rootView.findViewById(R.id.import_user_cert).setOnClickListener(
+ new ImportButtonOnClickListener(REQUEST_USER_CERT, "application/x-pkcs12"));
+ mCaCertTextView = (TextView) rootView.findViewById(R.id.selected_ca_cert);
+ mUserCertTextView = (TextView) rootView.findViewById(R.id.selected_user_cert);
+ mSsidEditText = (EditText) rootView.findViewById(R.id.ssid);
+ mCertPasswordEditText = (EditText) rootView.findViewById(R.id.wifi_client_cert_password);
+ mIdentityEditText = (EditText) rootView.findViewById(R.id.wifi_identity);
+ populateUi();
+ final AlertDialog dialog = new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.create_eap_tls_wifi_configuration)
+ .setView(rootView)
+ .setPositiveButton(R.string.wifi_save, null)
+ .setNegativeButton(R.string.wifi_cancel, null)
+ .create();
+ dialog.setOnShowListener(new DialogInterface.OnShowListener() {
+ @Override
+ public void onShow(DialogInterface dialogInterface) {
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ // Only dismiss the dialog when we saved the config.
+ if (extractInputDataAndSave()) {
+ dialog.dismiss();
+ }
+ }
+ });
+ }
+ });
+ return dialog;
+ }
+
+ private void populateUi() {
+ if (mWifiConfiguration == null) {
+ return;
+ }
+ if (!TextUtils.isEmpty(mWifiConfiguration.SSID)) {
+ mSsidEditText.setText(mWifiConfiguration.SSID.replace("\"", ""));
+ }
+ mIdentityEditText.setText(mWifiConfiguration.enterpriseConfig.getIdentity());
+ // Both ca cert and client are not populated in the WifiConfiguration object.
+ updateSelectedCert(mCaCertTextView, null);
+ updateSelectedCert(mUserCertTextView, null);
+ }
+
+ private boolean extractInputDataAndSave() {
+ String ssid = mSsidEditText.getText().toString();
+ if (TextUtils.isEmpty(ssid)) {
+ mSsidEditText.setError(getString(R.string.error_missing_ssid));
+ return false;
+ } else {
+ mSsidEditText.setError(null);
+ }
+ if (mCaCertUri == null) {
+ showToast(R.string.error_missing_ca_cert);
+ return false;
+ }
+ if (mUserCertUri == null) {
+ showToast(R.string.error_missing_client_cert);
+ return false;
+ }
+ X509Certificate caCert = parseX509Certificate(mCaCertUri);
+ String certPassword = mCertPasswordEditText.getText().toString();
+ CertificateUtil.PKCS12ParseInfo parseInfo = null;
+ try {
+ parseInfo = CertificateUtil.parsePKCS12Certificate(
+ getActivity().getContentResolver(), mUserCertUri, certPassword);
+ } catch (KeyStoreException | NoSuchAlgorithmException | IOException |
+ CertificateException | UnrecoverableKeyException e) {
+ Log.e(TAG, "Fail to parse the input certificate: ", e);
+ }
+ if (parseInfo == null) {
+ showToast(R.string.error_missing_client_cert);
+ return false;
+ }
+ String identity = mIdentityEditText.getText().toString();
+ boolean success = saveWifiConfiguration(ssid, caCert, parseInfo.privateKey,
+ parseInfo.certificate, identity);
+ if (success) {
+ showToast(R.string.wifi_configs_header);
+ return true;
+ } else {
+ showToast(R.string.wifi_config_fail);
+ }
+ return false;
+ }
+
+ private boolean saveWifiConfiguration(String ssid, X509Certificate caCert,
+ PrivateKey privateKey, X509Certificate userCert, String identity) {
+ mWifiConfiguration.SSID = ssid;
+ mWifiConfiguration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP);
+ mWifiConfiguration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X);
+ WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig();
+ enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.TLS);
+ enterpriseConfig.setCaCertificate(caCert);
+ enterpriseConfig.setClientKeyEntry(privateKey, userCert);
+ if (!TextUtils.isEmpty(identity)) {
+ enterpriseConfig.setIdentity(identity);
+ }
+ mWifiConfiguration.enterpriseConfig = enterpriseConfig;
+ return WifiConfigUtil.saveWifiConfiguration(getActivity(), mWifiConfiguration);
+ }
+
+ /**
+ * @param uri of the x509 certificate
+ * @return the X509Certificate object
+ */
+ private X509Certificate parseX509Certificate(Uri uri) {
+ try {
+ CertificateFactory factory = CertificateFactory.getInstance("X.509");
+ InputStream inputStream = getActivity().getContentResolver().openInputStream(uri);
+ return (X509Certificate) factory.generateCertificate(inputStream);
+ } catch (IOException | CertificateException ex) {
+ Log.e(TAG, "parseX509Certificate: ", ex);
+ return null;
+ }
+ }
+
+ private class ImportButtonOnClickListener implements View.OnClickListener {
+ private int mRequestCode;
+ private String mMimeType;
+
+ public ImportButtonOnClickListener(int requestCode, String mimeType) {
+ mRequestCode = requestCode;
+ mMimeType = mimeType;
+ }
+
+ @Override
+ public void onClick(View view) {
+ Intent certIntent = new Intent(Intent.ACTION_GET_CONTENT);
+ certIntent.setTypeAndNormalize(mMimeType);
+ try {
+ startActivityForResult(certIntent, mRequestCode);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "no file picker: ", e);
+ }
+ }
+ }
+
+ private void updateSelectedCert(TextView textView, Uri uri) {
+ String displayName = null;
+ if (uri == null) {
+ displayName = getString(R.string.selected_certificate_none);
+ } else {
+ final String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME};
+ Cursor cursor = getActivity().getContentResolver().query(uri, projection,
+ null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ displayName = cursor.getString(0);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ if (TextUtils.isEmpty(getString(R.string.wifi_unknown_cert))) {
+ displayName = getString(R.string.wifi_unknown_cert);
+ }
+ }
+ String selectedText = getString(R.string.selected_certificate, displayName);
+ textView.setText(selectedText);
+ }
+
+ private void showToast(int message) {
+ Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (resultCode == Activity.RESULT_OK) {
+ switch (requestCode) {
+ case REQUEST_CA_CERT:
+ mCaCertUri = intent.getData();
+ updateSelectedCert(mCaCertTextView, mCaCertUri);
+ break;
+ case REQUEST_USER_CERT:
+ mUserCertUri = intent.getData();
+ updateSelectedCert(mUserCertTextView, mUserCertUri);
+ break;
+ }
+ }
+ }
+}
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 851030b8..f592cc8c 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
@@ -17,6 +17,7 @@
package com.afwsamples.testdpc.policy.wifimanagement;
import android.app.AlertDialog;
+import android.app.DialogFragment;
import android.app.Fragment;
import android.content.Context;
import android.net.wifi.WifiConfiguration;
@@ -36,6 +37,8 @@
import java.util.ArrayList;
import java.util.List;
+import static android.net.wifi.WifiEnterpriseConfig.Eap;
+
/**
* Fragment for WiFi configuration editing.
*/
@@ -130,14 +133,18 @@ public View onCreateView(LayoutInflater inflater, final ViewGroup container,
@Override
public void onClick(View v) {
WifiConfiguration oldConf = getClickedItem();
- if (oldConf != null) {
- try {
- WifiConfigCreationDialog dialog = WifiConfigCreationDialog.newInstance(
+ try {
+ DialogFragment dialog;
+ if (oldConf.enterpriseConfig == null ||
+ oldConf.enterpriseConfig.getEapMethod() == Eap.NONE) {
+ dialog = WifiConfigCreationDialog.newInstance(
oldConf, WifiModificationFragment.this);
- dialog.show(getFragmentManager(), TAG_WIFI_CONFIG_MODIFICATION);
- } catch (SecurityException e) {
- showError(e.getMessage());
+ } else {
+ dialog = WifiEapTlsCreateDialogFragment.newInstance(oldConf);
}
+ dialog.show(getFragmentManager(), TAG_WIFI_CONFIG_MODIFICATION);
+ } catch (SecurityException e) {
+ showError(e.getMessage());
}
}
});
diff --git a/app/src/main/java/com/afwsamples/testdpc/profilepolicy/ProfilePolicyManagementFragment.java b/app/src/main/java/com/afwsamples/testdpc/profilepolicy/ProfilePolicyManagementFragment.java
index b9f4ffac..2b26dd28 100644
--- a/app/src/main/java/com/afwsamples/testdpc/profilepolicy/ProfilePolicyManagementFragment.java
+++ b/app/src/main/java/com/afwsamples/testdpc/profilepolicy/ProfilePolicyManagementFragment.java
@@ -28,19 +28,20 @@
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
-import android.os.UserManager;
-import android.preference.Preference;
-import android.preference.PreferenceFragment;
-import android.preference.SwitchPreference;
+import android.support.v14.preference.SwitchPreference;
+import android.support.v7.preference.Preference;
import android.widget.Toast;
import com.afwsamples.testdpc.DeviceAdminReceiver;
import com.afwsamples.testdpc.R;
import com.afwsamples.testdpc.common.AppInfoArrayAdapter;
import com.afwsamples.testdpc.common.ColorPicker;
+import com.afwsamples.testdpc.common.BaseSearchablePolicyPreferenceFragment;
import com.afwsamples.testdpc.common.Util;
-import com.afwsamples.testdpc.profilepolicy.crossprofileintentfilter.AddCrossProfileIntentFilterFragment;
-import com.afwsamples.testdpc.profilepolicy.crossprofilewidgetprovider.ManageCrossProfileWidgetProviderUtil;
+import com.afwsamples.testdpc.profilepolicy.crossprofileintentfilter
+ .AddCrossProfileIntentFilterFragment;
+import com.afwsamples.testdpc.profilepolicy.crossprofilewidgetprovider
+ .ManageCrossProfileWidgetProviderUtil;
import java.util.List;
@@ -60,7 +61,7 @@
* String)}
* 8) {@link DevicePolicyManager#setBluetoothContactSharingDisabled(ComponentName, boolean)}
*/
-public class ProfilePolicyManagementFragment extends PreferenceFragment implements
+public class ProfilePolicyManagementFragment extends BaseSearchablePolicyPreferenceFragment implements
Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener,
ColorPicker.OnColorSelectListener {
// Tag for creating this fragment. This tag can be used to retrieve this fragment.
@@ -109,14 +110,15 @@ public class ProfilePolicyManagementFragment extends PreferenceFragment implemen
@Override
public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
mAdminComponentName = DeviceAdminReceiver.getComponentName(getActivity());
mDevicePolicyManager = (DevicePolicyManager) getActivity().getSystemService(
Context.DEVICE_POLICY_SERVICE);
+ super.onCreate(savedInstanceState);
+ }
- addPreferencesFromResource(R.xml.profile_policy_header);
-
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ addPreferencesFromResource(getPreferenceXml());
mAddCrossProfileIntentFilterPreference = findPreference(
ADD_CROSS_PROFILE_INTENT_FILTER_PREFERENCE_KEY);
mAddCrossProfileIntentFilterPreference.setOnPreferenceClickListener(this);
@@ -136,15 +138,21 @@ public void onCreate(Bundle savedInstanceState) {
initializeOrganizationInfoPreferences();
}
+ @Override
+ public int getPreferenceXml() {
+ return R.xml.profile_policy_header;
+ }
+
+ @Override
+ public boolean isAvailable(Context context) {
+ return Util.isManagedProfile(context, DeviceAdminReceiver.getComponentName(context));
+ }
+
@Override
public void onResume() {
super.onResume();
getActivity().getActionBar().setTitle(R.string.profile_management_title);
-
- String packageName = getActivity().getPackageName();
- boolean isProfileOwner = mDevicePolicyManager.isProfileOwnerApp(packageName);
-
- if (!isProfileOwner) {
+ if (!isAvailable(getActivity())) {
// Safe net: should never happen.
showToast(R.string.setup_management_message);
getActivity().finish();
diff --git a/app/src/main/java/com/afwsamples/testdpc/profilepolicy/apprestrictions/AppRestrictionsManagingPackageFragment.java b/app/src/main/java/com/afwsamples/testdpc/profilepolicy/apprestrictions/AppRestrictionsManagingPackageFragment.java
index ba7681b6..10d24246 100644
--- a/app/src/main/java/com/afwsamples/testdpc/profilepolicy/apprestrictions/AppRestrictionsManagingPackageFragment.java
+++ b/app/src/main/java/com/afwsamples/testdpc/profilepolicy/apprestrictions/AppRestrictionsManagingPackageFragment.java
@@ -22,13 +22,12 @@
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.Bundle;
+import android.text.TextUtils;
import com.afwsamples.testdpc.DeviceAdminReceiver;
import com.afwsamples.testdpc.R;
import com.afwsamples.testdpc.common.SelectAppFragment;
-import java.lang.IllegalArgumentException;
-
/**
* This fragment lets the user select an app that can manage application restrictions for the
* current user. Related APIs:
@@ -54,6 +53,10 @@ public void onResume() {
@Override
protected void setSelectedPackage(String pkgName) {
+ // If the input pkgName is an empty string, we clear the app restriction manager.
+ if (TextUtils.isEmpty(pkgName)) {
+ pkgName = null;
+ }
try {
mDpm.setApplicationRestrictionsManagingPackage(
DeviceAdminReceiver.getComponentName(getActivity()), pkgName);
diff --git a/app/src/main/java/com/afwsamples/testdpc/profilepolicy/apprestrictions/AppRestrictionsProxyHandler.java b/app/src/main/java/com/afwsamples/testdpc/profilepolicy/apprestrictions/AppRestrictionsProxyHandler.java
index b8641eea..b6732e84 100644
--- a/app/src/main/java/com/afwsamples/testdpc/profilepolicy/apprestrictions/AppRestrictionsProxyHandler.java
+++ b/app/src/main/java/com/afwsamples/testdpc/profilepolicy/apprestrictions/AppRestrictionsProxyHandler.java
@@ -70,18 +70,18 @@ public AppRestrictionsProxyHandler(Context context, ComponentName admin) {
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SET_APPLICATION_RESTRICTIONS: {
- ensureCallerSignature(msg.sendingUid);
+ if (!isCallerAuthorized(msg.sendingUid)) {
+ return;
+ }
String packageName = msg.getData().getString(KEY_PACKAGE_NAME);
Bundle appRestrictions = msg.getData().getBundle(KEY_APPLICATION_RESTRICTIONS);
setApplicationRestrictions(packageName, appRestrictions);
break;
}
case MSG_CAN_SET_APPLICATION_RESTRICTIONS: {
- String callingPackage = mContext.getPackageManager().getNameForUid(msg.sendingUid);
- String managingPackage = getApplicationRestrictionsManagingPackage(mContext);
Bundle responseBundle = new Bundle();
responseBundle.putBoolean(KEY_CAN_SET_APPLICATION_RESTRICTIONS,
- callingPackage != null && callingPackage.equals(managingPackage));
+ isCallerAuthorized(msg.sendingUid));
Message response = Message.obtain();
response.setData(responseBundle);
try {
@@ -92,7 +92,9 @@ public void handleMessage(Message msg) {
break;
}
case MSG_GET_APPLICATION_RESTRICTIONS: {
- ensureCallerSignature(msg.sendingUid);
+ if (!isCallerAuthorized(msg.sendingUid)) {
+ return;
+ }
String packageName = msg.getData().getString(KEY_PACKAGE_NAME);
Bundle appRestrictions = getApplicationRestrictions(packageName);
Bundle responseBundle = new Bundle();
@@ -199,20 +201,19 @@ private Bundle getApplicationRestrictions(String packageName){
* that its signature has not changed since it was set.
*
* @param callerUid the UID of the caller
- *
- * @throws SecurityException if the DPC hasn't given permission to the caller to manage
- * application restrictions, or if the calling package's signature has changed since it was
- * set.
+ * @return whether the caller is the application restictions managing package
*/
- private void ensureCallerSignature(int callerUid) {
+ private boolean isCallerAuthorized(int callerUid) {
String appRestrictionsManagingPackage = getApplicationRestrictionsManagingPackage(mContext);
if (appRestrictionsManagingPackage == null) {
- throw new SecurityException("Caller is not app restrictions managing package");
+ Log.e(TAG, "There is no app restrictions managing package");
+ return false;
}
PackageManager packageManager = mContext.getPackageManager();
String callingPackageName = packageManager.getNameForUid(callerUid);
if (!appRestrictionsManagingPackage.equals(callingPackageName)) {
- throw new SecurityException("Caller is not app restrictions managing package");
+ Log.e(TAG, "Caller is not app restrictions managing package");
+ return false;
}
Set storedSignatures = PreferenceManager.getDefaultSharedPreferences(mContext)
@@ -235,7 +236,7 @@ private void ensureCallerSignature(int callerUid) {
"for package " + callingPackageName + ".");
}
} catch (NameNotFoundException e) {
- throw new SecurityException(e);
+ throw new IllegalArgumentException(e);
}
List expectedSignatures = new ArrayList<>(storedSignatures.size());
for (String signatureString : storedSignatures) {
@@ -244,10 +245,11 @@ private void ensureCallerSignature(int callerUid) {
for (Signature callingSignature : callingPackageSignatures) {
for (Signature expectedSignature : expectedSignatures) {
if (expectedSignature.equals(callingSignature)) {
- return;
+ return true;
}
}
}
- throw new SecurityException("Calling package signature doesn't match");
+ Log.e(TAG, "Calling package signature doesn't match");
+ return false;
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/afwsamples/testdpc/provision/CheckInState.java b/app/src/main/java/com/afwsamples/testdpc/provision/CheckInState.java
new file mode 100644
index 00000000..5d1b84e8
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/provision/CheckInState.java
@@ -0,0 +1,50 @@
+/*
+ * 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.provision;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.v4.content.LocalBroadcastManager;
+
+public class CheckInState {
+ private SharedPreferences mSharedPreferences;
+ private Context mContext;
+
+ private static final String KEY_FIRST_ACCOUNT_READY = "first_account_ready";
+ /**
+ * Broadcast Action: FIRST_ACCOUNT_READY broadcast is processed.
+ */
+ public static final String FIRST_ACCOUNT_READY_PROCESSED_ACTION =
+ "com.afwsamples.testdpc.FIRST_ACCOUNT_READY_PROCESSED";
+
+ public CheckInState(Context context) {
+ mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ mContext = context.getApplicationContext();
+ }
+
+ public boolean isFirstAccountReady() {
+ return mSharedPreferences.getBoolean(KEY_FIRST_ACCOUNT_READY, false);
+ }
+
+ public void setFirstAccountReady() {
+ mSharedPreferences.edit().putBoolean(KEY_FIRST_ACCOUNT_READY, true).apply();
+ LocalBroadcastManager.getInstance(mContext).sendBroadcast(
+ new Intent(FIRST_ACCOUNT_READY_PROCESSED_ACTION));
+ }
+}
diff --git a/app/src/main/java/com/afwsamples/testdpc/provision/ProvisioningUtil.java b/app/src/main/java/com/afwsamples/testdpc/provision/ProvisioningUtil.java
new file mode 100644
index 00000000..53878669
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/provision/ProvisioningUtil.java
@@ -0,0 +1,40 @@
+/*
+ * 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.provision;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+
+import com.afwsamples.testdpc.DeviceAdminReceiver;
+import com.afwsamples.testdpc.FirstAccountReadyBroadcastReceiver;
+import com.afwsamples.testdpc.R;
+
+public class ProvisioningUtil {
+ public static void enableProfile(Context context) {
+ FirstAccountReadyBroadcastReceiver.cancelFirstAccountReadyTimeoutAlarm(context);
+ DevicePolicyManager manager = (DevicePolicyManager) context.getSystemService(
+ Context.DEVICE_POLICY_SERVICE);
+ ComponentName componentName = DeviceAdminReceiver.getComponentName(context);
+ // This is the name for the newly created managed profile.
+ manager.setProfileName(componentName, context.getString(R.string.profile_name));
+ // We enable the profile here.
+ manager.setProfileEnabled(componentName);
+ // Just enabled the profile, not necessary to wait for first account ready anymore.
+ FirstAccountReadyBroadcastReceiver.setEnabled(context, false);
+ }
+}
diff --git a/app/src/main/java/com/afwsamples/testdpc/safetynet/SafetyNetFragment.java b/app/src/main/java/com/afwsamples/testdpc/safetynet/SafetyNetFragment.java
new file mode 100644
index 00000000..9829ef03
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/safetynet/SafetyNetFragment.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 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.safetynet;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.text.method.ScrollingMovementMethod;
+import android.util.Base64;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import com.afwsamples.testdpc.R;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.ResultCallbacks;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.safetynet.SafetyNet;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.security.SecureRandom;
+
+import static com.google.android.gms.safetynet.SafetyNetApi.AttestationResult;
+
+/**
+ * Demonstrate how to use SafetyNet API to check device compatibility.
+ * Please notice that you should verifying the payload in your server.
+ * For more details, please check http://developer.android.com/training/safetynet/index.html.
+ */
+public class SafetyNetFragment extends DialogFragment implements
+ GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener {
+ private GoogleApiClient mGoogleApiClient;
+ private TextView mMessageView;
+ private @ColorInt int BLACK, DARK_RED;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Kick start the checking
+ mGoogleApiClient = buildGoogleApiClient();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ updateMessageView(R.string.safetynet_running, false);
+ mGoogleApiClient.connect();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mGoogleApiClient.disconnect();
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ BLACK = ContextCompat.getColor(getActivity(), R.color.text_black);
+ DARK_RED = ContextCompat.getColor(getActivity(), R.color.dark_red);
+ LayoutInflater inflater = LayoutInflater.from(getActivity());
+ View rootView = inflater.inflate(R.layout.safety_net_attest_dialog, null);
+ mMessageView = (TextView) rootView.findViewById(R.id.message_view);
+ // Show scrollbar in textview.
+ mMessageView.setMovementMethod(new ScrollingMovementMethod());
+ return new AlertDialog.Builder(getActivity())
+ .setView(rootView)
+ .setTitle(R.string.safetynet_dialog_title)
+ .setNeutralButton(android.R.string.ok, null)
+ .create();
+ }
+
+ @Override
+ public void onConnected(@Nullable Bundle bundle) {
+ if (hasInternetConnection()) {
+ runSaftyNetTest();
+ } else {
+ updateMessageView(R.string.safetynet_fail_reason_no_internet, true);
+ }
+ }
+
+ @Override
+ public void onConnectionSuspended(int i) {
+ updateMessageView(R.string.cancel_safetynet_msg, true);
+ }
+
+ @Override
+ public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
+ if (connectionResult.getErrorCode() == ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED) {
+ updateMessageView(R.string.safetynet_fail_reason_gmscore_upgrade, true);
+ } else {
+ updateMessageView(getString(R.string.safetynet_fail_reason_error_code,
+ connectionResult.getErrorCode()), true);
+ }
+ }
+
+ private GoogleApiClient buildGoogleApiClient() {
+ return new GoogleApiClient.Builder(getActivity())
+ .addApi(SafetyNet.API)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .build();
+ }
+
+ /**
+ * For simplicity, we generate the nonce in the client. However, it should be generated on the
+ * server for anti-replay protection.
+ */
+ private byte[] generateNonce() {
+ byte[] nonce = new byte[32];
+ SecureRandom secureRandom = new SecureRandom();
+ secureRandom.nextBytes(nonce);
+ return nonce;
+ }
+
+ private void runSaftyNetTest() {
+ final byte[] nonce = generateNonce();
+ SafetyNet.SafetyNetApi.attest(mGoogleApiClient, nonce)
+ .setResultCallback(new ResultCallbacks() {
+ @Override
+ public void onSuccess(@NonNull AttestationResult attestationResult) {
+ if (isDetached()) {
+ return;
+ }
+ final String jws = attestationResult.getJwsResult();
+ try {
+ final JSONObject jsonObject = retrievePayloadFromJws(jws);
+ final String jsonString = jsonObject.toString(4);
+ final String verifyOnServerString
+ = getString(R.string.safetynet_verify_on_server);
+ updateMessageView(verifyOnServerString + "\n" + jsonString, false);
+ } catch (JSONException ex) {
+ updateMessageView(R.string.safetynet_fail_reason_invalid_jws, true);
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Status status) {
+ if (isDetached()) {
+ return;
+ }
+ updateMessageView(R.string.safetynet_fail_to_run_api, true);
+ }
+ });
+ }
+
+ private void updateMessageView(int message, boolean isError) {
+ updateMessageView(getString(message), isError);
+ }
+
+ private void updateMessageView(String message, boolean isError) {
+ mMessageView.setText(message);
+ mMessageView.setTextColor((isError) ? DARK_RED : BLACK);
+ }
+
+ private boolean hasInternetConnection() {
+ ConnectivityManager cm =
+ (ConnectivityManager) getActivity().getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
+ return activeNetwork != null && activeNetwork.isConnected();
+ }
+
+ private static JSONObject retrievePayloadFromJws(String jws) throws JSONException {
+ String[] parts = jws.split("\\.");
+ if (parts.length != 3) {
+ throw new JSONException("Invalid JWS");
+ }
+ return new JSONObject(new String(Base64.decode(parts[1], Base64.URL_SAFE)));
+ }
+}
diff --git a/app/src/main/java/com/afwsamples/testdpc/search/IndexableFragment.java b/app/src/main/java/com/afwsamples/testdpc/search/IndexableFragment.java
new file mode 100644
index 00000000..63c0dbc4
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/search/IndexableFragment.java
@@ -0,0 +1,34 @@
+package com.afwsamples.testdpc.search;
+
+import android.content.Context;
+import android.support.annotation.XmlRes;
+import android.util.Log;
+
+import com.afwsamples.testdpc.common.BaseSearchablePolicyPreferenceFragment;
+
+public class IndexableFragment {
+ private static final String TAG = "IndexableFragment";
+
+ public String fragmentName;
+ public @XmlRes int xmlRes;
+
+ public IndexableFragment(Class extends BaseSearchablePolicyPreferenceFragment> fragmentClass,
+ @XmlRes int xmlRes) {
+ this.fragmentName = fragmentClass.getName();
+ this.xmlRes = xmlRes;
+ }
+
+ public boolean isAvailable(Context context) {
+ try {
+ Class clazz =
+ (Class)
+ Class.forName(this.fragmentName);
+ BaseSearchablePolicyPreferenceFragment fragment = clazz.newInstance();
+ return fragment.isAvailable(context);
+ } catch (ClassNotFoundException | java.lang.InstantiationException | IllegalStateException
+ | IllegalAccessException e) {
+ Log.e(TAG, "isAvailable error", e);
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/java/com/afwsamples/testdpc/search/IndexableFragments.java b/app/src/main/java/com/afwsamples/testdpc/search/IndexableFragments.java
new file mode 100644
index 00000000..9ab56b40
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/search/IndexableFragments.java
@@ -0,0 +1,40 @@
+package com.afwsamples.testdpc.search;
+
+import com.afwsamples.testdpc.R;
+import com.afwsamples.testdpc.common.BaseSearchablePolicyPreferenceFragment;
+import com.afwsamples.testdpc.policy.PolicyManagementFragment;
+import com.afwsamples.testdpc.policy.keyguard.LockScreenPolicyFragment;
+import com.afwsamples.testdpc.policy.keyguard.PasswordConstraintsFragment;
+import com.afwsamples.testdpc.profilepolicy.ProfilePolicyManagementFragment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ * Stores all the indexable fragments.
+ *
+ *
+ * To index a newly added fragment, there are only two things needed to be done.
+ * Make you fragment extends {@link BaseSearchablePolicyPreferenceFragment}
+ * and add it to this class.
+ *
+ */
+public class IndexableFragments {
+ private static List sIndexableFragments = new ArrayList<>();
+
+ static {
+ sIndexableFragments.add(new IndexableFragment(PolicyManagementFragment.class,
+ R.xml.device_policy_header));
+ sIndexableFragments.add(new IndexableFragment(ProfilePolicyManagementFragment.class,
+ R.xml.profile_policy_header));
+ sIndexableFragments.add(new IndexableFragment(LockScreenPolicyFragment.class,
+ R.xml.lock_screen_preferences));
+ sIndexableFragments.add(new IndexableFragment(PasswordConstraintsFragment.class,
+ R.xml.password_constraint_preferences));
+ }
+
+ public static List values() {
+ return new ArrayList<>(sIndexableFragments);
+ }
+}
diff --git a/app/src/main/java/com/afwsamples/testdpc/search/PolicySearchFragment.java b/app/src/main/java/com/afwsamples/testdpc/search/PolicySearchFragment.java
new file mode 100644
index 00000000..6d452661
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/search/PolicySearchFragment.java
@@ -0,0 +1,151 @@
+package com.afwsamples.testdpc.search;
+
+import android.app.Fragment;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SearchView;
+
+import com.afwsamples.testdpc.R;
+import com.afwsamples.testdpc.common.BaseSearchablePolicyPreferenceFragment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Fragment that processes the search query and shows the result.
+ */
+public class PolicySearchFragment extends Fragment implements
+ SearchItemAdapter.OnItemClickListener {
+ private static final String TAG = "PolicySearchFragment";
+ private static final int MIN_LENGTH_TO_SEARCH = 3;
+
+ private SearchView mSearchView;
+ private PreferenceIndexSqliteOpenHelper mSqliteOpenHelper;
+ private SearchItemAdapter mAdapter;
+ private List mAvailableFragments;
+
+
+ public static PolicySearchFragment newInstance() {
+ return new PolicySearchFragment();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ mSqliteOpenHelper = PreferenceIndexSqliteOpenHelper.getInstance(getActivity());
+ mAdapter = new SearchItemAdapter(this);
+ mAvailableFragments = getAvailableFragments();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ RecyclerView recyclerView
+ = (RecyclerView) inflater.inflate(R.layout.search_result, container, false);
+ recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
+ recyclerView.setAdapter(mAdapter);
+ return recyclerView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ MenuItem showSearchMenu = menu.findItem(R.id.action_show_search);
+ if (showSearchMenu != null) {
+ showSearchMenu.setVisible(false);
+ }
+ inflater.inflate(R.menu.policy_search_menu, menu);
+ MenuItem searchItem = menu.findItem(R.id.action_search);
+ searchItem.expandActionView();
+ mSearchView = (SearchView) searchItem.getActionView();
+ mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String s) {
+ doSearchAsync(s);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String s) {
+ if (s != null && s.length() >= MIN_LENGTH_TO_SEARCH) {
+ doSearchAsync(s);
+ return true;
+ }
+ return false;
+ }
+ });
+ searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem menuItem) {
+ return false;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem menuItem) {
+ getFragmentManager().popBackStack();
+ return true;
+ }
+ });
+ }
+
+ private void doSearchAsync(final String query) {
+ new AsyncTask>() {
+ @Override
+ protected List doInBackground(Void... voids) {
+ return mSqliteOpenHelper.lookup(query, mAvailableFragments);
+ }
+
+ @Override
+ protected void onPostExecute(List result) {
+ mAdapter.setSearchResult(result);
+ mAdapter.notifyDataSetChanged();
+ }
+ }.execute();
+ }
+
+ @Override
+ public void onItemClick(PreferenceIndex preferenceIndex) {
+ try {
+ // Show the fragment that holds the preference.
+ Fragment fragment = (Fragment) Class.forName(preferenceIndex.fragmentClass)
+ .newInstance();
+ Bundle arguments = new Bundle();
+ arguments.putString(BaseSearchablePolicyPreferenceFragment.EXTRA_PREFERENCE_KEY,
+ preferenceIndex.key);
+ fragment.setArguments(arguments);
+ getFragmentManager()
+ .beginTransaction()
+ .replace(R.id.container, fragment)
+ .addToBackStack("search_" + fragment.getClass().getName())
+ .commit();
+ } catch (IllegalAccessException | ClassNotFoundException | java.lang
+ .InstantiationException ex) {
+ Log.e(TAG, "Fail to create the target fragment: ", ex);
+ }
+ }
+
+ /**
+ * @return a list of fragments that we are going to search for.
+ */
+ private List getAvailableFragments() {
+ List fragments = IndexableFragments.values();
+ List availableFragments = new ArrayList<>();
+ for (IndexableFragment fragment : fragments) {
+ if (fragment.isAvailable(getActivity())) {
+ availableFragments.add(fragment.fragmentName);
+ }
+ }
+ return availableFragments;
+ }
+}
diff --git a/app/src/main/java/com/afwsamples/testdpc/search/PreferenceCrawler.java b/app/src/main/java/com/afwsamples/testdpc/search/PreferenceCrawler.java
new file mode 100644
index 00000000..d6ccbbb7
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/search/PreferenceCrawler.java
@@ -0,0 +1,89 @@
+package com.afwsamples.testdpc.search;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TimingLogger;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Crawl indexable fragments to index all their preferences.
+ * Run adb shell setprop log.tag.PreferenceCrawler_Timer VERBOSE to see timing log.
+ * At the time of writing, nexus 5x spends 27ms to finish crawling.
+ */
+public class PreferenceCrawler {
+ private Context mContext;
+ private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen";
+ private static final String NODE_NAME_PREFERENCE_CATEGORY = "PreferenceCategory";
+ private static final String TAG = "PreferenceCrawler_Timer";
+
+ public PreferenceCrawler(Context context) {
+ mContext = context;
+ }
+
+ public List doCrawl() {
+ final TimingLogger logger = new TimingLogger(TAG, "doCrawl");
+ List indexablePreferences = new ArrayList<>();
+ List indexableFragments = IndexableFragments.values();
+ for (IndexableFragment indexableFragment : indexableFragments) {
+ indexablePreferences.addAll(crawlSingleIndexableResource(indexableFragment));
+ logger.addSplit("processed " + indexableFragment.fragmentName);
+ }
+ logger.addSplit("Finish crawling");
+ logger.dumpToLog();
+ return indexablePreferences;
+ }
+
+ /**
+ * Skim through the xml preference file.
+ * @return a list of indexable preference.
+ */
+ private List crawlSingleIndexableResource(
+ IndexableFragment indexableFragment) {
+ List indexablePreferences = new ArrayList<>();
+ XmlPullParser parser = mContext.getResources().getXml(indexableFragment.xmlRes);
+ int type;
+ try {
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && type != XmlPullParser.START_TAG) {
+ // Parse next until start tag is found
+ }
+ String nodeName = parser.getName();
+ if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) {
+ throw new RuntimeException(
+ "XML document must start with tag; found"
+ + nodeName + " at " + parser.getPositionDescription());
+ }
+
+ final int outerDepth = parser.getDepth();
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+ nodeName = parser.getName();
+ String key = PreferenceXmlUtil.getDataKey(mContext, attrs);
+ String title = PreferenceXmlUtil.getDataTitle(mContext, attrs);
+ if (NODE_NAME_PREFERENCE_CATEGORY.equals(nodeName) || TextUtils.isEmpty(key)
+ || TextUtils.isEmpty(title)) {
+ continue;
+ }
+ PreferenceIndex indexablePreference =
+ new PreferenceIndex(key, title, indexableFragment.fragmentName);
+ indexablePreferences.add(indexablePreference);
+ }
+ } catch (XmlPullParserException | IOException | ReflectiveOperationException ex) {
+ Log.e(TAG, "Error in parsing a preference xml file, skip it", ex);
+ }
+ return indexablePreferences;
+ }
+}
diff --git a/app/src/main/java/com/afwsamples/testdpc/search/PreferenceIndex.java b/app/src/main/java/com/afwsamples/testdpc/search/PreferenceIndex.java
new file mode 100644
index 00000000..c4548582
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/search/PreferenceIndex.java
@@ -0,0 +1,26 @@
+package com.afwsamples.testdpc.search;
+
+/**
+ * Represent index of a preference object.
+ */
+public class PreferenceIndex {
+
+ /**
+ * Key of preference.
+ */
+ public String key;
+ /**
+ * Title of preference.
+ */
+ public String title;
+ /**
+ * Class of fragment holding the preference.
+ */
+ public String fragmentClass;
+
+ public PreferenceIndex(String key, String title, String fragmentClass) {
+ this.key = key;
+ this.title = title;
+ this.fragmentClass = fragmentClass;
+ }
+}
diff --git a/app/src/main/java/com/afwsamples/testdpc/search/PreferenceIndexSqliteOpenHelper.java b/app/src/main/java/com/afwsamples/testdpc/search/PreferenceIndexSqliteOpenHelper.java
new file mode 100644
index 00000000..385e8255
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/search/PreferenceIndexSqliteOpenHelper.java
@@ -0,0 +1,222 @@
+package com.afwsamples.testdpc.search;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.os.Build;
+import android.preference.PreferenceManager;
+
+import com.afwsamples.testdpc.BuildConfig;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manage the preference index database.
+ */
+public class PreferenceIndexSqliteOpenHelper extends SQLiteOpenHelper {
+ private static final String DATABASE_NAME = "preference_index.db";
+ private static final int DATABASE_VERSION = 1;
+ private static final String CREATE_TABLE_PREFERENCE_INDEX =
+ "CREATE TABLE " + PreferenceIndexTable.TABLE_NAME + " (" +
+ PreferenceIndexTable._ID + " INTEGER PRIMARY KEY," +
+ PreferenceIndexTable.KEY + " TEXT NOT NULL," +
+ PreferenceIndexTable.TITLE + " TEXT NOT NULL," +
+ PreferenceIndexTable.FRAGMENT_CLASS + " TEXT NOT NULL" +
+ ");";
+ private static final String CREATE_FTS_TABLE =
+ "CREATE VIRTUAL TABLE " + PreferenceIndexFtsTable.TABLE_NAME +
+ " USING fts4 (content='" + PreferenceIndexTable.TABLE_NAME + "', " +
+ PreferenceIndexTable.TITLE +
+ ");";
+ private static final String REBUILD_FTS_SQL =
+ "INSERT INTO " + PreferenceIndexFtsTable.TABLE_NAME + "(" +
+ PreferenceIndexFtsTable.TABLE_NAME + ") VALUES('rebuild')";
+ private static final String LOOKUP_SQL = "SELECT * FROM " + PreferenceIndexTable.TABLE_NAME +
+ " WHERE _id IN (SELECT " + PreferenceIndexFtsTable.DOC_ID + " FROM " +
+ PreferenceIndexFtsTable.TABLE_NAME + " WHERE " + PreferenceIndexFtsTable.TABLE_NAME +
+ " MATCH ?) AND " + PreferenceIndexTable.FRAGMENT_CLASS + " IN(";
+
+ private static PreferenceIndexSqliteOpenHelper sInstance;
+ private static boolean sIndexed = false;
+
+ private Context mContext;
+ private SharedPreferencesHelper mSharedPreferencesHelper;
+
+ private PreferenceIndexSqliteOpenHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ mContext = context.getApplicationContext();
+ mSharedPreferencesHelper = new SharedPreferencesHelper(mContext);
+ }
+
+ public static synchronized PreferenceIndexSqliteOpenHelper getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new PreferenceIndexSqliteOpenHelper(context);
+ }
+ return sInstance;
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(CREATE_TABLE_PREFERENCE_INDEX);
+ db.execSQL(CREATE_FTS_TABLE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ }
+
+ private void clearDatabase() {
+ getWritableDatabase().delete(PreferenceIndexTable.TABLE_NAME, null, null);
+ }
+
+ public void insertIndexablePreferences(List preferenceIndexList) {
+ SQLiteDatabase db = getWritableDatabase();
+ db.beginTransaction();
+ try {
+ for (PreferenceIndex preferenceIndex : preferenceIndexList) {
+ db.insert(PreferenceIndexTable.TABLE_NAME, null,
+ PreferenceIndexTable.toContentValues(preferenceIndex));
+ }
+ // Rebuild the fts table.
+ db.execSQL(REBUILD_FTS_SQL);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * @param query the words to lookup
+ * @param targetFragments the fragments you are searching for
+ * @return the list of preferences that match the query
+ */
+ public List lookup(String query, List targetFragments) {
+ updateIndexIfNeeded();
+ SQLiteDatabase db = getReadableDatabase();
+ Cursor cursor = null;
+ try {
+ String[] selectionArgs = {query + "*"};
+ cursor = db.rawQuery(buildLookupSQL(targetFragments), selectionArgs);
+ List preferenceIndexList = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ preferenceIndexList.add(PreferenceIndexTable.fromCursor(cursor));
+ }
+ return preferenceIndexList;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ private String buildLookupSQL(List targetFragments) {
+ StringBuilder stringBuilder = new StringBuilder(LOOKUP_SQL);
+ for (String fragment : targetFragments) {
+ DatabaseUtils.appendEscapedSQLString(stringBuilder, fragment);
+ stringBuilder.append(",");
+ }
+ stringBuilder.setLength(stringBuilder.length() - 1); // Strip the last comma
+ stringBuilder.append(")");
+ return stringBuilder.toString();
+ }
+
+ private void updateIndexIfNeeded() {
+ if (shouldUpdateIndex()) {
+ updateIndex();
+ sIndexed = true;
+ mSharedPreferencesHelper.saveVersion();
+ }
+ }
+
+ private boolean shouldUpdateIndex() {
+ if (BuildConfig.DEBUG) {
+ // For dev build, we index every time when the process is started for the ease of
+ // development.
+ return !sIndexed;
+ } else {
+ // Rebuild the index only when it is a new version.
+ int storedVersion = mSharedPreferencesHelper.getVersion();
+ return BuildConfig.VERSION_CODE != storedVersion;
+ }
+ }
+
+ private void updateIndex() {
+ clearDatabase();
+ PreferenceCrawler preferenceCrawler = new PreferenceCrawler(mContext);
+ List preferenceIndexList = preferenceCrawler.doCrawl();
+ insertIndexablePreferences(preferenceIndexList);
+ }
+
+ private static class PreferenceIndexTable {
+ private static final String _ID = "_id";
+ /**
+ * Key of preference.
+ */
+ private static final String KEY = "key";
+ /**
+ * Title of preference.
+ */
+ private static final String TITLE = "title";
+ /**
+ * Class of fragment holding the preference.
+ */
+ private static final String FRAGMENT_CLASS = "fragment_class";
+ private static final String TABLE_NAME = "preference_index";
+
+ static ContentValues toContentValues(PreferenceIndex preferenceIndex) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(KEY, preferenceIndex.key);
+ contentValues.put(TITLE, preferenceIndex.title);
+ contentValues.put(FRAGMENT_CLASS, preferenceIndex.fragmentClass);
+ return contentValues;
+ }
+
+ static PreferenceIndex fromCursor(Cursor cursor) {
+ final int INDEX_KEY = cursor.getColumnIndex(KEY);
+ final int TITLE_INDEX = cursor.getColumnIndex(TITLE);
+ final int FRAGMENT_CLASS_INDEX = cursor.getColumnIndex(FRAGMENT_CLASS);
+ String key = cursor.getString(INDEX_KEY);
+ String title = cursor.getString(TITLE_INDEX);
+ String fragmentClass = cursor.getString(FRAGMENT_CLASS_INDEX);
+ return new PreferenceIndex(key, title, fragmentClass);
+ }
+ }
+
+ /**
+ * It is full text search table. We indexed {@link PreferenceIndexTable#TITLE}
+ * so that we can have full text search on it.
+ */
+ private static class PreferenceIndexFtsTable {
+ private static final String TABLE_NAME = "preference_index_fts";
+ /**
+ * It is the predefined column represents the id column in the table being indexed.
+ */
+ private static final String DOC_ID = "docid";
+ }
+
+ /**
+ * Helper class to store the app version.
+ */
+ private static class SharedPreferencesHelper {
+ private static final String KEY_VERSION = "version";
+ private SharedPreferences mSharedPreferences;
+
+ public SharedPreferencesHelper(Context context) {
+ mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ public void saveVersion() {
+ mSharedPreferences.edit().putInt(KEY_VERSION, BuildConfig.VERSION_CODE).apply();
+ }
+
+ public int getVersion() {
+ return mSharedPreferences.getInt(KEY_VERSION, 0);
+ }
+
+ }
+}
diff --git a/app/src/main/java/com/afwsamples/testdpc/search/PreferenceXmlUtil.java b/app/src/main/java/com/afwsamples/testdpc/search/PreferenceXmlUtil.java
new file mode 100644
index 00000000..0434efda
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/search/PreferenceXmlUtil.java
@@ -0,0 +1,84 @@
+package com.afwsamples.testdpc.search;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+
+import java.lang.reflect.Field;
+
+/**
+ * Util class to retrieve some values of attributes in preference xml.
+ * To achieve this, we need to:
+ * 1. Obtain the array android.R$styleable.Preference through reflection.
+ * Cache is introduced to reduce the performance overhead introduced by reflection.
+ * 2. Obtain the resource id of certain attributes that we care such as title and key using
+ * reflection. Again, cache is introduced.
+ * 3. Obtain the value of those attribute {@link TypedArray#peekValue(int)}.
+ */
+public class PreferenceXmlUtil {
+ private static Integer sPreferenceTitleId;
+ private static Integer sPreferenceKeyId;
+ private static int[] sPreferenceStyleArray;
+
+ public static String getDataTitle(Context context, AttributeSet attrs)
+ throws ReflectiveOperationException {
+ return getData(context, attrs, getPreferenceTitleId());
+ }
+
+ public static String getDataKey(Context context, AttributeSet attrs)
+ throws ReflectiveOperationException {
+ return getData(context, attrs, getPreferenceKeyId());
+ }
+
+ private static String getData(Context context, AttributeSet set, int resId)
+ throws ReflectiveOperationException {
+ int[] attrs = getPreferenceStyleArray();
+ final TypedArray sa = context.obtainStyledAttributes(set, attrs);
+ try {
+ final TypedValue tv = sa.peekValue(resId);
+ CharSequence data = null;
+ if (tv != null && tv.type == TypedValue.TYPE_STRING) {
+ if (tv.resourceId != 0) {
+ data = context.getText(tv.resourceId);
+ } else {
+ data = tv.string;
+ }
+ }
+ return (data != null) ? data.toString() : null;
+ } finally {
+ sa.recycle();
+ }
+ }
+
+ private static int getPreferenceTitleId() throws ReflectiveOperationException {
+ if (sPreferenceTitleId == null) {
+ sPreferenceTitleId = getStyleableId("Preference_title");
+ }
+ return sPreferenceTitleId;
+ }
+
+ private static int getPreferenceKeyId() throws ReflectiveOperationException {
+ if (sPreferenceKeyId == null) {
+ sPreferenceKeyId = getStyleableId("Preference_key");
+ }
+ return sPreferenceKeyId;
+ }
+
+ private static int[] getPreferenceStyleArray() throws ReflectiveOperationException {
+ if (sPreferenceStyleArray == null) {
+ sPreferenceStyleArray = getStyleableArray("Preference");
+ }
+ return sPreferenceStyleArray;
+ }
+
+ private static int getStyleableId(String name) throws ReflectiveOperationException {
+ Field field = Class.forName("android.R$styleable").getDeclaredField(name);
+ return (int) field.get(null);
+ }
+
+ private static final int[] getStyleableArray(String name) throws ReflectiveOperationException {
+ Field field = Class.forName("android.R$styleable").getDeclaredField(name);
+ return (int[]) field.get(null);
+ }
+}
diff --git a/app/src/main/java/com/afwsamples/testdpc/search/SearchItemAdapter.java b/app/src/main/java/com/afwsamples/testdpc/search/SearchItemAdapter.java
new file mode 100644
index 00000000..befdf4c2
--- /dev/null
+++ b/app/src/main/java/com/afwsamples/testdpc/search/SearchItemAdapter.java
@@ -0,0 +1,69 @@
+package com.afwsamples.testdpc.search;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.afwsamples.testdpc.R;
+import com.afwsamples.testdpc.search.SearchItemAdapter.SearchItemViewHolder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represent rows of search result in {@link PolicySearchFragment}.
+ */
+public class SearchItemAdapter extends RecyclerView.Adapter {
+ private List mPreferenceIndexList = new ArrayList<>();
+ private OnItemClickListener mOnItemClickListener;
+
+ public SearchItemAdapter(OnItemClickListener onItemClickListener) {
+ mOnItemClickListener = onItemClickListener;
+ }
+
+ @Override
+ public SearchItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View itemView = LayoutInflater.from(parent.getContext()).
+ inflate(R.layout.search_result_item, parent, false);
+ return new SearchItemViewHolder(itemView);
+ }
+
+ @Override
+ public void onBindViewHolder(final SearchItemViewHolder holder, int position) {
+ final PreferenceIndex preferenceIndex = mPreferenceIndexList.get(position);
+ holder.textView.setText(preferenceIndex.title);
+ holder.textView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ final int adapterPosition = holder.getAdapterPosition();
+ PreferenceIndex clickedItem = mPreferenceIndexList.get(adapterPosition);
+ mOnItemClickListener.onItemClick(clickedItem);
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return mPreferenceIndexList.size();
+ }
+
+ public void setSearchResult(List list) {
+ mPreferenceIndexList = list;
+ }
+
+ public interface OnItemClickListener {
+ void onItemClick(PreferenceIndex preferenceIndex);
+ }
+
+ public static class SearchItemViewHolder extends RecyclerView.ViewHolder {
+ public TextView textView;
+
+ public SearchItemViewHolder(View itemView) {
+ super(itemView);
+ textView = (TextView) itemView;
+ }
+ }
+}
+
diff --git a/app/src/main/res/drawable-hdpi/ic_search.png b/app/src/main/res/drawable-hdpi/ic_search.png
new file mode 100644
index 00000000..4af65e3e
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_search.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_search.png b/app/src/main/res/drawable-mdpi/ic_search.png
new file mode 100644
index 00000000..584feb94
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_search.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_search.png b/app/src/main/res/drawable-xhdpi/ic_search.png
new file mode 100644
index 00000000..69ed20f4
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_search.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_search.png b/app/src/main/res/drawable-xxhdpi/ic_search.png
new file mode 100644
index 00000000..8c4b574a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_search.png differ
diff --git a/app/src/main/res/layout/eap_tls_wifi_config_dialog.xml b/app/src/main/res/layout/eap_tls_wifi_config_dialog.xml
new file mode 100644
index 00000000..879db787
--- /dev/null
+++ b/app/src/main/res/layout/eap_tls_wifi_config_dialog.xml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/safety_net_attest_dialog.xml b/app/src/main/res/layout/safety_net_attest_dialog.xml
new file mode 100644
index 00000000..e3d68e68
--- /dev/null
+++ b/app/src/main/res/layout/safety_net_attest_dialog.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/search_result.xml b/app/src/main/res/layout/search_result.xml
new file mode 100644
index 00000000..fa5d4e0b
--- /dev/null
+++ b/app/src/main/res/layout/search_result.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/search_result_item.xml b/app/src/main/res/layout/search_result_item.xml
new file mode 100644
index 00000000..c79639d5
--- /dev/null
+++ b/app/src/main/res/layout/search_result_item.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/app/src/main/res/menu/policy_management_menu.xml b/app/src/main/res/menu/policy_management_menu.xml
new file mode 100644
index 00000000..276281b7
--- /dev/null
+++ b/app/src/main/res/menu/policy_management_menu.xml
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/policy_search_menu.xml b/app/src/main/res/menu/policy_search_menu.xml
new file mode 100644
index 00000000..07856668
--- /dev/null
+++ b/app/src/main/res/menu/policy_search_menu.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 55140311..9e5c8e4f 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -22,4 +22,7 @@
#0000ff#00796B#FF4081
+ #D3D3D3
+ #D32F2F
+ #388E3C
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9e3254aa..f2480da7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -35,6 +35,7 @@
Finish setupSetup finishedClick Finish to complete setup.
+ Putting finishing touchesFinishSetup finished successfullyTo manage the new managed profile, visit the
@@ -73,11 +74,11 @@
start TestDPCNo account found on incoming intent.Failed to become device/profile owner.
-
+ SearchThis app is currently not the device owner.This app is the device owner.
- This app is a managed profile owner.
+ This app is a profile owner.
Some of the management options are not available.Device owner managementSet auto (network) time required
@@ -189,6 +190,18 @@
Remove selectedError modifying configUnable to remove SSID \'%1$s\': netId = %2$d
+ Create EAP-TLS WI-FI configuration
+ Identity (if any)
+ Client certificate password (if any)
+ Import CA certificate
+ Import user certificate
+ Selected certificate: %1$s
+ None
+ Missing SSID
+ Missing CA cert
+ Missing client cert or invalid cert password
+ EAP_TLS with X.509 CA certificate and PKCS12 client certificate
+ Unknown certApps management
@@ -510,6 +523,7 @@
Disable unredacted notificationsDisable trust agentsDisable fingerprint
+ Disable text entry in secure keyguard screenTrust agent features
@@ -623,4 +637,19 @@
#AARRGGBBUnable to disable keyguardUnable to enable keyguard
+
+
+ SafetyNet
+ SafetyNet Attestation
+ Checking Device Compatibility with SafetyNet
+ SafetyNet Attestation
+ Running...
+ Cancel SafetyNet Attestation
+ Fail to run SafetyNet Attestation API
+ Error: No Internet connecion
+ Error: Google Play Service is out of date
+ Error code: %1$d
+ Invalid JWS response
+ Running… Please wait
+ You should verify the response in your server.
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index bc469cfd..e6886cd0 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -27,6 +27,7 @@
@color/teal@color/dark_teal@color/pink
+ @style/PreferenceThemeOverlay.v14.Material
+
+
+
+
diff --git a/app/src/main/res/xml/device_policy_header.xml b/app/src/main/res/xml/device_policy_header.xml
index 673ab4be..8a32b613 100644
--- a/app/src/main/res/xml/device_policy_header.xml
+++ b/app/src/main/res/xml/device_policy_header.xml
@@ -128,6 +128,9 @@
+
@@ -294,4 +297,12 @@
android:key="reboot"
android:title="@string/reboot"/>
+
+
+
+
diff --git a/app/src/main/res/xml/lock_screen_preferences.xml b/app/src/main/res/xml/lock_screen_preferences.xml
index 40894d41..6d032fd3 100644
--- a/app/src/main/res/xml/lock_screen_preferences.xml
+++ b/app/src/main/res/xml/lock_screen_preferences.xml
@@ -72,6 +72,9 @@
+