From ef7a40a0d65a4d3e43d454273c6334448158e555 Mon Sep 17 00:00:00 2001 From: Andres Morales Date: Mon, 6 Jan 2014 10:24:28 -0800 Subject: [PATCH] Write wifi config to NFC tag Configured networks expose the option to write config to an NFC tag. This tag can be then tapped to another device to configure the same network. Implemented according to WiFi Alliance WPS spec. Change-Id: I33a1be1610aab71cf1ab864418d494027370ebca --- res/layout/write_wifi_config_to_nfc.xml | 42 +++ res/values/strings.xml | 21 ++ .../android/settings/wifi/WifiSettings.java | 25 +- .../wifi/WriteWifiConfigToNfcDialog.java | 264 ++++++++++++++++++ 4 files changed, 348 insertions(+), 4 deletions(-) create mode 100644 res/layout/write_wifi_config_to_nfc.xml create mode 100644 src/com/android/settings/wifi/WriteWifiConfigToNfcDialog.java diff --git a/res/layout/write_wifi_config_to_nfc.xml b/res/layout/write_wifi_config_to_nfc.xml new file mode 100644 index 00000000000..416c1ae4ac2 --- /dev/null +++ b/res/layout/write_wifi_config_to_nfc.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 7ad8f8ea449..2fb21f732dc 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1494,6 +1494,8 @@ , secured with %1$s + + Enter your network password. WEP @@ -5059,4 +5061,23 @@ Notification settings + + + + + Set up WiFi NFC Tag + + Write + + Tap a tag to write... + + Invalid password, try again. + + Success! + + Unable to write data to NFC tag. If the problem persists, try a different tag + + NFC tag is not writable. Please use a different tag. + + diff --git a/src/com/android/settings/wifi/WifiSettings.java b/src/com/android/settings/wifi/WifiSettings.java index 43f9f759543..7b3cf7b6a4a 100644 --- a/src/com/android/settings/wifi/WifiSettings.java +++ b/src/com/android/settings/wifi/WifiSettings.java @@ -94,6 +94,7 @@ import java.util.concurrent.atomic.AtomicBoolean; */ public class WifiSettings extends RestrictedSettingsFragment implements DialogInterface.OnClickListener, Indexable { + private static final String TAG = "WifiSettings"; private static final int MENU_ID_WPS_PBC = Menu.FIRST; private static final int MENU_ID_WPS_PIN = Menu.FIRST + 1; @@ -104,12 +105,14 @@ public class WifiSettings extends RestrictedSettingsFragment private static final int MENU_ID_CONNECT = Menu.FIRST + 6; private static final int MENU_ID_FORGET = Menu.FIRST + 7; private static final int MENU_ID_MODIFY = Menu.FIRST + 8; + private static final int MENU_ID_WRITE_NFC = Menu.FIRST + 9; private static final int WIFI_DIALOG_ID = 1; private static final int WPS_PBC_DIALOG_ID = 2; private static final int WPS_PIN_DIALOG_ID = 3; private static final int WIFI_SKIPPED_DIALOG_ID = 4; private static final int WIFI_AND_MOBILE_SKIPPED_DIALOG_ID = 5; + private static final int WRITE_NFC_DIALOG_ID = 6; // Combo scans can take 5-6s to complete - set to 10s. private static final int WIFI_RESCAN_INTERVAL_MS = 10 * 1000; @@ -141,6 +144,7 @@ public class WifiSettings extends RestrictedSettingsFragment private final AtomicBoolean mConnected = new AtomicBoolean(false); private WifiDialog mDialog; + private WriteWifiConfigToNfcDialog mWifiToNfcDialog; private TextView mEmptyView; @@ -205,13 +209,13 @@ public class WifiSettings extends RestrictedSettingsFragment public void onCreate(Bundle icicle) { // Set this flag early, as it's needed by getHelpResource(), which is called by super mSetupWizardMode = getActivity().getIntent().getBooleanExtra(EXTRA_IS_FIRST_RUN, false); - super.onCreate(icicle); } @Override public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + if (mSetupWizardMode) { View view = inflater.inflate(R.layout.setup_preference, container, false); View other = view.findViewById(R.id.other_network); @@ -471,6 +475,7 @@ public class WifiSettings extends RestrictedSettingsFragment if (mWifiEnabler != null) { mWifiEnabler.pause(); } + getActivity().unregisterReceiver(mReceiver); mScanner.pause(); } @@ -599,6 +604,11 @@ public class WifiSettings extends RestrictedSettingsFragment if (mSelectedAccessPoint.networkId != INVALID_NETWORK_ID) { menu.add(Menu.NONE, MENU_ID_FORGET, 0, R.string.wifi_menu_forget); menu.add(Menu.NONE, MENU_ID_MODIFY, 0, R.string.wifi_menu_modify); + + if (mSelectedAccessPoint.security != AccessPoint.SECURITY_NONE) { + // Only allow writing of NFC tags for password-protected networks. + menu.add(Menu.NONE, MENU_ID_WRITE_NFC, 0, "Write to NFC Tag"); + } } } } @@ -632,6 +642,10 @@ public class WifiSettings extends RestrictedSettingsFragment showDialog(mSelectedAccessPoint, true); return true; } + case MENU_ID_WRITE_NFC: + showDialog(WRITE_NFC_DIALOG_ID); + return true; + } return super.onContextItemSelected(item); } @@ -681,7 +695,7 @@ public class WifiSettings extends RestrictedSettingsFragment mAccessPointSavedState = null; } } - // If it's still null, fine, it's for Add Network + // If it's null, fine, it's for Add Network mSelectedAccessPoint = ap; mDialog = new WifiDialog(getActivity(), this, ap, mDlgEdit); return mDialog; @@ -727,6 +741,10 @@ public class WifiSettings extends RestrictedSettingsFragment } }) .create(); + case WRITE_NFC_DIALOG_ID: + mWifiToNfcDialog =new WriteWifiConfigToNfcDialog( + getActivity(), mSelectedAccessPoint, mWifiManager); + return mWifiToNfcDialog; } return super.onCreateDialog(dialogId); @@ -991,8 +1009,7 @@ public class WifiSettings extends RestrictedSettingsFragment mRetry = 0; Activity activity = getActivity(); if (activity != null) { - Toast.makeText(activity, R.string.wifi_fail_to_scan, - Toast.LENGTH_LONG).show(); + Toast.makeText(activity, R.string.wifi_fail_to_scan, Toast.LENGTH_LONG).show(); } return; } diff --git a/src/com/android/settings/wifi/WriteWifiConfigToNfcDialog.java b/src/com/android/settings/wifi/WriteWifiConfigToNfcDialog.java new file mode 100644 index 00000000000..7515f5c5a3d --- /dev/null +++ b/src/com/android/settings/wifi/WriteWifiConfigToNfcDialog.java @@ -0,0 +1,264 @@ +package com.android.settings.wifi; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.net.wifi.WifiManager; +import android.nfc.FormatException; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.NfcAdapter; +import android.nfc.Tag; +import android.nfc.tech.Ndef; +import android.os.Bundle; +import android.os.Handler; +import android.os.PowerManager; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.android.settings.R; + +import java.io.IOException; + +class WriteWifiConfigToNfcDialog extends AlertDialog + implements TextWatcher, View.OnClickListener, CompoundButton.OnCheckedChangeListener { + + private static final String NFC_TOKEN_MIME_TYPE = "application/vnd.wfa.wsc"; + + private static final String TAG = WriteWifiConfigToNfcDialog.class.getName().toString(); + private static final String PASSWORD_FORMAT = "102700%s%s"; + + private final PowerManager.WakeLock mWakeLock; + + private AccessPoint mAccessPoint; + private View mView; + private Button mSubmitButton; + private Button mCancelButton; + private Handler mOnTextChangedHandler; + private TextView mPasswordView; + private TextView mLabelView; + private CheckBox mPasswordCheckBox; + private ProgressBar mProgressBar; + private WifiManager mWifiManager; + private String mWpsNfcConfigurationToken; + private Context mContext; + + WriteWifiConfigToNfcDialog(Context context, AccessPoint accessPoint, + WifiManager wifiManager) { + super(context); + this.mContext = context; + this.mWakeLock = ((PowerManager) context.getSystemService(Context.POWER_SERVICE)) + .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "WriteWifiConfigToNfcDialog:wakeLock"); + this.mAccessPoint = accessPoint; + this.mOnTextChangedHandler = new Handler(); + this.mWifiManager = wifiManager; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + mView = getLayoutInflater().inflate(R.layout.write_wifi_config_to_nfc, null); + + setView(mView); + setInverseBackgroundForced(true); + setTitle(R.string.setup_wifi_nfc_tag); + setCancelable(true); + setButton(DialogInterface.BUTTON_NEUTRAL, + mContext.getResources().getString(R.string.write_tag), (OnClickListener) null); + setButton(DialogInterface.BUTTON_NEGATIVE, + mContext.getResources().getString(com.android.internal.R.string.cancel), + (OnClickListener) null); + + mPasswordView = (TextView) mView.findViewById(R.id.password); + mLabelView = (TextView) mView.findViewById(R.id.password_label); + mPasswordView.addTextChangedListener(this); + mPasswordCheckBox = (CheckBox) mView.findViewById(R.id.show_password); + mPasswordCheckBox.setOnCheckedChangeListener(this); + mProgressBar = (ProgressBar) mView.findViewById(R.id.progress_bar); + + super.onCreate(savedInstanceState); + + mSubmitButton = getButton(DialogInterface.BUTTON_NEUTRAL); + mSubmitButton.setOnClickListener(this); + mSubmitButton.setEnabled(false); + + mCancelButton = getButton(DialogInterface.BUTTON_NEGATIVE); + } + + @Override + public void onClick(View v) { + mWakeLock.acquire(); + + String password = mPasswordView.getText().toString(); + String wpsNfcConfigurationToken + = mWifiManager.getWpsNfcConfigurationToken(mAccessPoint.networkId); + String passwordHex = byteArrayToHexString(password.getBytes()); + + String passwordLength = password.length() >= 16 + ? "" + Character.forDigit(password.length(), 16) + : "0" + Character.forDigit(password.length(), 16); + + passwordHex = String.format(PASSWORD_FORMAT, passwordLength, passwordHex).toUpperCase(); + + if (wpsNfcConfigurationToken.contains(passwordHex)) { + mWpsNfcConfigurationToken = wpsNfcConfigurationToken; + + Activity activity = getOwnerActivity(); + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(activity); + + nfcAdapter.enableReaderMode(activity, new NfcAdapter.ReaderCallback() { + @Override + public void onTagDiscovered(Tag tag) { + handleWriteNfcEvent(tag); + } + }, NfcAdapter.FLAG_READER_NFC_A | + NfcAdapter.FLAG_READER_NFC_B | + NfcAdapter.FLAG_READER_NFC_BARCODE | + NfcAdapter.FLAG_READER_NFC_F | + NfcAdapter.FLAG_READER_NFC_V, + null); + + mPasswordView.setVisibility(View.GONE); + mPasswordCheckBox.setVisibility(View.GONE); + mSubmitButton.setVisibility(View.GONE); + InputMethodManager imm = (InputMethodManager) + getOwnerActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mPasswordView.getWindowToken(), 0); + + mLabelView.setText(R.string.status_awaiting_tap); + + mView.findViewById(R.id.password_layout).setTextAlignment(View.TEXT_ALIGNMENT_CENTER); + mProgressBar.setVisibility(View.VISIBLE); + } else { + mLabelView.setText(R.string.status_invalid_password); + } + } + + private void handleWriteNfcEvent(Tag tag) { + Ndef ndef = Ndef.get(tag); + + if (ndef != null) { + if (ndef.isWritable()) { + NdefRecord record = NdefRecord.createMime( + NFC_TOKEN_MIME_TYPE, + hexStringToByteArray(mWpsNfcConfigurationToken)); + try { + ndef.connect(); + ndef.writeNdefMessage(new NdefMessage(record)); + getOwnerActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mProgressBar.setVisibility(View.GONE); + } + }); + setViewText(mLabelView, R.string.status_write_success); + setViewText(mCancelButton, com.android.internal.R.string.done_label); + } catch (IOException e) { + setViewText(mLabelView, R.string.status_failed_to_write); + Log.e(TAG, "Unable to write WiFi config to NFC tag.", e); + return; + } catch (FormatException e) { + setViewText(mLabelView, R.string.status_failed_to_write); + Log.e(TAG, "Unable to write WiFi config to NFC tag.", e); + return; + } + } else { + setViewText(mLabelView, R.string.status_tag_not_writable); + Log.e(TAG, "Tag is not writable"); + } + } else { + setViewText(mLabelView, R.string.status_tag_not_writable); + Log.e(TAG, "Tag does not support NDEF"); + } + } + + @Override + public void dismiss() { + if (mWakeLock.isHeld()) { + mWakeLock.release(); + } + + super.dismiss(); + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + mOnTextChangedHandler.post(new Runnable() { + @Override + public void run() { + enableSubmitIfAppropriate(); + } + }); + } + + private void enableSubmitIfAppropriate() { + + if (mPasswordView != null) { + if (mAccessPoint.security == AccessPoint.SECURITY_WEP) { + mSubmitButton.setEnabled(mPasswordView.length() > 0); + } else if (mAccessPoint.security == AccessPoint.SECURITY_PSK) { + mSubmitButton.setEnabled(mPasswordView.length() >= 8); + } + } else { + mSubmitButton.setEnabled(false); + } + + } + + private void setViewText(final TextView view, final int resid) { + getOwnerActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + view.setText(resid); + } + }); + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mPasswordView.setInputType( + InputType.TYPE_CLASS_TEXT | + (isChecked + ? InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + : InputType.TYPE_TEXT_VARIATION_PASSWORD)); + } + + private static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i + 1), 16)); + } + + return data; + } + + final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); + private static String byteArrayToHexString(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for ( int j = 0; j < bytes.length; j++ ) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override + public void afterTextChanged(Editable s) {} +}