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 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 @@ + + + + + + + + + + + + + + + +