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
This commit is contained in:
Andres Morales
2014-01-06 10:24:28 -08:00
parent ac6a3c3cb3
commit ef7a40a0d6
4 changed files with 348 additions and 4 deletions

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/wifi_section">
<LinearLayout android:id="@+id/password_layout"
style="@style/wifi_item"
android:padding="8dip"
android:orientation="vertical" >
<TextView
android:id="@+id/password_label"
android:layout_gravity="fill"
style="@style/wifi_item_label"
android:text="@string/wifi_password" />
<EditText android:id="@+id/password"
style="@style/wifi_item_edit_content"
android:singleLine="true"
android:password="true" />
<TextView
style="@style/wifi_item_label" />
<CheckBox android:id="@+id/show_password"
style="@style/wifi_item_content"
android:textSize="14sp"
android:text="@string/wifi_show_password" />
</LinearLayout>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|center_horizontal"
android:padding="8dip"
android:visibility="gone"
style="@android:style/Widget.ProgressBar.Large" />
</LinearLayout>

View File

@@ -1494,6 +1494,8 @@
<!-- Substring of wifi status for wifi with authentication. This version is for when the <!-- Substring of wifi status for wifi with authentication. This version is for when the
string is not first in the list (lowercase in english) --> string is not first in the list (lowercase in english) -->
<string name="wifi_secured_second_item">, secured with <xliff:g id="wifi_security_short">%1$s</xliff:g></string> <string name="wifi_secured_second_item">, secured with <xliff:g id="wifi_security_short">%1$s</xliff:g></string>
<!-- Message in WriteWifiConfigToNfcDialog when prompted to enter network password [CHAR LIMIT=150] -->
<string name="wifi_wps_nfc_enter_password">Enter your network password.</string>
<!-- Do not translate. Concise terminology for wifi with WEP security --> <!-- Do not translate. Concise terminology for wifi with WEP security -->
<string name="wifi_security_short_wep">WEP</string> <string name="wifi_security_short_wep">WEP</string>
@@ -5059,4 +5061,23 @@
<!-- [CHAR LIMIT=NONE] Content description for per-app notification <!-- [CHAR LIMIT=NONE] Content description for per-app notification
settings button --> settings button -->
<string name="notification_app_settings_button">Notification settings</string> <string name="notification_app_settings_button">Notification settings</string>
<!-- NFC WiFi pairing/setup strings-->
<!-- Write NFC tag for WiFi pairing/setup title -->
<string name="setup_wifi_nfc_tag">Set up WiFi NFC Tag</string>
<!-- Text for button to confirm writing tag -->
<string name="write_tag">Write</string>
<!-- Text to inform the user to tap a tag to complete the setup process -->
<string name="status_awaiting_tap">Tap a tag to write...</string>
<!-- Text to inform the user that the network key entered was incorrect -->
<string name="status_invalid_password">Invalid password, try again.</string>
<!-- Text displayed when tag successfully writen -->
<string name="status_write_success">Success!</string>
<!-- Text displayed in error cases (failure to write to tag) -->
<string name="status_failed_to_write">Unable to write data to NFC tag. If the problem persists, try a different tag</string>
<!-- Text displayed when tag is not writable -->
<string name="status_tag_not_writable">NFC tag is not writable. Please use a different tag.</string>
</resources> </resources>

View File

