diff --git a/packages/Osu/Android.mk b/packages/Osu/Android.mk index 15744c56fd21f..0c9006a4abbd2 100644 --- a/packages/Osu/Android.mk +++ b/packages/Osu/Android.mk @@ -6,6 +6,9 @@ LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_SRC_FILES += \ + src/com/android/hotspot2/app/IOSUAccessor.aidl \ + src/com/android/hotspot2/flow/IFlowService.aidl LOCAL_JAVA_LIBRARIES := telephony-common ims-common bouncycastle conscrypt diff --git a/packages/Osu/AndroidManifest.xml b/packages/Osu/AndroidManifest.xml index 288f1a488c66b..fa9a656e6b95f 100644 --- a/packages/Osu/AndroidManifest.xml +++ b/packages/Osu/AndroidManifest.xml @@ -1,6 +1,23 @@ + + package="com.android.hotspot2"> @@ -12,13 +29,12 @@ - + @@ -28,30 +44,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + diff --git a/packages/Osu/res/layout/osu_web_view.xml b/packages/Osu/res/layout/osu_web_view.xml new file mode 100644 index 0000000000000..4eafb39d17135 --- /dev/null +++ b/packages/Osu/res/layout/osu_web_view.xml @@ -0,0 +1,13 @@ + + + + diff --git a/packages/Osu/src/com/android/MainActivity.java b/packages/Osu/src/com/android/MainActivity.java deleted file mode 100644 index 763a988146b73..0000000000000 --- a/packages/Osu/src/com/android/MainActivity.java +++ /dev/null @@ -1,442 +0,0 @@ -package com.android; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.IntentService; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.TaskStackBuilder; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; -import android.net.wifi.WifiConfiguration; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; -import android.os.Binder; -import android.os.Bundle; -import android.os.IBinder; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.TextView; - -import com.android.anqp.OSUProvider; -import com.android.hotspot2.AppBridge; -import com.android.hotspot2.PasspointMatch; -import com.android.hotspot2.osu.OSUInfo; -import com.android.hotspot2.osu.OSUManager; - -import org.xml.sax.SAXException; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.TimeUnit; - -//import com.android.Osu.R; - -/** - * Main activity. - */ -public class MainActivity extends Activity { - private static final int NOTIFICATION_ID = 0; // Used for OSU count - private static final int NOTIFICATION_MESSAGE_ID = 1; // Used for other messages - private static final Locale LOCALE = java.util.Locale.getDefault(); - - private static volatile OSUService sOsuService; - - private ListView osuListView; - private OsuListAdapter2 osuListAdapter; - private String message; - - public MainActivity() { - - } - - @Override - protected void onResume() { - super.onResume(); - if (message != null) { - showDialog(message); - message = null; - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Intent intent = getIntent(); - Bundle bundle = intent.getExtras(); - - if (bundle == null) { // User interaction - if (sOsuService == null) { - Intent serviceIntent = new Intent(this, OSUService.class); - serviceIntent.putExtra(ACTION_KEY, "dummy-key"); - startService(serviceIntent); - return; - } - - List osuInfos = sOsuService.getOsuInfos(); - - setContentView(R.layout.activity_main); - Log.d("osu", "osu count:" + osuInfos.size()); - View noOsuView = findViewById(R.id.no_osu); - if (osuInfos.size() > 0) { - noOsuView.setVisibility(View.GONE); - osuListAdapter = new OsuListAdapter2(this, osuInfos); - osuListView = (ListView) findViewById(R.id.profile_list); - osuListView.setAdapter(osuListAdapter); - osuListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int position, long id) { - OSUInfo osuData = (OSUInfo) adapterView.getAdapter().getItem(position); - Log.d("osu", "launch osu:" + osuData.getName(LOCALE) - + " id:" + osuData.getOsuID()); - sOsuService.selectOsu(osuData.getOsuID()); - finish(); - } - }); - } else { - noOsuView.setVisibility(View.VISIBLE); - } - } else if (intent.getAction().equals(AppBridge.ACTION_OSU_NOTIFICATION)) { - if (bundle.containsKey(AppBridge.OSU_COUNT)) { - showOsuCount(bundle.getInt("osu-count", 0), Collections.emptyList()); - } else if (bundle.containsKey(AppBridge.PROV_SUCCESS)) { - showStatus(bundle.getBoolean(AppBridge.PROV_SUCCESS), - bundle.getString(AppBridge.SP_NAME), - bundle.getString(AppBridge.PROV_MESSAGE), - null); - } else if (bundle.containsKey(AppBridge.DEAUTH)) { - showDeauth(bundle.getString(AppBridge.SP_NAME), - bundle.getBoolean(AppBridge.DEAUTH), - bundle.getInt(AppBridge.DEAUTH_DELAY), - bundle.getString(AppBridge.DEAUTH_URL)); - } - /* - else if (bundle.containsKey(AppBridge.OSU_INFO)) { - List osus = printOsuDataList(bundle.getParcelableArray(AppBridge.OSU_INFO)); - showOsuList(osus); - } - */ - } - } - - private void showOsuCount(int osuCount, List osus) { - if (osuCount > 0) { - printOsuDataList(osus); - sendNotification(osuCount); - } else { - cancelNotification(); - } - finish(); - } - - private void showStatus(boolean provSuccess, String spName, String provMessage, - String remoteStatus) { - if (provSuccess) { - sendDialogMessage( - String.format("Credentials for %s was successfully installed", spName)); - } else { - if (spName != null) { - if (remoteStatus != null) { - sendDialogMessage( - String.format("Failed to install credentials for %s: %s: %s", - spName, provMessage, remoteStatus)); - } else { - sendDialogMessage( - String.format("Failed to install credentials for %s: %s", - spName, provMessage)); - } - } else { - sendDialogMessage( - String.format("Failed to contact OSU: %s", provMessage)); - } - } - } - - private void showDeauth(String spName, boolean ess, int delay, String url) { - String delayReadable = getReadableTimeInSeconds(delay); - if (ess) { - if (delay > 60) { - sendDialogMessage( - String.format("There is an issue connecting to %s [for the next %s]. " + - "Please visit %s for details", spName, delayReadable, url)); - } else { - sendDialogMessage( - String.format("There is an issue connecting to %s. " + - "Please visit %s for details", spName, url)); - } - } else { - sendDialogMessage( - String.format("There is an issue with the closest Access Point for %s. " + - "You may wait %s or move to another Access Point to " + - "regain access. Please visit %s for details.", - spName, delayReadable, url)); - } - } - - private static final String ACTION_KEY = "action"; - - public static class WifiReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context c, Intent intent) { - Log.d(OSUManager.TAG, "OSU App got intent: " + intent.getAction()); - Intent serviceIntent; - serviceIntent = new Intent(c, OSUService.class); - serviceIntent.putExtra(ACTION_KEY, intent.getAction()); - serviceIntent.putExtras(intent); - c.startService(serviceIntent); - } - } - - public static class OSUService extends IntentService { - private OSUManager mOsuManager; - private final IBinder mBinder = new Binder(); - - public OSUService() { - super("OSUService"); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - onHandleIntent(intent); - return START_STICKY; - } - - @Override - public void onCreate() { - super.onCreate(); - Log.d("YYY", String.format("Service %x running, OSU %x", - System.identityHashCode(this), System.identityHashCode(mOsuManager))); - if (mOsuManager == null) { - mOsuManager = new OSUManager(this); - } - sOsuService = this; - } - - @Override - public void onDestroy() { - super.onDestroy(); - Log.d("YYY", String.format("Service %x killed", System.identityHashCode(this))); - } - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - @Override - protected void onHandleIntent(Intent intent) { - if (intent == null) { - Log.d(OSUManager.TAG, "Null intent!"); - return; - } - Bundle bundle = intent.getExtras(); - WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); - Log.d(OSUManager.TAG, "OSU Service got intent: " + intent.getStringExtra(ACTION_KEY)); - switch (intent.getStringExtra(ACTION_KEY)) { - case WifiManager.SCAN_RESULTS_AVAILABLE_ACTION: - mOsuManager.pushScanResults(wifiManager.getScanResults()); - break; - case WifiManager.PASSPOINT_WNM_FRAME_RECEIVED_ACTION: - long bssid = bundle.getLong(WifiManager.EXTRA_PASSPOINT_WNM_BSSID); - String url = bundle.getString(WifiManager.EXTRA_PASSPOINT_WNM_URL); - - try { - if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_METHOD)) { - int method = bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_METHOD); - if (method != OSUProvider.OSUMethod.SoapXml.ordinal()) { - Log.w(OSUManager.TAG, "Unsupported remediation method: " + method); - } - PasspointMatch match = null; - if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_PPOINT_MATCH)) { - int ordinal = - bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_PPOINT_MATCH); - if (ordinal >= 0 && ordinal < PasspointMatch.values().length) { - match = PasspointMatch.values()[ordinal]; - } - } - mOsuManager.wnmRemediate(bssid, url, match); - } else if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_ESS)) { - boolean ess = bundle.getBoolean(WifiManager.EXTRA_PASSPOINT_WNM_ESS); - int delay = bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_DELAY); - mOsuManager.deauth(bssid, ess, delay, url); - } else { - Log.w(OSUManager.TAG, "Unknown WNM event"); - } - } catch (IOException | SAXException e) { - Log.w(OSUManager.TAG, "Remediation event failed to parse: " + e); - } - break; - case WifiManager.PASSPOINT_ICON_RECEIVED_ACTION: - mOsuManager.notifyIconReceived( - bundle.getLong(WifiManager.EXTRA_PASSPOINT_ICON_BSSID), - bundle.getString(WifiManager.EXTRA_PASSPOINT_ICON_FILE), - bundle.getByteArray(WifiManager.EXTRA_PASSPOINT_ICON_DATA)); - break; - case WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION: - boolean multiNetwork = - bundle.getBoolean(WifiManager.EXTRA_MULTIPLE_NETWORKS_CHANGED, false); - if (multiNetwork) { - mOsuManager.networkChanged(null); - } else { - WifiConfiguration configuration = - intent.getParcelableExtra(WifiManager.EXTRA_WIFI_CONFIGURATION); - switch (bundle.getInt(WifiManager.EXTRA_CHANGE_REASON, - WifiManager.CHANGE_REASON_CONFIG_CHANGE)) { - case WifiManager.CHANGE_REASON_ADDED: - break; - case WifiManager.CHANGE_REASON_REMOVED: - mOsuManager.networkDeleted(configuration); - break; - case WifiManager.CHANGE_REASON_CONFIG_CHANGE: - mOsuManager.networkChanged(configuration); - break; - } - } - mOsuManager.networkChanged((WifiConfiguration) - intent.getParcelableExtra(WifiManager.EXTRA_WIFI_CONFIGURATION)); - break; - case WifiManager.WIFI_STATE_CHANGED_ACTION: - int state = bundle.getInt(WifiManager.EXTRA_WIFI_STATE); - if (state == WifiManager.WIFI_STATE_DISABLED) { - mOsuManager.wifiStateChange(false); - } else if (state == WifiManager.WIFI_STATE_ENABLED) { - mOsuManager.wifiStateChange(true); - } - break; - case WifiManager.NETWORK_STATE_CHANGED_ACTION: - mOsuManager.networkConnectEvent((WifiInfo) - intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO)); - break; - } - } - - public List getOsuInfos() { - return mOsuManager.getAvailableOSUs(); - } - - public void selectOsu(int id) { - mOsuManager.setOSUSelection(id); - } - } - - private String getReadableTimeInSeconds(int timeSeconds) { - long hours = TimeUnit.SECONDS.toHours(timeSeconds); - long minutes = TimeUnit.SECONDS.toMinutes(timeSeconds) - TimeUnit.HOURS.toMinutes(hours); - long seconds = - timeSeconds - TimeUnit.HOURS.toSeconds(hours) - TimeUnit.MINUTES.toSeconds(minutes); - if (hours > 0) { - return String.format("%02d:%02d:%02d", hours, minutes, seconds); - } else { - return String.format("%ds", seconds); - } - } - - private void sendNotification(int count) { - Notification.Builder builder = - new Notification.Builder(this) - .setContentTitle(String.format("%s OSU available", count)) - .setContentText("Choose one to connect") - .setSmallIcon(android.R.drawable.ic_dialog_info) - .setAutoCancel(false); - Intent resultIntent = new Intent(this, MainActivity.class); - - TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); - stackBuilder.addParentStack(MainActivity.class); - stackBuilder.addNextIntent(resultIntent); - PendingIntent resultPendingIntent = - stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); - builder.setContentIntent(resultPendingIntent); - NotificationManager notificationManager = - (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - notificationManager.notify(NOTIFICATION_ID, builder.build()); - } - - private void cancelNotification() { - NotificationManager notificationManager = - (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - notificationManager.cancel(NOTIFICATION_ID); - } - - private void sendDialogMessage(String message) { -// sendNotificationMessage(message); - this.message = message; - } - - private void showDialog(String message) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(message) - .setTitle("OSU"); - builder.setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialogInterface) { - dialogInterface.cancel(); - finish(); - } - }); - AlertDialog dialog = builder.create(); - dialog.show(); - } - - private void sendNotificationMessage(String title) { - Notification.Builder builder = - new Notification.Builder(this) - .setContentTitle(title) - .setContentText("Click to dismiss.") - .setSmallIcon(android.R.drawable.ic_dialog_info) - .setAutoCancel(true); - NotificationManager notificationManager = - (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - notificationManager.notify(NOTIFICATION_MESSAGE_ID, builder.build()); - } - - private static class OsuListAdapter2 extends ArrayAdapter { - private Activity activity; - - public OsuListAdapter2(Activity activity, List osuDataList) { - super(activity, R.layout.list_item, osuDataList); - this.activity = activity; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = convertView; - if (view == null) { - view = LayoutInflater.from(getContext()).inflate(R.layout.list_item, parent, false); - } - OSUInfo osuData = getItem(position); - TextView osuName = (TextView) view.findViewById(R.id.profile_name); - osuName.setText(osuData.getName(LOCALE)); - TextView osuDetail = (TextView) view.findViewById(R.id.profile_detail); - osuDetail.setText(osuData.getServiceDescription(LOCALE)); - ImageView osuIcon = (ImageView) view.findViewById(R.id.profile_logo); - byte[] iconData = osuData.getIconFileElement().getIconData(); - osuIcon.setImageDrawable( - new BitmapDrawable(activity.getResources(), - BitmapFactory.decodeByteArray(iconData, 0, iconData.length))); - return view; - } - } - - private void printOsuDataList(List osuDataList) { - for (OSUInfo osuData : osuDataList) { - Log.d("osu", String.format("OSUData:[%s][%s][%d]", - osuData.getName(LOCALE), osuData.getServiceDescription(LOCALE), - osuData.getOsuID())); - } - } - -} diff --git a/packages/Osu/src/com/android/anqp/HSIconFileElement.java b/packages/Osu/src/com/android/anqp/HSIconFileElement.java index c5c49cc00fa0b..28c597a5d47a5 100644 --- a/packages/Osu/src/com/android/anqp/HSIconFileElement.java +++ b/packages/Osu/src/com/android/anqp/HSIconFileElement.java @@ -1,5 +1,9 @@ package com.android.anqp; +import android.os.Parcel; + +import com.android.hotspot2.Utils; + import java.net.ProtocolException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -71,4 +75,17 @@ public class HSIconFileElement extends ANQPElement { ", type='" + mType + '\'' + ", iconData=" + mIconData.length + " bytes }"; } + + public HSIconFileElement(Parcel in) { + super(Constants.ANQPElementType.HSIconFile); + mStatusCode = Utils.mapEnum(in.readInt(), StatusCode.class); + mType = in.readString(); + mIconData = in.readBlob(); + } + + public void writeParcel(Parcel out) { + out.writeInt(mStatusCode.ordinal()); + out.writeString(mType); + out.writeBlob(mIconData); + } } diff --git a/packages/Osu/src/com/android/anqp/I18Name.java b/packages/Osu/src/com/android/anqp/I18Name.java index 0a16db74b86fd..b048228722c5a 100644 --- a/packages/Osu/src/com/android/anqp/I18Name.java +++ b/packages/Osu/src/com/android/anqp/I18Name.java @@ -1,5 +1,7 @@ package com.android.anqp; +import android.os.Parcel; + import java.io.IOException; import java.net.ProtocolException; import java.nio.ByteBuffer; @@ -77,4 +79,15 @@ public class I18Name { public String toString() { return mText + ':' + mLocale.getLanguage(); } + + public I18Name(Parcel in) throws IOException { + mLanguage = in.readString(); + mText = in.readString(); + mLocale = Locale.forLanguageTag(mLanguage); + } + + public void writeParcel(Parcel out) { + out.writeString(mLanguage); + out.writeString(mText); + } } diff --git a/packages/Osu/src/com/android/anqp/IconInfo.java b/packages/Osu/src/com/android/anqp/IconInfo.java index 9e9f1eeba915e..ac507c7af948e 100644 --- a/packages/Osu/src/com/android/anqp/IconInfo.java +++ b/packages/Osu/src/com/android/anqp/IconInfo.java @@ -1,5 +1,7 @@ package com.android.anqp; +import android.os.Parcel; + import java.net.ProtocolException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -88,4 +90,20 @@ public class IconInfo { ", FileName='" + mFileName + '\'' + '}'; } + + public IconInfo(Parcel in) { + mWidth = in.readInt(); + mHeight = in.readInt(); + mLanguage = in.readString(); + mIconType = in.readString(); + mFileName = in.readString(); + } + + public void writeParcel(Parcel out) { + out.writeInt(mWidth); + out.writeInt(mHeight); + out.writeString(mLanguage); + out.writeString(mIconType); + out.writeString(mFileName); + } } diff --git a/packages/Osu/src/com/android/anqp/OSUProvider.java b/packages/Osu/src/com/android/anqp/OSUProvider.java index e2669d433c1e4..3724cf06d15bd 100644 --- a/packages/Osu/src/com/android/anqp/OSUProvider.java +++ b/packages/Osu/src/com/android/anqp/OSUProvider.java @@ -1,5 +1,10 @@ package com.android.anqp; +import android.os.Parcel; + +import com.android.hotspot2.Utils; + +import java.io.IOException; import java.net.ProtocolException; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -155,4 +160,54 @@ public class OSUProvider { ", serviceDescriptions=" + mServiceDescriptions + '}'; } + + public OSUProvider(Parcel in) throws IOException { + mSSID = in.readString(); + int nameCount = in.readInt(); + mNames = new ArrayList<>(nameCount); + for (int n = 0; n < nameCount; n++) { + mNames.add(new I18Name(in)); + } + mOSUServer = in.readString(); + int methodCount = in.readInt(); + mOSUMethods = new ArrayList<>(methodCount); + for (int n = 0; n < methodCount; n++) { + mOSUMethods.add(Utils.mapEnum(in.readInt(), OSUMethod.class)); + } + int iconCount = in.readInt(); + mIcons = new ArrayList<>(iconCount); + for (int n = 0; n < iconCount; n++) { + mIcons.add(new IconInfo(in)); + } + mOsuNai = in.readString(); + int serviceCount = in.readInt(); + mServiceDescriptions = new ArrayList<>(serviceCount); + for (int n = 0; n < serviceCount; n++) { + mServiceDescriptions.add(new I18Name(in)); + } + mHashCode = in.readInt(); + } + + public void writeParcel(Parcel out) { + out.writeString(mSSID); + out.writeInt(mNames.size()); + for (I18Name name : mNames) { + name.writeParcel(out); + } + out.writeString(mOSUServer); + out.writeInt(mOSUMethods.size()); + for (OSUMethod method : mOSUMethods) { + out.writeInt(method.ordinal()); + } + out.writeInt(mIcons.size()); + for (IconInfo iconInfo : mIcons) { + iconInfo.writeParcel(out); + } + out.writeString(mOsuNai); + out.writeInt(mServiceDescriptions.size()); + for (I18Name serviceDescription : mServiceDescriptions) { + serviceDescription.writeParcel(out); + } + out.writeInt(mHashCode); + } } diff --git a/packages/Osu/src/com/android/hotspot2/AppBridge.java b/packages/Osu/src/com/android/hotspot2/AppBridge.java index 95f5970bc74a7..81542f7fc466c 100644 --- a/packages/Osu/src/com/android/hotspot2/AppBridge.java +++ b/packages/Osu/src/com/android/hotspot2/AppBridge.java @@ -2,13 +2,9 @@ package com.android.hotspot2; import android.content.Context; import android.content.Intent; -import android.os.UserHandle; -import com.android.hotspot2.osu.OSUInfo; import com.android.hotspot2.osu.OSUOperationStatus; -import java.util.List; - public class AppBridge { public static final String ACTION_OSU_NOTIFICATION = "com.android.hotspot2.OSU_NOTIFICATION"; public static final String OSU_COUNT = "osu-count"; @@ -28,7 +24,7 @@ public class AppBridge { mContext = context; } - public void showOsuCount(int osuCount, List osus) { + public void showOsuCount(int osuCount) { Intent intent = new Intent(ACTION_OSU_NOTIFICATION); intent.putExtra(OSU_COUNT, osuCount); intent.setFlags( diff --git a/packages/Osu/src/com/android/hotspot2/Utils.java b/packages/Osu/src/com/android/hotspot2/Utils.java index 100b967f6cf30..880007f6e392b 100644 --- a/packages/Osu/src/com/android/hotspot2/Utils.java +++ b/packages/Osu/src/com/android/hotspot2/Utils.java @@ -3,6 +3,9 @@ package com.android.hotspot2; import com.android.anqp.Constants; import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; @@ -94,6 +97,14 @@ public abstract class Utils { return prefix; } + public static String toIpString(int leIp) { + return String.format("%d.%d.%d.%d", + leIp & BYTE_MASK, + (leIp >> 8) & BYTE_MASK, + (leIp >> 16) & BYTE_MASK, + (leIp >> 24) & BYTE_MASK); + } + public static String bssidsToString(Collection bssids) { StringBuilder sb = new StringBuilder(); for (Long bssid : bssids) { @@ -274,16 +285,107 @@ public abstract class Utils { c.get(Calendar.SECOND)); } - public static String unquote(String s) { - if (s == null) { - return null; - } else if (s.length() > 1 && s.startsWith("\"") && s.endsWith("\"")) { - return s.substring(1, s.length() - 1); - } else { - return s; + /** + * Decode a wpa_supplicant SSID. wpa_supplicant uses double quotes around plain strings, or + * expects a hex-string if no quotes appear. + * For Ascii encoded string, any octet < 32 or > 127 is encoded as + * a "\x" followed by the hex representation of the octet. + * Exception chars are ", \, \e, \n, \r, \t which are escaped by a \ + * See src/utils/common.c for the implementation in the supplicant. + * + * @param ssid The SSID from the config. + * @return The actual string content of the SSID + */ + public static String decodeSsid(String ssid) { + if (ssid.length() <= 1) { + return ssid; + } else if (ssid.startsWith("\"") && ssid.endsWith("\"")) { + return unescapeSsid(ssid.substring(1, ssid.length() - 1)); + } else if ((ssid.length() & 1) == 1) { + return ssid; + } + + byte[] codepoints; + try { + codepoints = new byte[ssid.length() / 2]; + for (int n = 0; n < ssid.length(); n += 2) { + codepoints[n / 2] = (byte) decodeHexPair(ssid, n); + } + } catch (NumberFormatException nfe) { + return ssid; + } + + try { + CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); + return decoder.decode(ByteBuffer.wrap(codepoints)).toString(); + } catch (CharacterCodingException cce) { + /* Do nothing, try LATIN-1 */ + } + try { + CharsetDecoder decoder = StandardCharsets.ISO_8859_1.newDecoder(); + return decoder.decode(ByteBuffer.wrap(codepoints)).toString(); + } catch (CharacterCodingException cce) { // Should not be possible. + return ssid; } } + private static String unescapeSsid(String s) { + StringBuilder sb = new StringBuilder(); + for (int n = 0; n < s.length(); n++) { + char ch = s.charAt(n); + if (ch != '\\' || n >= s.length() - 1) { + sb.append(ch); + } else { + n++; + ch = s.charAt(n); + switch (ch) { + case '"': + case '\\': + default: + sb.append(ch); + break; + case 'e': + sb.append((char) 27); // Escape char + break; + case 'n': + sb.append('\n'); + break; + case 'r': + sb.append('\r'); + break; + case 't': + sb.append('\t'); + break; + case 'x': + if (s.length() - n < 3) { + sb.append('\\').append(ch); + } else { + n++; + sb.append((char) decodeHexPair(s, n)); + n++; + } + break; + } + } + } + return sb.toString(); + } + + private static int decodeHexPair(String s, int position) { + return fromHex(s.charAt(position)) << 4 | fromHex(s.charAt(position + 1)); + } + + private static int fromHex(char ch) { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } else if (ch >= 'A' && ch <= 'F') { + return ch - 'A' + 10; + } else if (ch >= 'a' && ch <= 'f') { + return ch - 'a' + 10; + } else { + throw new NumberFormatException(String.format("Not hex: '%c'", ch)); + } + } public static void delay(long ms) { long until = System.currentTimeMillis() + ms; @@ -297,4 +399,9 @@ public abstract class Utils { } catch (InterruptedException ie) { /**/ } } } + + public static > T mapEnum(int ordinal, Class enumClass) { + T[] constants = enumClass.getEnumConstants(); + return ordinal >= 0 && ordinal < constants.length ? constants[ordinal]: null; + } } diff --git a/packages/Osu/src/com/android/hotspot2/WifiNetworkAdapter.java b/packages/Osu/src/com/android/hotspot2/WifiNetworkAdapter.java deleted file mode 100644 index 84cafa219595d..0000000000000 --- a/packages/Osu/src/com/android/hotspot2/WifiNetworkAdapter.java +++ /dev/null @@ -1,394 +0,0 @@ -package com.android.hotspot2; - -import android.content.Context; -import android.content.Intent; -import android.net.CaptivePortal; -import android.net.ConnectivityManager; -import android.net.ICaptivePortal; -import android.net.Network; -import android.net.wifi.PasspointManagementObjectDefinition; -import android.net.wifi.WifiConfiguration; -import android.net.wifi.WifiEnterpriseConfig; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; -import android.util.Log; - -import com.android.configparse.ConfigBuilder; -import com.android.hotspot2.omadm.MOManager; -import com.android.hotspot2.omadm.MOTree; -import com.android.hotspot2.omadm.OMAConstants; -import com.android.hotspot2.omadm.OMAException; -import com.android.hotspot2.omadm.OMAParser; -import com.android.hotspot2.osu.OSUCertType; -import com.android.hotspot2.osu.OSUInfo; -import com.android.hotspot2.osu.OSUManager; -import com.android.hotspot2.osu.commands.MOData; -import com.android.hotspot2.pps.HomeSP; - -import org.xml.sax.SAXException; - -import java.io.IOException; -import java.net.URL; -import java.security.GeneralSecurityException; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class WifiNetworkAdapter { - private final Context mContext; - private final OSUManager mOSUManager; - private final Map mPasspointConfigs = new HashMap<>(); - - private static class PasspointConfig { - private final WifiConfiguration mWifiConfiguration; - private final MOTree mMOTree; - private final HomeSP mHomeSP; - - private PasspointConfig(WifiConfiguration config) throws IOException, SAXException { - mWifiConfiguration = config; - OMAParser omaParser = new OMAParser(); - mMOTree = omaParser.parse(config.getMoTree(), OMAConstants.PPS_URN); - List spList = MOManager.buildSPs(mMOTree); - if (spList.size() != 1) { - throw new OMAException("Expected exactly one HomeSP, got " + spList.size()); - } - mHomeSP = spList.iterator().next(); - } - - public WifiConfiguration getWifiConfiguration() { - return mWifiConfiguration; - } - - public HomeSP getHomeSP() { - return mHomeSP; - } - - public MOTree getmMOTree() { - return mMOTree; - } - } - - public WifiNetworkAdapter(Context context, OSUManager osuManager) { - mOSUManager = osuManager; - mContext = context; - } - - public void initialize() { - loadAllSps(); - } - - public void networkConfigChange(WifiConfiguration configuration) { - // !!! Watch out for changed r2 configs - remove the MO. - loadAllSps(); - } - - private void loadAllSps() { - WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); - int count = 0; - for (WifiConfiguration config : wifiManager.getPrivilegedConfiguredNetworks()) { - String moTree = config.getMoTree(); - if (moTree != null) { - try { - mPasspointConfigs.put(config.FQDN, new PasspointConfig(config)); - count++; - } catch (IOException | SAXException e) { - Log.w(OSUManager.TAG, "Failed to parse MO: " + e); - } - } - } - Log.d(OSUManager.TAG, "Loaded " + count + " SPs"); - // !!! Detect adds/deletes - } - - public Collection getLoadedSPs() { - List homeSPs = new ArrayList<>(); - for (PasspointConfig config : mPasspointConfigs.values()) { - homeSPs.add(config.getHomeSP()); - } - return homeSPs; - } - - public MOTree getMOTree(HomeSP homeSP) { - PasspointConfig config = mPasspointConfigs.get(homeSP.getFQDN()); - return config != null ? config.getmMOTree() : null; - } - - public void launchBrowser(URL target, Network network, URL endRedirect) { - Log.d(OSUManager.TAG, "Browser to " + target + ", land at " + endRedirect); - - final Intent intent = new Intent( - ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN); - intent.putExtra(ConnectivityManager.EXTRA_NETWORK, network); - intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL, - new CaptivePortal(new ICaptivePortal.Stub() { - @Override - public void appResponse(int response) { - } - })); - //intent.setData(Uri.parse(target.toString())); !!! Doesn't work! - intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL, target.toString()); - intent.setFlags( - Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(intent); - } - - public int addSP(String xml) throws IOException, SAXException { - WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); - return wifiManager.addPasspointManagementObject(xml); - } - - public int modifySP(HomeSP homeSP, Collection mods) throws IOException { - WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); - List defMods = new ArrayList<>(mods.size()); - for (MOData mod : mods) { - defMods.add(new PasspointManagementObjectDefinition(mod.getBaseURI(), - mod.getURN(), mod.getMOTree().toXml())); - } - return wifiManager.modifyPasspointManagementObject(homeSP.getFQDN(), defMods); - } - - public Network getCurrentNetwork() { - WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); - return wifiManager.getCurrentNetwork(); - } - - public WifiConfiguration getActiveWifiConfig() { - WifiInfo wifiInfo = getConnectionInfo(); - if (wifiInfo == null) { - return null; - } - WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); - for (WifiConfiguration config : wifiManager.getConfiguredNetworks()) { - if (config.networkId == wifiInfo.getNetworkId()) { - return config; - } - } - return null; - } - - public WifiInfo getConnectionInfo() { - WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); - return wifiManager.getConnectionInfo(); - } - - public PasspointMatch matchProviderWithCurrentNetwork(String fqdn) { - WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); - int ordinal = wifiManager.matchProviderWithCurrentNetwork(fqdn); - return ordinal >= 0 && ordinal < PasspointMatch.values().length ? - PasspointMatch.values()[ordinal] : null; - } - - public WifiConfiguration getWifiConfig(HomeSP homeSP) { - PasspointConfig passpointConfig = mPasspointConfigs.get(homeSP.getFQDN()); - return passpointConfig != null ? passpointConfig.getWifiConfiguration() : null; - } - - public HomeSP getHomeSP(WifiConfiguration configuration) { - if (configuration.isPasspoint()) { - PasspointConfig config = mPasspointConfigs.get(configuration.FQDN); - return config != null ? config.getHomeSP() : null; - } - return null; - } - - public HomeSP getCurrentSP() { - PasspointConfig passpointConfig = getActivePasspointConfig(); - return passpointConfig != null ? passpointConfig.getHomeSP() : null; - } - - private PasspointConfig getActivePasspointConfig() { - WifiInfo wifiInfo = getConnectionInfo(); - if (wifiInfo == null) { - return null; - } - - for (PasspointConfig passpointConfig : mPasspointConfigs.values()) { - if (passpointConfig.getWifiConfiguration().networkId == wifiInfo.getNetworkId()) { - return passpointConfig; - } - } - return null; - } - - public void doIconQuery(long bssid, String fileName) { - WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); - Log.d("ZXZ", String.format("Icon query for %012x '%s'", bssid, fileName)); - wifiManager.queryPasspointIcon(bssid, fileName); - } - - public Integer addNetwork(HomeSP homeSP, Map> certs, - PrivateKey privateKey, Network osuNetwork) - throws IOException, GeneralSecurityException { - - List aaaTrust = certs.get(OSUCertType.AAA); - if (aaaTrust.isEmpty()) { - aaaTrust = certs.get(OSUCertType.CA); // Get the CAs from the EST flow. - } - - WifiConfiguration config = ConfigBuilder.buildConfig(homeSP, - aaaTrust.iterator().next(), - certs.get(OSUCertType.Client), privateKey); - - WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); - int nwkId = wifiManager.addNetwork(config); - boolean saved = false; - if (nwkId >= 0) { - saved = wifiManager.saveConfiguration(); - } - Log.d(OSUManager.TAG, "Wifi configuration " + nwkId + - " " + (saved ? "saved" : "not saved")); - - if (saved) { - reconnect(osuNetwork, nwkId); - return nwkId; - } else { - return null; - } - } - - public void updateNetwork(HomeSP homeSP, X509Certificate caCert, - List clientCerts, PrivateKey privateKey) - throws IOException, GeneralSecurityException { - - WifiConfiguration config = getWifiConfig(homeSP); - if (config == null) { - throw new IOException("Failed to find matching network config"); - } - Log.d(OSUManager.TAG, "Found matching config " + config.networkId + ", updating"); - - WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig; - WifiConfiguration newConfig = ConfigBuilder.buildConfig(homeSP, - caCert != null ? caCert : enterpriseConfig.getCaCertificate(), - clientCerts, privateKey); - newConfig.networkId = config.networkId; - - WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); - wifiManager.save(newConfig, null); - wifiManager.saveConfiguration(); - } - - /** - * Connect to an OSU provisioning network. The connection should not bring down other existing - * connection and the network should not be made the default network since the connection - * is solely for sign up and is neither intended for nor likely provides access to any - * generic resources. - * - * @param osuInfo The OSU info object that defines the parameters for the network. An OSU - * network is either an open network, or, if the OSU NAI is set, an "OSEN" - * network, which is an anonymous EAP-TLS network with special keys. - * @param info An opaque string that is passed on to any user notification. The string is used - * for the name of the service provider. - * @return an Integer holding the network-id of the just added network configuration, or null - * if the network existed prior to this call (was not added by the OSU infrastructure). - * The value will be used at the end of the OSU flow to delete the network as applicable. - * @throws IOException Issues: - * 1. The network id is not returned. addNetwork cannot be called from here since the method - * runs in the context of the app and doesn't have the appropriate permission. - * 2. The connection is not immediately usable if the network was not previously selected - * manually. - */ - public Integer connect(OSUInfo osuInfo, final String info) throws IOException { - WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); - - WifiConfiguration config = new WifiConfiguration(); - config.SSID = '"' + osuInfo.getOsuSsid() + '"'; - if (osuInfo.getOSUBssid() != 0) { - config.BSSID = Utils.macToString(osuInfo.getOSUBssid()); - Log.d(OSUManager.TAG, String.format("Setting BSSID of '%s' to %012x", - osuInfo.getOsuSsid(), osuInfo.getOSUBssid())); - } - - if (osuInfo.getOSUProvider().getOsuNai() == null) { - config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); - } else { - config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.OSEN); - config.allowedProtocols.set(WifiConfiguration.Protocol.OSEN); - config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP); - config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.GTK_NOT_USED); - config.enterpriseConfig = new WifiEnterpriseConfig(); - config.enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.UNAUTH_TLS); - config.enterpriseConfig.setIdentity(osuInfo.getOSUProvider().getOsuNai()); - // !!! OSEN CA Cert??? - } - - int networkId = wifiManager.addNetwork(config); - if (wifiManager.enableNetwork(networkId, true)) { - return networkId; - } else { - return null; - } - - /* sequence of addNetwork(), enableNetwork(), saveConfiguration() and reconnect() - wifiManager.connect(config, new WifiManager.ActionListener() { - @Override - public void onSuccess() { - // Connection event comes from network change intent registered in initialize - } - - @Override - public void onFailure(int reason) { - mOSUManager.notifyUser(OSUOperationStatus.ProvisioningFailure, - "Cannot connect to OSU network: " + reason, info); - } - }); - return null; - - /* - try { - int nwkID = wifiManager.addOrUpdateOSUNetwork(config); - if (nwkID == WifiConfiguration.INVALID_NETWORK_ID) { - throw new IOException("Failed to add OSU network"); - } - wifiManager.enableNetwork(nwkID, false); - wifiManager.reconnect(); - return nwkID; - } - catch (SecurityException se) { - Log.d("ZXZ", "Blah: " + se, se); - wifiManager.connect(config, new WifiManager.ActionListener() { - @Override - public void onSuccess() { - // Connection event comes from network change intent registered in initialize - } - - @Override - public void onFailure(int reason) { - mOSUManager.notifyUser(OSUOperationStatus.ProvisioningFailure, - "Cannot connect to OSU network: " + reason, info); - } - }); - return null; - } - */ - } - - private void reconnect(Network osuNetwork, int newNwkId) { - WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); - if (osuNetwork != null) { - wifiManager.disableNetwork(osuNetwork.netId); - } - if (newNwkId != WifiConfiguration.INVALID_NETWORK_ID) { - wifiManager.enableNetwork(newNwkId, true); - } - } - - public void deleteNetwork(int id) { - WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); - wifiManager.disableNetwork(id); - wifiManager.forget(id, null); - } - - /** - * Set the re-authentication hold off time for the current network - * - * @param holdoff hold off time in milliseconds - * @param ess set if the hold off pertains to an ESS rather than a BSS - */ - public void setHoldoffTime(long holdoff, boolean ess) { - - } -} diff --git a/packages/Osu/src/com/android/hotspot2/app/IOSUAccessor.aidl b/packages/Osu/src/com/android/hotspot2/app/IOSUAccessor.aidl new file mode 100644 index 0000000000000..500dd2ee32c48 --- /dev/null +++ b/packages/Osu/src/com/android/hotspot2/app/IOSUAccessor.aidl @@ -0,0 +1,8 @@ +package com.android.hotspot2.app; + +import com.android.hotspot2.app.OSUData; + +interface IOSUAccessor { + List getOsuData(); + void selectOsu(int id); +} diff --git a/packages/Osu/src/com/android/hotspot2/app/LocalServiceBinder.java b/packages/Osu/src/com/android/hotspot2/app/LocalServiceBinder.java new file mode 100644 index 0000000000000..8801839fe2624 --- /dev/null +++ b/packages/Osu/src/com/android/hotspot2/app/LocalServiceBinder.java @@ -0,0 +1,15 @@ +package com.android.hotspot2.app; + +import android.os.Binder; + +public class LocalServiceBinder extends Binder { + private final OSUService mDelegate; + + public LocalServiceBinder(OSUService delegate) { + mDelegate = delegate; + } + + public OSUService getService() { + return mDelegate; + } +} diff --git a/packages/Osu/src/com/android/hotspot2/app/MainActivity.java b/packages/Osu/src/com/android/hotspot2/app/MainActivity.java new file mode 100644 index 0000000000000..ae0a45c78ff9c --- /dev/null +++ b/packages/Osu/src/com/android/hotspot2/app/MainActivity.java @@ -0,0 +1,303 @@ +package com.android.hotspot2.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.TaskStackBuilder; +import android.content.ComponentName; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.ServiceConnection; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import com.android.hotspot2.AppBridge; +import com.android.hotspot2.R; +import com.android.hotspot2.osu.OSUManager; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Main activity. + */ +public class MainActivity extends Activity { + private static final int NOTIFICATION_ID = 0; // Used for OSU count + private static final int NOTIFICATION_MESSAGE_ID = 1; // Used for other messages + private static final String ACTION_SVC_BOUND = "SVC_BOUND"; + + private volatile OSUService mLocalService; + + private final ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + LocalServiceBinder binder = (LocalServiceBinder) service; + mLocalService = binder.getService(); + showOsuSelection(mLocalService); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mLocalService = null; + } + }; + + private ListView osuListView; + private OsuListAdapter osuListAdapter; + private String message; + + public MainActivity() { + + } + + @Override + protected void onStop() { + super.onStop(); + if (mLocalService != null) { + unbindService(mConnection); + mLocalService = null; + } + } + + @Override + protected void onResume() { + super.onResume(); + if (message != null) { + showDialog(message); + message = null; + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Intent intent = getIntent(); + Bundle bundle = intent.getExtras(); + + if (intent.getAction() == null) { + if (mLocalService == null) { + bindService(new Intent(this, OSUService.class), mConnection, 0); + } + } else if (intent.getAction().equals(AppBridge.ACTION_OSU_NOTIFICATION)) { + if (bundle == null) { + Log.d(OSUManager.TAG, "No parameters for OSU notification"); + return; + } + if (bundle.containsKey(AppBridge.OSU_COUNT)) { + showOsuCount(bundle.getInt("osu-count", 0), Collections.emptyList()); + } else if (bundle.containsKey(AppBridge.PROV_SUCCESS)) { + showStatus(bundle.getBoolean(AppBridge.PROV_SUCCESS), + bundle.getString(AppBridge.SP_NAME), + bundle.getString(AppBridge.PROV_MESSAGE), + null); + } else if (bundle.containsKey(AppBridge.DEAUTH)) { + showDeauth(bundle.getString(AppBridge.SP_NAME), + bundle.getBoolean(AppBridge.DEAUTH), + bundle.getInt(AppBridge.DEAUTH_DELAY), + bundle.getString(AppBridge.DEAUTH_URL)); + } + } + } + + private void showOsuSelection(final OSUService osuService) { + List osuData = osuService.getOsuData(); + + setContentView(R.layout.activity_main); + Log.d("osu", "osu count:" + osuData.size()); + View noOsuView = findViewById(R.id.no_osu); + if (osuData.size() > 0) { + noOsuView.setVisibility(View.GONE); + osuListAdapter = new OsuListAdapter(this, osuData); + osuListView = (ListView) findViewById(R.id.profile_list); + osuListView.setAdapter(osuListAdapter); + osuListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long id) { + OSUData osuData = (OSUData) adapterView.getAdapter().getItem(position); + Log.d("osu", "launch osu:" + osuData.getName() + + " id:" + osuData.getId()); + osuService.selectOsu(osuData.getId()); + finish(); + } + }); + } else { + noOsuView.setVisibility(View.VISIBLE); + } + } + + private void showOsuCount(int osuCount, List osus) { + if (osuCount > 0) { + printOsuDataList(osus); + sendNotification(osuCount); + } else { + cancelNotification(); + } + finish(); + } + + private void showStatus(boolean provSuccess, String spName, String provMessage, + String remoteStatus) { + if (provSuccess) { + sendDialogMessage( + String.format("Credentials for %s was successfully installed", spName)); + } else { + if (spName != null) { + if (remoteStatus != null) { + sendDialogMessage( + String.format("Failed to install credentials for %s: %s: %s", + spName, provMessage, remoteStatus)); + } else { + sendDialogMessage( + String.format("Failed to install credentials for %s: %s", + spName, provMessage)); + } + } else { + sendDialogMessage( + String.format("Failed to contact OSU: %s", provMessage)); + } + } + } + + private void showDeauth(String spName, boolean ess, int delay, String url) { + String delayReadable = getReadableTimeInSeconds(delay); + if (ess) { + if (delay > 60) { + sendDialogMessage( + String.format("There is an issue connecting to %s [for the next %s]. " + + "Please visit %s for details", spName, delayReadable, url)); + } else { + sendDialogMessage( + String.format("There is an issue connecting to %s. " + + "Please visit %s for details", spName, url)); + } + } else { + sendDialogMessage( + String.format("There is an issue with the closest Access Point for %s. " + + "You may wait %s or move to another Access Point to " + + "regain access. Please visit %s for details.", + spName, delayReadable, url)); + } + } + + private String getReadableTimeInSeconds(int timeSeconds) { + long hours = TimeUnit.SECONDS.toHours(timeSeconds); + long minutes = TimeUnit.SECONDS.toMinutes(timeSeconds) - TimeUnit.HOURS.toMinutes(hours); + long seconds = + timeSeconds - TimeUnit.HOURS.toSeconds(hours) - TimeUnit.MINUTES.toSeconds(minutes); + if (hours > 0) { + return String.format("%02d:%02d:%02d", hours, minutes, seconds); + } else { + return String.format("%ds", seconds); + } + } + + private void sendNotification(int count) { + Notification.Builder builder = + new Notification.Builder(this) + .setContentTitle(String.format("%s OSU available", count)) + .setContentText("Choose one to connect") + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setAutoCancel(false); + Intent resultIntent = new Intent(this, MainActivity.class); + + TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); + stackBuilder.addParentStack(MainActivity.class); + stackBuilder.addNextIntent(resultIntent); + PendingIntent resultPendingIntent = + stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + builder.setContentIntent(resultPendingIntent); + NotificationManager notificationManager = + (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + notificationManager.notify(NOTIFICATION_ID, builder.build()); + } + + private void cancelNotification() { + NotificationManager notificationManager = + (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + notificationManager.cancel(NOTIFICATION_ID); + } + + private void sendDialogMessage(String message) { +// sendNotificationMessage(message); + this.message = message; + } + + private void showDialog(String message) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(message) + .setTitle("OSU"); + builder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + dialogInterface.cancel(); + finish(); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + private void sendNotificationMessage(String title) { + Notification.Builder builder = + new Notification.Builder(this) + .setContentTitle(title) + .setContentText("Click to dismiss.") + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setAutoCancel(true); + NotificationManager notificationManager = + (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + notificationManager.notify(NOTIFICATION_MESSAGE_ID, builder.build()); + } + + private static class OsuListAdapter extends ArrayAdapter { + private Activity activity; + + public OsuListAdapter(Activity activity, List osuDataList) { + super(activity, R.layout.list_item, osuDataList); + this.activity = activity; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + view = LayoutInflater.from(getContext()).inflate(R.layout.list_item, parent, false); + } + OSUData osuData = getItem(position); + TextView osuName = (TextView) view.findViewById(R.id.profile_name); + osuName.setText(osuData.getName()); + TextView osuDetail = (TextView) view.findViewById(R.id.profile_detail); + osuDetail.setText(osuData.getServiceDescription()); + ImageView osuIcon = (ImageView) view.findViewById(R.id.profile_logo); + byte[] iconData = osuData.getIconData(); + osuIcon.setImageDrawable( + new BitmapDrawable(activity.getResources(), + BitmapFactory.decodeByteArray(iconData, 0, iconData.length))); + return view; + } + } + + private void printOsuDataList(List osuDataList) { + for (OSUData osuData : osuDataList) { + Log.d("osu", String.format("OSUData:[%s][%s][%d]", + osuData.getName(), osuData.getServiceDescription(), + osuData.getId())); + } + } + +} diff --git a/packages/Osu/src/com/android/hotspot2/app/OSUData.aidl b/packages/Osu/src/com/android/hotspot2/app/OSUData.aidl new file mode 100644 index 0000000000000..3407f47cbd627 --- /dev/null +++ b/packages/Osu/src/com/android/hotspot2/app/OSUData.aidl @@ -0,0 +1,4 @@ +package com.android.hotspot2.app; + +parcelable OSUData; + diff --git a/packages/Osu/src/com/android/hotspot2/app/OSUData.java b/packages/Osu/src/com/android/hotspot2/app/OSUData.java new file mode 100644 index 0000000000000..17cc49b773348 --- /dev/null +++ b/packages/Osu/src/com/android/hotspot2/app/OSUData.java @@ -0,0 +1,69 @@ +package com.android.hotspot2.app; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.hotspot2.flow.OSUInfo; +import com.android.hotspot2.osu.OSUManager; + +public class OSUData implements Parcelable { + private final String mName; + private final String mServiceDescription; + private final byte[] mIconData; + private final int mId; + + public OSUData(OSUInfo osuInfo) { + mName = osuInfo.getName(OSUManager.LOCALE); + mServiceDescription = osuInfo.getServiceDescription(OSUManager.LOCALE); + mIconData = osuInfo.getIconFileElement().getIconData(); + mId = osuInfo.getOsuID(); + } + + public String getName() { + return mName; + } + + public String getServiceDescription() { + return mServiceDescription; + } + + public byte[] getIconData() { + return mIconData; + } + + public int getId() { + return mId; + } + + private OSUData(Parcel in) { + mName = in.readString(); + mServiceDescription = in.readString(); + int iconSize = in.readInt(); + mIconData = new byte[iconSize]; + in.readByteArray(mIconData); + mId = in.readInt(); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public OSUData createFromParcel(Parcel in) { + return new OSUData(in); + } + + public OSUData[] newArray(int size) { + return new OSUData[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mName); + dest.writeString(mServiceDescription); + dest.writeByteArray(mIconData); + dest.writeInt(mId); + } +} diff --git a/packages/Osu/src/com/android/hotspot2/app/OSUService.java b/packages/Osu/src/com/android/hotspot2/app/OSUService.java new file mode 100644 index 0000000000000..62a203dbfde0b --- /dev/null +++ b/packages/Osu/src/com/android/hotspot2/app/OSUService.java @@ -0,0 +1,202 @@ +package com.android.hotspot2.app; + +import android.app.IntentService; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; + +import com.android.anqp.OSUProvider; +import com.android.hotspot2.PasspointMatch; +import com.android.hotspot2.osu.OSUManager; + +import java.io.IOException; +import java.util.List; + +/** + * This is the Hotspot 2.0 release 2 OSU background service that is continuously running and caches + * OSU information. + * + * The OSU App is made up of two services; FlowService and OSUService. + * + * OSUService is a long running light weight service, kept alive throughout the lifetime of the + * operating system by being bound from the framework (in WifiManager in stage + * PHASE_THIRD_PARTY_APPS_CAN_START), and is responsible for continuously caching OSU information + * and notifying the UI when OSUs are available. + * + * FlowService is only started on demand from OSUService and is responsible for handling actual + * provisioning and remediation flows, and requires a fairly significant memory footprint. + * + * FlowService is defined to run in its own process through the definition + * + * in the AndroidManifest. + * This is done as a means to keep total app memory footprint low (pss < 10M) and only start the + * FlowService on demand and make it available for "garbage collection" by the OS when not in use. + */ +public class OSUService extends IntentService { + public static final String REMEDIATION_DONE_ACTION = "com.android.hotspot2.REMEDIATION_DONE"; + public static final String REMEDIATION_FQDN_EXTRA = "com.android.hotspot2.REMEDIATION_FQDN"; + public static final String REMEDIATION_POLICY_EXTRA = "com.android.hotspot2.REMEDIATION_POLICY"; + + private static final String[] INTENTS = { + WifiManager.SCAN_RESULTS_AVAILABLE_ACTION, + WifiManager.PASSPOINT_WNM_FRAME_RECEIVED_ACTION, + WifiManager.PASSPOINT_ICON_RECEIVED_ACTION, + WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION, + WifiManager.WIFI_STATE_CHANGED_ACTION, + WifiManager.NETWORK_STATE_CHANGED_ACTION, + REMEDIATION_DONE_ACTION + }; + + private OSUManager mOsuManager; + private final LocalServiceBinder mLocalServiceBinder; + + public OSUService() { + super("OSUService"); + mLocalServiceBinder = new LocalServiceBinder(this); + } + + /* + public final class OSUAccessorImpl extends IOSUAccessor.Stub { + public List getOsuData() { + List infos = getOsuInfos(); + List data = new ArrayList<>(infos.size()); + for (OSUInfo osuInfo : infos) { + data.add(new OSUData(osuInfo)); + } + return data; + } + + public void selectOsu(int id) { + OSUService.this.selectOsu(id); + } + } + */ + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + onHandleIntent(intent); + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleIntent(intent.getAction(), intent); + } + }; + for (String intentString : INTENTS) { + registerReceiver(receiver, new IntentFilter(intentString)); + } + return mLocalServiceBinder; + } + + @Override + protected void onHandleIntent(Intent intent) { + if (intent == null) { + Log.d(OSUManager.TAG, "Null intent!"); + return; + } + //handleIntent(intent.getStringExtra(MainActivity.ACTION_KEY), intent); + } + + private void handleIntent(String action, Intent intent) { + WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); + Bundle bundle = intent.getExtras(); + if (mOsuManager == null) { + mOsuManager = new OSUManager(this); + } + Log.d(OSUManager.TAG, "Got intent " + intent.getAction()); + + switch (action) { + case WifiManager.SCAN_RESULTS_AVAILABLE_ACTION: + mOsuManager.pushScanResults(wifiManager.getScanResults()); + break; + case WifiManager.PASSPOINT_WNM_FRAME_RECEIVED_ACTION: + long bssid = bundle.getLong(WifiManager.EXTRA_PASSPOINT_WNM_BSSID); + String url = bundle.getString(WifiManager.EXTRA_PASSPOINT_WNM_URL); + + try { + if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_METHOD)) { + int method = bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_METHOD); + if (method != OSUProvider.OSUMethod.SoapXml.ordinal()) { + Log.w(OSUManager.TAG, "Unsupported remediation method: " + method); + return; + } + PasspointMatch match = null; + if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_PPOINT_MATCH)) { + int ordinal = + bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_PPOINT_MATCH); + if (ordinal >= 0 && ordinal < PasspointMatch.values().length) { + match = PasspointMatch.values()[ordinal]; + } + } + mOsuManager.wnmRemediate(bssid, url, match); + } else if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_ESS)) { + boolean ess = bundle.getBoolean(WifiManager.EXTRA_PASSPOINT_WNM_ESS); + int delay = bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_DELAY); + mOsuManager.deauth(bssid, ess, delay, url); + } else { + Log.w(OSUManager.TAG, "Unknown WNM event"); + } + } catch (IOException e) { + Log.w(OSUManager.TAG, "Remediation event failed to parse: " + e); + } + break; + case WifiManager.PASSPOINT_ICON_RECEIVED_ACTION: + mOsuManager.notifyIconReceived( + bundle.getLong(WifiManager.EXTRA_PASSPOINT_ICON_BSSID), + bundle.getString(WifiManager.EXTRA_PASSPOINT_ICON_FILE), + bundle.getByteArray(WifiManager.EXTRA_PASSPOINT_ICON_DATA)); + break; + case WifiManager.NETWORK_STATE_CHANGED_ACTION: + mOsuManager.networkConnectChange( + (WifiInfo) intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO)); + break; + case WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION: + boolean multiNetwork = + bundle.getBoolean(WifiManager.EXTRA_MULTIPLE_NETWORKS_CHANGED, false); + if (multiNetwork) { + mOsuManager.networkConfigChanged(); + } else if (bundle.getInt(WifiManager.EXTRA_CHANGE_REASON, + WifiManager.CHANGE_REASON_CONFIG_CHANGE) + == WifiManager.CHANGE_REASON_REMOVED) { + WifiConfiguration configuration = + intent.getParcelableExtra(WifiManager.EXTRA_WIFI_CONFIGURATION); + mOsuManager.networkDeleted(configuration); + } else { + mOsuManager.networkConfigChanged(); + } + break; + case WifiManager.WIFI_STATE_CHANGED_ACTION: + int state = bundle.getInt(WifiManager.EXTRA_WIFI_STATE); + if (state == WifiManager.WIFI_STATE_DISABLED) { + mOsuManager.wifiStateChange(false); + } else if (state == WifiManager.WIFI_STATE_ENABLED) { + mOsuManager.wifiStateChange(true); + } + break; + case REMEDIATION_DONE_ACTION: + String fqdn = bundle.getString(REMEDIATION_FQDN_EXTRA); + boolean policy = bundle.getBoolean(REMEDIATION_POLICY_EXTRA); + mOsuManager.remediationDone(fqdn, policy); + break; + } + } + + public List getOsuData() { + return mOsuManager.getAvailableOSUs(); + } + + public void selectOsu(int id) { + mOsuManager.setOSUSelection(id); + } +} diff --git a/packages/Osu/src/com/android/hotspot2/est/ESTHandler.java b/packages/Osu/src/com/android/hotspot2/est/ESTHandler.java index a83eba4e2e904..cdcff80174975 100644 --- a/packages/Osu/src/com/android/hotspot2/est/ESTHandler.java +++ b/packages/Osu/src/com/android/hotspot2/est/ESTHandler.java @@ -14,7 +14,7 @@ import com.android.hotspot2.asn1.Asn1Object; import com.android.hotspot2.asn1.Asn1Oid; import com.android.hotspot2.asn1.OidMappings; import com.android.hotspot2.osu.HTTPHandler; -import com.android.hotspot2.osu.OSUManager; +import com.android.hotspot2.osu.OSUFlowManager; import com.android.hotspot2.osu.OSUSocketFactory; import com.android.hotspot2.osu.commands.GetCertData; import com.android.hotspot2.pps.HomeSP; @@ -81,7 +81,7 @@ public class ESTHandler implements AutoCloseable { private PrivateKey mClientKey; public ESTHandler(GetCertData certData, Network network, OMADMAdapter omadmAdapter, - KeyManager km, KeyStore ks, HomeSP homeSP, OSUManager.FlowType flowType) + KeyManager km, KeyStore ks, HomeSP homeSP, OSUFlowManager.FlowType flowType) throws IOException, GeneralSecurityException { mURL = new URL(certData.getServer()); mUser = certData.getUserName(); diff --git a/packages/Osu/src/com/android/hotspot2/flow/FlowService.java b/packages/Osu/src/com/android/hotspot2/flow/FlowService.java new file mode 100644 index 0000000000000..8bbbf06344d56 --- /dev/null +++ b/packages/Osu/src/com/android/hotspot2/flow/FlowService.java @@ -0,0 +1,168 @@ +package com.android.hotspot2.flow; + +import android.annotation.Nullable; +import android.app.IntentService; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Network; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.IBinder; +import android.util.Log; + +import com.android.hotspot2.osu.OSUFlowManager; +import com.android.hotspot2.osu.OSUManager; +import com.android.hotspot2.osu.OSUOperationStatus; +import com.android.hotspot2.pps.HomeSP; + +import java.io.IOException; + +/** + * This is the Hotspot 2.0 release 2 service that handles actual provisioning and remediation flows. + * + * The OSU App is made up of two services; FlowService and OSUService. + * + * OSUService is a long running light weight service, kept alive throughout the lifetime of the + * operating system by being bound from the framework (in WifiManager in stage + * PHASE_THIRD_PARTY_APPS_CAN_START), and is responsible for continuously caching OSU information + * and notifying the UI when OSUs are available. + * + * FlowService is only started on demand from OSUService and is responsible for handling actual + * provisioning and remediation flows, and requires a fairly significant memory footprint. + * + * FlowService is defined to run in its own process through the definition + * + * in the AndroidManifest. + * This is done as a means to keep total app memory footprint low (pss < 10M) and only start the + * FlowService on demand and make it available for "garbage collection" by the OS when not in use. + */ +public class FlowService extends IntentService { + private static final String[] INTENTS = { + WifiManager.NETWORK_STATE_CHANGED_ACTION + }; + + private OSUFlowManager mOSUFlowManager; + private PlatformAdapter mPlatformAdapter; + private final FlowServiceImpl mOSUAccessor = new FlowServiceImpl(); + + /* + public FlowService(Context context) { + super("FlowService"); + mOSUFlowManager = new OSUFlowManager(); + mPlatformAdapter = new PlatformAdapter(context); + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleIntent(intent.getAction(), intent); + } + }; + for (String intentString : INTENTS) { + context.registerReceiver(receiver, new IntentFilter(intentString)); + } + } + */ + + public FlowService() { + super("FlowService"); + } + + public final class FlowServiceImpl extends IFlowService.Stub { + public void provision(OSUInfo osuInfo) { + FlowService.this.provision(osuInfo); + } + + public void remediate(String spFqdn, String url, boolean policy, Network network) { + FlowService.this.remediate(spFqdn, url, policy, network); + } + + public void spDeleted(String fqdn) { + FlowService.this.serviceProviderDeleted(fqdn); + } + } + + public void provision(OSUInfo osuInfo) { + try { + mOSUFlowManager.appendFlow(new OSUFlowManager.OSUFlow(osuInfo, mPlatformAdapter, + mPlatformAdapter.getKeyManager(null))); + } catch (IOException ioe) { + mPlatformAdapter.notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(), + osuInfo.getName(PlatformAdapter.LOCALE)); + } + } + + /** + * Initiate remediation + * @param spFqdn The FQDN of the current SP, not set for WNM based remediation + * @param url The URL of the remediation server + * @param policy Set if this is a policy update rather than a subscription update + * @param network The network to use for remediation + */ + public void remediate(String spFqdn, String url, boolean policy, Network network) { + Log.d(OSUManager.TAG, "Starting remediation for " + spFqdn + " to " + url); + if (spFqdn != null) { + HomeSP homeSP = mPlatformAdapter.getHomeSP(spFqdn); + if (homeSP == null) { + Log.e(OSUManager.TAG, "No HomeSP object matches '" + spFqdn + "'"); + return; + } + + try { + mOSUFlowManager.appendFlow(new OSUFlowManager.OSUFlow(network, url, + mPlatformAdapter, mPlatformAdapter.getKeyManager(homeSP), + homeSP, policy + ? OSUFlowManager.FlowType.Policy : OSUFlowManager.FlowType.Remediation)); + } catch (IOException ioe) { + Log.e(OSUManager.TAG, "Failed to remediate: " + ioe, ioe); + } + } else { + HomeSP homeSP = mPlatformAdapter.getCurrentSP(); + if (homeSP == null) { + Log.e(OSUManager.TAG, "Remediation request on unidentified Passpoint network "); + return; + } + + try { + mOSUFlowManager.appendFlow(new OSUFlowManager.OSUFlow(network, url, + mPlatformAdapter, mPlatformAdapter.getKeyManager(homeSP), homeSP, + OSUFlowManager.FlowType.Remediation)); + } catch (IOException ioe) { + Log.e(OSUManager.TAG, "Failed to start remediation: " + ioe, ioe); + } + } + } + + public void serviceProviderDeleted(String fqdn) { + mPlatformAdapter.serviceProviderDeleted(fqdn); + } + + @Override + public IBinder onBind(Intent intent) { + mOSUFlowManager = new OSUFlowManager(this); + mPlatformAdapter = new PlatformAdapter(this); + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleIntent(intent.getAction(), intent); + } + }; + for (String intentString : INTENTS) { + registerReceiver(receiver, new IntentFilter(intentString)); + } + return mOSUAccessor; + } + + @Override + protected void onHandleIntent(@Nullable Intent intent) { + } + + private void handleIntent(String action, Intent intent) { + if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) { + WifiInfo wifiInfo = intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO); + if (wifiInfo != null) { + mOSUFlowManager.networkChange(); + } + } + } +} diff --git a/packages/Osu/src/com/android/hotspot2/flow/IFlowService.aidl b/packages/Osu/src/com/android/hotspot2/flow/IFlowService.aidl new file mode 100644 index 0000000000000..a61274fe88125 --- /dev/null +++ b/packages/Osu/src/com/android/hotspot2/flow/IFlowService.aidl @@ -0,0 +1,10 @@ +package com.android.hotspot2.flow; + +import com.android.hotspot2.flow.OSUInfo; +import android.net.Network; + +interface IFlowService { + void provision(in OSUInfo osuInfo); + void remediate(String spFqdn, String url, boolean policy, in Network network); + void spDeleted(String fqdn); +} diff --git a/packages/Osu/src/com/android/hotspot2/flow/OSUInfo.aidl b/packages/Osu/src/com/android/hotspot2/flow/OSUInfo.aidl new file mode 100644 index 0000000000000..172486e3f3bf3 --- /dev/null +++ b/packages/Osu/src/com/android/hotspot2/flow/OSUInfo.aidl @@ -0,0 +1,3 @@ +package com.android.hotspot2.flow; + +parcelable OSUInfo; diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUInfo.java b/packages/Osu/src/com/android/hotspot2/flow/OSUInfo.java similarity index 73% rename from packages/Osu/src/com/android/hotspot2/osu/OSUInfo.java rename to packages/Osu/src/com/android/hotspot2/flow/OSUInfo.java index 1fa1dbd3ec523..401eccb96d050 100644 --- a/packages/Osu/src/com/android/hotspot2/osu/OSUInfo.java +++ b/packages/Osu/src/com/android/hotspot2/flow/OSUInfo.java @@ -1,6 +1,8 @@ -package com.android.hotspot2.osu; +package com.android.hotspot2.flow; import android.net.wifi.ScanResult; +import android.os.Parcel; +import android.os.Parcelable; import android.util.Log; import com.android.anqp.HSIconFileElement; @@ -8,15 +10,18 @@ import com.android.anqp.I18Name; import com.android.anqp.IconInfo; import com.android.anqp.OSUProvider; import com.android.hotspot2.Utils; +import com.android.hotspot2.osu.OSUManager; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.ListIterator; import java.util.Locale; import java.util.Set; -public class OSUInfo { +public class OSUInfo implements Parcelable { public static final String GenericLocale = "zxx"; public enum IconStatus { @@ -29,7 +34,6 @@ public class OSUInfo { private final long mBSSID; private final long mHESSID; private final int mAnqpDomID; - private final String mOsuSsid; private final String mAdvertisingSSID; private final OSUProvider mOSUProvider; private final int mOsuID; @@ -39,13 +43,12 @@ public class OSUInfo { private String mIconFileName; private IconInfo mIconInfo; - public OSUInfo(ScanResult scanResult, String ssid, OSUProvider osuProvider, int osuID) { + public OSUInfo(ScanResult scanResult, OSUProvider osuProvider, int osuID) { mOsuID = osuID; mBSSID = Utils.parseMac(scanResult.BSSID); mHESSID = scanResult.hessid; mAnqpDomID = scanResult.anqpDomainId; mAdvertisingSSID = scanResult.SSID; - mOsuSsid = ssid; mOSUProvider = osuProvider; } @@ -99,11 +102,11 @@ public class OSUInfo { if (locale == null || name.getLocale().equals(locale)) { return name.getText(); } - scoreList.add(new ScoreEntry(name.getText(), + scoreList.add(new ScoreEntry<>(name.getText(), languageScore(name.getLanguage(), locale))); } Collections.sort(scoreList); - return scoreList.isEmpty() ? null : scoreList.iterator().next().getData(); + return scoreList.isEmpty() ? null : scoreList.get(scoreList.size() - 1).getData(); } public String getServiceDescription(Locale locale) { @@ -116,7 +119,7 @@ public class OSUInfo { languageScore(service.getLanguage(), locale))); } Collections.sort(scoreList); - return scoreList.isEmpty() ? null : scoreList.iterator().next().getData(); + return scoreList.isEmpty() ? null : scoreList.get(scoreList.size() - 1).getData(); } public int getOsuID() { @@ -182,6 +185,11 @@ public class OSUInfo { public int compareTo(ScoreEntry other) { return Integer.compare(mScore, other.mScore); } + + @Override + public String toString() { + return String.format("%d for '%s'", mScore, mData); + } } public List getIconInfo(Locale locale, Set types, int width, int height) { @@ -219,8 +227,9 @@ public class OSUInfo { } Collections.sort(matches); List icons = new ArrayList<>(matches.size()); - for (ScoreEntry scoredIcon : matches) { - icons.add(scoredIcon.getData()); + ListIterator> matchIterator = matches.listIterator(matches.size()); + while (matchIterator.hasPrevious()) { + icons.add(matchIterator.previous().getData()); } return icons; } @@ -243,7 +252,7 @@ public class OSUInfo { } public String getOsuSsid() { - return mOsuSsid; + return mOSUProvider.getSSID(); } public OSUProvider getOSUProvider() { @@ -253,6 +262,60 @@ public class OSUInfo { @Override public String toString() { return String.format("OSU Info '%s' %012x -> %s, icon %s", - mOsuSsid, mBSSID, getServiceDescription(null), mIconStatus); + getOsuSsid(), mBSSID, getServiceDescription(null), mIconStatus); + } + + private OSUInfo(Parcel in) { + mBSSID = in.readLong(); + mHESSID = in.readLong(); + mAnqpDomID = in.readInt(); + mAdvertisingSSID = in.readString(); + mOsuID = in.readInt(); + mOSUBssid = in.readLong(); + mIconFileName = in.readString(); + mIconStatus = Utils.mapEnum(in.readInt(), IconStatus.class); + OSUProvider osuProvider; + try { + osuProvider = new OSUProvider(in); + } catch (IOException ioe) { + osuProvider = null; + } + mOSUProvider = osuProvider; + if (osuProvider == null) { + return; + } + mIconFileElement = new HSIconFileElement(in); + int iconIndex = in.readInt(); + mIconInfo = iconIndex >= 0 && iconIndex < mOSUProvider.getIcons().size() + ? mOSUProvider.getIcons().get(iconIndex) : null; + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public OSUInfo createFromParcel(Parcel in) { + return new OSUInfo(in); + } + + public OSUInfo[] newArray(int size) { + return new OSUInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mBSSID); + dest.writeLong(mHESSID); + dest.writeInt(mAnqpDomID); + dest.writeString(mAdvertisingSSID); + dest.writeInt(mOsuID); + dest.writeLong(mOSUBssid); + dest.writeString(mIconFileName); + dest.writeInt(mIconStatus.ordinal()); + mOSUProvider.writeParcel(dest); + mIconFileElement.writeParcel(dest); } } diff --git a/packages/Osu/src/com/android/hotspot2/flow/PlatformAdapter.java b/packages/Osu/src/com/android/hotspot2/flow/PlatformAdapter.java new file mode 100644 index 0000000000000..0c901711cec3c --- /dev/null +++ b/packages/Osu/src/com/android/hotspot2/flow/PlatformAdapter.java @@ -0,0 +1,611 @@ +package com.android.hotspot2.flow; + +import android.content.Context; +import android.content.Intent; +import android.net.Network; +import android.net.wifi.PasspointManagementObjectDefinition; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiEnterpriseConfig; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.util.Log; + +import com.android.configparse.ConfigBuilder; +import com.android.hotspot2.AppBridge; +import com.android.hotspot2.Utils; +import com.android.hotspot2.app.OSUService; +import com.android.hotspot2.omadm.MOManager; +import com.android.hotspot2.omadm.MOTree; +import com.android.hotspot2.omadm.OMAConstants; +import com.android.hotspot2.omadm.OMAException; +import com.android.hotspot2.omadm.OMAParser; +import com.android.hotspot2.osu.ClientKeyManager; +import com.android.hotspot2.osu.OSUCertType; +import com.android.hotspot2.osu.OSUManager; +import com.android.hotspot2.osu.OSUOperationStatus; +import com.android.hotspot2.osu.OSUSocketFactory; +import com.android.hotspot2.osu.WiFiKeyManager; +import com.android.hotspot2.osu.commands.MOData; +import com.android.hotspot2.pps.HomeSP; + +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.net.ssl.KeyManager; + +public class PlatformAdapter { + private static final String TAG = "OSUFLOW"; + + public static final Locale LOCALE = Locale.getDefault(); + + public static final String CERT_WFA_ALIAS = "wfa-root-"; + public static final String CERT_REM_ALIAS = "rem-"; + public static final String CERT_POLICY_ALIAS = "pol-"; + public static final String CERT_SHARED_ALIAS = "shr-"; + public static final String CERT_CLT_CERT_ALIAS = "clt-"; + public static final String CERT_CLT_KEY_ALIAS = "prv-"; + public static final String CERT_CLT_CA_ALIAS = "aaa-"; + + private static final String KEYSTORE_FILE = "passpoint.ks"; + + private final Context mContext; + private final File mKeyStoreFile; + private final KeyStore mKeyStore; + private final AppBridge mAppBridge; + private final Map mPasspointConfigs; + + public PlatformAdapter(Context context) { + mContext = context; + mAppBridge = new AppBridge(context); + + File appFolder = context.getFilesDir(); + mKeyStoreFile = new File(appFolder, KEYSTORE_FILE); + Log.d(TAG, "KS file: " + mKeyStoreFile.getPath()); + KeyStore ks = null; + try { + //ks = loadKeyStore(KEYSTORE_FILE, readCertsFromDisk(WFA_CA_LOC)); + ks = loadKeyStore(mKeyStoreFile, OSUSocketFactory.buildCertSet()); + } catch (IOException e) { + Log.e(TAG, "Failed to initialize Passpoint keystore, OSU disabled", e); + } + mKeyStore = ks; + + mPasspointConfigs = loadAllSps(context); + } + + private static KeyStore loadKeyStore(File ksFile, Set diskCerts) + throws IOException { + try { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + if (ksFile.exists()) { + try (FileInputStream in = new FileInputStream(ksFile)) { + keyStore.load(in, null); + } + + // Note: comparing two sets of certs does not work. + boolean mismatch = false; + int loadCount = 0; + for (int n = 0; n < 1000; n++) { + String alias = String.format("%s%d", CERT_WFA_ALIAS, n); + Certificate cert = keyStore.getCertificate(alias); + if (cert == null) { + break; + } + + loadCount++; + boolean matched = false; + Iterator iter = diskCerts.iterator(); + while (iter.hasNext()) { + X509Certificate diskCert = iter.next(); + if (cert.equals(diskCert)) { + iter.remove(); + matched = true; + break; + } + } + if (!matched) { + mismatch = true; + break; + } + } + if (mismatch || !diskCerts.isEmpty()) { + Log.d(TAG, "Re-seeding Passpoint key store with " + + diskCerts.size() + " WFA certs"); + for (int n = 0; n < 1000; n++) { + String alias = String.format("%s%d", CERT_WFA_ALIAS, n); + Certificate cert = keyStore.getCertificate(alias); + if (cert == null) { + break; + } else { + keyStore.deleteEntry(alias); + } + } + int index = 0; + for (X509Certificate caCert : diskCerts) { + keyStore.setCertificateEntry( + String.format("%s%d", CERT_WFA_ALIAS, index), caCert); + index++; + } + + try (FileOutputStream out = new FileOutputStream(ksFile)) { + keyStore.store(out, null); + } + } else { + Log.d(TAG, "Loaded Passpoint key store with " + loadCount + " CA certs"); + Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + Log.d("ZXC", "KS Alias '" + aliases.nextElement() + "'"); + } + } + } else { + keyStore.load(null, null); + int index = 0; + for (X509Certificate caCert : diskCerts) { + keyStore.setCertificateEntry( + String.format("%s%d", CERT_WFA_ALIAS, index), caCert); + index++; + } + + try (FileOutputStream out = new FileOutputStream(ksFile)) { + keyStore.store(out, null); + } + Log.d(TAG, "Initialized Passpoint key store with " + + diskCerts.size() + " CA certs"); + } + return keyStore; + } catch (GeneralSecurityException gse) { + throw new IOException(gse); + } + } + + private static Map loadAllSps(Context context) { + Map passpointConfigs = new HashMap<>(); + + WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + List configs = wifiManager.getPrivilegedConfiguredNetworks(); + if (configs == null) { + return passpointConfigs; + } + int count = 0; + for (WifiConfiguration config : configs) { + String moTree = config.getMoTree(); + if (moTree != null) { + try { + passpointConfigs.put(config.FQDN, new PasspointConfig(config)); + count++; + } catch (IOException | SAXException e) { + Log.w(OSUManager.TAG, "Failed to parse MO: " + e); + } + } + } + Log.d(OSUManager.TAG, "Loaded " + count + " SPs"); + return passpointConfigs; + } + + public KeyStore getKeyStore() { + return mKeyStore; + } + + public Context getContext() { + return mContext; + } + + /** + * Connect to an OSU provisioning network. The connection should not bring down other existing + * connection and the network should not be made the default network since the connection + * is solely for sign up and is neither intended for nor likely provides access to any + * generic resources. + * + * @param osuInfo The OSU info object that defines the parameters for the network. An OSU + * network is either an open network, or, if the OSU NAI is set, an "OSEN" + * network, which is an anonymous EAP-TLS network with special keys. + * @return an Integer holding the network-id of the just added network configuration, or null + * if the network existed prior to this call (was not added by the OSU infrastructure). + * The value will be used at the end of the OSU flow to delete the network as applicable. + * @throws IOException Issues: + * 1. The network id is not returned. addNetwork cannot be called from here since the method + * runs in the context of the app and doesn't have the appropriate permission. + * 2. The connection is not immediately usable if the network was not previously selected + * manually. + */ + public Integer connect(OSUInfo osuInfo) throws IOException { + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + + WifiConfiguration config = new WifiConfiguration(); + config.SSID = '"' + osuInfo.getOsuSsid() + '"'; + if (osuInfo.getOSUBssid() != 0) { + config.BSSID = Utils.macToString(osuInfo.getOSUBssid()); + Log.d(OSUManager.TAG, String.format("Setting BSSID of '%s' to %012x", + osuInfo.getOsuSsid(), osuInfo.getOSUBssid())); + } + + if (osuInfo.getOSUProvider().getOsuNai() == null) { + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); + } else { + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.OSEN); + config.allowedProtocols.set(WifiConfiguration.Protocol.OSEN); + config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP); + config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.GTK_NOT_USED); + config.enterpriseConfig = new WifiEnterpriseConfig(); + config.enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.UNAUTH_TLS); + config.enterpriseConfig.setIdentity(osuInfo.getOSUProvider().getOsuNai()); + Set cas = OSUSocketFactory.buildCertSet(); + config.enterpriseConfig.setCaCertificates(cas.toArray(new X509Certificate[cas.size()])); + } + + int networkId = wifiManager.addNetwork(config); + if (networkId < 0) { + throw new IOException("Failed to add OSU network"); + } + if (wifiManager.enableNetwork(networkId, true)) { + return networkId; + } else { + throw new IOException("Failed to enable OSU network"); + } + } + + /** + * @param homeSP The Home SP associated with the keying material in question. Passing + * null returns a "system wide" KeyManager to support pre-provisioned certs based + * on names retrieved from the ClientCertInfo request. + * @return A key manager suitable for the given configuration (or pre-provisioned keys). + */ + public KeyManager getKeyManager(HomeSP homeSP) throws IOException { + return homeSP != null + ? new ClientKeyManager(homeSP, mKeyStore) : new WiFiKeyManager(mKeyStore); + } + + public void provisioningComplete(OSUInfo osuInfo, + MOData moData, Map> certs, + PrivateKey privateKey, Network osuNetwork) { + try { + String xml = moData.getMOTree().toXml(); + HomeSP homeSP = MOManager.buildSP(xml); + + Integer spNwk = addNetwork(homeSP, certs, privateKey, osuNetwork); + if (spNwk == null) { + notifyUser(OSUOperationStatus.ProvisioningFailure, + "Failed to save network configuration", osuInfo.getName(LOCALE)); + } else { + if (addSP(xml) < 0) { + deleteNetwork(spNwk); + Log.e(TAG, "Failed to provision: " + homeSP.getFQDN()); + notifyUser(OSUOperationStatus.ProvisioningFailure, "Failed to add MO", + osuInfo.getName(LOCALE)); + return; + } + Set rootCerts = OSUSocketFactory.getRootCerts(mKeyStore); + X509Certificate remCert = getCert(certs, OSUCertType.Remediation); + X509Certificate polCert = getCert(certs, OSUCertType.Policy); + int newCerts = 0; + if (privateKey != null) { + X509Certificate cltCert = getCert(certs, OSUCertType.Client); + mKeyStore.setKeyEntry(CERT_CLT_KEY_ALIAS + homeSP.getFQDN(), + privateKey, null, new X509Certificate[]{cltCert}); + mKeyStore.setCertificateEntry(CERT_CLT_CERT_ALIAS + homeSP.getFQDN(), cltCert); + newCerts++; + } + boolean usingShared = false; + if (remCert != null) { + if (!rootCerts.contains(remCert)) { + if (remCert.equals(polCert)) { + mKeyStore.setCertificateEntry(CERT_SHARED_ALIAS + homeSP.getFQDN(), + remCert); + usingShared = true; + newCerts++; + } else { + mKeyStore.setCertificateEntry(CERT_REM_ALIAS + homeSP.getFQDN(), + remCert); + newCerts++; + } + } + } + if (!usingShared && polCert != null) { + if (!rootCerts.contains(polCert)) { + mKeyStore.setCertificateEntry(CERT_POLICY_ALIAS + homeSP.getFQDN(), + remCert); + newCerts++; + } + } + + + if (newCerts > 0) { + try (FileOutputStream out = new FileOutputStream(mKeyStoreFile)) { + mKeyStore.store(out, null); + } + } + notifyUser(OSUOperationStatus.ProvisioningSuccess, null, osuInfo.getName(LOCALE)); + Log.d(TAG, "Provisioning complete."); + } + } catch (IOException | GeneralSecurityException | SAXException e) { + Log.e(TAG, "Failed to provision: " + e, e); + notifyUser(OSUOperationStatus.ProvisioningFailure, e.toString(), + osuInfo.getName(LOCALE)); + } + } + + public void remediationComplete(HomeSP homeSP, Collection mods, + Map> certs, + PrivateKey privateKey, boolean policy) + throws IOException, GeneralSecurityException { + + HomeSP altSP = null; + if (modifySP(homeSP, mods) > 0) { + altSP = MOManager.modifySP(homeSP, getMOTree(homeSP), mods); + } + + X509Certificate caCert = null; + List clientCerts = null; + if (certs != null) { + List certList = certs.get(OSUCertType.AAA); + caCert = certList != null && !certList.isEmpty() ? certList.iterator().next() : null; + clientCerts = certs.get(OSUCertType.Client); + } + if (altSP != null || certs != null) { + if (altSP == null) { + altSP = homeSP; + } + updateNetwork(altSP, caCert, clientCerts, privateKey); + } + + Intent intent = new Intent(OSUService.REMEDIATION_DONE_ACTION); + intent.putExtra(OSUService.REMEDIATION_FQDN_EXTRA, homeSP.getFQDN()); + intent.putExtra(OSUService.REMEDIATION_POLICY_EXTRA, policy); + mContext.sendBroadcast(intent); + + notifyUser(OSUOperationStatus.ProvisioningSuccess, null, homeSP.getFriendlyName()); + } + + public void serviceProviderDeleted(String fqdn) { + int count = deleteCerts(mKeyStore, fqdn, + CERT_REM_ALIAS, CERT_POLICY_ALIAS, CERT_SHARED_ALIAS, CERT_CLT_CERT_ALIAS); + + Log.d(TAG, "Passpoint network deleted, removing " + count + " key store entries"); + + try { + if (mKeyStore.getKey(CERT_CLT_KEY_ALIAS + fqdn, null) != null) { + mKeyStore.deleteEntry(CERT_CLT_KEY_ALIAS + fqdn); + } + } catch (GeneralSecurityException e) { + /**/ + } + + if (count > 0) { + try (FileOutputStream out = new FileOutputStream(mKeyStoreFile)) { + mKeyStore.store(out, null); + } catch (IOException | GeneralSecurityException e) { + Log.w(TAG, "Failed to remove certs from key store: " + e); + } + } + } + + private static int deleteCerts(KeyStore keyStore, String fqdn, String... prefixes) { + int count = 0; + for (String prefix : prefixes) { + try { + String alias = prefix + fqdn; + Certificate cert = keyStore.getCertificate(alias); + if (cert != null) { + keyStore.deleteEntry(alias); + count++; + } + } catch (KeyStoreException kse) { + /**/ + } + } + return count; + } + + private static X509Certificate getCert(Map> certMap, + OSUCertType certType) { + List certs = certMap.get(certType); + if (certs == null || certs.isEmpty()) { + return null; + } + return certs.iterator().next(); + } + + public String notifyUser(OSUOperationStatus status, String message, String spName) { + if (status == OSUOperationStatus.UserInputComplete) { + return null; + } + mAppBridge.showStatus(status, spName, message, null); + return null; + } + + public void provisioningFailed(String spName, String message) { + notifyUser(OSUOperationStatus.ProvisioningFailure, message, spName); + } + + private Integer addNetwork(HomeSP homeSP, Map> certs, + PrivateKey privateKey, Network osuNetwork) + throws IOException, GeneralSecurityException { + + List aaaTrust = certs.get(OSUCertType.AAA); + if (aaaTrust.isEmpty()) { + aaaTrust = certs.get(OSUCertType.CA); // Get the CAs from the EST flow. + } + + WifiConfiguration config = ConfigBuilder.buildConfig(homeSP, + aaaTrust.iterator().next(), + certs.get(OSUCertType.Client), privateKey); + + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + int nwkId = wifiManager.addNetwork(config); + boolean saved = false; + if (nwkId >= 0) { + saved = wifiManager.saveConfiguration(); + } + Log.d(OSUManager.TAG, "Wifi configuration " + nwkId + + " " + (saved ? "saved" : "not saved")); + + if (saved) { + reconnect(osuNetwork, nwkId); + return nwkId; + } else { + return null; + } + } + + private void updateNetwork(HomeSP homeSP, X509Certificate caCert, + List clientCerts, PrivateKey privateKey) + throws IOException, GeneralSecurityException { + + WifiConfiguration config = getWifiConfig(homeSP); + if (config == null) { + throw new IOException("Failed to find matching network config"); + } + Log.d(OSUManager.TAG, "Found matching config " + config.networkId + ", updating"); + + WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig; + WifiConfiguration newConfig = ConfigBuilder.buildConfig(homeSP, + caCert != null ? caCert : enterpriseConfig.getCaCertificate(), + clientCerts, privateKey); + newConfig.networkId = config.networkId; + + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + wifiManager.save(newConfig, null); + wifiManager.saveConfiguration(); + } + + private WifiConfiguration getWifiConfig(HomeSP homeSP) { + PasspointConfig passpointConfig = mPasspointConfigs.get(homeSP.getFQDN()); + return passpointConfig != null ? passpointConfig.getWifiConfiguration() : null; + } + + public MOTree getMOTree(HomeSP homeSP) { + PasspointConfig config = mPasspointConfigs.get(homeSP.getFQDN()); + return config != null ? config.getmMOTree() : null; + } + + public HomeSP getHomeSP(String fqdn) { + PasspointConfig passpointConfig = mPasspointConfigs.get(fqdn); + return passpointConfig != null ? passpointConfig.getHomeSP() : null; + } + + public HomeSP getCurrentSP() { + PasspointConfig passpointConfig = getActivePasspointConfig(); + return passpointConfig != null ? passpointConfig.getHomeSP() : null; + } + + private PasspointConfig getActivePasspointConfig() { + WifiInfo wifiInfo = getConnectionInfo(); + if (wifiInfo == null) { + return null; + } + + for (PasspointConfig passpointConfig : mPasspointConfigs.values()) { + if (passpointConfig.getWifiConfiguration().networkId == wifiInfo.getNetworkId()) { + return passpointConfig; + } + } + return null; + } + + private int addSP(String xml) throws IOException, SAXException { + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + return wifiManager.addPasspointManagementObject(xml); + } + + private int modifySP(HomeSP homeSP, Collection mods) throws IOException { + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + List defMods = new ArrayList<>(mods.size()); + for (MOData mod : mods) { + defMods.add(new PasspointManagementObjectDefinition(mod.getBaseURI(), + mod.getURN(), mod.getMOTree().toXml())); + } + return wifiManager.modifyPasspointManagementObject(homeSP.getFQDN(), defMods); + } + + private void reconnect(Network osuNetwork, int newNwkId) { + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + if (osuNetwork != null) { + wifiManager.disableNetwork(osuNetwork.netId); + } + if (newNwkId != WifiConfiguration.INVALID_NETWORK_ID) { + wifiManager.enableNetwork(newNwkId, true); + } + } + + public void deleteNetwork(int id) { + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + wifiManager.disableNetwork(id); + wifiManager.forget(id, null); + } + + public WifiInfo getConnectionInfo() { + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + return wifiManager.getConnectionInfo(); + } + + public Network getCurrentNetwork() { + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + return wifiManager.getCurrentNetwork(); + } + + public WifiConfiguration getActiveWifiConfig() { + WifiInfo wifiInfo = getConnectionInfo(); + if (wifiInfo == null) { + return null; + } + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + List configs = wifiManager.getConfiguredNetworks(); + if (configs == null) { + return null; + } + for (WifiConfiguration config : configs) { + if (config.networkId == wifiInfo.getNetworkId()) { + return config; + } + } + return null; + } + + private static class PasspointConfig { + private final WifiConfiguration mWifiConfiguration; + private final MOTree mMOTree; + private final HomeSP mHomeSP; + + private PasspointConfig(WifiConfiguration config) throws IOException, SAXException { + mWifiConfiguration = config; + OMAParser omaParser = new OMAParser(); + mMOTree = omaParser.parse(config.getMoTree(), OMAConstants.PPS_URN); + List spList = MOManager.buildSPs(mMOTree); + if (spList.size() != 1) { + throw new OMAException("Expected exactly one HomeSP, got " + spList.size()); + } + mHomeSP = spList.iterator().next(); + } + + public WifiConfiguration getWifiConfiguration() { + return mWifiConfiguration; + } + + public HomeSP getHomeSP() { + return mHomeSP; + } + + public MOTree getmMOTree() { + return mMOTree; + } + } +} diff --git a/packages/Osu/src/com/android/hotspot2/osu/ClientKeyManager.java b/packages/Osu/src/com/android/hotspot2/osu/ClientKeyManager.java index 47b2f9347b282..cfc84bbc62259 100644 --- a/packages/Osu/src/com/android/hotspot2/osu/ClientKeyManager.java +++ b/packages/Osu/src/com/android/hotspot2/osu/ClientKeyManager.java @@ -2,6 +2,7 @@ package com.android.hotspot2.osu; import android.util.Log; +import com.android.hotspot2.flow.PlatformAdapter; import com.android.hotspot2.pps.HomeSP; import java.io.IOException; @@ -30,9 +31,9 @@ public class ClientKeyManager implements X509KeyManager { public ClientKeyManager(HomeSP homeSP, KeyStore keyStore) throws IOException { mKeyStore = keyStore; mAliasMap = new HashMap<>(); - mAliasMap.put(OSUCertType.AAA, OSUManager.CERT_CLT_CA_ALIAS + homeSP.getFQDN()); - mAliasMap.put(OSUCertType.Client, OSUManager.CERT_CLT_CERT_ALIAS + homeSP.getFQDN()); - mAliasMap.put(OSUCertType.PrivateKey, OSUManager.CERT_CLT_KEY_ALIAS + homeSP.getFQDN()); + mAliasMap.put(OSUCertType.AAA, PlatformAdapter.CERT_CLT_CA_ALIAS + homeSP.getFQDN()); + mAliasMap.put(OSUCertType.Client, PlatformAdapter.CERT_CLT_CERT_ALIAS + homeSP.getFQDN()); + mAliasMap.put(OSUCertType.PrivateKey, PlatformAdapter.CERT_CLT_KEY_ALIAS + homeSP.getFQDN()); mTempKeys = new HashMap<>(); } diff --git a/packages/Osu/src/com/android/hotspot2/osu/HTTPHandler.java b/packages/Osu/src/com/android/hotspot2/osu/HTTPHandler.java index 1a66fcf198c55..4b583df1ce443 100644 --- a/packages/Osu/src/com/android/hotspot2/osu/HTTPHandler.java +++ b/packages/Osu/src/com/android/hotspot2/osu/HTTPHandler.java @@ -171,8 +171,10 @@ public class HTTPHandler implements AutoCloseable { } public void close() throws IOException { + mSocket.shutdownInput(); + mSocket.shutdownOutput(); + mSocket.close(); mIn.close(); mOut.close(); - mSocket.close(); } } diff --git a/packages/Osu/src/com/android/hotspot2/osu/IconCache.java b/packages/Osu/src/com/android/hotspot2/osu/IconCache.java index 2d82dd86caea5..bd8a018a5f0be 100644 --- a/packages/Osu/src/com/android/hotspot2/osu/IconCache.java +++ b/packages/Osu/src/com/android/hotspot2/osu/IconCache.java @@ -5,6 +5,7 @@ import android.util.Log; import com.android.anqp.HSIconFileElement; import com.android.anqp.IconInfo; import com.android.hotspot2.Utils; +import com.android.hotspot2.flow.OSUInfo; import java.net.ProtocolException; import java.nio.BufferUnderflowException; @@ -132,7 +133,7 @@ public class IconCache extends Thread { if (!mBssids.contains(bssid)) { return 0; } - Log.d("ZXZ", "Updating icon on " + mQueued.size() + " osus"); + Log.d(OSUManager.TAG, "Updating icon on " + mQueued.size() + " osus"); for (OSUInfo osuInfo : mQueued) { osuInfo.setIconFileElement(iconFileElement, mFileName); } @@ -188,29 +189,28 @@ public class IconCache extends Thread { HSIconFileElement iconFileElement = get(key, fileName); if (iconFileElement != null) { osuInfo.setIconFileElement(iconFileElement, fileName); - Log.d("ZXZ", "Icon cache hit for " + osuInfo + "/" + fileName); + Log.d(OSUManager.TAG, "Icon cache hit for " + osuInfo + "/" + fileName); modCount++; } else { FileEntry fileEntry = enqueue(key, fileName, osuInfo); if (fileEntry != null) { - Log.d("ZXZ", "Initiating icon query for " + osuInfo + "/" + fileName); + Log.d(OSUManager.TAG, "Initiating icon query for " + + osuInfo + "/" + fileName); mOsuManager.doIconQuery(osuInfo.getBSSID(), fileName); } else { - Log.d("ZXZ", "Piggybacking icon query for " + osuInfo + "/" + fileName); + Log.d(OSUManager.TAG, "Piggybacking icon query for " + + osuInfo + "/" + fileName); } } } } - Log.d("ZXZ", "Current keys " + current + " from " + osuInfos); - // Drop all non-current ESS's Iterator pendingKeys = mPending.keySet().iterator(); while (pendingKeys.hasNext()) { EssKey key = pendingKeys.next(); if (!current.contains(key)) { pendingKeys.remove(); - Log.d("ZXZ", "Removing pending " + key); } } Iterator cacheKeys = mCache.keySet().iterator(); @@ -218,7 +218,6 @@ public class IconCache extends Thread { EssKey key = cacheKeys.next(); if (!current.contains(key)) { cacheKeys.remove(); - Log.d("ZXZ", "Removing cache " + key); } } return modCount; @@ -235,7 +234,7 @@ public class IconCache extends Thread { } public int notifyIconReceived(long bssid, String fileName, byte[] iconData) { - Log.d("ZXZ", String.format("Icon '%s':%d received from %012x", + Log.d(OSUManager.TAG, String.format("Icon '%s':%d received from %012x", fileName, iconData != null ? iconData.length : -1, bssid)); if (fileName == null || iconData == null) { return 0; @@ -273,7 +272,6 @@ public class IconCache extends Thread { } public void tick(boolean wifiOff) { - Log.d("ZXZ", "Ticking icon cache: " + wifiOff); if (wifiOff) { mPending.clear(); mCache.clear(); @@ -291,10 +289,8 @@ public class IconCache extends Thread { FileEntry fileEntry = fileEntries.next().getValue(); long age = now - fileEntry.getTimestamp(); if (age > REQUERY_TIMEOUT || fileEntry.getAndIncrementRetry() > MAX_RETRY) { - Log.d("ZXZ", "Timing out " + fileEntry); fileEntries.remove(); } else if (age > REQUERY_TIME) { - Log.d("ZXZ", "Retrying " + fileEntry); mOsuManager.doIconQuery(fileEntry.getLastBssid(), fileEntry.getFileName()); } } diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUCache.java b/packages/Osu/src/com/android/hotspot2/osu/OSUCache.java index dadda26dbf0de..260fb723ab369 100644 --- a/packages/Osu/src/com/android/hotspot2/osu/OSUCache.java +++ b/packages/Osu/src/com/android/hotspot2/osu/OSUCache.java @@ -81,6 +81,8 @@ public class OSUCache { for (ScanResult scanResult : scanResults) { AnqpInformationElement[] osuInfo = scanResult.anqpElements; if (osuInfo != null && osuInfo.length > 0) { + Log.d(OSUManager.TAG, scanResult.SSID + + " has " + osuInfo.length + " ANQP elements"); putResult(scanResult, osuInfo); } } @@ -89,6 +91,8 @@ public class OSUCache { private void putResult(ScanResult scanResult, AnqpInformationElement[] elements) { for (AnqpInformationElement ie : elements) { + Log.d(OSUManager.TAG, String.format("ANQP IE %d vid %x size %d", ie.getElementId(), + ie.getVendorId(), ie.getPayload().length)); if (ie.getElementId() == AnqpInformationElement.HS_OSU_PROVIDERS && ie.getVendorId() == AnqpInformationElement.HOTSPOT20_VENDOR_ID) { try { @@ -106,6 +110,7 @@ public class OSUCache { } private void putProviders(ScanResult scanResult, HSOsuProvidersElement osuProviders) { + Log.d(OSUManager.TAG, osuProviders.getProviders().size() + " OSU providers in element"); for (OSUProvider provider : osuProviders.getProviders()) { // Make a predictive put ScanResult existing = mBatchedOSUs.put(provider, scanResult); @@ -126,13 +131,14 @@ public class OSUCache { mCache.put(entry.getKey(), new ScanInstance(entry.getValue(), mInstant)); changes++; if (current == null) { - Log.d("ZXZ", "Add OSU " + entry.getKey() + " from " + entry.getValue().SSID); + Log.d(OSUManager.TAG, + "Add OSU " + entry.getKey() + " from " + entry.getValue().SSID); } else { - Log.d("ZXZ", "Update OSU " + entry.getKey() + " with " + + Log.d(OSUManager.TAG, "Update OSU " + entry.getKey() + " with " + entry.getValue().SSID + " to " + current); } } else { - Log.d("ZXZ", "Existing OSU " + entry.getKey() + ", " + Log.d(OSUManager.TAG, "Existing OSU " + entry.getKey() + ", " + current.getInstant() + " -> " + mInstant); current.updateInstant(mInstant); } @@ -140,7 +146,7 @@ public class OSUCache { for (Map.Entry entry : aged.entrySet()) { if (mInstant - entry.getValue().getInstant() > SCAN_BATCH_HISTORY_SIZE) { - Log.d("ZXZ", "Remove OSU " + entry.getKey() + ", " + Log.d(OSUManager.TAG, "Remove OSU " + entry.getKey() + ", " + entry.getValue().getInstant() + " @ " + mInstant); mCache.remove(entry.getKey()); changes++; diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUClient.java b/packages/Osu/src/com/android/hotspot2/osu/OSUClient.java index d842dbf405885..8179a631dd290 100644 --- a/packages/Osu/src/com/android/hotspot2/osu/OSUClient.java +++ b/packages/Osu/src/com/android/hotspot2/osu/OSUClient.java @@ -11,17 +11,22 @@ package com.android.hotspot2.osu; * subscription-server.r2-testbed-rks IN A 10.123.107.107 */ +import android.content.Context; +import android.content.Intent; import android.net.Network; import android.util.Log; import com.android.hotspot2.OMADMAdapter; import com.android.hotspot2.est.ESTHandler; +import com.android.hotspot2.flow.OSUInfo; +import com.android.hotspot2.flow.PlatformAdapter; import com.android.hotspot2.omadm.OMAConstants; import com.android.hotspot2.omadm.OMANode; import com.android.hotspot2.osu.commands.BrowserURI; import com.android.hotspot2.osu.commands.ClientCertInfo; import com.android.hotspot2.osu.commands.GetCertData; import com.android.hotspot2.osu.commands.MOData; +import com.android.hotspot2.osu.service.RedirectListener; import com.android.hotspot2.pps.Credential; import com.android.hotspot2.pps.HomeSP; import com.android.hotspot2.pps.UpdateInfo; @@ -38,6 +43,7 @@ import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -48,38 +54,46 @@ import javax.net.ssl.KeyManager; public class OSUClient { private static final String TAG = "OSUCLT"; - private static final String TTLS_OSU = - "https://osu-server.r2-testbed-rks.wi-fi.org:9447/OnlineSignup/services/newUser/digest"; - private static final String TLS_OSU = - "https://osu-server.r2-testbed-rks.wi-fi.org:9446/OnlineSignup/services/newUser/certificate"; private final OSUInfo mOSUInfo; private final URL mURL; private final KeyStore mKeyStore; + private final Context mContext; + private volatile HTTPHandler mHTTPHandler; + private volatile RedirectListener mRedirectListener; - public OSUClient(OSUInfo osuInfo, KeyStore ks) throws MalformedURLException { + public OSUClient(OSUInfo osuInfo, KeyStore ks, Context context) throws MalformedURLException { mOSUInfo = osuInfo; mURL = new URL(osuInfo.getOSUProvider().getOSUServer()); mKeyStore = ks; + mContext = context; } - public OSUClient(String osu, KeyStore ks) throws MalformedURLException { + public OSUClient(String osu, KeyStore ks, Context context) throws MalformedURLException { mOSUInfo = null; mURL = new URL(osu); mKeyStore = ks; + mContext = context; } - public void provision(OSUManager osuManager, Network network, KeyManager km) + public OSUInfo getOSUInfo() { + return mOSUInfo; + } + + public void provision(PlatformAdapter platformAdapter, Network network, KeyManager km) throws IOException, GeneralSecurityException { try (HTTPHandler httpHandler = new HTTPHandler(StandardCharsets.UTF_8, - OSUSocketFactory.getSocketFactory(mKeyStore, null, OSUManager.FlowType.Provisioning, - network, mURL, km, true))) { + OSUSocketFactory.getSocketFactory(mKeyStore, null, + OSUFlowManager.FlowType.Provisioning, network, mURL, km, true))) { + + mHTTPHandler = httpHandler; SPVerifier spVerifier = new SPVerifier(mOSUInfo); spVerifier.verify(httpHandler.getOSUCertificate(mURL)); - URL redirectURL = osuManager.prepareUserInput(mOSUInfo.getName(Locale.getDefault())); - OMADMAdapter omadmAdapter = osuManager.getOMADMAdapter(); + URL redirectURL = prepareUserInput(platformAdapter, + mOSUInfo.getName(Locale.getDefault())); + OMADMAdapter omadmAdapter = getOMADMAdapter(); String regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRegistration, null, @@ -135,7 +149,7 @@ public class OSUClient { throw new IOException("Bad or missing session ID in webURL"); } - if (!osuManager.startUserInput(new URL(webURL), network)) { + if (!startUserInput(new URL(webURL), network)) { throw new IOException("User session failed"); } @@ -159,8 +173,8 @@ public class OSUClient { moData = (MOData) provResponse.getCommandData(); } else { try (ESTHandler estHandler = new ESTHandler((GetCertData) provResponse. - getCommandData(), network, osuManager.getOMADMAdapter(), - km, mKeyStore, null, OSUManager.FlowType.Provisioning)) { + getCommandData(), network, getOMADMAdapter(), + km, mKeyStore, null, OSUFlowManager.FlowType.Provisioning)) { estHandler.execute(false); certs.put(OSUCertType.CA, estHandler.getCACerts()); certs.put(OSUCertType.Client, estHandler.getClientCerts()); @@ -197,16 +211,19 @@ public class OSUClient { } retrieveCerts(moData.getMOTree().getRoot(), certs, network, km, mKeyStore); - osuManager.provisioningComplete(mOSUInfo, moData, certs, clientKey, network); + platformAdapter.provisioningComplete(mOSUInfo, moData, certs, clientKey, network); } } - public void remediate(OSUManager osuManager, Network network, KeyManager km, HomeSP homeSP, - OSUManager.FlowType flowType) + public void remediate(PlatformAdapter platformAdapter, Network network, KeyManager km, + HomeSP homeSP, OSUFlowManager.FlowType flowType) throws IOException, GeneralSecurityException { try (HTTPHandler httpHandler = createHandler(network, homeSP, km, flowType)) { - URL redirectURL = osuManager.prepareUserInput(homeSP.getFriendlyName()); - OMADMAdapter omadmAdapter = osuManager.getOMADMAdapter(); + + mHTTPHandler = httpHandler; + + URL redirectURL = prepareUserInput(platformAdapter, homeSP.getFriendlyName()); + OMADMAdapter omadmAdapter = getOMADMAdapter(); String regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRemediation, null, @@ -233,7 +250,7 @@ public class OSUClient { redirectURL.toString(), omadmAdapter.getMO(OMAConstants.DevInfoURN), omadmAdapter.getMO(OMAConstants.DevDetailURN), - osuManager.getMOTree(homeSP)); + platformAdapter.getMOTree(homeSP)); Log.d(TAG, "Upload MO: " + ulMessage); @@ -245,7 +262,7 @@ public class OSUClient { } if (pddResponse.getExecCommand() == ExecCommand.Browser) { - if (flowType == OSUManager.FlowType.Policy) { + if (flowType == OSUFlowManager.FlowType.Policy) { throw new IOException("Browser launch requested in policy flow"); } String webURL = ((BrowserURI) pddResponse.getCommandData()).getURI(); @@ -256,7 +273,7 @@ public class OSUClient { throw new IOException("Bad or missing session ID in webURL"); } - if (!osuManager.startUserInput(new URL(webURL), network)) { + if (!startUserInput(new URL(webURL), network)) { throw new IOException("User session failed"); } @@ -275,7 +292,7 @@ public class OSUClient { } else if (pddResponse.getExecCommand() == ExecCommand.GetCert) { certs = new HashMap<>(); try (ESTHandler estHandler = new ESTHandler((GetCertData) pddResponse. - getCommandData(), network, osuManager.getOMADMAdapter(), + getCommandData(), network, getOMADMAdapter(), km, mKeyStore, homeSP, flowType)) { estHandler.execute(true); certs.put(OSUCertType.CA, estHandler.getCACerts()); @@ -347,17 +364,54 @@ public class OSUClient { // There's a chicken and egg here: If the config is saved before sending update complete // the network is lost and the remediation flow fails. try { - osuManager.remediationComplete(homeSP, mods, certs, clientKey); + platformAdapter.remediationComplete(homeSP, mods, certs, clientKey, + flowType == OSUFlowManager.FlowType.Policy); } catch (IOException | GeneralSecurityException e) { - osuManager.provisioningFailed(homeSP.getFriendlyName(), e.getMessage(), homeSP, - OSUManager.FlowType.Remediation); + platformAdapter.provisioningFailed(homeSP.getFriendlyName(), e.getMessage()); error = OSUError.CommandFailed; } } } + private OMADMAdapter getOMADMAdapter() { + return OMADMAdapter.getInstance(mContext); + } + + private URL prepareUserInput(PlatformAdapter platformAdapter, String spName) + throws IOException { + mRedirectListener = new RedirectListener(platformAdapter, spName); + return mRedirectListener.getURL(); + } + + private boolean startUserInput(URL target, Network network) + throws IOException { + mRedirectListener.startService(); + + Intent intent = new Intent(mContext, OSUWebView.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(OSUWebView.OSU_NETWORK, network); + intent.putExtra(OSUWebView.OSU_URL, target.toString()); + mContext.startActivity(intent); + + return mRedirectListener.waitForUser(); + } + + public void close(boolean abort) { + if (mRedirectListener != null) { + mRedirectListener.abort(); + mRedirectListener = null; + } + if (abort) { + try { + mHTTPHandler.close(); + } catch (IOException ioe) { + /**/ + } + } + } + private HTTPHandler createHandler(Network network, HomeSP homeSP, KeyManager km, - OSUManager.FlowType flowType) + OSUFlowManager.FlowType flowType) throws GeneralSecurityException, IOException { Credential credential = homeSP.getCredential(); @@ -367,7 +421,7 @@ public class OSUClient { String user; byte[] password; UpdateInfo subscriptionUpdate; - if (flowType == OSUManager.FlowType.Policy) { + if (flowType == OSUFlowManager.FlowType.Policy) { subscriptionUpdate = homeSP.getPolicy() != null ? homeSP.getPolicy().getPolicyUpdate() : null; } else { @@ -439,8 +493,8 @@ public class OSUClient { for (String urlString : urls) { URL url = new URL(urlString); HTTPHandler httpHandler = new HTTPHandler(StandardCharsets.UTF_8, - OSUSocketFactory.getSocketFactory(ks, null, OSUManager.FlowType.Provisioning, - network, url, km, false)); + OSUSocketFactory.getSocketFactory(ks, null, + OSUFlowManager.FlowType.Provisioning, network, url, km, false)); certs.add((X509Certificate) certFactory.generateCertificate(httpHandler.doGet(url))); } @@ -457,7 +511,7 @@ public class OSUClient { case "?": for (OMANode node : root.getChildren()) { if (!node.isLeaf()) { - nodes = Arrays.asList(node); + nodes = Collections.singletonList(node); break; } } @@ -466,7 +520,7 @@ public class OSUClient { nodes = root.getChildren(); break; default: - nodes = Arrays.asList(root.getChild(name)); + nodes = Collections.singletonList(root.getChild(name)); break; } diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUFlowManager.java b/packages/Osu/src/com/android/hotspot2/osu/OSUFlowManager.java new file mode 100644 index 0000000000000..0123d69881df7 --- /dev/null +++ b/packages/Osu/src/com/android/hotspot2/osu/OSUFlowManager.java @@ -0,0 +1,392 @@ +package com.android.hotspot2.osu; + +import android.content.Context; +import android.content.Intent; +import android.net.Network; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import android.os.SystemClock; +import android.util.Log; + +import com.android.hotspot2.Utils; +import com.android.hotspot2.flow.FlowService; +import com.android.hotspot2.flow.OSUInfo; +import com.android.hotspot2.flow.PlatformAdapter; +import com.android.hotspot2.pps.HomeSP; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Iterator; +import java.util.LinkedList; + +import javax.net.ssl.KeyManager; + +public class OSUFlowManager { + private static final boolean MATCH_BSSID = false; + private static final long WAIT_QUANTA = 10000L; + private static final long WAIT_TIMEOUT = 1800000L; + + private final Context mContext; + private final LinkedList mQueue; + private FlowWorker mWorker; + private OSUFlow mCurrent; + + public OSUFlowManager(Context context) { + mContext = context; + mQueue = new LinkedList<>(); + } + + public enum FlowType {Provisioning, Remediation, Policy} + + public static class OSUFlow implements Runnable { + private final OSUClient mOSUClient; + private final PlatformAdapter mPlatformAdapter; + private final HomeSP mHomeSP; + private final String mSpName; + private final FlowType mFlowType; + private final KeyManager mKeyManager; + private final Object mNetworkLock = new Object(); + private final Network mNetwork; + private Network mResultNetwork; + private boolean mNetworkCreated; + private int mWifiNetworkId; + private volatile long mLaunchTime; + private volatile boolean mAborted; + + /** + * A policy flow. + * @param osuInfo The OSU information for the flow (SSID, BSSID, URL) + * @param platformAdapter the platform adapter + * @param km A key manager for TLS + * @throws MalformedURLException + */ + public OSUFlow(OSUInfo osuInfo, PlatformAdapter platformAdapter, KeyManager km) + throws MalformedURLException { + + mWifiNetworkId = -1; + mNetwork = null; + mOSUClient = new OSUClient(osuInfo, + platformAdapter.getKeyStore(), platformAdapter.getContext()); + mPlatformAdapter = platformAdapter; + mHomeSP = null; + mSpName = osuInfo.getName(OSUManager.LOCALE); + mFlowType = FlowType.Provisioning; + mKeyManager = km; + } + + /** + * A Remediation flow for credential or policy provisioning. + * @param network The network to use, only set for timed provisioning + * @param osuURL The URL to connect to. + * @param platformAdapter the platform adapter + * @param km A key manager for TLS + * @param homeSP The Home SP to which this remediation flow pertains. + * @param flowType Remediation or Policy + * @throws MalformedURLException + */ + public OSUFlow(Network network, String osuURL, + PlatformAdapter platformAdapter, KeyManager km, HomeSP homeSP, + FlowType flowType) throws MalformedURLException { + + mNetwork = network; + mWifiNetworkId = network.netId; + mOSUClient = new OSUClient(osuURL, + platformAdapter.getKeyStore(), platformAdapter.getContext()); + mPlatformAdapter = platformAdapter; + mHomeSP = homeSP; + mSpName = homeSP.getFriendlyName(); + mFlowType = flowType; + mKeyManager = km; + } + + private boolean deleteNetwork(OSUFlow next) { + synchronized (mNetworkLock) { + if (!mNetworkCreated) { + return false; + } else if (next.getFlowType() != FlowType.Provisioning) { + return true; + } + OSUInfo thisInfo = mOSUClient.getOSUInfo(); + OSUInfo thatInfo = next.mOSUClient.getOSUInfo(); + if (thisInfo.getOsuSsid().equals(thatInfo.getOsuSsid()) + && thisInfo.getOSUBssid() == thatInfo.getOSUBssid()) { + // Reuse the OSU network from previous and carry forward the creation fact. + mNetworkCreated = true; + return false; + } else { + return true; + } + } + } + + private Network connect() throws IOException { + Network network = networkMatch(); + + synchronized (mNetworkLock) { + mResultNetwork = network; + if (mResultNetwork != null) { + return mResultNetwork; + } + } + + Log.d(OSUManager.TAG, "No network match for " + toString()); + + int osuNetworkId = -1; + boolean created = false; + + if (mFlowType == FlowType.Provisioning) { + osuNetworkId = mPlatformAdapter.connect(mOSUClient.getOSUInfo()); + created = true; + } + + synchronized (mNetworkLock) { + mNetworkCreated = created; + if (created) { + mWifiNetworkId = osuNetworkId; + } + Log.d(OSUManager.TAG, String.format("%s waiting for %snet ID %d", + toString(), created ? "created " : "existing ", osuNetworkId)); + + while (mResultNetwork == null && !mAborted) { + try { + mNetworkLock.wait(); + } catch (InterruptedException ie) { + throw new IOException("Interrupted"); + } + } + if (mAborted) { + throw new IOException("Aborted"); + } + Utils.delay(500L); + } + return mResultNetwork; + } + + private Network networkMatch() { + if (mFlowType == FlowType.Provisioning) { + OSUInfo match = mOSUClient.getOSUInfo(); + WifiConfiguration config = mPlatformAdapter.getActiveWifiConfig(); + if (config != null && bssidMatch(match, mPlatformAdapter) + && Utils.decodeSsid(config.SSID).equals(match.getOsuSsid())) { + synchronized (mNetworkLock) { + mWifiNetworkId = config.networkId; + } + return mPlatformAdapter.getCurrentNetwork(); + } + } else { + WifiConfiguration config = mPlatformAdapter.getActiveWifiConfig(); + synchronized (mNetworkLock) { + mWifiNetworkId = config != null ? config.networkId : -1; + } + return mNetwork; + } + return null; + } + + private void networkChange() { + WifiInfo connectionInfo = mPlatformAdapter.getConnectionInfo(); + if (connectionInfo == null) { + return; + } + Network network = mPlatformAdapter.getCurrentNetwork(); + Log.d(OSUManager.TAG, "New network " + network + + ", current OSU " + mOSUClient.getOSUInfo() + + ", addr " + Utils.toIpString(connectionInfo.getIpAddress())); + + synchronized (mNetworkLock) { + if (mResultNetwork == null && network != null && connectionInfo.getIpAddress() != 0 + && connectionInfo.getNetworkId() == mWifiNetworkId) { + mResultNetwork = network; + mNetworkLock.notifyAll(); + } + } + } + + public boolean createdNetwork() { + synchronized (mNetworkLock) { + return mNetworkCreated; + } + } + + public FlowType getFlowType() { + return mFlowType; + } + + public PlatformAdapter getPlatformAdapter() { + return mPlatformAdapter; + } + + private void setLaunchTime() { + mLaunchTime = SystemClock.currentThreadTimeMillis(); + } + + public long getLaunchTime() { + return mLaunchTime; + } + + private int getWifiNetworkId() { + synchronized (mNetworkLock) { + return mWifiNetworkId; + } + } + + @Override + public void run() { + try { + Network network = connect(); + Log.d(OSUManager.TAG, "OSU SSID Associated at " + network); + + if (mFlowType == FlowType.Provisioning) { + mOSUClient.provision(mPlatformAdapter, network, mKeyManager); + } else { + mOSUClient.remediate(mPlatformAdapter, network, + mKeyManager, mHomeSP, mFlowType); + } + } catch (Throwable t) { + if (mAborted) { + Log.d(OSUManager.TAG, "OSU flow aborted: " + t, t); + } else { + Log.w(OSUManager.TAG, "OSU flow failed: " + t, t); + mPlatformAdapter.provisioningFailed(mSpName, t.getMessage()); + } + } finally { + if (!mAborted) { + mOSUClient.close(false); + } + } + } + + public void abort() { + synchronized (mNetworkLock) { + mAborted = true; + mNetworkLock.notifyAll(); + } + // Sockets cannot be closed on the main thread... + // TODO: Might want to change this to a handler. + new Thread() { + @Override + public void run() { + try { + mOSUClient.close(true); + } catch (Throwable t) { + Log.d(OSUManager.TAG, "Exception aborting " + toString()); + } + } + }.start(); + } + + @Override + public String toString() { + return mFlowType + " for " + mSpName; + } + } + + private class FlowWorker extends Thread { + private final PlatformAdapter mPlatformAdapter; + + private FlowWorker(PlatformAdapter platformAdapter) { + mPlatformAdapter = platformAdapter; + } + + @Override + public void run() { + for (; ; ) { + synchronized (mQueue) { + if (mCurrent != null && mCurrent.createdNetwork() + && (mQueue.isEmpty() || mCurrent.deleteNetwork(mQueue.getLast()))) { + mPlatformAdapter.deleteNetwork(mCurrent.getWifiNetworkId()); + } + + mCurrent = null; + while (mQueue.isEmpty()) { + try { + mQueue.wait(WAIT_QUANTA); + } catch (InterruptedException ie) { + return; + } + if (mQueue.isEmpty()) { + // Bail out on time out + Log.d(OSUManager.TAG, "Flow worker terminating."); + mWorker = null; + mContext.stopService(new Intent(mContext, FlowService.class)); + return; + } + } + mCurrent = mQueue.removeLast(); + mCurrent.setLaunchTime(); + } + Log.d(OSUManager.TAG, "Starting " + mCurrent); + mCurrent.run(); + Log.d(OSUManager.TAG, "Exiting " + mCurrent); + } + } + } + + /* + * Provisioning: Wait until there is an active WiFi info and the active WiFi config + * matches SSID and optionally BSSID. + * WNM Remediation: Wait until the active WiFi info matches BSSID. + * Timed remediation: The network is given (may be cellular). + */ + + public void appendFlow(OSUFlow flow) { + synchronized (mQueue) { + if (mCurrent != null && + SystemClock.currentThreadTimeMillis() + - mCurrent.getLaunchTime() >= WAIT_TIMEOUT) { + Log.d(OSUManager.TAG, "Aborting stale OSU flow " + mCurrent); + mCurrent.abort(); + mCurrent = null; + } + + if (flow.getFlowType() == FlowType.Provisioning) { + // Kill any outstanding provisioning flows. + Iterator flows = mQueue.iterator(); + while (flows.hasNext()) { + if (flows.next().getFlowType() == FlowType.Provisioning) { + flows.remove(); + } + } + + if (mCurrent != null + && mCurrent.getFlowType() == FlowType.Provisioning) { + Log.d(OSUManager.TAG, "Aborting current provisioning flow " + mCurrent); + mCurrent.abort(); + mCurrent = null; + } + + mQueue.addLast(flow); + } else { + mQueue.addFirst(flow); + } + + if (mWorker == null) { + // TODO: Might want to change this to a handler. + mWorker = new FlowWorker(flow.getPlatformAdapter()); + mWorker.start(); + } + + mQueue.notifyAll(); + } + } + + public void networkChange() { + OSUFlow pending; + synchronized (mQueue) { + pending = mCurrent; + } + Log.d(OSUManager.TAG, "Network change, current flow: " + pending); + if (pending != null) { + pending.networkChange(); + } + } + + private static boolean bssidMatch(OSUInfo osuInfo, PlatformAdapter platformAdapter) { + if (MATCH_BSSID) { + WifiInfo wifiInfo = platformAdapter.getConnectionInfo(); + return wifiInfo != null && Utils.parseMac(wifiInfo.getBSSID()) == osuInfo.getOSUBssid(); + } else { + return true; + } + } +} diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUManager.java b/packages/Osu/src/com/android/hotspot2/osu/OSUManager.java index b8ca3fe270205..24cd10f1e583c 100644 --- a/packages/Osu/src/com/android/hotspot2/osu/OSUManager.java +++ b/packages/Osu/src/com/android/hotspot2/osu/OSUManager.java @@ -1,111 +1,55 @@ package com.android.hotspot2.osu; +import android.content.ComponentName; import android.content.Context; -import android.net.Network; +import android.content.Intent; +import android.content.ServiceConnection; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.IBinder; +import android.os.RemoteException; import android.util.Log; -import com.android.anqp.Constants; import com.android.anqp.HSIconFileElement; import com.android.anqp.OSUProvider; import com.android.hotspot2.AppBridge; -import com.android.hotspot2.OMADMAdapter; import com.android.hotspot2.PasspointMatch; import com.android.hotspot2.Utils; -import com.android.hotspot2.WifiNetworkAdapter; -import com.android.hotspot2.omadm.MOManager; -import com.android.hotspot2.omadm.MOTree; -import com.android.hotspot2.osu.commands.MOData; -import com.android.hotspot2.osu.service.RedirectListener; -import com.android.hotspot2.osu.service.SubscriptionTimer; -import com.android.hotspot2.pps.HomeSP; -import com.android.hotspot2.pps.UpdateInfo; - -import org.xml.sax.SAXException; +import com.android.hotspot2.app.OSUData; +import com.android.hotspot2.flow.FlowService; +import com.android.hotspot2.flow.OSUInfo; +import com.android.hotspot2.osu.service.RemediationHandler; +import com.android.hotspot2.flow.IFlowService; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; import java.net.MalformedURLException; -import java.net.URL; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.PrivateKey; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; -import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import javax.net.ssl.KeyManager; - public class OSUManager { public static final String TAG = "OSUMGR"; - public static final boolean R2_ENABLED = true; public static final boolean R2_MOCK = true; - private static final boolean MATCH_BSSID = false; - - private static final String KEYSTORE_FILE = "passpoint.ks"; - - private static final String OSU_COUNT = "osu-count"; - private static final String SP_NAME = "sp-name"; - private static final String PROV_SUCCESS = "prov-success"; - private static final String DEAUTH = "deauth"; - private static final String DEAUTH_DELAY = "deauth-delay"; - private static final String DEAUTH_URL = "deauth-url"; - private static final String PROV_MESSAGE = "prov-message"; - - private static final long REMEDIATION_TIMEOUT = 120000L; - // How many scan result batches to hang on to - - public enum FlowType {Provisioning, Remediation, Policy} - - public static final String CERT_WFA_ALIAS = "wfa-root-"; - public static final String CERT_REM_ALIAS = "rem-"; - public static final String CERT_POLICY_ALIAS = "pol-"; - public static final String CERT_SHARED_ALIAS = "shr-"; - public static final String CERT_CLT_CERT_ALIAS = "clt-"; - public static final String CERT_CLT_KEY_ALIAS = "prv-"; - public static final String CERT_CLT_CA_ALIAS = "aaa-"; - private static final long THREAD_TIMEOUT = 10; // Seconds + private static final String REMEDIATION_FILE = "remediation.state"; // Preferred icon parameters public static final Locale LOCALE = java.util.Locale.getDefault(); - private final Object mFlowLock = new Object(); - private final LinkedBlockingQueue mWorkQueue = new LinkedBlockingQueue<>(); - private FlowRunner mFlowThread; - - private final WifiNetworkAdapter mWifiNetworkAdapter; - private final AppBridge mAppBridge; private final Context mContext; private final IconCache mIconCache; - private final SubscriptionTimer mSubscriptionTimer; + private final RemediationHandler mRemediationHandler; private final Set mOSUSSIDs = new HashSet<>(); private final Map mOSUMap = new HashMap<>(); - private final File mKeyStoreFile; - private final KeyStore mKeyStore; - private volatile RedirectListener mRedirectListener; private final AtomicInteger mOSUSequence = new AtomicInteger(); - private volatile Network mActiveNetwork; - private volatile FlowWorker mRemediationFlow; - private volatile OSUInfo mPendingOSU; - private volatile Integer mOSUNwkID; private final OSUCache mOSUCache; @@ -113,315 +57,31 @@ public class OSUManager { mContext = context; mAppBridge = new AppBridge(context); mIconCache = new IconCache(this); - mWifiNetworkAdapter = new WifiNetworkAdapter(context, this); - mSubscriptionTimer = new SubscriptionTimer(this, mWifiNetworkAdapter, context); + File appFolder = context.getFilesDir(); + mRemediationHandler = + new RemediationHandler(context, new File(appFolder, REMEDIATION_FILE)); mOSUCache = new OSUCache(); - mKeyStoreFile = new File(context.getFilesDir(), KEYSTORE_FILE); - Log.d(TAG, "KS file: " + mKeyStoreFile.getPath()); - KeyStore ks = null; - try { - //ks = loadKeyStore(KEYSTORE_FILE, readCertsFromDisk(WFA_CA_LOC)); - ks = loadKeyStore(mKeyStoreFile, OSUSocketFactory.buildCertSet()); - } catch (IOException e) { - Log.e(TAG, "Failed to initialize Passpoint keystore, OSU disabled", e); - } - mKeyStore = ks; } - private static KeyStore loadKeyStore(File ksFile, Set diskCerts) - throws IOException { - try { - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - if (ksFile.exists()) { - try (FileInputStream in = new FileInputStream(ksFile)) { - keyStore.load(in, null); - } - - // Note: comparing two sets of certs does not work. - boolean mismatch = false; - int loadCount = 0; - for (int n = 0; n < 1000; n++) { - String alias = String.format("%s%d", CERT_WFA_ALIAS, n); - Certificate cert = keyStore.getCertificate(alias); - if (cert == null) { - break; - } - - loadCount++; - boolean matched = false; - Iterator iter = diskCerts.iterator(); - while (iter.hasNext()) { - X509Certificate diskCert = iter.next(); - if (cert.equals(diskCert)) { - iter.remove(); - matched = true; - break; - } - } - if (!matched) { - mismatch = true; - break; - } - } - if (mismatch || !diskCerts.isEmpty()) { - Log.d(TAG, "Re-seeding Passpoint key store with " + - diskCerts.size() + " WFA certs"); - for (int n = 0; n < 1000; n++) { - String alias = String.format("%s%d", CERT_WFA_ALIAS, n); - Certificate cert = keyStore.getCertificate(alias); - if (cert == null) { - break; - } else { - keyStore.deleteEntry(alias); - } - } - int index = 0; - for (X509Certificate caCert : diskCerts) { - keyStore.setCertificateEntry( - String.format("%s%d", CERT_WFA_ALIAS, index), caCert); - index++; - } - - try (FileOutputStream out = new FileOutputStream(ksFile)) { - keyStore.store(out, null); - } - } else { - Log.d(TAG, "Loaded Passpoint key store with " + loadCount + " CA certs"); - Enumeration aliases = keyStore.aliases(); - while (aliases.hasMoreElements()) { - Log.d("ZXC", "KS Alias '" + aliases.nextElement() + "'"); - } - } - } else { - keyStore.load(null, null); - int index = 0; - for (X509Certificate caCert : diskCerts) { - keyStore.setCertificateEntry( - String.format("%s%d", CERT_WFA_ALIAS, index), caCert); - index++; - } - - try (FileOutputStream out = new FileOutputStream(ksFile)) { - keyStore.store(out, null); - } - Log.d(TAG, "Initialized Passpoint key store with " + - diskCerts.size() + " CA certs"); - } - return keyStore; - } catch (GeneralSecurityException gse) { - throw new IOException(gse); - } + public Context getContext() { + return mContext; } - public KeyStore getKeyStore() { - return mKeyStore; - } - - private static class FlowWorker implements Runnable { - private final OSUClient mOSUClient; - private final OSUManager mOSUManager; - private final HomeSP mHomeSP; - private final String mSpName; - private final FlowType mFlowType; - private final KeyManager mKeyManager; - private final long mLaunchTime; - private final Network mNetwork; - - private FlowWorker(Network network, OSUInfo osuInfo, OSUManager osuManager, KeyManager km) - throws MalformedURLException { - mNetwork = network; - mOSUClient = new OSUClient(osuInfo, osuManager.getKeyStore()); - mOSUManager = osuManager; - mHomeSP = null; - mSpName = osuInfo.getName(LOCALE); - mFlowType = FlowType.Provisioning; - mKeyManager = km; - mLaunchTime = System.currentTimeMillis(); - } - - private FlowWorker(Network network, String osuURL, OSUManager osuManager, KeyManager km, - HomeSP homeSP, FlowType flowType) throws MalformedURLException { - mNetwork = network; - mOSUClient = new OSUClient(osuURL, osuManager.getKeyStore()); - mOSUManager = osuManager; - mHomeSP = homeSP; - mSpName = homeSP.getFriendlyName(); - mFlowType = flowType; - mKeyManager = km; - mLaunchTime = System.currentTimeMillis(); - } - - public long getLaunchTime() { - return mLaunchTime; - } - - private Network getNetwork() { - return mNetwork; - } - - @Override - public void run() { - Log.d(TAG, "OSU SSID Associated at " + mNetwork); - try { - if (mFlowType == FlowType.Provisioning) { - mOSUClient.provision(mOSUManager, mNetwork, mKeyManager); - } else { - mOSUClient.remediate(mOSUManager, mNetwork, mKeyManager, mHomeSP, mFlowType); - } - } catch (Throwable t) { - Log.w(TAG, "OSU flow failed: " + t, t); - mOSUManager.provisioningFailed(mSpName, t.getMessage(), mHomeSP, mFlowType); - } - } - - @Override - public String toString() { - return mFlowType + " for " + mSpName; - } - } - - private static class FlowRunner extends Thread { - private final LinkedBlockingQueue mWorkQueue; - private final OSUManager mOSUManager; - - private FlowRunner(LinkedBlockingQueue workQueue, OSUManager osuManager) { - mWorkQueue = workQueue; - mOSUManager = osuManager; - setDaemon(true); - setName("OSU Client Thread"); - } - - @Override - public void run() { - for (;;) { - FlowWorker flowWorker; - try { - flowWorker = mWorkQueue.poll(THREAD_TIMEOUT, TimeUnit.SECONDS); - } catch (InterruptedException ie) { - flowWorker = null; - } - if (flowWorker == null) { - if (mOSUManager.serviceThreadExit()) { - return; - } else { - continue; - } - } - Log.d(TAG, "Starting " + flowWorker); - flowWorker.run(); - } - } - } - - private void startOsuFlow(FlowWorker flowWorker) { - synchronized (mFlowLock) { - mWorkQueue.offer(flowWorker); - if (mFlowThread == null) { - mFlowThread = new FlowRunner(mWorkQueue, this); - mFlowThread.start(); - } - } - } - - private boolean serviceThreadExit() { - synchronized (mFlowLock) { - if (mWorkQueue.isEmpty()) { - mFlowThread = null; - return true; - } else { - return false; - } - } - } - - /* - public void startOSU() { - registerUserInputListener(new UserInputListener() { - @Override - public void requestUserInput(URL target, Network network, URL endRedirect) { - Log.d(TAG, "Browser to " + target + ", land at " + endRedirect); - - final Intent intent = new Intent( - ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN); - intent.putExtra(ConnectivityManager.EXTRA_NETWORK, network); - intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL, - new CaptivePortal(new ICaptivePortal.Stub() { - @Override - public void appResponse(int response) { - } - })); - //intent.setData(Uri.parse(target.toString())); !!! Doesn't work! - intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL, target.toString()); - intent.setFlags( - Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivityAsUser(intent, UserHandle.CURRENT); - } - - @Override - public String operationStatus(String spIdentity, OSUOperationStatus status, - String message) { - Log.d(TAG, "OSU OP Status: " + status + ", message " + message); - Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION); - intent.putExtra(SP_NAME, spIdentity); - intent.putExtra(PROV_SUCCESS, status == OSUOperationStatus.ProvisioningSuccess); - if (message != null) { - intent.putExtra(PROV_MESSAGE, message); - } - intent.setFlags( - Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivityAsUser(intent, UserHandle.CURRENT); - return null; - } - - @Override - public void deAuthNotification(String spIdentity, boolean ess, int delay, URL url) { - Log.i(TAG, "De-authentication imminent for " + (ess ? "ess" : "bss") + - ", redirect to " + url); - Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION); - intent.putExtra(SP_NAME, spIdentity); - intent.putExtra(DEAUTH, ess); - intent.putExtra(DEAUTH_DELAY, delay); - intent.putExtra(DEAUTH_URL, url.toString()); - intent.setFlags( - Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivityAsUser(intent, UserHandle.CURRENT); - } - }); - addOSUListener(new OSUListener() { - @Override - public void osuNotification(int count) { - Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION); - intent.putExtra(OSU_COUNT, count); - intent.setFlags( - Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivityAsUser(intent, UserHandle.CURRENT); - } - }); - mWifiNetworkAdapter.initialize(); - mSubscriptionTimer.checkUpdates(); - } - */ - - public List getAvailableOSUs() { + public List getAvailableOSUs() { synchronized (mOSUMap) { - List completeOSUs = new ArrayList<>(); + List completeOSUs = new ArrayList<>(); for (OSUInfo osuInfo : mOSUMap.values()) { if (osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) { - completeOSUs.add(osuInfo); + completeOSUs.add(new OSUData(osuInfo)); } } return completeOSUs; } } - public void recheckTimers() { - mSubscriptionTimer.checkUpdates(); - } - public void setOSUSelection(int osuID) { OSUInfo selection = null; for (OSUInfo osuInfo : mOSUMap.values()) { - Log.d("ZXZ", "In select: " + osuInfo + ", id " + osuInfo.getOsuID()); if (osuInfo.getOsuID() == osuID && osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) { selection = osuInfo; @@ -429,137 +89,84 @@ public class OSUManager { } } - Log.d(TAG, "Selected OSU ID " + osuID + ", matches " + selection); + Log.d(TAG, "Selected OSU ID " + osuID + ": " + selection); if (selection == null) { - mPendingOSU = null; return; } - mPendingOSU = selection; - WifiConfiguration config = mWifiNetworkAdapter.getActiveWifiConfig(); + final OSUInfo osu = selection; - if (config != null && - bssidMatch(selection) && - Utils.unquote(config.SSID).equals(selection.getOsuSsid())) { - - try { - // Go straight to provisioning if the network is already selected. - // Also note that mOSUNwkID is left unset to leave the network around after - // flow completion since it was not added by the OSU flow. - initiateProvisioning(mPendingOSU, mWifiNetworkAdapter.getCurrentNetwork()); - } catch (IOException ioe) { - notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(), - mPendingOSU.getName(LOCALE)); - } finally { - mPendingOSU = null; + mContext.bindService(new Intent(mContext, FlowService.class), new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + try { + IFlowService fs = IFlowService.Stub.asInterface(service); + fs.provision(osu); + } catch (RemoteException re) { + Log.e(OSUManager.TAG, "Caught re: " + re); + } } - } else { - try { - mOSUNwkID = mWifiNetworkAdapter.connect(selection, mPendingOSU.getName(LOCALE)); - } catch (IOException ioe) { - notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(), - selection.getName(LOCALE)); + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.d(OSUManager.TAG, "Service disconnect: " + name); } - } + }, Context.BIND_AUTO_CREATE); } - public void networkDeleted(WifiConfiguration configuration) { - Log.d("ZXZ", "Network deleted: " + configuration.FQDN); - HomeSP homeSP = mWifiNetworkAdapter.getHomeSP(configuration); - if (homeSP != null) { - spDeleted(homeSP.getFQDN()); + public void networkDeleted(final WifiConfiguration configuration) { + if (configuration.FQDN == null) { + return; } + + mRemediationHandler.networkConfigChange(); + mContext.bindService(new Intent(mContext, FlowService.class), new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + try { + IFlowService fs = IFlowService.Stub.asInterface(service); + fs.spDeleted(configuration.FQDN); + } catch (RemoteException re) { + Log.e(OSUManager.TAG, "Caught re: " + re); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + + } + }, Context.BIND_AUTO_CREATE); } - public void networkChanged(WifiConfiguration configuration) { - mWifiNetworkAdapter.networkConfigChange(configuration); + public void networkConnectChange(WifiInfo newNetwork) { + mRemediationHandler.newConnection(newNetwork); } - public void networkConnectEvent(WifiInfo wifiInfo) { - if (wifiInfo != null) { - setActiveNetwork(mWifiNetworkAdapter.getActiveWifiConfig(), - mWifiNetworkAdapter.getCurrentNetwork()); - } + public void networkConfigChanged() { + mRemediationHandler.networkConfigChange(); } public void wifiStateChange(boolean on) { - if (!on) { - int current = mOSUMap.size(); - mOSUMap.clear(); - mOSUCache.clearAll(); - mIconCache.tick(true); - if (current > 0) { - notifyOSUCount(); - } - } - } - - private boolean bssidMatch(OSUInfo osuInfo) { - if (MATCH_BSSID) { - WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo(); - return wifiInfo != null && Utils.parseMac(wifiInfo.getBSSID()) == osuInfo.getBSSID(); - } else { - return true; - } - } - - public void setActiveNetwork(WifiConfiguration wifiConfiguration, Network network) { - Log.d(TAG, "Network change: " + network + ", cfg " + - (wifiConfiguration != null ? wifiConfiguration.SSID : "-") + ", osu " + mPendingOSU); - mActiveNetwork = network; - if (mPendingOSU != null && - wifiConfiguration != null && - network != null && - bssidMatch(mPendingOSU) && - Utils.unquote(wifiConfiguration.SSID).equals(mPendingOSU.getOsuSsid())) { - - try { - Log.d(TAG, "New network " + network + ", current OSU " + mPendingOSU); - initiateProvisioning(mPendingOSU, network); - } catch (IOException ioe) { - notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(), - mPendingOSU.getName(LOCALE)); - } finally { - mPendingOSU = null; - } + if (on) { + return; } - if (mRemediationFlow != null && network != null && - mRemediationFlow.getNetwork().netId == network.netId) { - startOsuFlow(mRemediationFlow); - mRemediationFlow = null; + // Notify the remediation handler that there are no WiFi networks available. + // Do NOT turn it off though as remediation, per at least this implementation, can take + // place over cellular. The subject of remediation over cellular (when restriction is + // "unrestricted") is not addresses by the WFA spec and direct ask to authors gives no + // distinct answer one way or the other. + mRemediationHandler.newConnection(null); + int current = mOSUMap.size(); + mOSUMap.clear(); + mOSUCache.clearAll(); + mIconCache.tick(true); + if (current > 0) { + notifyOSUCount(); } } - - /** - * Called when an OSU has been selected and the associated network is fully connected. - * - * @param osuInfo The selected OSUInfo or null if the current OSU flow is cancelled externally, - * e.g. WiFi is turned off or the OSU network is otherwise detected as - * unreachable. - * @param network The currently associated network (for the OSU SSID). - * @throws IOException - * @throws GeneralSecurityException - */ - private void initiateProvisioning(OSUInfo osuInfo, Network network) - throws IOException { - startOsuFlow(new FlowWorker(network, osuInfo, this, getKeyManager(null, mKeyStore))); - } - - /** - * @param homeSP The Home SP associated with the keying material in question. Passing - * null returns a "system wide" KeyManager to support pre-provisioned certs based - * on names retrieved from the ClientCertInfo request. - * @return A key manager suitable for the given configuration (or pre-provisioned keys). - */ - private static KeyManager getKeyManager(HomeSP homeSP, KeyStore keyStore) - throws IOException { - return homeSP != null ? new ClientKeyManager(homeSP, keyStore) : - new WiFiKeyManager(keyStore); - } - public boolean isOSU(String ssid) { synchronized (mOSUMap) { return mOSUSSIDs.contains(ssid); @@ -581,17 +188,17 @@ public class OSUManager { long bssid = Utils.parseMac(entry.getValue().BSSID); if (existing == null) { - osus.put(entry.getKey(), new OSUInfo(entry.getValue(), entry.getKey().getSSID(), - entry.getKey(), mOSUSequence.getAndIncrement())); + osus.put(entry.getKey(), new OSUInfo(entry.getValue(), entry.getKey(), + mOSUSequence.getAndIncrement())); } else if (existing.getBSSID() != bssid) { HSIconFileElement icon = mIconCache.getIcon(existing); if (icon != null && icon.equals(existing.getIconFileElement())) { - OSUInfo osuInfo = new OSUInfo(entry.getValue(), entry.getKey().getSSID(), - entry.getKey(), existing.getOsuID()); + OSUInfo osuInfo = new OSUInfo(entry.getValue(), entry.getKey(), + existing.getOsuID()); osuInfo.setIconFileElement(icon, existing.getIconFileName()); osus.put(entry.getKey(), osuInfo); } else { - osus.put(entry.getKey(), new OSUInfo(entry.getValue(), entry.getKey().getSSID(), + osus.put(entry.getKey(), new OSUInfo(entry.getValue(), entry.getKey(), mOSUSequence.getAndIncrement())); } } else { @@ -622,7 +229,8 @@ public class OSUManager { } public void doIconQuery(long bssid, String fileName) { - mWifiNetworkAdapter.doIconQuery(bssid, fileName); + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + wifiManager.queryPasspointIcon(bssid, fileName); } private void notifyOSUCount() { @@ -633,292 +241,24 @@ public class OSUManager { } } Log.d(TAG, "Latest OSU info: " + count + " with icons, map " + mOSUMap); - mAppBridge.showOsuCount(count, getAvailableOSUs()); + mAppBridge.showOsuCount(count); } - public void deauth(long bssid, boolean ess, int delay, String url) throws MalformedURLException { + public void deauth(long bssid, boolean ess, int delay, String url) + throws MalformedURLException { Log.d(TAG, String.format("De-auth imminent on %s, delay %ss to '%s'", - ess ? "ess" : "bss", - delay, - url)); - mWifiNetworkAdapter.setHoldoffTime(delay * Constants.MILLIS_IN_A_SEC, ess); - HomeSP homeSP = mWifiNetworkAdapter.getCurrentSP(); - String spName = homeSP != null ? homeSP.getFriendlyName() : "unknown"; + ess ? "ess" : "bss", delay, url)); + // TODO: Missing framework functionality: + // mWifiNetworkAdapter.setHoldoffTime(delay * Constants.MILLIS_IN_A_SEC, ess); + String spName = mRemediationHandler.getCurrentSpName(); mAppBridge.showDeauth(spName, ess, delay, url); } - // !!! Consistently check passpoint match. - public void wnmRemediate(long bssid, String url, PasspointMatch match) - throws IOException, SAXException { - HomeSP homeSP = mWifiNetworkAdapter.getCurrentSP(); - if (homeSP == null) { - throw new IOException("Remediation request on unidentified Passpoint network "); - } - Network network = mWifiNetworkAdapter.getCurrentNetwork(); - if (network == null) { - throw new IOException("Failed to determine current network"); - } - WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo(); - if (wifiInfo == null || Utils.parseMac(wifiInfo.getBSSID()) != bssid) { - throw new IOException("Mismatching BSSID"); - } - Log.d(TAG, "WNM Remediation on " + network.netId + " FQDN " + homeSP.getFQDN()); - - FlowWorker flowWorker = new FlowWorker(network, url, this, - getKeyManager(homeSP, mKeyStore), homeSP, FlowType.Remediation); - - if (mActiveNetwork != null && wifiInfo.getNetworkId() == mActiveNetwork.netId) { - startOsuFlow(flowWorker); - } else { - mRemediationFlow = flowWorker; - } + public void wnmRemediate(final long bssid, final String url, PasspointMatch match) { + mRemediationHandler.wnmReceived(bssid, url); } - public void remediate(HomeSP homeSP, boolean policy) throws IOException, SAXException { - UpdateInfo updateInfo; - if (policy) { - if (homeSP.getPolicy() == null) { - throw new IOException("No policy object"); - } - updateInfo = homeSP.getPolicy().getPolicyUpdate(); - } else { - updateInfo = homeSP.getSubscriptionUpdate(); - } - switch (updateInfo.getUpdateRestriction()) { - case HomeSP: { - Network network = mWifiNetworkAdapter.getCurrentNetwork(); - if (network == null) { - throw new IOException("Failed to determine current network"); - } - - HomeSP activeSP = mWifiNetworkAdapter.getCurrentSP(); - - if (activeSP == null || !activeSP.getFQDN().equals(homeSP.getFQDN())) { - throw new IOException("Remediation restricted to HomeSP"); - } - doRemediate(updateInfo.getURI(), network, homeSP, policy); - break; - } - case RoamingPartner: { - Network network = mWifiNetworkAdapter.getCurrentNetwork(); - if (network == null) { - throw new IOException("Failed to determine current network"); - } - - WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo(); - if (wifiInfo == null) { - throw new IOException("Unable to determine WiFi info"); - } - - PasspointMatch match = mWifiNetworkAdapter. - matchProviderWithCurrentNetwork(homeSP.getFQDN()); - - if (match == PasspointMatch.HomeProvider || - match == PasspointMatch.RoamingProvider) { - doRemediate(updateInfo.getURI(), network, homeSP, policy); - } else { - throw new IOException("No roaming network match: " + match); - } - break; - } - case Unrestricted: { - Network network = mWifiNetworkAdapter.getCurrentNetwork(); - doRemediate(updateInfo.getURI(), network, homeSP, policy); - break; - } - } - } - - private void doRemediate(String url, Network network, HomeSP homeSP, boolean policy) - throws IOException { - - startOsuFlow(new FlowWorker(network, url, this, - getKeyManager(homeSP, mKeyStore), - homeSP, policy ? FlowType.Policy : FlowType.Remediation)); - } - - public MOTree getMOTree(HomeSP homeSP) throws IOException { - return mWifiNetworkAdapter.getMOTree(homeSP); - } - - protected URL prepareUserInput(String spName) throws IOException { - mRedirectListener = new RedirectListener(this, spName); - return mRedirectListener.getURL(); - } - - protected boolean startUserInput(URL target, Network network) throws IOException { - mRedirectListener.startService(); - mWifiNetworkAdapter.launchBrowser(target, network, mRedirectListener.getURL()); - return mRedirectListener.waitForUser(); - } - - public String notifyUser(OSUOperationStatus status, String message, String spName) { - if (status == OSUOperationStatus.UserInputComplete) { - return null; - } - if (mOSUNwkID != null) { - // Delete the OSU network if it was added by the OSU flow - mWifiNetworkAdapter.deleteNetwork(mOSUNwkID); - mOSUNwkID = null; - } - mAppBridge.showStatus(status, spName, message, null); - return null; - } - - public void provisioningFailed(String spName, String message, - HomeSP homeSP, FlowType flowType) { - if (mRedirectListener != null) { - mRedirectListener.abort(); - mRedirectListener = null; - } - notifyUser(OSUOperationStatus.ProvisioningFailure, message, spName); - } - - public void provisioningComplete(OSUInfo osuInfo, - MOData moData, Map> certs, - PrivateKey privateKey, Network osuNetwork) { - try { - String xml = moData.getMOTree().toXml(); - HomeSP homeSP = MOManager.buildSP(xml); - - Integer spNwk = mWifiNetworkAdapter.addNetwork(homeSP, certs, privateKey, osuNetwork); - if (spNwk == null) { - notifyUser(OSUOperationStatus.ProvisioningFailure, - "Failed to save network configuration", osuInfo.getName(LOCALE)); - } else { - if (mWifiNetworkAdapter.addSP(xml) < 0) { - mWifiNetworkAdapter.deleteNetwork(spNwk); - Log.e(TAG, "Failed to provision: " + homeSP.getFQDN()); - notifyUser(OSUOperationStatus.ProvisioningFailure, "Failed to add MO", - osuInfo.getName(LOCALE)); - return; - } - Set rootCerts = OSUSocketFactory.getRootCerts(mKeyStore); - X509Certificate remCert = getCert(certs, OSUCertType.Remediation); - X509Certificate polCert = getCert(certs, OSUCertType.Policy); - int newCerts = 0; - if (privateKey != null) { - X509Certificate cltCert = getCert(certs, OSUCertType.Client); - mKeyStore.setKeyEntry(CERT_CLT_KEY_ALIAS + homeSP.getFQDN(), - privateKey, null, new X509Certificate[]{cltCert}); - mKeyStore.setCertificateEntry(CERT_CLT_CERT_ALIAS + homeSP.getFQDN(), cltCert); - newCerts++; - } - boolean usingShared = false; - if (remCert != null) { - if (!rootCerts.contains(remCert)) { - if (remCert.equals(polCert)) { - mKeyStore.setCertificateEntry(CERT_SHARED_ALIAS + homeSP.getFQDN(), - remCert); - usingShared = true; - newCerts++; - } else { - mKeyStore.setCertificateEntry(CERT_REM_ALIAS + homeSP.getFQDN(), - remCert); - newCerts++; - } - } - } - if (!usingShared && polCert != null) { - if (!rootCerts.contains(polCert)) { - mKeyStore.setCertificateEntry(CERT_POLICY_ALIAS + homeSP.getFQDN(), - remCert); - newCerts++; - } - } - - Log.d("ZXZ", "Got " + newCerts + " new certs."); - if (newCerts > 0) { - try (FileOutputStream out = new FileOutputStream(mKeyStoreFile)) { - mKeyStore.store(out, null); - } - } - notifyUser(OSUOperationStatus.ProvisioningSuccess, null, osuInfo.getName(LOCALE)); - Log.d(TAG, "Provisioning complete."); - } - } catch (IOException | GeneralSecurityException | SAXException e) { - Log.e(TAG, "Failed to provision: " + e, e); - notifyUser(OSUOperationStatus.ProvisioningFailure, e.toString(), - osuInfo.getName(LOCALE)); - } - } - - private static X509Certificate getCert(Map> certMap, - OSUCertType certType) { - List certs = certMap.get(certType); - if (certs == null || certs.isEmpty()) { - return null; - } - return certs.iterator().next(); - } - - public void spDeleted(String fqdn) { - int count = deleteCerts(mKeyStore, fqdn, - CERT_REM_ALIAS, CERT_POLICY_ALIAS, CERT_SHARED_ALIAS, CERT_CLT_CERT_ALIAS); - - Log.d(TAG, "Passpoint network deleted, removing " + count + " key store entries"); - - try { - if (mKeyStore.getKey(CERT_CLT_KEY_ALIAS + fqdn, null) != null) { - mKeyStore.deleteEntry(CERT_CLT_KEY_ALIAS + fqdn); - } - } catch (GeneralSecurityException e) { - /**/ - } - - if (count > 0) { - try (FileOutputStream out = new FileOutputStream(mKeyStoreFile)) { - mKeyStore.store(out, null); - } catch (IOException | GeneralSecurityException e) { - Log.w(TAG, "Failed to remove certs from key store: " + e); - } - } - } - - private static int deleteCerts(KeyStore keyStore, String fqdn, String... prefixes) { - int count = 0; - for (String prefix : prefixes) { - try { - String alias = prefix + fqdn; - Certificate cert = keyStore.getCertificate(alias); - if (cert != null) { - keyStore.deleteEntry(alias); - count++; - } - } catch (KeyStoreException kse) { - /**/ - } - } - return count; - } - - public void remediationComplete(HomeSP homeSP, Collection mods, - Map> certs, - PrivateKey privateKey) - throws IOException, GeneralSecurityException { - - HomeSP altSP = null; - if (mWifiNetworkAdapter.modifySP(homeSP, mods) > 0) { - altSP = MOManager.modifySP(homeSP, mWifiNetworkAdapter.getMOTree(homeSP), mods); - } - - X509Certificate caCert = null; - List clientCerts = null; - if (certs != null) { - List certList = certs.get(OSUCertType.AAA); - caCert = certList != null && !certList.isEmpty() ? certList.iterator().next() : null; - clientCerts = certs.get(OSUCertType.Client); - } - if (altSP != null || certs != null) { - if (altSP == null) { - altSP = homeSP; - } - mWifiNetworkAdapter.updateNetwork(altSP, caCert, clientCerts, privateKey); - } - notifyUser(OSUOperationStatus.ProvisioningSuccess, null, homeSP.getFriendlyName()); - } - - protected OMADMAdapter getOMADMAdapter() { - return OMADMAdapter.getInstance(mContext); + public void remediationDone(String fqdn, boolean policy) { + mRemediationHandler.remediationDone(fqdn, policy); } } diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUSocketFactory.java b/packages/Osu/src/com/android/hotspot2/osu/OSUSocketFactory.java index e8335f77e0117..1f5547bf1f9af 100644 --- a/packages/Osu/src/com/android/hotspot2/osu/OSUSocketFactory.java +++ b/packages/Osu/src/com/android/hotspot2/osu/OSUSocketFactory.java @@ -5,6 +5,7 @@ import android.util.Base64; import android.util.Log; import com.android.hotspot2.Utils; +import com.android.hotspot2.flow.PlatformAdapter; import com.android.hotspot2.pps.HomeSP; import java.io.ByteArrayInputStream; @@ -76,7 +77,7 @@ public class OSUSocketFactory { } public static OSUSocketFactory getSocketFactory(KeyStore ks, HomeSP homeSP, - OSUManager.FlowType flowType, + OSUFlowManager.FlowType flowType, Network network, URL url, KeyManager km, boolean enforceSecurity) throws GeneralSecurityException, IOException { @@ -87,7 +88,7 @@ public class OSUSocketFactory { return new OSUSocketFactory(ks, homeSP, flowType, network, url, km); } - private OSUSocketFactory(KeyStore ks, HomeSP homeSP, OSUManager.FlowType flowType, + private OSUSocketFactory(KeyStore ks, HomeSP homeSP, OSUFlowManager.FlowType flowType, Network network, URL url, KeyManager km) throws GeneralSecurityException, IOException { mNetwork = network; @@ -217,10 +218,10 @@ public class OSUSocketFactory { private static class WFATrustManager implements X509TrustManager { private final KeyStore mKeyStore; private final HomeSP mHomeSP; - private final OSUManager.FlowType mFlowType; + private final OSUFlowManager.FlowType mFlowType; private X509Certificate[] mTrustChain; - private WFATrustManager(KeyStore ks, HomeSP homeSP, OSUManager.FlowType flowType) + private WFATrustManager(KeyStore ks, HomeSP homeSP, OSUFlowManager.FlowType flowType) throws CertificateException { mKeyStore = ks; mHomeSP = homeSP; @@ -250,12 +251,13 @@ public class OSUSocketFactory { trustAnchors.add(new TrustAnchor(cert, null)); } } else { - String prefix = mFlowType == OSUManager.FlowType.Remediation ? - OSUManager.CERT_REM_ALIAS : OSUManager.CERT_POLICY_ALIAS; + String prefix = mFlowType == OSUFlowManager.FlowType.Remediation ? + PlatformAdapter.CERT_REM_ALIAS : PlatformAdapter.CERT_POLICY_ALIAS; X509Certificate cert = getCert(mKeyStore, prefix + mHomeSP.getFQDN()); if (cert == null) { - cert = getCert(mKeyStore, OSUManager.CERT_SHARED_ALIAS + mHomeSP.getFQDN()); + cert = getCert(mKeyStore, + PlatformAdapter.CERT_SHARED_ALIAS + mHomeSP.getFQDN()); } if (cert == null) { for (X509Certificate root : getRootCerts(mKeyStore)) { @@ -300,7 +302,7 @@ public class OSUSocketFactory { int index = 0; for (int n = 0; n < 1000; n++) { Certificate cert = keyStore.getCertificate( - String.format("%s%d", OSUManager.CERT_WFA_ALIAS, index)); + String.format("%s%d", PlatformAdapter.CERT_WFA_ALIAS, index)); if (cert == null) { break; } else if (cert instanceof X509Certificate) { diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUWebView.java b/packages/Osu/src/com/android/hotspot2/osu/OSUWebView.java new file mode 100644 index 0000000000000..afbd0d151b3d4 --- /dev/null +++ b/packages/Osu/src/com/android/hotspot2/osu/OSUWebView.java @@ -0,0 +1,91 @@ +package com.android.hotspot2.osu; + +import android.annotation.Nullable; +import android.app.Activity; +import android.graphics.Bitmap; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.http.SslError; +import android.os.Bundle; +import android.util.Log; +import android.util.TypedValue; +import android.webkit.SslErrorHandler; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import com.android.hotspot2.R; + +public class OSUWebView extends Activity { + public static final String OSU_URL = "com.android.hotspot2.osu.URL"; + public static final String OSU_NETWORK = "com.android.hotspot2.osu.NETWORK"; + + private String mUrl; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.d(OSUManager.TAG, "Opening OSU Web View"); + + ConnectivityManager connectivityManager = ConnectivityManager.from(this); + + mUrl = getIntent().getStringExtra(OSU_URL); + Network network = getIntent().getParcelableExtra(OSU_NETWORK); + connectivityManager.bindProcessToNetwork(network); + + getActionBar().setDisplayShowHomeEnabled(false); + setContentView(R.layout.osu_web_view); + getActionBar().setDisplayShowHomeEnabled(false); + + final WebView myWebView = (WebView) findViewById(R.id.webview); + myWebView.clearCache(true); + WebSettings webSettings = myWebView.getSettings(); + webSettings.setJavaScriptEnabled(true); + MyWebViewClient mWebViewClient = new MyWebViewClient(); + myWebView.setWebViewClient(mWebViewClient); + Log.d(OSUManager.TAG, "OSU Web View to " + mUrl); + myWebView.loadUrl(mUrl); + Log.d(OSUManager.TAG, "OSU Web View loading"); + //myWebView.setWebChromeClient(new MyWebChromeClient()); + // Start initial page load so WebView finishes loading proxy settings. + // Actual load of mUrl is initiated by MyWebViewClient. + //myWebView.loadData("", "text/html", null); + } + + private class MyWebViewClient extends WebViewClient { + private static final String INTERNAL_ASSETS = "file:///android_asset/"; + // How many Android device-independent-pixels per scaled-pixel + // dp/sp = (px/sp) / (px/dp) = (1/sp) / (1/dp) + private final float mDpPerSp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 1, + getResources().getDisplayMetrics()) / + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, + getResources().getDisplayMetrics()); + private int mPagesLoaded; + + // If we haven't finished cleaning up the history, don't allow going back. + public boolean allowBack() { + return mPagesLoaded > 1; + } + + // Convert Android device-independent-pixels (dp) to HTML size. + private String dp(int dp) { + // HTML px's are scaled just like dp's, so just add "px" suffix. + return Integer.toString(dp) + "px"; + } + + // Convert Android scaled-pixels (sp) to HTML size. + private String sp(int sp) { + // Convert sp to dp's. + float dp = sp * mDpPerSp; + // Apply a scale factor to make things look right. + dp *= 1.3; + // Convert dp's to HTML size. + return dp((int)dp); + } + + @Override + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { + Log.d(OSUManager.TAG, "TLS error in Web View: " + error); + } + } +} diff --git a/packages/Osu/src/com/android/hotspot2/osu/SPVerifier.java b/packages/Osu/src/com/android/hotspot2/osu/SPVerifier.java index c5290534fafbd..f193b6a4a7cdc 100644 --- a/packages/Osu/src/com/android/hotspot2/osu/SPVerifier.java +++ b/packages/Osu/src/com/android/hotspot2/osu/SPVerifier.java @@ -15,6 +15,7 @@ import com.android.hotspot2.asn1.Asn1Octets; import com.android.hotspot2.asn1.Asn1Oid; import com.android.hotspot2.asn1.Asn1String; import com.android.hotspot2.asn1.OidMappings; +import com.android.hotspot2.flow.OSUInfo; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/packages/Osu/src/com/android/hotspot2/osu/service/RedirectListener.java b/packages/Osu/src/com/android/hotspot2/osu/service/RedirectListener.java index ce26afdd7c509..105a96dddda31 100644 --- a/packages/Osu/src/com/android/hotspot2/osu/service/RedirectListener.java +++ b/packages/Osu/src/com/android/hotspot2/osu/service/RedirectListener.java @@ -2,7 +2,7 @@ package com.android.hotspot2.osu.service; import android.util.Log; -import com.android.hotspot2.osu.OSUManager; +import com.android.hotspot2.flow.PlatformAdapter; import com.android.hotspot2.osu.OSUOperationStatus; import java.io.BufferedReader; @@ -36,7 +36,7 @@ public class RedirectListener extends Thread { "" + "\r\n"; - private final OSUManager mOSUManager; + private final PlatformAdapter mPlatformAdapter; private final String mSpName; private final ServerSocket mServerSocket; private final String mPath; @@ -47,8 +47,8 @@ public class RedirectListener extends Thread { private OSUOperationStatus mUserStatus; private volatile boolean mAborted; - public RedirectListener(OSUManager osuManager, String spName) throws IOException { - mOSUManager = osuManager; + public RedirectListener(PlatformAdapter platformAdapter, String spName) throws IOException { + mPlatformAdapter = platformAdapter; mSpName = spName; mServerSocket = new ServerSocket(0, 5, InetAddress.getLocalHost()); Random rnd = new Random(System.currentTimeMillis()); @@ -109,6 +109,10 @@ public class RedirectListener extends Thread { public void abort() { try { + synchronized (mLock) { + mUserStatus = OSUOperationStatus.UserInputAborted; + mLock.notifyAll(); + } mAborted = true; mServerSocket.close(); } catch (IOException ioe) { @@ -197,6 +201,6 @@ public class RedirectListener extends Thread { String message = (status == OSUOperationStatus.UserInputAborted) ? "Browser closed" : null; - return mOSUManager.notifyUser(status, message, mSpName); + return mPlatformAdapter.notifyUser(status, message, mSpName); } } diff --git a/packages/Osu/src/com/android/hotspot2/osu/service/RemediationHandler.java b/packages/Osu/src/com/android/hotspot2/osu/service/RemediationHandler.java new file mode 100644 index 0000000000000..b5533fb5a2ea2 --- /dev/null +++ b/packages/Osu/src/com/android/hotspot2/osu/service/RemediationHandler.java @@ -0,0 +1,581 @@ +package com.android.hotspot2.osu.service; + +import android.app.AlarmManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.Network; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.android.hotspot2.PasspointMatch; +import com.android.hotspot2.Utils; +import com.android.hotspot2.flow.FlowService; +import com.android.hotspot2.omadm.MOManager; +import com.android.hotspot2.omadm.MOTree; +import com.android.hotspot2.omadm.OMAConstants; +import com.android.hotspot2.omadm.OMAException; +import com.android.hotspot2.omadm.OMAParser; +import com.android.hotspot2.osu.OSUManager; +import com.android.hotspot2.pps.HomeSP; +import com.android.hotspot2.pps.UpdateInfo; +import com.android.hotspot2.flow.IFlowService; + +import org.xml.sax.SAXException; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import static com.android.hotspot2.pps.UpdateInfo.UpdateRestriction; + +public class RemediationHandler implements AlarmManager.OnAlarmListener { + private final Context mContext; + private final File mStateFile; + + private final Map mPasspointConfigs = new HashMap<>(); + private final Map> mUpdates = new HashMap<>(); + private final LinkedList mOutstanding = new LinkedList<>(); + + private WifiInfo mActiveWifiInfo; + private PasspointConfig mActivePasspointConfig; + + public RemediationHandler(Context context, File stateFile) { + mContext = context; + mStateFile = stateFile; + Log.d(OSUManager.TAG, "State file: " + stateFile); + reloadAll(context, mPasspointConfigs, stateFile, mUpdates); + mActivePasspointConfig = getActivePasspointConfig(); + calculateTimeout(); + } + + /** + * Network configs change: Re-evaluate set of HomeSPs and recalculate next time-out. + */ + public void networkConfigChange() { + Log.d(OSUManager.TAG, "Networks changed"); + mPasspointConfigs.clear(); + mUpdates.clear(); + Iterator updates = mOutstanding.iterator(); + while (updates.hasNext()) { + PendingUpdate update = updates.next(); + if (!update.isWnmBased()) { + updates.remove(); + } + } + reloadAll(mContext, mPasspointConfigs, mStateFile, mUpdates); + calculateTimeout(); + } + + /** + * Connected to new network: Try to rematch any outstanding remediation entries to the new + * config. + */ + public void newConnection(WifiInfo newNetwork) { + mActivePasspointConfig = newNetwork != null ? getActivePasspointConfig() : null; + if (mActivePasspointConfig != null) { + Log.d(OSUManager.TAG, "New connection to " + + mActivePasspointConfig.getHomeSP().getFQDN()); + } else { + Log.d(OSUManager.TAG, "No passpoint connection"); + return; + } + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + Network network = wifiManager.getCurrentNetwork(); + + Iterator updates = mOutstanding.iterator(); + while (updates.hasNext()) { + PendingUpdate update = updates.next(); + try { + if (update.matches(wifiInfo, mActivePasspointConfig.getHomeSP())) { + update.remediate(network); + updates.remove(); + } else if (update.isWnmBased()) { + Log.d(OSUManager.TAG, "WNM sender mismatches with BSS, cancelling remediation"); + // Drop WNM update if it doesn't match the connected network + updates.remove(); + } + } catch (IOException ioe) { + updates.remove(); + } + } + } + + /** + * Remediation timer fired: Iterate HomeSP and either pass on to remediation if there is a + * policy match or put on hold-off queue until a new network connection is made. + */ + @Override + public void onAlarm() { + Log.d(OSUManager.TAG, "Remediation timer"); + calculateTimeout(); + } + + /** + * Remediation frame received, either pass on to pre-remediation check right away or await + * network connection. + */ + public void wnmReceived(long bssid, String url) { + PendingUpdate update = new PendingUpdate(bssid, url); + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + try { + if (mActivePasspointConfig != null + && update.matches(wifiInfo, mActivePasspointConfig.getHomeSP())) { + Log.d(OSUManager.TAG, "WNM frame received, remediating now"); + update.remediate(wifiManager.getCurrentNetwork()); + } else { + Log.d(OSUManager.TAG, "WNM frame received, adding to outstanding remediations"); + mOutstanding.addFirst(new PendingUpdate(bssid, url)); + } + } catch (IOException ioe) { + Log.w(OSUManager.TAG, "Failed to remediate from WNM: " + ioe); + } + } + + /** + * Callback to indicate that remediation has succeeded. + * @param fqdn The SPs FQDN + * @param policy set if this update was a policy update rather than a subscription update. + */ + public void remediationDone(String fqdn, boolean policy) { + Log.d(OSUManager.TAG, "Remediation complete for " + fqdn); + long now = System.currentTimeMillis(); + List events = mUpdates.get(fqdn); + if (events == null) { + events = new ArrayList<>(); + events.add(new RemediationEvent(fqdn, policy, now)); + mUpdates.put(fqdn, events); + } else { + Iterator eventsIterator = events.iterator(); + while (eventsIterator.hasNext()) { + RemediationEvent event = eventsIterator.next(); + if (event.isPolicy() == policy) { + eventsIterator.remove(); + } + } + events.add(new RemediationEvent(fqdn, policy, now)); + } + saveUpdates(mStateFile, mUpdates); + } + + public String getCurrentSpName() { + PasspointConfig config = getActivePasspointConfig(); + return config != null ? config.getHomeSP().getFriendlyName() : "unknown"; + } + + private PasspointConfig getActivePasspointConfig() { + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + mActiveWifiInfo = wifiManager.getConnectionInfo(); + if (mActiveWifiInfo == null) { + return null; + } + + for (PasspointConfig passpointConfig : mPasspointConfigs.values()) { + if (passpointConfig.getWifiConfiguration().networkId + == mActiveWifiInfo.getNetworkId()) { + return passpointConfig; + } + } + return null; + } + + private void calculateTimeout() { + long now = System.currentTimeMillis(); + long next = Long.MAX_VALUE; + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + Network network = wifiManager.getCurrentNetwork(); + + boolean newBaseTimes = false; + for (PasspointConfig passpointConfig : mPasspointConfigs.values()) { + HomeSP homeSP = passpointConfig.getHomeSP(); + + for (boolean policy : new boolean[] {false, true}) { + Long expiry = getNextUpdate(homeSP, policy, now); + Log.d(OSUManager.TAG, "Next remediation for " + homeSP.getFQDN() + + (policy ? "/policy" : "/subscription") + + " is " + toExpiry(expiry)); + if (expiry == null || inProgress(homeSP, policy)) { + continue; + } else if (expiry < 0) { + next = now - expiry; + newBaseTimes = true; + continue; + } + + if (expiry <= now) { + String uri = policy ? homeSP.getPolicy().getPolicyUpdate().getURI() + : homeSP.getSubscriptionUpdate().getURI(); + PendingUpdate update = new PendingUpdate(homeSP, uri, policy); + try { + if (update.matches(mActiveWifiInfo, homeSP)) { + update.remediate(network); + } else { + Log.d(OSUManager.TAG, "Remediation for " + + homeSP.getFQDN() + " pending"); + mOutstanding.addLast(update); + } + } catch (IOException ioe) { + Log.w(OSUManager.TAG, "Failed to remediate " + + homeSP.getFQDN() + ": " + ioe); + } + } else { + next = Math.min(next, expiry); + } + } + } + if (newBaseTimes) { + saveUpdates(mStateFile, mUpdates); + } + Log.d(OSUManager.TAG, "Next time-out at " + toExpiry(next)); + AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + alarmManager.set(AlarmManager.RTC, next, "osu-remediation", this, null); + } + + private static String toExpiry(Long time) { + if (time == null) { + return "n/a"; + } else if (time < 0) { + return Utils.toHMS(-time) + " from now"; + } else if (time > 0xffffffffffffL) { + return "infinity"; + } else { + return Utils.toUTCString(time); + } + } + + /** + * Get the next update time for the homeSP subscription or policy entry. Automatically add a + * wall time reference if it is missing. + * @param homeSP The HomeSP to check + * @param policy policy or subscription object. + * @return -interval if no wall time ref, null if n/a, otherwise wall time of next update. + */ + private Long getNextUpdate(HomeSP homeSP, boolean policy, long now) { + long interval; + if (policy) { + interval = homeSP.getPolicy().getPolicyUpdate().getInterval(); + } else if (homeSP.getSubscriptionUpdate() != null) { + interval = homeSP.getSubscriptionUpdate().getInterval(); + } else { + return null; + } + if (interval < 0) { + return null; + } + + RemediationEvent event = getMatchingEvent(mUpdates.get(homeSP.getFQDN()), policy); + if (event == null) { + List events = mUpdates.get(homeSP.getFQDN()); + if (events == null) { + events = new ArrayList<>(); + mUpdates.put(homeSP.getFQDN(), events); + } + events.add(new RemediationEvent(homeSP.getFQDN(), policy, now)); + return -interval; + } + return event.getLastUpdate() + interval; + } + + private boolean inProgress(HomeSP homeSP, boolean policy) { + Iterator updates = mOutstanding.iterator(); + while (updates.hasNext()) { + PendingUpdate update = updates.next(); + if (update.getHomeSP() != null + && update.getHomeSP().getFQDN().equals(homeSP.getFQDN())) { + if (update.isPolicy() && !policy) { + // Subscription updates takes precedence over policy updates + updates.remove(); + return false; + } else { + return true; + } + } + } + return false; + } + + private static RemediationEvent getMatchingEvent( + List events, boolean policy) { + if (events == null) { + return null; + } + for (RemediationEvent event : events) { + if (event.isPolicy() == policy) { + return event; + } + } + return null; + } + + private static void reloadAll(Context context, Map passpointConfigs, + File stateFile, Map> updates) { + + loadAllSps(context, passpointConfigs); + try { + loadUpdates(stateFile, updates); + } catch (IOException ioe) { + Log.w(OSUManager.TAG, "Failed to load updates file: " + ioe); + } + + boolean change = false; + Iterator>> events = updates.entrySet().iterator(); + while (events.hasNext()) { + Map.Entry> event = events.next(); + if (!passpointConfigs.containsKey(event.getKey())) { + events.remove(); + change = true; + } + } + Log.d(OSUManager.TAG, "Updates: " + updates); + if (change) { + saveUpdates(stateFile, updates); + } + } + + private static void loadAllSps(Context context, Map passpointConfigs) { + WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + List configs = wifiManager.getPrivilegedConfiguredNetworks(); + if (configs == null) { + return; + } + int count = 0; + for (WifiConfiguration config : configs) { + String moTree = config.getMoTree(); + if (moTree != null) { + try { + passpointConfigs.put(config.FQDN, new PasspointConfig(config)); + count++; + } catch (IOException | SAXException e) { + Log.w(OSUManager.TAG, "Failed to parse MO: " + e); + } + } + } + Log.d(OSUManager.TAG, "Loaded " + count + " SPs"); + } + + private static void loadUpdates(File file, Map> updates) + throws IOException { + try (BufferedReader in = new BufferedReader(new FileReader(file))) { + String line; + while ((line = in.readLine()) != null) { + try { + RemediationEvent event = new RemediationEvent(line); + List events = updates.get(event.getFqdn()); + if (events == null) { + events = new ArrayList<>(); + updates.put(event.getFqdn(), events); + } + events.add(event); + } catch (IOException | NumberFormatException e) { + Log.w(OSUManager.TAG, "Bad line in " + file + ": '" + line + "': " + e); + } + } + } + } + + private static void saveUpdates(File file, Map> updates) { + try (BufferedWriter out = new BufferedWriter(new FileWriter(file, false))) { + for (List events : updates.values()) { + for (RemediationEvent event : events) { + Log.d(OSUManager.TAG, "Writing wall time ref for " + event); + out.write(event.toString()); + out.newLine(); + } + } + } catch (IOException ioe) { + Log.w(OSUManager.TAG, "Failed to save update state: " + ioe); + } + } + + private static class PasspointConfig { + private final WifiConfiguration mWifiConfiguration; + private final MOTree mMOTree; + private final HomeSP mHomeSP; + + private PasspointConfig(WifiConfiguration config) throws IOException, SAXException { + mWifiConfiguration = config; + OMAParser omaParser = new OMAParser(); + mMOTree = omaParser.parse(config.getMoTree(), OMAConstants.PPS_URN); + List spList = MOManager.buildSPs(mMOTree); + if (spList.size() != 1) { + throw new OMAException("Expected exactly one HomeSP, got " + spList.size()); + } + mHomeSP = spList.iterator().next(); + } + + public WifiConfiguration getWifiConfiguration() { + return mWifiConfiguration; + } + + public HomeSP getHomeSP() { + return mHomeSP; + } + + public MOTree getMOTree() { + return mMOTree; + } + } + + private static class RemediationEvent { + private final String mFqdn; + private final boolean mPolicy; + private final long mLastUpdate; + + private RemediationEvent(String value) throws IOException { + String[] segments = value.split(" "); + if (segments.length != 3) { + throw new IOException("Bad line: '" + value + "'"); + } + mFqdn = segments[0]; + mPolicy = segments[1].equals("1"); + mLastUpdate = Long.parseLong(segments[2]); + } + + private RemediationEvent(String fqdn, boolean policy, long now) { + mFqdn = fqdn; + mPolicy = policy; + mLastUpdate = now; + } + + public String getFqdn() { + return mFqdn; + } + + public boolean isPolicy() { + return mPolicy; + } + + public long getLastUpdate() { + return mLastUpdate; + } + + @Override + public String toString() { + return String.format("%s %c %d", mFqdn, mPolicy ? '1' : '0', mLastUpdate); + } + } + + private class PendingUpdate { + private final HomeSP mHomeSP; // For time based updates + private final long mBssid; // WNM based + private final String mUrl; // WNM based + private final boolean mPolicy; + + private PendingUpdate(HomeSP homeSP, String url, boolean policy) { + mHomeSP = homeSP; + mPolicy = policy; + mBssid = 0L; + mUrl = url; + } + + private PendingUpdate(long bssid, String url) { + mBssid = bssid; + mUrl = url; + mHomeSP = null; + mPolicy = false; + } + + private boolean matches(WifiInfo wifiInfo, HomeSP activeSP) throws IOException { + if (mHomeSP == null) { + // WNM initiated remediation, HomeSP restriction + Log.d(OSUManager.TAG, String.format("Checking applicability of %s to %012x\n", + wifiInfo != null ? wifiInfo.getBSSID() : "-", mBssid)); + return wifiInfo != null + && Utils.parseMac(wifiInfo.getBSSID()) == mBssid; + //&& passesRestriction(activeSP); // !!! b/28600780 + } else { + return passesRestriction(mHomeSP); + } + } + + private boolean passesRestriction(HomeSP restrictingSP) + throws IOException { + UpdateInfo updateInfo; + if (mPolicy) { + if (restrictingSP.getPolicy() == null) { + throw new IOException("No policy object"); + } + updateInfo = restrictingSP.getPolicy().getPolicyUpdate(); + } else { + updateInfo = restrictingSP.getSubscriptionUpdate(); + } + + if (updateInfo.getUpdateRestriction() == UpdateRestriction.Unrestricted) { + return true; + } + + PasspointMatch match = matchProviderWithCurrentNetwork(restrictingSP.getFQDN()); + Log.d(OSUManager.TAG, "Current match for '" + restrictingSP.getFQDN() + + "' is " + match + ", restriction " + updateInfo.getUpdateRestriction()); + return match == PasspointMatch.HomeProvider + || (match == PasspointMatch.RoamingProvider + && updateInfo.getUpdateRestriction() == UpdateRestriction.RoamingPartner); + } + + private void remediate(Network network) { + RemediationHandler.this.remediate(mHomeSP != null ? mHomeSP.getFQDN() : null, + mUrl, mPolicy, network); + } + + private HomeSP getHomeSP() { + return mHomeSP; + } + + private boolean isPolicy() { + return mPolicy; + } + + private boolean isWnmBased() { + return mHomeSP == null; + } + + private PasspointMatch matchProviderWithCurrentNetwork(String fqdn) { + WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + return Utils.mapEnum(wifiManager.matchProviderWithCurrentNetwork(fqdn), + PasspointMatch.class); + } + } + + /** + * Initiate remediation + * @param spFqdn The FQDN of the current SP, not set for WNM based remediation + * @param url The URL of the remediation server + * @param policy Set if this is a policy update rather than a subscription update + * @param network The network to use for remediation + */ + private void remediate(final String spFqdn, final String url, + final boolean policy, final Network network) { + mContext.bindService(new Intent(mContext, FlowService.class), new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + try { + IFlowService fs = IFlowService.Stub.asInterface(service); + fs.remediate(spFqdn, url, policy, network); + } catch (RemoteException re) { + Log.e(OSUManager.TAG, "Caught re: " + re); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + + } + }, Context.BIND_AUTO_CREATE); + } +} diff --git a/packages/Osu/src/com/android/hotspot2/osu/service/SubscriptionTimer.java b/packages/Osu/src/com/android/hotspot2/osu/service/SubscriptionTimer.java deleted file mode 100644 index 783d14b87b354..0000000000000 --- a/packages/Osu/src/com/android/hotspot2/osu/service/SubscriptionTimer.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.android.hotspot2.osu.service; - -import android.content.Context; -import android.os.Handler; -import android.util.Log; - -import com.android.hotspot2.Utils; -import com.android.hotspot2.WifiNetworkAdapter; -import com.android.hotspot2.osu.OSUManager; -import com.android.hotspot2.pps.HomeSP; - -import org.xml.sax.SAXException; - -import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -public class SubscriptionTimer implements Runnable { - private final Handler mHandler; - private final OSUManager mOSUManager; - private final WifiNetworkAdapter mWifiNetworkAdapter; - private final Map mOutstanding = new HashMap<>(); - - private static class UpdateAction { - private final long mRemediation; - private final long mPolicy; - - private UpdateAction(HomeSP homeSP, long now) { - mRemediation = homeSP.getSubscriptionUpdate() != null ? - now + homeSP.getSubscriptionUpdate().getInterval() : -1; - mPolicy = homeSP.getPolicy() != null ? - now + homeSP.getPolicy().getPolicyUpdate().getInterval() : -1; - - Log.d(OSUManager.TAG, "Timer set for " + homeSP.getFQDN() + - ", remediation: " + Utils.toUTCString(mRemediation) + - ", policy: " + Utils.toUTCString(mPolicy)); - } - - private boolean remediate(long now) { - return mRemediation > 0 && now >= mRemediation; - } - - private boolean policyUpdate(long now) { - return mPolicy > 0 && now >= mPolicy; - } - - private long nextExpiry(long now) { - long min = Long.MAX_VALUE; - if (mRemediation > now) { - min = mRemediation; - } - if (mPolicy > now) { - min = Math.min(min, mPolicy); - } - return min; - } - } - - private static final String ACTION_TIMER = - "com.android.hotspot2.osu.service.SubscriptionTimer.action.TICK"; - - public SubscriptionTimer(OSUManager osuManager, - WifiNetworkAdapter wifiNetworkAdapter, Context context) { - mOSUManager = osuManager; - mWifiNetworkAdapter = wifiNetworkAdapter; - mHandler = new Handler(); - } - - @Override - public void run() { - checkUpdates(); - } - - public void checkUpdates() { - mHandler.removeCallbacks(this); - long now = System.currentTimeMillis(); - long next = Long.MAX_VALUE; - Collection homeSPs = mWifiNetworkAdapter.getLoadedSPs(); - if (homeSPs.isEmpty()) { - return; - } - for (HomeSP homeSP : homeSPs) { - UpdateAction updateAction = mOutstanding.get(homeSP); - try { - if (updateAction == null) { - updateAction = new UpdateAction(homeSP, now); - mOutstanding.put(homeSP, updateAction); - } else if (updateAction.remediate(now)) { - mOSUManager.remediate(homeSP, false); - mOutstanding.put(homeSP, new UpdateAction(homeSP, now)); - } else if (updateAction.policyUpdate(now)) { - mOSUManager.remediate(homeSP, true); - mOutstanding.put(homeSP, new UpdateAction(homeSP, now)); - } - next = Math.min(next, updateAction.nextExpiry(now)); - } catch (IOException | SAXException e) { - Log.d(OSUManager.TAG, "Failed subscription update: " + e.getMessage()); - } - } - setAlarm(next); - } - - private void setAlarm(long tod) { - long delay = tod - System.currentTimeMillis(); - mHandler.postAtTime(this, Math.max(1, delay)); - } -} diff --git a/packages/Osu/src/com/android/hotspot2/pps/UpdateInfo.java b/packages/Osu/src/com/android/hotspot2/pps/UpdateInfo.java index e32f6c387bcfe..645e1fa485a3d 100644 --- a/packages/Osu/src/com/android/hotspot2/pps/UpdateInfo.java +++ b/packages/Osu/src/com/android/hotspot2/pps/UpdateInfo.java @@ -23,6 +23,8 @@ import static com.android.hotspot2.omadm.MOManager.TAG_UsernamePassword; public class UpdateInfo { public enum UpdateRestriction {HomeSP, RoamingPartner, Unrestricted} + public static final long NO_UPDATE = 0xffffffffL; + private final long mInterval; private final boolean mSPPClientInitiated; private final UpdateRestriction mUpdateRestriction; @@ -33,8 +35,8 @@ public class UpdateInfo { private final String mCertFP; public UpdateInfo(OMANode policyUpdate) throws OMAException { - mInterval = MOManager.getLong(policyUpdate, TAG_UpdateInterval, null) * - MOManager.IntervalFactor; + long minutes = MOManager.getLong(policyUpdate, TAG_UpdateInterval, null); + mInterval = minutes == NO_UPDATE ? -1 : minutes * MOManager.IntervalFactor; mSPPClientInitiated = MOManager.getSelection(policyUpdate, TAG_UpdateMethod); mUpdateRestriction = MOManager.getSelection(policyUpdate, TAG_Restriction); mURI = MOManager.getString(policyUpdate, TAG_URI); diff --git a/packages/Osu/src/com/android/hotspot2/utils/HTTPResponse.java b/packages/Osu/src/com/android/hotspot2/utils/HTTPResponse.java index ba1b16713a9dc..b82814d11fd47 100644 --- a/packages/Osu/src/com/android/hotspot2/utils/HTTPResponse.java +++ b/packages/Osu/src/com/android/hotspot2/utils/HTTPResponse.java @@ -36,13 +36,9 @@ public class HTTPResponse implements HTTPMessage { while (offset < expected) { int amount = in.read(input, offset, input.length - offset); - Log.d(OSUManager.TAG, String.format("Reading into %d from %d, amount %d -> %d", - input.length, offset, input.length - offset, amount)); if (amount < 0) { throw new EOFException(); } - //Log.d("ZXZ", "HTTP response: '" - // + new String(input, 0, offset + amount, StandardCharsets.ISO_8859_1)); if (body < 0) { for (int n = offset; n < offset + amount; n++) {