@@ -94,6 +94,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/ */
public class WifiSettings extends RestrictedSettingsFragment public class WifiSettings extends RestrictedSettingsFragment
implements DialogInterface.OnClickListener, Indexable { implements DialogInterface.OnClickListener, Indexable {
private static final String TAG = "WifiSettings"; private static final String TAG = "WifiSettings";
private static final int MENU_ID_WPS_PBC = Menu.FIRST; private static final int MENU_ID_WPS_PBC = Menu.FIRST;
private static final int MENU_ID_WPS_PIN = Menu.FIRST + 1; 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_CONNECT = Menu.FIRST + 6;
private static final int MENU_ID_FORGET = Menu.FIRST + 7; 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_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 WIFI_DIALOG_ID = 1;
private static final int WPS_PBC_DIALOG_ID = 2; private static final int WPS_PBC_DIALOG_ID = 2;
private static final int WPS_PIN_DIALOG_ID = 3; private static final int WPS_PIN_DIALOG_ID = 3;
private static final int WIFI_SKIPPED_DIALOG_ID = 4; private static final int WIFI_SKIPPED_DIALOG_ID = 4;
private static final int WIFI_AND_MOBILE_SKIPPED_DIALOG_ID = 5; 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. // Combo scans can take 5-6s to complete - set to 10s.
private static final int WIFI_RESCAN_INTERVAL_MS = 10 * 1000; 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 final AtomicBoolean mConnected = new AtomicBoolean(false);
private WifiDialog mDialog; private WifiDialog mDialog;
private WriteWifiConfigToNfcDialog mWifiToNfcDialog;
private TextView mEmptyView; private TextView mEmptyView;
@@ -205,13 +209,13 @@ public class WifiSettings extends RestrictedSettingsFragment
public void onCreate(Bundle icicle) { public void onCreate(Bundle icicle) {
// Set this flag early, as it's needed by getHelpResource(), which is called by super // Set this flag early, as it's needed by getHelpResource(), which is called by super
mSetupWizardMode = getActivity().getIntent().getBooleanExtra(EXTRA_IS_FIRST_RUN, false); mSetupWizardMode = getActivity().getIntent().getBooleanExtra(EXTRA_IS_FIRST_RUN, false);
super.onCreate(icicle); super.onCreate(icicle);
} }
@Override @Override
public View onCreateView(final LayoutInflater inflater, ViewGroup container, public View onCreateView(final LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
if (mSetupWizardMode) { if (mSetupWizardMode) {
View view = inflater.inflate(R.layout.setup_preference, container, false); View view = inflater.inflate(R.layout.setup_preference, container, false);
View other = view.findViewById(R.id.other_network); View other = view.findViewById(R.id.other_network);
@@ -471,6 +475,7 @@ public class WifiSettings extends RestrictedSettingsFragment
if (mWifiEnabler != null) { if (mWifiEnabler != null) {
mWifiEnabler.pause(); mWifiEnabler.pause();
} }
getActivity().unregisterReceiver(mReceiver); getActivity().unregisterReceiver(mReceiver);
mScanner.pause(); mScanner.pause();
} }
@@ -599,6 +604,11 @@ public class WifiSettings extends RestrictedSettingsFragment
if (mSelectedAccessPoint.networkId != INVALID_NETWORK_ID) { 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_FORGET, 0, R.string.wifi_menu_forget);
menu.add(Menu.NONE, MENU_ID_MODIFY, 0, R.string.wifi_menu_modify); 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); showDialog(mSelectedAccessPoint, true);
return true; return true;
} }
case MENU_ID_WRITE_NFC:
showDialog(WRITE_NFC_DIALOG_ID);
return true;
} }
return super.onContextItemSelected(item); return super.onContextItemSelected(item);
} }
@@ -681,7 +695,7 @@ public class WifiSettings extends RestrictedSettingsFragment
mAccessPointSavedState = null; 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; mSelectedAccessPoint = ap;
mDialog = new WifiDialog(getActivity(), this, ap, mDlgEdit); mDialog = new WifiDialog(getActivity(), this, ap, mDlgEdit);
return mDialog; return mDialog;
@@ -727,6 +741,10 @@ public class WifiSettings extends RestrictedSettingsFragment
} }
}) })
.create(); .create();
case WRITE_NFC_DIALOG_ID:
mWifiToNfcDialog =new WriteWifiConfigToNfcDialog(
getActivity(), mSelectedAccessPoint, mWifiManager);
return mWifiToNfcDialog;
} }
return super.onCreateDialog(dialogId); return super.onCreateDialog(dialogId);
@@ -991,8 +1009,7 @@ public class WifiSettings extends RestrictedSettingsFragment
mRetry = 0; mRetry = 0;
Activity activity = getActivity(); Activity activity = getActivity();
if (activity != null) { if (activity != null) {
Toast.makeText(activity, R.string.wifi_fail_to_scan, Toast.makeText(activity, R.string.wifi_fail_to_scan, Toast.LENGTH_LONG).show();
Toast.LENGTH_LONG).show();
} }
return; return;
} }

View File

@@ -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) {}
}