Merge "Migrate default app data on non-FBE devices."
diff --git a/api/current.txt b/api/current.txt
index 9df1023..5c0628c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -36444,6 +36444,7 @@
field public static final java.lang.String KEY_DEFAULT_SIM_CALL_MANAGER_STRING = "default_sim_call_manager_string";
field public static final java.lang.String KEY_DISABLE_CDMA_ACTIVATION_CODE_BOOL = "disable_cdma_activation_code_bool";
field public static final java.lang.String KEY_DTMF_TYPE_ENABLED_BOOL = "dtmf_type_enabled_bool";
+ field public static final java.lang.String KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT = "duration_blocking_disabled_after_emergency_int";
field public static final java.lang.String KEY_EDITABLE_ENHANCED_4G_LTE_BOOL = "editable_enhanced_4g_lte_bool";
field public static final java.lang.String KEY_ENABLE_DIALER_KEY_VIBRATION_BOOL = "enable_dialer_key_vibration_bool";
field public static final java.lang.String KEY_FORCE_HOME_NETWORK_BOOL = "force_home_network_bool";
diff --git a/api/system-current.txt b/api/system-current.txt
index 03167f6..2adb1e6 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -39059,6 +39059,7 @@
field public static final java.lang.String KEY_DEFAULT_SIM_CALL_MANAGER_STRING = "default_sim_call_manager_string";
field public static final java.lang.String KEY_DISABLE_CDMA_ACTIVATION_CODE_BOOL = "disable_cdma_activation_code_bool";
field public static final java.lang.String KEY_DTMF_TYPE_ENABLED_BOOL = "dtmf_type_enabled_bool";
+ field public static final java.lang.String KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT = "duration_blocking_disabled_after_emergency_int";
field public static final java.lang.String KEY_EDITABLE_ENHANCED_4G_LTE_BOOL = "editable_enhanced_4g_lte_bool";
field public static final java.lang.String KEY_ENABLE_DIALER_KEY_VIBRATION_BOOL = "enable_dialer_key_vibration_bool";
field public static final java.lang.String KEY_FORCE_HOME_NETWORK_BOOL = "force_home_network_bool";
diff --git a/api/test-current.txt b/api/test-current.txt
index d6175af..7abc6d4 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -36459,6 +36459,7 @@
field public static final java.lang.String KEY_DEFAULT_SIM_CALL_MANAGER_STRING = "default_sim_call_manager_string";
field public static final java.lang.String KEY_DISABLE_CDMA_ACTIVATION_CODE_BOOL = "disable_cdma_activation_code_bool";
field public static final java.lang.String KEY_DTMF_TYPE_ENABLED_BOOL = "dtmf_type_enabled_bool";
+ field public static final java.lang.String KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT = "duration_blocking_disabled_after_emergency_int";
field public static final java.lang.String KEY_EDITABLE_ENHANCED_4G_LTE_BOOL = "editable_enhanced_4g_lte_bool";
field public static final java.lang.String KEY_ENABLE_DIALER_KEY_VIBRATION_BOOL = "enable_dialer_key_vibration_bool";
field public static final java.lang.String KEY_FORCE_HOME_NETWORK_BOOL = "force_home_network_bool";
diff --git a/cmds/am/src/com/android/commands/am/Am.java b/cmds/am/src/com/android/commands/am/Am.java
index a74a1ca..acc68cf 100644
--- a/cmds/am/src/com/android/commands/am/Am.java
+++ b/cmds/am/src/com/android/commands/am/Am.java
@@ -124,7 +124,7 @@
PrintWriter pw = new PrintWriter(out);
pw.println(
"usage: am [subcommand] [options]\n" +
- "usage: am start [-D] [-W] [-P <FILE>] [--start-profiler <FILE>]\n" +
+ "usage: am start [-D] [-N] [-W] [-P <FILE>] [--start-profiler <FILE>]\n" +
" [--sampling INTERVAL] [-R COUNT] [-S]\n" +
" [--track-allocation] [--user <USER_ID> | current] <INTENT>\n" +
" am startservice [--user <USER_ID> | current] <INTENT>\n" +
@@ -183,6 +183,7 @@
"\n" +
"am start: start an Activity. Options are:\n" +
" -D: enable debugging\n" +
+ " -N: enable native debugging\n" +
" -W: wait for launch to complete\n" +
" --start-profiler <FILE>: start profiler and send results to <FILE>\n" +
" --sampling INTERVAL: use sample profiling with INTERVAL microseconds\n" +
@@ -478,6 +479,8 @@
public boolean handleOption(String opt, ShellCommand cmd) {
if (opt.equals("-D")) {
mStartFlags |= ActivityManager.START_FLAG_DEBUG;
+ } else if (opt.equals("-N")) {
+ mStartFlags |= ActivityManager.START_FLAG_NATIVE_DEBUGGING;
} else if (opt.equals("-W")) {
mWaitOption = true;
} else if (opt.equals("-P")) {
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index 1ab55dd..980329f 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -1030,6 +1030,20 @@
}
}
+ /**
+ * @hide
+ * TODO: For animatorSet defined in XML, we can use a flag to indicate what the play order
+ * if defined (i.e. sequential or together), then we can use the flag instead of calculate
+ * dynamically.
+ * @return whether all the animators in the set are supposed to play together
+ */
+ public boolean shouldPlayTogether() {
+ updateAnimatorsDuration();
+ createDependencyGraph();
+ // All the child nodes are set out to play right after the delay animation
+ return mRootNode.mChildNodes.size() == mNodes.size() - 1;
+ }
+
@Override
public long getTotalDuration() {
updateAnimatorsDuration();
diff --git a/core/java/android/animation/PathKeyframes.java b/core/java/android/animation/PathKeyframes.java
index 2a47b68..8230ac5 100644
--- a/core/java/android/animation/PathKeyframes.java
+++ b/core/java/android/animation/PathKeyframes.java
@@ -231,7 +231,7 @@
}
}
- private abstract static class IntKeyframesBase extends SimpleKeyframes implements IntKeyframes {
+ abstract static class IntKeyframesBase extends SimpleKeyframes implements IntKeyframes {
@Override
public Class getType() {
return Integer.class;
@@ -243,7 +243,7 @@
}
}
- private abstract static class FloatKeyframesBase extends SimpleKeyframes
+ abstract static class FloatKeyframesBase extends SimpleKeyframes
implements FloatKeyframes {
@Override
public Class getType() {
diff --git a/core/java/android/animation/PropertyValuesHolder.java b/core/java/android/animation/PropertyValuesHolder.java
index e993cca..6ba5b96 100644
--- a/core/java/android/animation/PropertyValuesHolder.java
+++ b/core/java/android/animation/PropertyValuesHolder.java
@@ -21,6 +21,7 @@
import android.util.FloatProperty;
import android.util.IntProperty;
import android.util.Log;
+import android.util.PathParser;
import android.util.Property;
import java.lang.reflect.InvocationTargetException;
@@ -1046,6 +1047,43 @@
return mAnimatedValue;
}
+ /**
+ * PropertyValuesHolder is Animators use to hold internal animation related data.
+ * Therefore, in order to replicate the animation behavior, we need to get data out of
+ * PropertyValuesHolder.
+ * @hide
+ */
+ public void getPropertyValues(PropertyValues values) {
+ init();
+ values.propertyName = mPropertyName;
+ values.type = mValueType;
+ values.startValue = mKeyframes.getValue(0);
+ if (values.startValue instanceof PathParser.PathData) {
+ // PathData evaluator returns the same mutable PathData object when query fraction,
+ // so we have to make a copy here.
+ values.startValue = new PathParser.PathData((PathParser.PathData) values.startValue);
+ }
+ values.endValue = mKeyframes.getValue(1);
+ if (values.endValue instanceof PathParser.PathData) {
+ // PathData evaluator returns the same mutable PathData object when query fraction,
+ // so we have to make a copy here.
+ values.endValue = new PathParser.PathData((PathParser.PathData) values.endValue);
+ }
+ // TODO: We need a better way to get data out of keyframes.
+ if (mKeyframes instanceof PathKeyframes.FloatKeyframesBase
+ || mKeyframes instanceof PathKeyframes.IntKeyframesBase) {
+ // property values will animate based on external data source (e.g. Path)
+ values.dataSource = new PropertyValues.DataSource() {
+ @Override
+ public Object getValueAtFraction(float fraction) {
+ return mKeyframes.getValue(fraction);
+ }
+ };
+ } else {
+ values.dataSource = null;
+ }
+ }
+
@Override
public String toString() {
return mPropertyName + ": " + mKeyframes.toString();
@@ -1601,6 +1639,24 @@
}
};
+ /**
+ * @hide
+ */
+ public static class PropertyValues {
+ public String propertyName;
+ public Class type;
+ public Object startValue;
+ public Object endValue;
+ public DataSource dataSource = null;
+ public interface DataSource {
+ Object getValueAtFraction(float fraction);
+ }
+ public String toString() {
+ return ("property name: " + propertyName + ", type: " + type + ", startValue: "
+ + startValue.toString() + ", endValue: " + endValue.toString());
+ }
+ }
+
native static private long nGetIntMethod(Class targetClass, String methodName);
native static private long nGetFloatMethod(Class targetClass, String methodName);
native static private long nGetMultipleIntMethod(Class targetClass, String methodName,
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index cb29419..1eb2fe2 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -245,6 +245,13 @@
public static final int START_FLAG_TRACK_ALLOCATION = 1<<2;
/**
+ * Flag for IActivityManaqer.startActivity: launch the app with
+ * native debugging support.
+ * @hide
+ */
+ public static final int START_FLAG_NATIVE_DEBUGGING = 1<<3;
+
+ /**
* Result for IActivityManaqer.broadcastIntent: success!
* @hide
*/
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index 52fa2ed..b33e807 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -147,6 +147,11 @@
public static final int WAKE_TYPE_DRAW = 18;
/**
+ * A constant indicating a bluetooth scan timer.
+ */
+ public static final int BLUETOOTH_SCAN_ON = 19;
+
+ /**
* Include all of the data in the stats, including previously saved data.
*/
public static final int STATS_SINCE_CHARGED = 0;
@@ -438,6 +443,7 @@
public abstract Timer getFlashlightTurnedOnTimer();
public abstract Timer getCameraTurnedOnTimer();
public abstract Timer getForegroundActivityTimer();
+ public abstract Timer getBluetoothScanTimer();
// Time this uid has any processes in the top state.
public static final int PROCESS_STATE_TOP = 0;
@@ -1179,6 +1185,7 @@
public static final int STATE2_PHONE_IN_CALL_FLAG = 1<<23;
public static final int STATE2_BLUETOOTH_ON_FLAG = 1<<22;
public static final int STATE2_CAMERA_FLAG = 1<<21;
+ public static final int STATE2_BLUETOOTH_SCAN_FLAG = 1 << 20;
public static final int MOST_INTERESTING_STATES2 =
STATE2_POWER_SAVE_FLAG | STATE2_WIFI_ON_FLAG | STATE2_DEVICE_IDLE_MASK
@@ -1922,6 +1929,7 @@
HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT, "wifi_suppl", "Wsp",
WIFI_SUPPL_STATE_NAMES, WIFI_SUPPL_STATE_SHORT_NAMES),
new BitDescription(HistoryItem.STATE2_CAMERA_FLAG, "camera", "ca"),
+ new BitDescription(HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG, "ble_scan", "bles"),
};
public static final String[] HISTORY_EVENT_NAMES = new String[] {
@@ -2041,6 +2049,13 @@
*/
public abstract long getCameraOnTime(long elapsedRealtimeUs, int which);
+ /**
+ * Returns the time in microseconds that bluetooth scans were running while the device was
+ * on battery.
+ *
+ * {@hide}
+ */
+ public abstract long getBluetoothScanTime(long elapsedRealtimeUs, int which);
public static final int NETWORK_MOBILE_RX_DATA = 0;
public static final int NETWORK_MOBILE_TX_DATA = 1;
@@ -2797,9 +2812,12 @@
final long mobileTxTotalPackets = getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, which);
final long wifiRxTotalPackets = getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which);
final long wifiTxTotalPackets = getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which);
+ final long btRxTotalBytes = getNetworkActivityBytes(NETWORK_BT_RX_DATA, which);
+ final long btTxTotalBytes = getNetworkActivityBytes(NETWORK_BT_TX_DATA, which);
dumpLine(pw, 0 /* uid */, category, GLOBAL_NETWORK_DATA,
mobileRxTotalBytes, mobileTxTotalBytes, wifiRxTotalBytes, wifiTxTotalBytes,
- mobileRxTotalPackets, mobileTxTotalPackets, wifiRxTotalPackets, wifiTxTotalPackets);
+ mobileRxTotalPackets, mobileTxTotalPackets, wifiRxTotalPackets, wifiTxTotalPackets,
+ btRxTotalBytes, btTxTotalBytes);
// Dump Modem controller stats
dumpControllerActivityLine(pw, 0 /* uid */, category, GLOBAL_MODEM_CONTROLLER_DATA,
@@ -3017,14 +3035,18 @@
final int mobileActiveCount = u.getMobileRadioActiveCount(which);
final long wifiPacketsRx = u.getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which);
final long wifiPacketsTx = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which);
+ final long btBytesRx = u.getNetworkActivityBytes(NETWORK_BT_RX_DATA, which);
+ final long btBytesTx = u.getNetworkActivityBytes(NETWORK_BT_TX_DATA, which);
if (mobileBytesRx > 0 || mobileBytesTx > 0 || wifiBytesRx > 0 || wifiBytesTx > 0
|| mobilePacketsRx > 0 || mobilePacketsTx > 0 || wifiPacketsRx > 0
- || wifiPacketsTx > 0 || mobileActiveTime > 0 || mobileActiveCount > 0) {
+ || wifiPacketsTx > 0 || mobileActiveTime > 0 || mobileActiveCount > 0
+ || btBytesRx > 0 || btBytesTx > 0) {
dumpLine(pw, uid, category, NETWORK_DATA, mobileBytesRx, mobileBytesTx,
wifiBytesRx, wifiBytesTx,
mobilePacketsRx, mobilePacketsTx,
wifiPacketsRx, wifiPacketsTx,
- mobileActiveTime, mobileActiveCount);
+ mobileActiveTime, mobileActiveCount,
+ btBytesRx, btBytesTx);
}
// Dump modem controller data, per UID.
@@ -3046,6 +3068,9 @@
dumpControllerActivityLine(pw, uid, category, WIFI_CONTROLLER_DATA,
u.getWifiControllerActivity(), which);
+ dumpControllerActivityLine(pw, uid, category, BLUETOOTH_CONTROLLER_DATA,
+ u.getBluetoothControllerActivity(), which);
+
if (u.hasUserActivity()) {
args = new Object[Uid.NUM_USER_ACTIVITY_TYPES];
boolean hasData = false;
@@ -3668,6 +3693,12 @@
pw.print(" Bluetooth total received: "); pw.print(formatBytesLocked(btRxTotalBytes));
pw.print(", sent: "); pw.println(formatBytesLocked(btTxTotalBytes));
+ final long bluetoothScanTimeMs = getBluetoothScanTime(rawRealtime, which) / 1000;
+ sb.setLength(0);
+ sb.append(prefix);
+ sb.append(" Bluetooth scan time: "); formatTimeMs(sb, bluetoothScanTimeMs);
+ pw.println(sb.toString());
+
printControllerActivity(pw, sb, prefix, "Bluetooth", getBluetoothControllerActivity(),
which);
@@ -3793,6 +3824,10 @@
pw.print(" wifi=");
printmAh(pw, bs.wifiPowerMah);
}
+ if (bs.bluetoothPowerMah != 0) {
+ pw.print(" bt=");
+ printmAh(pw, bs.bluetoothPowerMah);
+ }
if (bs.gpsPowerMah != 0) {
pw.print(" gps=");
printmAh(pw, bs.gpsPowerMah);
@@ -4035,6 +4070,9 @@
pw.println(" sent");
}
+ uidActivity |= printTimer(pw, sb, u.getBluetoothScanTimer(), rawRealtime, which, prefix,
+ "Bluetooth Scan");
+
if (u.hasUserActivity()) {
boolean hasData = false;
for (int i=0; i<Uid.NUM_USER_ACTIVITY_TYPES; i++) {
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index b2cabd8..b51d2dfb 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -653,6 +653,12 @@
if ((debugFlags & Zygote.DEBUG_GENERATE_DEBUG_INFO) != 0) {
argsForZygote.add("--generate-debug-info");
}
+ if ((debugFlags & Zygote.DEBUG_ALWAYS_JIT) != 0) {
+ argsForZygote.add("--always-jit");
+ }
+ if ((debugFlags & Zygote.DEBUG_NATIVE_DEBUGGABLE) != 0) {
+ argsForZygote.add("--native-debuggable");
+ }
if ((debugFlags & Zygote.DEBUG_ENABLE_ASSERT) != 0) {
argsForZygote.add("--enable-assert");
}
diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java
index 330fcf6..dfdd36d 100644
--- a/core/java/android/provider/ContactsContract.java
+++ b/core/java/android/provider/ContactsContract.java
@@ -2079,10 +2079,11 @@
if (preferHighres) {
final Uri displayPhotoUri = Uri.withAppendedPath(contactUri,
Contacts.Photo.DISPLAY_PHOTO);
- InputStream inputStream;
try {
AssetFileDescriptor fd = cr.openAssetFileDescriptor(displayPhotoUri, "r");
- return fd.createInputStream();
+ if (fd != null) {
+ return fd.createInputStream();
+ }
} catch (IOException e) {
// fallback to the thumbnail code
}
diff --git a/core/java/android/util/PathParser.java b/core/java/android/util/PathParser.java
index 78d5bcd..29a72fd 100644
--- a/core/java/android/util/PathParser.java
+++ b/core/java/android/util/PathParser.java
@@ -104,6 +104,7 @@
}
super.finalize();
}
+
}
/**
diff --git a/core/java/android/view/RenderNode.java b/core/java/android/view/RenderNode.java
index 3122c0b..7017ff5 100644
--- a/core/java/android/view/RenderNode.java
+++ b/core/java/android/view/RenderNode.java
@@ -22,6 +22,7 @@
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Rect;
+import android.graphics.drawable.AnimatedVectorDrawable;
/**
* <p>A display list records a series of graphics related operations and can replay
@@ -774,6 +775,14 @@
mOwningView.mAttachInfo.mViewRootImpl.registerAnimatingRenderNode(this);
}
+ public void addAnimator(AnimatedVectorDrawable.VectorDrawableAnimator animatorSet) {
+ if (mOwningView == null || mOwningView.mAttachInfo == null) {
+ throw new IllegalStateException("Cannot start this animator on a detached view!");
+ }
+ nAddAnimator(mNativeRenderNode, animatorSet.getAnimatorNativePtr());
+ mOwningView.mAttachInfo.mViewRootImpl.registerAnimatingRenderNode(this);
+ }
+
public void endAllAnimators() {
nEndAllAnimators(mNativeRenderNode);
}
diff --git a/core/java/android/view/RenderNodeAnimatorSetHelper.java b/core/java/android/view/RenderNodeAnimatorSetHelper.java
new file mode 100644
index 0000000..ba592d29
--- /dev/null
+++ b/core/java/android/view/RenderNodeAnimatorSetHelper.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.view;
+
+import android.animation.TimeInterpolator;
+import com.android.internal.view.animation.FallbackLUTInterpolator;
+import com.android.internal.view.animation.NativeInterpolatorFactory;
+import com.android.internal.view.animation.NativeInterpolatorFactoryHelper;
+
+/**
+ * This is a helper class to get access to methods and fields needed for RenderNodeAnimatorSet
+ * that are internal or package private to android.view package.
+ *
+ * @hide
+ */
+public class RenderNodeAnimatorSetHelper {
+
+ public static RenderNode getTarget(DisplayListCanvas recordingCanvas) {
+ return recordingCanvas.mNode;
+ }
+
+ public static long createNativeInterpolator(TimeInterpolator interpolator, long
+ duration) {
+ if (interpolator == null) {
+ // create LinearInterpolator
+ return NativeInterpolatorFactoryHelper.createLinearInterpolator();
+ } else if (RenderNodeAnimator.isNativeInterpolator(interpolator)) {
+ return ((NativeInterpolatorFactory)interpolator).createNativeInterpolator();
+ } else {
+ return FallbackLUTInterpolator.createNativeInterpolator(interpolator, duration);
+ }
+ }
+
+}
diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java
index ee73092..1321221 100644
--- a/core/java/android/widget/GridView.java
+++ b/core/java/android/widget/GridView.java
@@ -2404,7 +2404,7 @@
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
- final boolean isHeading = lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+ final boolean isHeading = lp != null && lp.viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
final boolean isSelected = isItemChecked(position);
final CollectionItemInfo itemInfo = CollectionItemInfo.obtain(
row, 1, column, 1, isHeading, isSelected);
diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl
index ec53a2e..74fe94f 100644
--- a/core/java/com/android/internal/app/IBatteryStats.aidl
+++ b/core/java/com/android/internal/app/IBatteryStats.aidl
@@ -124,4 +124,5 @@
void noteBleScanStarted(in WorkSource ws);
void noteBleScanStopped(in WorkSource ws);
+ void noteResetBleScan();
}
diff --git a/core/java/com/android/internal/os/BatterySipper.java b/core/java/com/android/internal/os/BatterySipper.java
index 049d3eb..d92e596 100644
--- a/core/java/com/android/internal/os/BatterySipper.java
+++ b/core/java/com/android/internal/os/BatterySipper.java
@@ -44,6 +44,7 @@
public long wakeLockTimeMs;
public long cameraTimeMs;
public long flashlightTimeMs;
+ public long bluetoothRunningTimeMs;
public long mobileRxPackets;
public long mobileTxPackets;
@@ -56,6 +57,8 @@
public long mobileTxBytes;
public long wifiRxBytes;
public long wifiTxBytes;
+ public long btRxBytes;
+ public long btTxBytes;
public double percent;
public double noCoveragePercent;
public String[] mPackages;
@@ -71,6 +74,7 @@
public double sensorPowerMah;
public double cameraPowerMah;
public double flashlightPowerMah;
+ public double bluetoothPowerMah;
public enum DrainType {
IDLE,
@@ -142,6 +146,7 @@
wakeLockTimeMs += other.wakeLockTimeMs;
cameraTimeMs += other.cameraTimeMs;
flashlightTimeMs += other.flashlightTimeMs;
+ bluetoothRunningTimeMs += other.bluetoothRunningTimeMs;
mobileRxPackets += other.mobileRxPackets;
mobileTxPackets += other.mobileTxPackets;
mobileActive += other.mobileActive;
@@ -152,6 +157,8 @@
mobileTxBytes += other.mobileTxBytes;
wifiRxBytes += other.wifiRxBytes;
wifiTxBytes += other.wifiTxBytes;
+ btRxBytes += other.btRxBytes;
+ btTxBytes += other.btTxBytes;
wifiPowerMah += other.wifiPowerMah;
gpsPowerMah += other.gpsPowerMah;
cpuPowerMah += other.cpuPowerMah;
@@ -160,6 +167,7 @@
wakeLockPowerMah += other.wakeLockPowerMah;
cameraPowerMah += other.cameraPowerMah;
flashlightPowerMah += other.flashlightPowerMah;
+ bluetoothPowerMah += other.bluetoothPowerMah;
}
/**
@@ -169,6 +177,6 @@
public double sumPower() {
return totalPowerMah = usagePowerMah + wifiPowerMah + gpsPowerMah + cpuPowerMah +
sensorPowerMah + mobileRadioPowerMah + wakeLockPowerMah + cameraPowerMah +
- flashlightPowerMah;
+ flashlightPowerMah + bluetoothPowerMah;
}
}
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index e2ccaae..648b1a5 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -107,7 +107,7 @@
private static final int MAGIC = 0xBA757475; // 'BATSTATS'
// Current on-disk Parcel version
- private static final int VERSION = 140 + (USE_OLD_HISTORY ? 1000 : 0);
+ private static final int VERSION = 141 + (USE_OLD_HISTORY ? 1000 : 0);
// Maximum number of items we will record in the history.
private static final int MAX_HISTORY_ITEMS = 2000;
@@ -186,8 +186,13 @@
}
public interface ExternalStatsSync {
- void scheduleSync(String reason);
- void scheduleWifiSync(String reason);
+ public static final int UPDATE_CPU = 0x01;
+ public static final int UPDATE_WIFI = 0x02;
+ public static final int UPDATE_RADIO = 0x04;
+ public static final int UPDATE_BT = 0x08;
+ public static final int UPDATE_ALL = UPDATE_CPU | UPDATE_WIFI | UPDATE_RADIO | UPDATE_BT;
+
+ void scheduleSync(String reason, int flags);
void scheduleCpuSyncDueToRemovedUid(int uid);
}
@@ -224,6 +229,7 @@
final ArrayList<StopwatchTimer> mVideoTurnedOnTimers = new ArrayList<>();
final ArrayList<StopwatchTimer> mFlashlightTurnedOnTimers = new ArrayList<>();
final ArrayList<StopwatchTimer> mCameraTurnedOnTimers = new ArrayList<>();
+ final ArrayList<StopwatchTimer> mBluetoothScanOnTimers = new ArrayList<>();
// Last partial timers we use for distributing CPU usage.
final ArrayList<StopwatchTimer> mLastPartialTimers = new ArrayList<>();
@@ -435,6 +441,9 @@
final StopwatchTimer[] mWifiSignalStrengthsTimer =
new StopwatchTimer[NUM_WIFI_SIGNAL_STRENGTH_BINS];
+ int mBluetoothScanNesting;
+ StopwatchTimer mBluetoothScanTimer;
+
int mMobileRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
long mMobileRadioActiveStartTime;
StopwatchTimer mMobileRadioActiveTimer;
@@ -3714,7 +3723,7 @@
addHistoryRecordLocked(elapsedRealtime, uptime);
mWifiOn = true;
mWifiOnTimer.startRunningLocked(elapsedRealtime);
- scheduleSyncExternalWifiStatsLocked("wifi-off");
+ scheduleSyncExternalStatsLocked("wifi-off", ExternalStatsSync.UPDATE_WIFI);
}
}
@@ -3728,7 +3737,7 @@
addHistoryRecordLocked(elapsedRealtime, uptime);
mWifiOn = false;
mWifiOnTimer.stopRunningLocked(elapsedRealtime);
- scheduleSyncExternalWifiStatsLocked("wifi-on");
+ scheduleSyncExternalStatsLocked("wifi-on", ExternalStatsSync.UPDATE_WIFI);
}
}
@@ -3946,6 +3955,65 @@
}
}
+ private void noteBluetoothScanStartedLocked(int uid) {
+ uid = mapUid(uid);
+ final long elapsedRealtime = SystemClock.elapsedRealtime();
+ final long uptime = SystemClock.uptimeMillis();
+ if (mBluetoothScanNesting == 0) {
+ mHistoryCur.states2 |= HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG;
+ if (DEBUG_HISTORY) Slog.v(TAG, "BLE scan started for: "
+ + Integer.toHexString(mHistoryCur.states2));
+ addHistoryRecordLocked(elapsedRealtime, uptime);
+ }
+ mBluetoothScanNesting++;
+ getUidStatsLocked(uid).noteBluetoothScanStartedLocked(elapsedRealtime);
+ }
+
+ public void noteBluetoothScanStartedFromSourceLocked(WorkSource ws) {
+ final int N = ws.size();
+ for (int i = 0; i < N; i++) {
+ noteBluetoothScanStartedLocked(ws.get(i));
+ }
+ }
+
+ private void noteBluetoothScanStoppedLocked(int uid) {
+ uid = mapUid(uid);
+ final long elapsedRealtime = SystemClock.elapsedRealtime();
+ final long uptime = SystemClock.uptimeMillis();
+ mBluetoothScanNesting--;
+ if (mBluetoothScanNesting == 0) {
+ mHistoryCur.states2 &= ~HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG;
+ if (DEBUG_HISTORY) Slog.v(TAG, "BLE scan stopped for: "
+ + Integer.toHexString(mHistoryCur.states2));
+ addHistoryRecordLocked(elapsedRealtime, uptime);
+ }
+ getUidStatsLocked(uid).noteBluetoothScanStoppedLocked(elapsedRealtime);
+ }
+
+ public void noteBluetoothScanStoppedFromSourceLocked(WorkSource ws) {
+ final int N = ws.size();
+ for (int i = 0; i < N; i++) {
+ noteBluetoothScanStoppedLocked(ws.get(i));
+ }
+ }
+
+ public void noteResetBluetoothScanLocked() {
+ if (mBluetoothScanNesting > 0) {
+ final long elapsedRealtime = SystemClock.elapsedRealtime();
+ final long uptime = SystemClock.uptimeMillis();
+ mBluetoothScanNesting = 0;
+ mHistoryCur.states2 &= ~HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG;
+ if (DEBUG_HISTORY) Slog.v(TAG, "BLE can stopped for: "
+ + Integer.toHexString(mHistoryCur.states2));
+ addHistoryRecordLocked(elapsedRealtime, uptime);
+ mBluetoothScanTimer.stopAllRunningLocked(elapsedRealtime);
+ for (int i=0; i<mUidStats.size(); i++) {
+ BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
+ uid.noteResetBluetoothScanLocked(elapsedRealtime);
+ }
+ }
+ }
+
public void noteWifiRadioPowerState(int powerState, long timestampNs) {
final long elapsedRealtime = SystemClock.elapsedRealtime();
final long uptime = SystemClock.uptimeMillis();
@@ -3980,7 +4048,7 @@
int uid = mapUid(ws.get(i));
getUidStatsLocked(uid).noteWifiRunningLocked(elapsedRealtime);
}
- scheduleSyncExternalWifiStatsLocked("wifi-running");
+ scheduleSyncExternalStatsLocked("wifi-running", ExternalStatsSync.UPDATE_WIFI);
} else {
Log.w(TAG, "noteWifiRunningLocked -- called while WIFI running");
}
@@ -4019,7 +4087,7 @@
int uid = mapUid(ws.get(i));
getUidStatsLocked(uid).noteWifiStoppedLocked(elapsedRealtime);
}
- scheduleSyncExternalWifiStatsLocked("wifi-stopped");
+ scheduleSyncExternalStatsLocked("wifi-stopped", ExternalStatsSync.UPDATE_WIFI);
} else {
Log.w(TAG, "noteWifiStoppedLocked -- called while WIFI not running");
}
@@ -4034,7 +4102,7 @@
}
mWifiState = wifiState;
mWifiStateTimer[wifiState].startRunningLocked(elapsedRealtime);
- scheduleSyncExternalWifiStatsLocked("wifi-state");
+ scheduleSyncExternalStatsLocked("wifi-state", ExternalStatsSync.UPDATE_WIFI);
}
}
@@ -4530,6 +4598,11 @@
}
@Override
+ public long getBluetoothScanTime(long elapsedRealtimeUs, int which) {
+ return mBluetoothScanTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+ }
+
+ @Override
public long getNetworkActivityBytes(int type, int which) {
if (type >= 0 && type < mNetworkByteActivityCounters.length) {
return mNetworkByteActivityCounters[type].getCountLocked(which);
@@ -4603,9 +4676,8 @@
StopwatchTimer mVideoTurnedOnTimer;
StopwatchTimer mFlashlightTurnedOnTimer;
StopwatchTimer mCameraTurnedOnTimer;
-
-
StopwatchTimer mForegroundActivityTimer;
+ StopwatchTimer mBluetoothScanTimer;
int mProcessState = ActivityManager.PROCESS_STATE_NONEXISTENT;
StopwatchTimer[] mProcessStateTimer;
@@ -4997,6 +5069,30 @@
return mForegroundActivityTimer;
}
+ public StopwatchTimer createBluetoothScanTimerLocked() {
+ if (mBluetoothScanTimer == null) {
+ mBluetoothScanTimer = new StopwatchTimer(Uid.this, BLUETOOTH_SCAN_ON,
+ mBluetoothScanOnTimers, mOnBatteryTimeBase);
+ }
+ return mBluetoothScanTimer;
+ }
+
+ public void noteBluetoothScanStartedLocked(long elapsedRealtimeMs) {
+ createBluetoothScanTimerLocked().startRunningLocked(elapsedRealtimeMs);
+ }
+
+ public void noteBluetoothScanStoppedLocked(long elapsedRealtimeMs) {
+ if (mBluetoothScanTimer != null) {
+ mBluetoothScanTimer.stopRunningLocked(elapsedRealtimeMs);
+ }
+ }
+
+ public void noteResetBluetoothScanLocked(long elapsedRealtimeMs) {
+ if (mBluetoothScanTimer != null) {
+ mBluetoothScanTimer.stopAllRunningLocked(elapsedRealtimeMs);
+ }
+ }
+
@Override
public void noteActivityResumedLocked(long elapsedRealtimeMs) {
// We always start, since we want multiple foreground PIDs to nest
@@ -5110,6 +5206,11 @@
return mForegroundActivityTimer;
}
+ @Override
+ public Timer getBluetoothScanTimer() {
+ return mBluetoothScanTimer;
+ }
+
void makeProcessState(int i, Parcel in) {
if (i < 0 || i >= NUM_PROCESS_STATE) return;
@@ -5335,6 +5436,9 @@
if (mForegroundActivityTimer != null) {
active |= !mForegroundActivityTimer.reset(false);
}
+ if (mBluetoothScanTimer != null) {
+ active |= !mBluetoothScanTimer.reset(false);
+ }
if (mProcessStateTimer != null) {
for (int i = 0; i < NUM_PROCESS_STATE; i++) {
if (mProcessStateTimer[i] != null) {
@@ -5509,6 +5613,10 @@
mForegroundActivityTimer.detach();
mForegroundActivityTimer = null;
}
+ if (mBluetoothScanTimer != null) {
+ mBluetoothScanTimer.detach();
+ mBluetoothScanTimer = null;
+ }
if (mUserActivityCounters != null) {
for (int i=0; i<NUM_USER_ACTIVITY_TYPES; i++) {
mUserActivityCounters[i].detach();
@@ -5669,6 +5777,12 @@
} else {
out.writeInt(0);
}
+ if (mBluetoothScanTimer != null) {
+ out.writeInt(1);
+ mBluetoothScanTimer.writeToParcel(out, elapsedRealtimeUs);
+ } else {
+ out.writeInt(0);
+ }
for (int i = 0; i < NUM_PROCESS_STATE; i++) {
if (mProcessStateTimer[i] != null) {
out.writeInt(1);
@@ -5874,6 +5988,12 @@
} else {
mForegroundActivityTimer = null;
}
+ if (in.readInt() != 0) {
+ mBluetoothScanTimer = new StopwatchTimer(Uid.this, BLUETOOTH_SCAN_ON,
+ mBluetoothScanOnTimers, mOnBatteryTimeBase, in);
+ } else {
+ mBluetoothScanTimer = null;
+ }
mProcessState = ActivityManager.PROCESS_STATE_NONEXISTENT;
for (int i = 0; i < NUM_PROCESS_STATE; i++) {
if (in.readInt() != 0) {
@@ -7133,6 +7253,7 @@
mVideoOnTimer = new StopwatchTimer(null, -8, null, mOnBatteryTimeBase);
mFlashlightOnTimer = new StopwatchTimer(null, -9, null, mOnBatteryTimeBase);
mCameraOnTimer = new StopwatchTimer(null, -13, null, mOnBatteryTimeBase);
+ mBluetoothScanTimer = new StopwatchTimer(null, -14, null, mOnBatteryTimeBase);
mOnBattery = mOnBatteryInternal = false;
long uptime = SystemClock.uptimeMillis() * 1000;
long realtime = SystemClock.elapsedRealtime() * 1000;
@@ -7732,6 +7853,7 @@
mVideoOnTimer.reset(false);
mFlashlightOnTimer.reset(false);
mCameraOnTimer.reset(false);
+ mBluetoothScanTimer.reset(false);
for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) {
mPhoneSignalStrengthsTimer[i].reset(false);
}
@@ -8264,41 +8386,168 @@
Slog.d(TAG, "Updating bluetooth stats: " + info);
}
- if (info != null && mOnBatteryInternal) {
- mHasBluetoothReporting = true;
- mBluetoothActivity.getRxTimeCounter().addCountLocked(
- info.getControllerRxTimeMillis());
- mBluetoothActivity.getTxTimeCounters()[0].addCountLocked(
- info.getControllerTxTimeMillis());
- mBluetoothActivity.getIdleTimeCounter().addCountLocked(
- info.getControllerIdleTimeMillis());
+ if (info == null || !mOnBatteryInternal) {
+ return;
+ }
- // POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE is measured in mV, so convert to V.
- final double opVolt = mPowerProfile.getAveragePower(
- PowerProfile.POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE) / 1000.0;
- if (opVolt != 0) {
- // We store the power drain as mAms.
- mBluetoothActivity.getPowerCounter().addCountLocked(
- (long) (info.getControllerEnergyUsed() / opVolt));
+ mHasBluetoothReporting = true;
+
+ final long elapsedRealtimeMs = SystemClock.elapsedRealtime();
+ final long rxTimeMs = info.getControllerRxTimeMillis();
+ final long txTimeMs = info.getControllerTxTimeMillis();
+
+ if (DEBUG_ENERGY) {
+ Slog.d(TAG, "------ BEGIN BLE power blaming ------");
+ Slog.d(TAG, " Tx Time: " + txTimeMs + " ms");
+ Slog.d(TAG, " Rx Time: " + rxTimeMs + " ms");
+ Slog.d(TAG, " Idle Time: " + info.getControllerIdleTimeMillis() + " ms");
+ }
+
+ long totalScanTimeMs = 0;
+
+ final int uidCount = mUidStats.size();
+ for (int i = 0; i < uidCount; i++) {
+ final Uid u = mUidStats.valueAt(i);
+ if (u.mBluetoothScanTimer == null) {
+ continue;
}
- final UidTraffic[] uidTraffic = info.getUidTraffic();
- final int numUids = uidTraffic != null ? uidTraffic.length : 0;
+ totalScanTimeMs += u.mBluetoothScanTimer.getTimeSinceMarkLocked(
+ elapsedRealtimeMs * 1000) / 1000;
+ }
+
+ final boolean normalizeScanRxTime = (totalScanTimeMs > rxTimeMs);
+ final boolean normalizeScanTxTime = (totalScanTimeMs > txTimeMs);
+
+ if (DEBUG_ENERGY) {
+ Slog.d(TAG, "Normalizing scan power for RX=" + normalizeScanRxTime
+ + " TX=" + normalizeScanTxTime);
+ }
+
+ long leftOverRxTimeMs = rxTimeMs;
+ long leftOverTxTimeMs = txTimeMs;
+
+ for (int i = 0; i < uidCount; i++) {
+ final Uid u = mUidStats.valueAt(i);
+ if (u.mBluetoothScanTimer == null) {
+ continue;
+ }
+
+ long scanTimeSinceMarkMs = u.mBluetoothScanTimer.getTimeSinceMarkLocked(
+ elapsedRealtimeMs * 1000) / 1000;
+ if (scanTimeSinceMarkMs > 0) {
+ // Set the new mark so that next time we get new data since this point.
+ u.mBluetoothScanTimer.setMark(elapsedRealtimeMs);
+
+ long scanTimeRxSinceMarkMs = scanTimeSinceMarkMs;
+ long scanTimeTxSinceMarkMs = scanTimeSinceMarkMs;
+
+ if (normalizeScanRxTime) {
+ // Scan time is longer than the total rx time in the controller,
+ // so distribute the scan time proportionately. This means regular traffic
+ // will not blamed, but scans are more expensive anyways.
+ scanTimeRxSinceMarkMs = (rxTimeMs * scanTimeRxSinceMarkMs) / totalScanTimeMs;
+ }
+
+ if (normalizeScanTxTime) {
+ // Scan time is longer than the total tx time in the controller,
+ // so distribute the scan time proportionately. This means regular traffic
+ // will not blamed, but scans are more expensive anyways.
+ scanTimeTxSinceMarkMs = (txTimeMs * scanTimeTxSinceMarkMs) / totalScanTimeMs;
+ }
+
+ final ControllerActivityCounterImpl counter =
+ u.getOrCreateBluetoothControllerActivityLocked();
+ counter.getRxTimeCounter().addCountLocked(scanTimeRxSinceMarkMs);
+ counter.getTxTimeCounters()[0].addCountLocked(scanTimeTxSinceMarkMs);
+
+ leftOverRxTimeMs -= scanTimeRxSinceMarkMs;
+ leftOverTxTimeMs -= scanTimeTxSinceMarkMs;
+ }
+ }
+
+ if (DEBUG_ENERGY) {
+ Slog.d(TAG, "Left over time for traffic RX=" + leftOverRxTimeMs
+ + " TX=" + leftOverTxTimeMs);
+ }
+
+ //
+ // Now distribute blame to apps that did bluetooth traffic.
+ //
+
+ long totalTxBytes = 0;
+ long totalRxBytes = 0;
+
+ final UidTraffic[] uidTraffic = info.getUidTraffic();
+ final int numUids = uidTraffic != null ? uidTraffic.length : 0;
+ for (int i = 0; i < numUids; i++) {
+ final UidTraffic traffic = uidTraffic[i];
+
+ // Add to the global counters.
+ mNetworkByteActivityCounters[NETWORK_BT_RX_DATA].addCountLocked(
+ traffic.getRxBytes());
+ mNetworkByteActivityCounters[NETWORK_BT_TX_DATA].addCountLocked(
+ traffic.getTxBytes());
+
+ // Add to the UID counters.
+ final Uid u = getUidStatsLocked(mapUid(traffic.getUid()));
+ u.noteNetworkActivityLocked(NETWORK_BT_RX_DATA, traffic.getRxBytes(), 0);
+ u.noteNetworkActivityLocked(NETWORK_BT_TX_DATA, traffic.getTxBytes(), 0);
+
+ // Calculate the total traffic.
+ totalTxBytes += traffic.getTxBytes();
+ totalRxBytes += traffic.getRxBytes();
+ }
+
+ if ((totalTxBytes != 0 || totalRxBytes != 0) &&
+ (leftOverRxTimeMs != 0 || leftOverTxTimeMs != 0)) {
for (int i = 0; i < numUids; i++) {
final UidTraffic traffic = uidTraffic[i];
- // Add to the global counters.
- mNetworkByteActivityCounters[NETWORK_BT_RX_DATA].addCountLocked(
- traffic.getRxBytes());
- mNetworkByteActivityCounters[NETWORK_BT_TX_DATA].addCountLocked(
- traffic.getTxBytes());
-
- // Add to the UID counters.
final Uid u = getUidStatsLocked(mapUid(traffic.getUid()));
- u.noteNetworkActivityLocked(NETWORK_BT_RX_DATA, traffic.getRxBytes(), 0);
- u.noteNetworkActivityLocked(NETWORK_BT_TX_DATA, traffic.getTxBytes(), 0);
+ final ControllerActivityCounterImpl counter =
+ u.getOrCreateBluetoothControllerActivityLocked();
+
+ if (totalRxBytes > 0 && traffic.getRxBytes() > 0) {
+ final long timeRxMs = (leftOverRxTimeMs * traffic.getRxBytes()) / totalRxBytes;
+
+ if (DEBUG_ENERGY) {
+ Slog.d(TAG, "UID=" + traffic.getUid() + " rx_bytes=" + traffic.getRxBytes()
+ + " rx_time=" + timeRxMs);
+ }
+ counter.getRxTimeCounter().addCountLocked(timeRxMs);
+ leftOverRxTimeMs -= timeRxMs;
+ }
+
+ if (totalTxBytes > 0 && traffic.getTxBytes() > 0) {
+ final long timeTxMs = (leftOverTxTimeMs * traffic.getTxBytes()) / totalTxBytes;
+
+ if (DEBUG_ENERGY) {
+ Slog.d(TAG, "UID=" + traffic.getUid() + " tx_bytes=" + traffic.getTxBytes()
+ + " tx_time=" + timeTxMs);
+ }
+
+ counter.getTxTimeCounters()[0].addCountLocked(timeTxMs);
+ leftOverTxTimeMs -= timeTxMs;
+ }
}
}
+
+ mBluetoothActivity.getRxTimeCounter().addCountLocked(
+ info.getControllerRxTimeMillis());
+ mBluetoothActivity.getTxTimeCounters()[0].addCountLocked(
+ info.getControllerTxTimeMillis());
+ mBluetoothActivity.getIdleTimeCounter().addCountLocked(
+ info.getControllerIdleTimeMillis());
+
+ // POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE is measured in mV, so convert to V.
+ final double opVolt = mPowerProfile.getAveragePower(
+ PowerProfile.POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE) / 1000.0;
+ if (opVolt != 0) {
+ // We store the power drain as mAms.
+ mBluetoothActivity.getPowerCounter().addCountLocked(
+ (long) (info.getControllerEnergyUsed() / opVolt));
+ }
}
/**
@@ -8739,15 +8988,9 @@
}
}
- private void scheduleSyncExternalStatsLocked(String reason) {
+ private void scheduleSyncExternalStatsLocked(String reason, int updateFlags) {
if (mExternalSync != null) {
- mExternalSync.scheduleSync(reason);
- }
- }
-
- private void scheduleSyncExternalWifiStatsLocked(String reason) {
- if (mExternalSync != null) {
- mExternalSync.scheduleWifiSync(reason);
+ mExternalSync.scheduleSync(reason, updateFlags);
}
}
@@ -8815,7 +9058,7 @@
// TODO(adamlesinski): Schedule the creation of a HistoryStepDetails record
// which will pull external stats.
- scheduleSyncExternalStatsLocked("battery-level");
+ scheduleSyncExternalStatsLocked("battery-level", ExternalStatsSync.UPDATE_ALL);
}
if (mHistoryCur.batteryStatus != status) {
mHistoryCur.batteryStatus = (byte)status;
@@ -9596,6 +9839,8 @@
mFlashlightOnTimer.readSummaryFromParcelLocked(in);
mCameraOnNesting = 0;
mCameraOnTimer.readSummaryFromParcelLocked(in);
+ mBluetoothScanNesting = 0;
+ mBluetoothScanTimer.readSummaryFromParcelLocked(in);
int NKW = in.readInt();
if (NKW > 10000) {
@@ -9666,6 +9911,9 @@
if (in.readInt() != 0) {
u.createForegroundActivityTimerLocked().readSummaryFromParcelLocked(in);
}
+ if (in.readInt() != 0) {
+ u.createBluetoothScanTimerLocked().readSummaryFromParcelLocked(in);
+ }
u.mProcessState = ActivityManager.PROCESS_STATE_NONEXISTENT;
for (int i = 0; i < Uid.NUM_PROCESS_STATE; i++) {
if (in.readInt() != 0) {
@@ -9928,6 +10176,7 @@
out.writeInt(mNumConnectivityChange);
mFlashlightOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
mCameraOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+ mBluetoothScanTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
out.writeInt(mKernelWakelockStats.size());
for (Map.Entry<String, SamplingTimer> ent : mKernelWakelockStats.entrySet()) {
@@ -10021,6 +10270,12 @@
} else {
out.writeInt(0);
}
+ if (u.mBluetoothScanTimer != null) {
+ out.writeInt(1);
+ u.mBluetoothScanTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+ } else {
+ out.writeInt(0);
+ }
for (int i = 0; i < Uid.NUM_PROCESS_STATE; i++) {
if (u.mProcessStateTimer[i] != null) {
out.writeInt(1);
@@ -10289,6 +10544,8 @@
mFlashlightOnTimer = new StopwatchTimer(null, -9, null, mOnBatteryTimeBase, in);
mCameraOnNesting = 0;
mCameraOnTimer = new StopwatchTimer(null, -13, null, mOnBatteryTimeBase, in);
+ mBluetoothScanNesting = 0;
+ mBluetoothScanTimer = new StopwatchTimer(null, -14, null, mOnBatteryTimeBase, in);
mDischargeUnplugLevel = in.readInt();
mDischargePlugLevel = in.readInt();
mDischargeCurrentLevel = in.readInt();
@@ -10436,6 +10693,7 @@
out.writeInt(mUnpluggedNumConnectivityChange);
mFlashlightOnTimer.writeToParcel(out, uSecRealtime);
mCameraOnTimer.writeToParcel(out, uSecRealtime);
+ mBluetoothScanTimer.writeToParcel(out, uSecRealtime);
out.writeInt(mDischargeUnplugLevel);
out.writeInt(mDischargePlugLevel);
out.writeInt(mDischargeCurrentLevel);
diff --git a/core/java/com/android/internal/os/BluetoothPowerCalculator.java b/core/java/com/android/internal/os/BluetoothPowerCalculator.java
index 531d1fa..2f383eac 100644
--- a/core/java/com/android/internal/os/BluetoothPowerCalculator.java
+++ b/core/java/com/android/internal/os/BluetoothPowerCalculator.java
@@ -24,6 +24,8 @@
private final double mIdleMa;
private final double mRxMa;
private final double mTxMa;
+ private double mAppTotalPowerMah = 0;
+ private long mAppTotalTimeMs = 0;
public BluetoothPowerCalculator(PowerProfile profile) {
mIdleMa = profile.getAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_IDLE);
@@ -34,7 +36,31 @@
@Override
public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
long rawUptimeUs, int statsType) {
- // No per-app distribution yet.
+
+ final BatteryStats.ControllerActivityCounter counter = u.getBluetoothControllerActivity();
+ if (counter == null) {
+ return;
+ }
+
+ final long idleTimeMs = counter.getIdleTimeCounter().getCountLocked(statsType);
+ final long rxTimeMs = counter.getRxTimeCounter().getCountLocked(statsType);
+ final long txTimeMs = counter.getTxTimeCounters()[0].getCountLocked(statsType);
+ final long totalTimeMs = idleTimeMs + txTimeMs + rxTimeMs;
+ double powerMah = counter.getPowerCounter().getCountLocked(statsType)
+ / (double)(1000*60*60);
+
+ if (powerMah == 0) {
+ powerMah = ((idleTimeMs * mIdleMa) + (rxTimeMs * mRxMa) + (txTimeMs * mTxMa))
+ / (1000*60*60);
+ }
+
+ app.bluetoothPowerMah = powerMah;
+ app.bluetoothRunningTimeMs = totalTimeMs;
+ app.btRxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_BT_RX_DATA, statsType);
+ app.btTxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_BT_TX_DATA, statsType);
+
+ mAppTotalPowerMah += powerMah;
+ mAppTotalTimeMs += totalTimeMs;
}
@Override
@@ -56,12 +82,21 @@
/ (1000*60*60);
}
+ // Subtract what the apps used, but clamp to 0.
+ powerMah = Math.max(0, powerMah - mAppTotalPowerMah);
+
if (DEBUG && powerMah != 0) {
Log.d(TAG, "Bluetooth active: time=" + (totalTimeMs)
+ " power=" + BatteryStatsHelper.makemAh(powerMah));
}
- app.usagePowerMah = powerMah;
- app.usageTimeMs = totalTimeMs;
+ app.bluetoothPowerMah = powerMah;
+ app.bluetoothRunningTimeMs = Math.max(0, totalTimeMs - mAppTotalTimeMs);
+ }
+
+ @Override
+ public void reset() {
+ mAppTotalPowerMah = 0;
+ mAppTotalTimeMs = 0;
}
}
diff --git a/core/java/com/android/internal/os/WifiPowerCalculator.java b/core/java/com/android/internal/os/WifiPowerCalculator.java
index 2a27f70..b447039 100644
--- a/core/java/com/android/internal/os/WifiPowerCalculator.java
+++ b/core/java/com/android/internal/os/WifiPowerCalculator.java
@@ -29,6 +29,7 @@
private final double mTxCurrentMa;
private final double mRxCurrentMa;
private double mTotalAppPowerDrain = 0;
+ private long mTotalAppRunningTime = 0;
public WifiPowerCalculator(PowerProfile profile) {
mIdleCurrentMa = profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_IDLE);
@@ -48,6 +49,8 @@
final long txTime = counter.getTxTimeCounters()[0].getCountLocked(statsType);
final long rxTime = counter.getRxTimeCounter().getCountLocked(statsType);
app.wifiRunningTimeMs = idleTime + rxTime + txTime;
+ mTotalAppRunningTime += app.wifiRunningTimeMs;
+
app.wifiPowerMah =
((idleTime * mIdleCurrentMa) + (txTime * mTxCurrentMa) + (rxTime * mRxCurrentMa))
/ (1000*60*60);
@@ -76,7 +79,9 @@
final long idleTimeMs = counter.getIdleTimeCounter().getCountLocked(statsType);
final long txTimeMs = counter.getTxTimeCounters()[0].getCountLocked(statsType);
final long rxTimeMs = counter.getRxTimeCounter().getCountLocked(statsType);
- app.wifiRunningTimeMs = idleTimeMs + rxTimeMs + txTimeMs;
+
+ app.wifiRunningTimeMs = Math.max(0,
+ (idleTimeMs + rxTimeMs + txTimeMs) - mTotalAppRunningTime);
double powerDrainMah = counter.getPowerCounter().getCountLocked(statsType)
/ (double)(1000*60*60);
@@ -95,5 +100,6 @@
@Override
public void reset() {
mTotalAppPowerDrain = 0;
+ mTotalAppRunningTime = 0;
}
}
diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java
index d23f26d..919254a 100644
--- a/core/java/com/android/internal/os/Zygote.java
+++ b/core/java/com/android/internal/os/Zygote.java
@@ -41,6 +41,10 @@
public static final int DEBUG_ENABLE_JNI_LOGGING = 1 << 4;
/** Force generation of native debugging information. */
public static final int DEBUG_GENERATE_DEBUG_INFO = 1 << 5;
+ /** Always use JIT-ed code. */
+ public static final int DEBUG_ALWAYS_JIT = 1 << 6;
+ /** Make the code debuggable with turning off some optimizations. */
+ public static final int DEBUG_NATIVE_DEBUGGABLE = 1 << 7;
/** No external storage should be mounted. */
public static final int MOUNT_EXTERNAL_NONE = 0;
diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java
index a40f9a8..85d84bb 100644
--- a/core/java/com/android/internal/os/ZygoteConnection.java
+++ b/core/java/com/android/internal/os/ZygoteConnection.java
@@ -434,6 +434,10 @@
debugFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;
} else if (arg.equals("--generate-debug-info")) {
debugFlags |= Zygote.DEBUG_GENERATE_DEBUG_INFO;
+ } else if (arg.equals("--always-jit")) {
+ debugFlags |= Zygote.DEBUG_ALWAYS_JIT;
+ } else if (arg.equals("--native-debuggable")) {
+ debugFlags |= Zygote.DEBUG_NATIVE_DEBUGGABLE;
} else if (arg.equals("--enable-jni-logging")) {
debugFlags |= Zygote.DEBUG_ENABLE_JNI_LOGGING;
} else if (arg.equals("--enable-assert")) {
diff --git a/core/java/com/android/server/backup/UsageStatsBackupHelper.java b/core/java/com/android/server/backup/UsageStatsBackupHelper.java
index bde2396..d6a70d3 100644
--- a/core/java/com/android/server/backup/UsageStatsBackupHelper.java
+++ b/core/java/com/android/server/backup/UsageStatsBackupHelper.java
@@ -2,11 +2,8 @@
import android.app.backup.BlobBackupHelper;
-import android.app.usage.IUsageStatsManager;
import android.app.usage.UsageStatsManagerInternal;
import android.content.Context;
-import android.os.RemoteException;
-import android.os.ServiceManager;
import android.os.UserHandle;
import android.util.Log;
@@ -26,7 +23,7 @@
static final int BLOB_VERSION = 1;
// Key under which the payload blob is stored
- // same as UsageStatsBackupHelperAssistant.KEY_USAGE_STATS
+ // same as UsageStatsDatabase.KEY_USAGE_STATS
static final String KEY_USAGE_STATS = "usage_stats";
public UsageStatsBackupHelper(Context context) {
@@ -35,14 +32,15 @@
@Override
protected byte[] getBackupPayload(String key) {
- if(KEY_USAGE_STATS.equals(key)) {
- UsageStatsManagerInternal localUsageStatsManager = LocalServices.getService(UsageStatsManagerInternal.class);
+ if (KEY_USAGE_STATS.equals(key)) {
+ UsageStatsManagerInternal localUsageStatsManager =
+ LocalServices.getService(UsageStatsManagerInternal.class);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(baos);
- try{
+ try {
out.writeInt(UserHandle.USER_SYSTEM);
out.write(localUsageStatsManager.getBackupPayload(UserHandle.USER_SYSTEM, key));
- } catch (IOException ioe){
+ } catch (IOException ioe) {
if (DEBUG) Log.e(TAG, "Failed to backup Usage Stats", ioe);
baos.reset();
}
@@ -55,14 +53,15 @@
@Override
protected void applyRestoredPayload(String key, byte[] payload) {
if (KEY_USAGE_STATS.equals(key)) {
- UsageStatsManagerInternal localUsageStatsManager = LocalServices.getService(UsageStatsManagerInternal.class);
+ UsageStatsManagerInternal localUsageStatsManager =
+ LocalServices.getService(UsageStatsManagerInternal.class);
DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
- try{
+ try {
int user = in.readInt();
byte[] restoreData = new byte[payload.length - 4];
- in.read(restoreData, 0, payload.length-4);
+ in.read(restoreData, 0, restoreData.length);
localUsageStatsManager.applyRestoredPayload(user, key, restoreData);
- } catch (IOException ioe){
+ } catch (IOException ioe) {
if (DEBUG) Log.e(TAG, "Failed to restore Usage Stats", ioe);
}
}
diff --git a/core/jni/Android.mk b/core/jni/Android.mk
index ffa3fa6..1b6b53a 100644
--- a/core/jni/Android.mk
+++ b/core/jni/Android.mk
@@ -51,6 +51,7 @@
android_database_SQLiteConnection.cpp \
android_database_SQLiteGlobal.cpp \
android_database_SQLiteDebug.cpp \
+ android_graphics_drawable_AnimatedVectorDrawable.cpp \
android_graphics_drawable_VectorDrawable.cpp \
android_view_DisplayEventReceiver.cpp \
android_view_DisplayListCanvas.cpp \
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 2e45f8c..223fc1a 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -131,6 +131,7 @@
extern int register_android_graphics_Region(JNIEnv* env);
extern int register_android_graphics_SurfaceTexture(JNIEnv* env);
extern int register_android_graphics_Xfermode(JNIEnv* env);
+extern int register_android_graphics_drawable_AnimatedVectorDrawable(JNIEnv* env);
extern int register_android_graphics_drawable_VectorDrawable(JNIEnv* env);
extern int register_android_graphics_pdf_PdfDocument(JNIEnv* env);
extern int register_android_graphics_pdf_PdfEditor(JNIEnv* env);
@@ -1321,6 +1322,7 @@
REG_JNI(register_android_graphics_Typeface),
REG_JNI(register_android_graphics_Xfermode),
REG_JNI(register_android_graphics_YuvImage),
+ REG_JNI(register_android_graphics_drawable_AnimatedVectorDrawable),
REG_JNI(register_android_graphics_drawable_VectorDrawable),
REG_JNI(register_android_graphics_pdf_PdfDocument),
REG_JNI(register_android_graphics_pdf_PdfEditor),
diff --git a/core/jni/android_graphics_drawable_AnimatedVectorDrawable.cpp b/core/jni/android_graphics_drawable_AnimatedVectorDrawable.cpp
new file mode 100644
index 0000000..7a3c598
--- /dev/null
+++ b/core/jni/android_graphics_drawable_AnimatedVectorDrawable.cpp
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#define LOG_TAG "OpenGLRenderer"
+
+#include "jni.h"
+#include "GraphicsJNI.h"
+#include "core_jni_helpers.h"
+#include "log/log.h"
+
+#include "Animator.h"
+#include "Interpolator.h"
+#include "PropertyValuesAnimatorSet.h"
+#include "PropertyValuesHolder.h"
+#include "VectorDrawable.h"
+
+namespace android {
+using namespace uirenderer;
+using namespace VectorDrawable;
+
+static struct {
+ jclass clazz;
+ jmethodID callOnFinished;
+} gVectorDrawableAnimatorClassInfo;
+
+static JNIEnv* getEnv(JavaVM* vm) {
+ JNIEnv* env;
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ return 0;
+ }
+ return env;
+}
+
+static AnimationListener* createAnimationListener(JNIEnv* env, jobject finishListener) {
+ class AnimationListenerBridge : public AnimationListener {
+ public:
+ AnimationListenerBridge(JNIEnv* env, jobject finishListener) {
+ mFinishListener = env->NewGlobalRef(finishListener);
+ env->GetJavaVM(&mJvm);
+ }
+
+ virtual ~AnimationListenerBridge() {
+ if (mFinishListener) {
+ onAnimationFinished(NULL);
+ }
+ }
+
+ virtual void onAnimationFinished(BaseRenderNodeAnimator*) {
+ LOG_ALWAYS_FATAL_IF(!mFinishListener, "Finished listener twice?");
+ JNIEnv* env = getEnv(mJvm);
+ env->CallStaticVoidMethod(
+ gVectorDrawableAnimatorClassInfo.clazz,
+ gVectorDrawableAnimatorClassInfo.callOnFinished,
+ mFinishListener);
+ releaseJavaObject();
+ }
+
+ private:
+ void releaseJavaObject() {
+ JNIEnv* env = getEnv(mJvm);
+ env->DeleteGlobalRef(mFinishListener);
+ mFinishListener = NULL;
+ }
+
+ JavaVM* mJvm;
+ jobject mFinishListener;
+ };
+ return new AnimationListenerBridge(env, finishListener);
+}
+
+static void addAnimator(JNIEnv*, jobject, jlong animatorSetPtr, jlong propertyHolderPtr,
+ jlong interpolatorPtr, jlong startDelay, jlong duration, jint repeatCount) {
+ PropertyValuesAnimatorSet* set = reinterpret_cast<PropertyValuesAnimatorSet*>(animatorSetPtr);
+ PropertyValuesHolder* holder = reinterpret_cast<PropertyValuesHolder*>(propertyHolderPtr);
+ Interpolator* interpolator = reinterpret_cast<Interpolator*>(interpolatorPtr);
+ set->addPropertyAnimator(holder, interpolator, startDelay, duration, repeatCount);
+}
+
+static jlong createAnimatorSet(JNIEnv*, jobject) {
+ PropertyValuesAnimatorSet* animatorSet = new PropertyValuesAnimatorSet();
+ return reinterpret_cast<jlong>(animatorSet);
+}
+
+static jlong createGroupPropertyHolder(JNIEnv*, jobject, jlong nativePtr, jint propertyId,
+ jfloat startValue, jfloat endValue) {
+ VectorDrawable::Group* group = reinterpret_cast<VectorDrawable::Group*>(nativePtr);
+ GroupPropertyValuesHolder* newHolder = new GroupPropertyValuesHolder(group, propertyId,
+ startValue, endValue);
+ return reinterpret_cast<jlong>(newHolder);
+}
+
+static jlong createPathDataPropertyHolder(JNIEnv*, jobject, jlong nativePtr, jlong startValuePtr,
+ jlong endValuePtr) {
+ VectorDrawable::Path* path = reinterpret_cast<VectorDrawable::Path*>(nativePtr);
+ PathData* startData = reinterpret_cast<PathData*>(startValuePtr);
+ PathData* endData = reinterpret_cast<PathData*>(endValuePtr);
+ PathDataPropertyValuesHolder* newHolder = new PathDataPropertyValuesHolder(path,
+ startData, endData);
+ return reinterpret_cast<jlong>(newHolder);
+}
+
+static jlong createPathColorPropertyHolder(JNIEnv*, jobject, jlong nativePtr, jint propertyId,
+ int startValue, jint endValue) {
+ VectorDrawable::FullPath* fullPath = reinterpret_cast<VectorDrawable::FullPath*>(nativePtr);
+ FullPathColorPropertyValuesHolder* newHolder = new FullPathColorPropertyValuesHolder(fullPath,
+ propertyId, startValue, endValue);
+ return reinterpret_cast<jlong>(newHolder);
+}
+
+static jlong createPathPropertyHolder(JNIEnv*, jobject, jlong nativePtr, jint propertyId,
+ float startValue, jfloat endValue) {
+ VectorDrawable::FullPath* fullPath = reinterpret_cast<VectorDrawable::FullPath*>(nativePtr);
+ FullPathPropertyValuesHolder* newHolder = new FullPathPropertyValuesHolder(fullPath,
+ propertyId, startValue, endValue);
+ return reinterpret_cast<jlong>(newHolder);
+}
+
+static jlong createRootAlphaPropertyHolder(JNIEnv*, jobject, jlong nativePtr, jfloat startValue,
+ float endValue) {
+ VectorDrawable::Tree* tree = reinterpret_cast<VectorDrawable::Tree*>(nativePtr);
+ RootAlphaPropertyValuesHolder* newHolder = new RootAlphaPropertyValuesHolder(tree,
+ startValue, endValue);
+ return reinterpret_cast<jlong>(newHolder);
+}
+static void setPropertyHolderData(JNIEnv* env, jobject, jlong propertyHolderPtr,
+ jfloatArray srcData, jint length) {
+
+ jfloat* propertyData = env->GetFloatArrayElements(srcData, nullptr);
+ PropertyValuesHolder* holder = reinterpret_cast<PropertyValuesHolder*>(propertyHolderPtr);
+ holder->setPropertyDataSource(propertyData, length);
+ env->ReleaseFloatArrayElements(srcData, propertyData, JNI_ABORT);
+}
+static void start(JNIEnv* env, jobject, jlong animatorSetPtr, jobject finishListener) {
+ PropertyValuesAnimatorSet* set = reinterpret_cast<PropertyValuesAnimatorSet*>(animatorSetPtr);
+ // TODO: keep a ref count in finish listener
+ AnimationListener* listener = createAnimationListener(env, finishListener);
+ set->start(listener);
+}
+
+static void reverse(JNIEnv* env, jobject, jlong animatorSetPtr, jobject finishListener) {
+ // TODO: implement reverse
+}
+
+static void end(JNIEnv*, jobject, jlong animatorSetPtr) {
+ PropertyValuesAnimatorSet* set = reinterpret_cast<PropertyValuesAnimatorSet*>(animatorSetPtr);
+ set->end();
+}
+
+static void reset(JNIEnv*, jobject, jlong animatorSetPtr) {
+ PropertyValuesAnimatorSet* set = reinterpret_cast<PropertyValuesAnimatorSet*>(animatorSetPtr);
+ set->reset();
+}
+
+static const JNINativeMethod gMethods[] = {
+ {"nCreateAnimatorSet", "()J", (void*)createAnimatorSet},
+ {"nAddAnimator", "(JJJJJI)V", (void*)addAnimator},
+ {"nCreateGroupPropertyHolder", "!(JIFF)J", (void*)createGroupPropertyHolder},
+ {"nCreatePathDataPropertyHolder", "!(JJJ)J", (void*)createPathDataPropertyHolder},
+ {"nCreatePathColorPropertyHolder", "!(JIII)J", (void*)createPathColorPropertyHolder},
+ {"nCreatePathPropertyHolder", "!(JIFF)J", (void*)createPathPropertyHolder},
+ {"nCreateRootAlphaPropertyHolder", "!(JFF)J", (void*)createRootAlphaPropertyHolder},
+ {"nSetPropertyHolderData", "(J[FI)V", (void*)setPropertyHolderData},
+ {"nStart", "(JLandroid/graphics/drawable/AnimatedVectorDrawable$VectorDrawableAnimator;)V", (void*)start},
+ {"nReverse", "(JLandroid/graphics/drawable/AnimatedVectorDrawable$VectorDrawableAnimator;)V", (void*)reverse},
+ {"nEnd", "!(J)V", (void*)end},
+ {"nReset", "!(J)V", (void*)reset},
+};
+
+const char* const kClassPathName = "android/graphics/drawable/AnimatedVectorDrawable$VectorDrawableAnimator";
+int register_android_graphics_drawable_AnimatedVectorDrawable(JNIEnv* env) {
+ gVectorDrawableAnimatorClassInfo.clazz = FindClassOrDie(env, kClassPathName);
+ gVectorDrawableAnimatorClassInfo.clazz = MakeGlobalRefOrDie(env,
+ gVectorDrawableAnimatorClassInfo.clazz);
+
+ gVectorDrawableAnimatorClassInfo.callOnFinished = GetStaticMethodIDOrDie(
+ env, gVectorDrawableAnimatorClassInfo.clazz, "callOnFinished",
+ "(Landroid/graphics/drawable/AnimatedVectorDrawable$VectorDrawableAnimator;)V");
+ return RegisterMethodsOrDie(env, "android/graphics/drawable/AnimatedVectorDrawable",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/core/jni/android_graphics_drawable_VectorDrawable.cpp b/core/jni/android_graphics_drawable_VectorDrawable.cpp
index 563ec8b..7314fbc 100644
--- a/core/jni/android_graphics_drawable_VectorDrawable.cpp
+++ b/core/jni/android_graphics_drawable_VectorDrawable.cpp
@@ -32,11 +32,6 @@
return reinterpret_cast<jlong>(tree);
}
-static void deleteTree(JNIEnv*, jobject, jlong treePtr) {
- VectorDrawable::Tree* tree = reinterpret_cast<VectorDrawable::Tree*>(treePtr);
- delete tree;
-}
-
static void setTreeViewportSize(JNIEnv*, jobject, jlong treePtr,
jfloat viewportWidth, jfloat viewportHeight) {
VectorDrawable::Tree* tree = reinterpret_cast<VectorDrawable::Tree*>(treePtr);
@@ -143,11 +138,6 @@
return reinterpret_cast<jlong>(newGroup);
}
-static void deleteNode(JNIEnv*, jobject, jlong nodePtr) {
- VectorDrawable::Node* node = reinterpret_cast<VectorDrawable::Node*>(nodePtr);
- delete node;
-}
-
static void setNodeName(JNIEnv* env, jobject, jlong nodePtr, jstring nameStr) {
VectorDrawable::Node* node = reinterpret_cast<VectorDrawable::Node*>(nodePtr);
const char* nodeName = env->GetStringUTFChars(nameStr, NULL);
@@ -333,7 +323,6 @@
static const JNINativeMethod gMethods[] = {
{"nCreateRenderer", "!(J)J", (void*)createTree},
- {"nDestroyRenderer", "!(J)V", (void*)deleteTree},
{"nSetRendererViewportSize", "!(JFF)V", (void*)setTreeViewportSize},
{"nSetRootAlpha", "!(JF)Z", (void*)setRootAlpha},
{"nGetRootAlpha", "!(J)F", (void*)getRootAlpha},
@@ -352,7 +341,6 @@
{"nCreateClipPath", "!(J)J", (void*)createClipPath},
{"nCreateGroup", "!()J", (void*)createEmptyGroup},
{"nCreateGroup", "!(J)J", (void*)createGroup},
- {"nDestroy", "!(J)V", (void*)deleteNode},
{"nSetName", "(JLjava/lang/String;)V", (void*)setNodeName},
{"nUpdateGroupProperties", "!(JFFFFFFF)V", (void*)updateGroupProperties},
diff --git a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
index c48c371..af8ccf5 100644
--- a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
+++ b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
@@ -19,8 +19,14 @@
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.Animator.AnimatorListener;
+import android.animation.PropertyValuesHolder;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.animation.ObjectAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.app.Application;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
@@ -31,17 +37,27 @@
import android.graphics.Outline;
import android.graphics.PorterDuff;
import android.graphics.Rect;
+import android.os.Build;
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.LongArray;
+import android.util.PathParser;
+import android.util.TimeUtils;
+import android.view.Choreographer;
+import android.view.DisplayListCanvas;
+import android.view.RenderNode;
+import android.view.RenderNodeAnimatorSetHelper;
import android.view.View;
import com.android.internal.R;
+import com.android.internal.util.VirtualRefBasePtr;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
+import java.lang.ref.WeakReference;
import java.util.ArrayList;
/**
@@ -138,7 +154,7 @@
private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false;
/** Local, mutable animator set. */
- private final AnimatorSet mAnimatorSet = new AnimatorSet();
+ private final VectorDrawableAnimator mAnimatorSet = new VectorDrawableAnimator();
/**
* The resources against which this drawable was created. Used to attempt
@@ -187,6 +203,24 @@
mMutated = false;
}
+ /**
+ * In order to avoid breaking old apps, we only throw exception on invalid VectorDrawable
+ * animations * for apps targeting N and later. For older apps, we ignore (i.e. quietly skip)
+ * these animations.
+ *
+ * @return whether invalid animations for vector drawable should be ignored.
+ */
+ private static boolean shouldIgnoreInvalidAnimation() {
+ Application app = ActivityThread.currentApplication();
+ if (app == null || app.getApplicationInfo() == null) {
+ return true;
+ }
+ if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
+ return true;
+ }
+ return false;
+ }
+
@Override
public ConstantState getConstantState() {
mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations();
@@ -200,6 +234,9 @@
@Override
public void draw(Canvas canvas) {
+ if (canvas.isHardwareAccelerated()) {
+ mAnimatorSet.recordLastSeenTarget((DisplayListCanvas) canvas);
+ }
mAnimatedVectorState.mVectorDrawable.draw(canvas);
if (isStarted()) {
invalidateSelf();
@@ -582,9 +619,8 @@
* Resets the AnimatedVectorDrawable to the start state as specified in the animators.
*/
public void reset() {
- // TODO: Use reverse or seek to implement reset, when AnimatorSet supports them.
- start();
- mAnimatorSet.cancel();
+ mAnimatorSet.reset();
+ invalidateSelf();
}
@Override
@@ -603,8 +639,12 @@
@NonNull
private void ensureAnimatorSet() {
if (!mHasAnimatorSet) {
- mAnimatedVectorState.prepareLocalAnimators(mAnimatorSet, mRes);
+ // TODO: Skip the AnimatorSet creation and init the VectorDrawableAnimator directly
+ // with a list of LocalAnimators.
+ AnimatorSet set = new AnimatorSet();
+ mAnimatedVectorState.prepareLocalAnimators(set, mRes);
mHasAnimatorSet = true;
+ mAnimatorSet.initWithAnimatorSet(set);
mRes = null;
}
}
@@ -694,13 +734,13 @@
}
};
}
- mAnimatorSet.addListener(mAnimatorListener);
+ mAnimatorSet.setListener(mAnimatorListener);
}
// A helper function to clean up the animator listener in the mAnimatorSet.
private void removeAnimatorSetListener() {
if (mAnimatorListener != null) {
- mAnimatorSet.removeListener(mAnimatorListener);
+ mAnimatorSet.removeListener();
mAnimatorListener = null;
}
}
@@ -729,4 +769,427 @@
mAnimationCallbacks.clear();
}
+
+ /**
+ * @hide
+ */
+ public static class VectorDrawableAnimator {
+ private AnimatorListener mListener = null;
+ private final LongArray mStartDelays = new LongArray();
+ private PropertyValuesHolder.PropertyValues mTmpValues =
+ new PropertyValuesHolder.PropertyValues();
+ private long mSetPtr = 0;
+ private boolean mContainsSequentialAnimators = false;
+ private boolean mStarted = false;
+ private boolean mInitialized = false;
+ private boolean mAnimationPending = false;
+ private boolean mIsReversible = false;
+ // This needs to be set before parsing starts.
+ private boolean mShouldIgnoreInvalidAnim;
+ // TODO: Consider using NativeAllocationRegistery to track native allocation
+ private final VirtualRefBasePtr mSetRefBasePtr;
+ private WeakReference<RenderNode> mTarget = null;
+ private WeakReference<RenderNode> mLastSeenTarget = null;
+
+
+ VectorDrawableAnimator() {
+ mSetPtr = nCreateAnimatorSet();
+ // Increment ref count on native AnimatorSet, so it doesn't get released before Java
+ // side is done using it.
+ mSetRefBasePtr = new VirtualRefBasePtr(mSetPtr);
+ }
+
+ private void initWithAnimatorSet(AnimatorSet set) {
+ if (mInitialized) {
+ // Already initialized
+ throw new UnsupportedOperationException("VectorDrawableAnimator cannot be " +
+ "re-initialized");
+ }
+ mShouldIgnoreInvalidAnim = shouldIgnoreInvalidAnimation();
+ parseAnimatorSet(set, 0);
+ mInitialized = true;
+
+ // Check reversible.
+ if (mContainsSequentialAnimators) {
+ mIsReversible = false;
+ } else {
+ // Check if there's any start delay set on child
+ for (int i = 0; i < mStartDelays.size(); i++) {
+ if (mStartDelays.get(i) > 0) {
+ mIsReversible = false;
+ return;
+ }
+ }
+ }
+ mIsReversible = true;
+ }
+
+ private void parseAnimatorSet(AnimatorSet set, long startTime) {
+ ArrayList<Animator> animators = set.getChildAnimations();
+
+ boolean playTogether = set.shouldPlayTogether();
+ // Convert AnimatorSet to VectorDrawableAnimator
+ for (int i = 0; i < animators.size(); i++) {
+ Animator animator = animators.get(i);
+ // Here we only support ObjectAnimator
+ if (animator instanceof AnimatorSet) {
+ parseAnimatorSet((AnimatorSet) animator, startTime);
+ } else if (animator instanceof ObjectAnimator) {
+ createRTAnimator((ObjectAnimator) animator, startTime);
+ } // ignore ValueAnimators and others because they don't directly modify VD
+ // therefore will be useless to AVD.
+
+ if (!playTogether) {
+ // Assume not play together means play sequentially
+ startTime += animator.getTotalDuration();
+ mContainsSequentialAnimators = true;
+ }
+ }
+ }
+
+ // TODO: This method reads animation data from already parsed Animators. We need to move
+ // this step further up the chain in the parser to avoid the detour.
+ private void createRTAnimator(ObjectAnimator animator, long startTime) {
+ PropertyValuesHolder[] values = animator.getValues();
+ Object target = animator.getTarget();
+ if (target instanceof VectorDrawable.VGroup) {
+ createRTAnimatorForGroup(values, animator, (VectorDrawable.VGroup) target,
+ startTime);
+ } else if (target instanceof VectorDrawable.VPath) {
+ for (int i = 0; i < values.length; i++) {
+ values[i].getPropertyValues(mTmpValues);
+ if (mTmpValues.endValue instanceof PathParser.PathData &&
+ mTmpValues.propertyName.equals("pathData")) {
+ createRTAnimatorForPath(animator, (VectorDrawable.VPath) target,
+ startTime);
+ } else if (target instanceof VectorDrawable.VFullPath) {
+ createRTAnimatorForFullPath(animator, (VectorDrawable.VFullPath) target,
+ startTime);
+ } else if (!mShouldIgnoreInvalidAnim) {
+ throw new IllegalArgumentException("ClipPath only supports PathData " +
+ "property");
+ }
+
+ }
+ } else if (target instanceof VectorDrawable.VectorDrawableState) {
+ createRTAnimatorForRootGroup(values, animator,
+ (VectorDrawable.VectorDrawableState) target, startTime);
+ } else if (!mShouldIgnoreInvalidAnim) {
+ // Should never get here
+ throw new UnsupportedOperationException("Target should be either VGroup, VPath, " +
+ "or ConstantState, " + target == null ? "Null target" : target.getClass() +
+ " is not supported");
+ }
+ }
+
+ private void createRTAnimatorForGroup(PropertyValuesHolder[] values,
+ ObjectAnimator animator, VectorDrawable.VGroup target,
+ long startTime) {
+
+ long nativePtr = target.getNativePtr();
+ int propertyId;
+ for (int i = 0; i < values.length; i++) {
+ // TODO: We need to support the rare case in AVD where no start value is provided
+ values[i].getPropertyValues(mTmpValues);
+ propertyId = VectorDrawable.VGroup.getPropertyIndex(mTmpValues.propertyName);
+ if (mTmpValues.type != Float.class && mTmpValues.type != float.class) {
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.e(LOGTAG, "Unsupported type: " +
+ mTmpValues.type + ". Only float value is supported for Groups.");
+ }
+ continue;
+ }
+ if (propertyId < 0) {
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.e(LOGTAG, "Unsupported property: " +
+ mTmpValues.propertyName + " for Vector Drawable Group");
+ }
+ continue;
+ }
+ long propertyPtr = nCreateGroupPropertyHolder(nativePtr, propertyId,
+ (Float) mTmpValues.startValue, (Float) mTmpValues.endValue);
+ if (mTmpValues.dataSource != null) {
+ float[] dataPoints = createDataPoints(mTmpValues.dataSource, animator
+ .getDuration());
+ nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length);
+ }
+ createNativeChildAnimator(propertyPtr, startTime, animator);
+ }
+ }
+ private void createRTAnimatorForPath( ObjectAnimator animator, VectorDrawable.VPath target,
+ long startTime) {
+
+ long nativePtr = target.getNativePtr();
+ long startPathDataPtr = ((PathParser.PathData) mTmpValues.startValue)
+ .getNativePtr();
+ long endPathDataPtr = ((PathParser.PathData) mTmpValues.endValue)
+ .getNativePtr();
+ long propertyPtr = nCreatePathDataPropertyHolder(nativePtr, startPathDataPtr,
+ endPathDataPtr);
+ createNativeChildAnimator(propertyPtr, startTime, animator);
+ }
+
+ private void createRTAnimatorForFullPath(ObjectAnimator animator,
+ VectorDrawable.VFullPath target, long startTime) {
+
+ int propertyId = target.getPropertyIndex(mTmpValues.propertyName);
+ long propertyPtr;
+ long nativePtr = target.getNativePtr();
+ if (mTmpValues.type == Float.class || mTmpValues.type == float.class) {
+ if (propertyId < 0) {
+ if (mShouldIgnoreInvalidAnim) {
+ return;
+ } else {
+ throw new IllegalArgumentException("Property: " + mTmpValues.propertyName
+ + " is not supported for FullPath");
+ }
+ }
+ propertyPtr = nCreatePathPropertyHolder(nativePtr, propertyId,
+ (Float) mTmpValues.startValue, (Float) mTmpValues.endValue);
+
+ } else if (mTmpValues.type == Integer.class || mTmpValues.type == int.class) {
+ propertyPtr = nCreatePathColorPropertyHolder(nativePtr, propertyId,
+ (Integer) mTmpValues.startValue, (Integer) mTmpValues.endValue);
+ } else {
+ if (mShouldIgnoreInvalidAnim) {
+ return;
+ } else {
+ throw new UnsupportedOperationException("Unsupported type: " +
+ mTmpValues.type + ". Only float, int or PathData value is " +
+ "supported for Paths.");
+ }
+ }
+ if (mTmpValues.dataSource != null) {
+ float[] dataPoints = createDataPoints(mTmpValues.dataSource, animator
+ .getDuration());
+ nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length);
+ }
+ createNativeChildAnimator(propertyPtr, startTime, animator);
+ }
+
+ private void createRTAnimatorForRootGroup(PropertyValuesHolder[] values,
+ ObjectAnimator animator, VectorDrawable.VectorDrawableState target,
+ long startTime) {
+ long nativePtr = target.getNativeRenderer();
+ if (!animator.getPropertyName().equals("alpha")) {
+ if (mShouldIgnoreInvalidAnim) {
+ return;
+ } else {
+ throw new UnsupportedOperationException("Only alpha is supported for root "
+ + "group");
+ }
+ }
+ Float startValue = null;
+ Float endValue = null;
+ for (int i = 0; i < values.length; i++) {
+ values[i].getPropertyValues(mTmpValues);
+ if (mTmpValues.propertyName.equals("alpha")) {
+ startValue = (Float) mTmpValues.startValue;
+ endValue = (Float) mTmpValues.endValue;
+ break;
+ }
+ }
+ if (startValue == null && endValue == null) {
+ if (mShouldIgnoreInvalidAnim) {
+ return;
+ } else {
+ throw new UnsupportedOperationException("No alpha values are specified");
+ }
+ }
+ long propertyPtr = nCreateRootAlphaPropertyHolder(nativePtr, startValue, endValue);
+ createNativeChildAnimator(propertyPtr, startTime, animator);
+ }
+
+ // These are the data points that define the value of the animating properties.
+ // e.g. translateX and translateY can animate along a Path, at any fraction in [0, 1]
+ // a point on the path corresponds to the values of translateX and translateY.
+ // TODO: (Optimization) We should pass the path down in native and chop it into segments
+ // in native.
+ private static float[] createDataPoints(
+ PropertyValuesHolder.PropertyValues.DataSource dataSource, long duration) {
+ long frameIntervalNanos = Choreographer.getInstance().getFrameIntervalNanos();
+ int animIntervalMs = (int) (frameIntervalNanos / TimeUtils.NANOS_PER_MS);
+ int numAnimFrames = (int) Math.ceil(((double) duration) / animIntervalMs);
+ float values[] = new float[numAnimFrames];
+ float lastFrame = numAnimFrames - 1;
+ for (int i = 0; i < numAnimFrames; i++) {
+ float fraction = i / lastFrame;
+ values[i] = (Float) dataSource.getValueAtFraction(fraction);
+ }
+ return values;
+ }
+
+ private void createNativeChildAnimator(long propertyPtr, long extraDelay,
+ ObjectAnimator animator) {
+ long duration = animator.getDuration();
+ int repeatCount = animator.getRepeatCount();
+ long startDelay = extraDelay + animator.getStartDelay();
+ TimeInterpolator interpolator = animator.getInterpolator();
+ long nativeInterpolator =
+ RenderNodeAnimatorSetHelper.createNativeInterpolator(interpolator, duration);
+
+ startDelay *= ValueAnimator.getDurationScale();
+ duration *= ValueAnimator.getDurationScale();
+
+ mStartDelays.add(startDelay);
+ nAddAnimator(mSetPtr, propertyPtr, nativeInterpolator, startDelay, duration,
+ repeatCount);
+ }
+
+ /**
+ * Holds a weak reference to the target that was last seen (through the DisplayListCanvas
+ * in the last draw call), so that when animator set needs to start, we can add the animator
+ * to the last seen RenderNode target and start right away.
+ */
+ protected void recordLastSeenTarget(DisplayListCanvas canvas) {
+ if (mAnimationPending) {
+ mLastSeenTarget = new WeakReference<RenderNode>(
+ RenderNodeAnimatorSetHelper.getTarget(canvas));
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.d(LOGTAG, "Target is set in the next frame");
+ }
+ mAnimationPending = false;
+ start();
+ } else {
+ mLastSeenTarget = new WeakReference<RenderNode>(
+ RenderNodeAnimatorSetHelper.getTarget(canvas));
+ }
+
+ }
+
+ private boolean setTarget(RenderNode node) {
+ if (mTarget != null && mTarget.get() != null) {
+ // TODO: Maybe we want to support target change.
+ throw new IllegalStateException("Target already set!");
+ }
+
+ node.addAnimator(this);
+ mTarget = new WeakReference<RenderNode>(node);
+ return true;
+ }
+
+ private boolean useLastSeenTarget() {
+ if (mLastSeenTarget != null && mLastSeenTarget.get() != null) {
+ setTarget(mLastSeenTarget.get());
+ return true;
+ }
+ return false;
+ }
+
+ public void start() {
+ if (!mInitialized) {
+ return;
+ }
+
+ if (mStarted) {
+ return;
+ }
+
+ if (!useLastSeenTarget()) {
+ mAnimationPending = true;
+ return;
+ }
+
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.d(LOGTAG, "Target is set. Starting VDAnimatorSet from java");
+ }
+
+ nStart(mSetPtr, this);
+ if (mListener != null) {
+ mListener.onAnimationStart(null);
+ }
+ mStarted = true;
+ }
+
+ public void end() {
+ if (mInitialized && mStarted) {
+ nEnd(mSetPtr);
+ onAnimationEnd();
+ }
+ }
+
+ void reset() {
+ if (!mInitialized) {
+ return;
+ }
+ // TODO: Need to implement reset.
+ Log.w(LOGTAG, "Reset is yet to be implemented");
+ nReset(mSetPtr);
+ }
+
+ // Current (imperfect) Java AnimatorSet cannot be reversed when the set contains sequential
+ // animators or when the animator set has a start delay
+ void reverse() {
+ if (!mIsReversible) {
+ return;
+ }
+ // TODO: Need to support reverse (non-public API)
+ Log.w(LOGTAG, "Reverse is yet to be implemented");
+ nReverse(mSetPtr, this);
+ }
+
+ public long getAnimatorNativePtr() {
+ return mSetPtr;
+ }
+
+ boolean canReverse() {
+ return mIsReversible;
+ }
+
+ boolean isStarted() {
+ return mStarted;
+ }
+
+ boolean isRunning() {
+ if (!mInitialized) {
+ return false;
+ }
+ return mStarted;
+ }
+
+ void setListener(AnimatorListener listener) {
+ mListener = listener;
+ }
+
+ void removeListener() {
+ mListener = null;
+ }
+
+ private void onAnimationEnd() {
+ mStarted = false;
+ if (mListener != null) {
+ mListener.onAnimationEnd(null);
+ }
+ mTarget = null;
+ }
+
+ // onFinished: should be called from native
+ private static void callOnFinished(VectorDrawableAnimator set) {
+ if (DBG_ANIMATION_VECTOR_DRAWABLE) {
+ Log.d(LOGTAG, "on finished called from native");
+ }
+ set.onAnimationEnd();
+ }
+ }
+
+ private static native long nCreateAnimatorSet();
+ private static native void nAddAnimator(long setPtr, long propertyValuesHolder,
+ long nativeInterpolator, long startDelay, long duration, int repeatCount);
+
+ private static native long nCreateGroupPropertyHolder(long nativePtr, int propertyId,
+ float startValue, float endValue);
+
+ private static native long nCreatePathDataPropertyHolder(long nativePtr, long startValuePtr,
+ long endValuePtr);
+ private static native long nCreatePathColorPropertyHolder(long nativePtr, int propertyId,
+ int startValue, int endValue);
+ private static native long nCreatePathPropertyHolder(long nativePtr, int propertyId,
+ float startValue, float endValue);
+ private static native long nCreateRootAlphaPropertyHolder(long nativePtr, float startValue,
+ float endValue);
+ private static native void nSetPropertyHolderData(long nativePtr, float[] data, int length);
+ private static native void nStart(long animatorSetPtr, VectorDrawableAnimator set);
+ private static native void nReverse(long animatorSetPtr, VectorDrawableAnimator set);
+ private static native void nEnd(long animatorSetPtr);
+ private static native void nReset(long animatorSetPtr);
}
diff --git a/graphics/java/android/graphics/drawable/VectorDrawable.java b/graphics/java/android/graphics/drawable/VectorDrawable.java
index 1fc1b83..bdbf3c0 100644
--- a/graphics/java/android/graphics/drawable/VectorDrawable.java
+++ b/graphics/java/android/graphics/drawable/VectorDrawable.java
@@ -39,6 +39,7 @@
import android.util.Xml;
import com.android.internal.R;
+import com.android.internal.util.VirtualRefBasePtr;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -47,6 +48,7 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.Stack;
/**
@@ -522,13 +524,13 @@
public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Theme theme)
throws XmlPullParserException, IOException {
- if (mVectorState.mRootGroup != null || mVectorState.mNativeRendererPtr != 0) {
+ if (mVectorState.mRootGroup != null || mVectorState.mNativeRendererRefBase != null) {
// This VD has been used to display other VD resource content, clean up.
mVectorState.mRootGroup = new VGroup();
- if (mVectorState.mNativeRendererPtr != 0) {
- nDestroyRenderer(mVectorState.mNativeRendererPtr);
+ if (mVectorState.mNativeRendererRefBase != null) {
+ mVectorState.mNativeRendererRefBase.release();
}
- mVectorState.mNativeRendererPtr = nCreateRenderer(mVectorState.mRootGroup.mNativePtr);
+ mVectorState.createNativeRenderer(mVectorState.mRootGroup.mNativePtr);
}
final VectorDrawableState state = mVectorState;
state.setDensity(Drawable.resolveDensity(r, 0));
@@ -707,7 +709,7 @@
return mVectorState.mAutoMirrored;
}
- private static class VectorDrawableState extends ConstantState {
+ static class VectorDrawableState extends ConstantState {
// Variables below need to be copied (deep copy if applicable) for mutation.
int[] mThemeAttrs;
int mChangingConfigurations;
@@ -722,7 +724,7 @@
Insets mOpticalInsets = Insets.NONE;
String mRootName = null;
VGroup mRootGroup;
- long mNativeRendererPtr;
+ VirtualRefBasePtr mNativeRendererRefBase = null;
int mDensity = DisplayMetrics.DENSITY_DEFAULT;
final ArrayMap<String, Object> mVGTargetsMap = new ArrayMap<>();
@@ -743,7 +745,7 @@
mTintMode = copy.mTintMode;
mAutoMirrored = copy.mAutoMirrored;
mRootGroup = new VGroup(copy.mRootGroup, mVGTargetsMap);
- mNativeRendererPtr = nCreateRenderer(mRootGroup.mNativePtr);
+ createNativeRenderer(mRootGroup.mNativePtr);
mBaseWidth = copy.mBaseWidth;
mBaseHeight = copy.mBaseHeight;
@@ -758,18 +760,15 @@
}
}
- @Override
- public void finalize() throws Throwable {
- if (mNativeRendererPtr != 0) {
- nDestroyRenderer(mNativeRendererPtr);
- mNativeRendererPtr = 0;
- }
- super.finalize();
+ private void createNativeRenderer(long rootGroupPtr) {
+ mNativeRendererRefBase = new VirtualRefBasePtr(nCreateRenderer(rootGroupPtr));
}
-
long getNativeRenderer() {
- return mNativeRendererPtr;
+ if (mNativeRendererRefBase == null) {
+ return 0;
+ }
+ return mNativeRendererRefBase.get();
}
public boolean canReuseCache() {
@@ -808,7 +807,7 @@
public VectorDrawableState() {
mRootGroup = new VGroup();
- mNativeRendererPtr = nCreateRenderer(mRootGroup.mNativePtr);
+ createNativeRenderer(mRootGroup.mNativePtr);
}
@Override
@@ -872,16 +871,16 @@
* has changed.
*/
public boolean setAlpha(float alpha) {
- return nSetRootAlpha(mNativeRendererPtr, alpha);
+ return nSetRootAlpha(mNativeRendererRefBase.get(), alpha);
}
@SuppressWarnings("unused")
public float getAlpha() {
- return nGetRootAlpha(mNativeRendererPtr);
+ return nGetRootAlpha(mNativeRendererRefBase.get());
}
}
- private static class VGroup implements VObject {
+ static class VGroup implements VObject {
private static final int ROTATE_INDEX = 0;
private static final int PIVOT_X_INDEX = 1;
private static final int PIVOT_Y_INDEX = 2;
@@ -891,6 +890,28 @@
private static final int TRANSLATE_Y_INDEX = 6;
private static final int TRANSFORM_PROPERTY_COUNT = 7;
+ private static final HashMap<String, Integer> sPropertyMap =
+ new HashMap<String, Integer>() {
+ {
+ put("translateX", TRANSLATE_X_INDEX);
+ put("translateY", TRANSLATE_Y_INDEX);
+ put("scaleX", SCALE_X_INDEX);
+ put("scaleY", SCALE_Y_INDEX);
+ put("pivotX", PIVOT_X_INDEX);
+ put("pivotY", PIVOT_Y_INDEX);
+ put("rotation", ROTATE_INDEX);
+ }
+ };
+
+ static int getPropertyIndex(String propertyName) {
+ if (sPropertyMap.containsKey(propertyName)) {
+ return sPropertyMap.get(propertyName);
+ } else {
+ // property not found
+ return -1;
+ }
+ }
+
// Temp array to store transform values obtained from native.
private float[] mTransform;
/////////////////////////////////////////////////////
@@ -903,8 +924,11 @@
private int mChangingConfigurations;
private int[] mThemeAttrs;
private String mGroupName = null;
- private long mNativePtr = 0;
+ // The native object will be created in the constructor and will be destroyed in native
+ // when the neither java nor native has ref to the tree. This pointer should be valid
+ // throughout this VGroup Java object's life.
+ private final long mNativePtr;
public VGroup(VGroup copy, ArrayMap<String, Object> targetsMap) {
mIsStateful = copy.mIsStateful;
@@ -1044,16 +1068,6 @@
}
@Override
- protected void finalize() throws Throwable {
- if (mNativePtr != 0) {
- nDestroy(mNativePtr);
- mNativePtr = 0;
- }
- super.finalize();
- }
-
-
- @Override
public void applyTheme(Theme t) {
if (mThemeAttrs != null) {
final TypedArray a = t.resolveAttributes(mThemeAttrs,
@@ -1149,7 +1163,7 @@
/**
* Common Path information for clip path and normal path.
*/
- private static abstract class VPath implements VObject {
+ static abstract class VPath implements VObject {
protected PathParser.PathData mPathData = null;
String mPathName;
@@ -1187,10 +1201,10 @@
* Clip path, which only has name and pathData.
*/
private static class VClipPath extends VPath {
- long mNativePtr = 0;
+ private final long mNativePtr;
+
public VClipPath() {
mNativePtr = nCreateClipPath();
- // Empty constructor.
}
public VClipPath(VClipPath copy) {
@@ -1204,14 +1218,6 @@
}
@Override
- protected void finalize() throws Throwable {
- if (mNativePtr != 0) {
- nDestroy(mNativePtr);
- mNativePtr = 0;
- }
- super.finalize();
- }
- @Override
public void inflate(Resources r, AttributeSet attrs, Theme theme) {
final TypedArray a = obtainAttributes(r, theme, attrs,
R.styleable.VectorDrawableClipPath);
@@ -1260,7 +1266,7 @@
/**
* Normal path, which contains all the fill / paint information.
*/
- private static class VFullPath extends VPath {
+ static class VFullPath extends VPath {
private static final int STROKE_WIDTH_INDEX = 0;
private static final int STROKE_COLOR_INDEX = 1;
private static final int STROKE_ALPHA_INDEX = 2;
@@ -1274,6 +1280,20 @@
private static final int STROKE_MITER_LIMIT_INDEX = 10;
private static final int TOTAL_PROPERTY_COUNT = 11;
+ private final static HashMap<String, Integer> sPropertyMap
+ = new HashMap<String, Integer> () {
+ {
+ put("strokeWidth", STROKE_WIDTH_INDEX);
+ put("strokeColor", STROKE_COLOR_INDEX);
+ put("strokeAlpha", STROKE_ALPHA_INDEX);
+ put("fillColor", FILL_COLOR_INDEX);
+ put("fillAlpha", FILL_ALPHA_INDEX);
+ put("trimPathStart", TRIM_PATH_START_INDEX);
+ put("trimPathEnd", TRIM_PATH_END_INDEX);
+ put("trimPathOffset", TRIM_PATH_OFFSET_INDEX);
+ }
+ };
+
// Temp array to store property data obtained from native getter.
private byte[] mPropertyData;
/////////////////////////////////////////////////////
@@ -1282,10 +1302,9 @@
ComplexColor mStrokeColors = null;
ComplexColor mFillColors = null;
- private long mNativePtr = 0;
+ private final long mNativePtr;
public VFullPath() {
- // Empty constructor.
mNativePtr = nCreateFullPath();
}
@@ -1297,6 +1316,14 @@
mFillColors = copy.mFillColors;
}
+ int getPropertyIndex(String propertyName) {
+ if (!sPropertyMap.containsKey(propertyName)) {
+ return -1;
+ } else {
+ return sPropertyMap.get(propertyName);
+ }
+ }
+
@Override
public boolean onStateChange(int[] stateSet) {
boolean changed = false;
@@ -1341,15 +1368,6 @@
a.recycle();
}
- @Override
- protected void finalize() throws Throwable {
- if (mNativePtr != 0) {
- nDestroy(mNativePtr);
- mNativePtr = 0;
- }
- super.finalize();
- }
-
private void updateStateFromTypedArray(TypedArray a) {
int byteCount = TOTAL_PROPERTY_COUNT * 4;
if (mPropertyData == null) {
@@ -1595,7 +1613,6 @@
}
private static native long nCreateRenderer(long rootGroupPtr);
- private static native void nDestroyRenderer(long rendererPtr);
private static native void nSetRendererViewportSize(long rendererPtr, float viewportWidth,
float viewportHeight);
private static native boolean nSetRootAlpha(long rendererPtr, float alpha);
@@ -1605,7 +1622,7 @@
private static native void nDraw(long rendererPtr, long canvasWrapperPtr,
long colorFilterPtr, Rect bounds, boolean needsMirroring, boolean canReuseCache);
private static native long nCreateFullPath();
- private static native long nCreateFullPath(long mNativeFullPathPtr);
+ private static native long nCreateFullPath(long nativeFullPathPtr);
private static native boolean nGetFullPathProperties(long pathPtr, byte[] properties,
int length);
@@ -1621,7 +1638,6 @@
private static native long nCreateGroup();
private static native long nCreateGroup(long groupPtr);
- private static native void nDestroy(long nodePtr);
private static native void nSetName(long nodePtr, String name);
private static native boolean nGetGroupProperties(long groupPtr, float[] properties,
int length);
diff --git a/keystore/java/android/security/keystore/KeyInfo.java b/keystore/java/android/security/keystore/KeyInfo.java
index 7cf4b04..d726880 100644
--- a/keystore/java/android/security/keystore/KeyInfo.java
+++ b/keystore/java/android/security/keystore/KeyInfo.java
@@ -269,7 +269,7 @@
/**
* Returns {@code true} if the requirement that this key can only be used if the user has been
- * authenticated if enforced by secure hardware (e.g., Trusted Execution Environment (TEE) or
+ * authenticated is enforced by secure hardware (e.g., Trusted Execution Environment (TEE) or
* Secure Element (SE)).
*
* @see #isUserAuthenticationRequired()
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk
index 1fb9ac5..7b43947 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -78,6 +78,8 @@
Program.cpp \
ProgramCache.cpp \
Properties.cpp \
+ PropertyValuesHolder.cpp \
+ PropertyValuesAnimatorSet.cpp \
RenderBufferCache.cpp \
RenderNode.cpp \
RenderProperties.cpp \
@@ -250,7 +252,8 @@
tests/unit/SkiaBehaviorTests.cpp \
tests/unit/StringUtilsTests.cpp \
tests/unit/TextDropShadowCacheTests.cpp \
- tests/unit/VectorDrawableTests.cpp
+ tests/unit/VectorDrawableTests.cpp \
+ tests/unit/GradientCacheTests.cpp
ifeq (true, $(HWUI_NEW_OPS))
LOCAL_SRC_FILES += \
diff --git a/libs/hwui/Animator.cpp b/libs/hwui/Animator.cpp
index 5ca2a2f..7bd2b24 100644
--- a/libs/hwui/Animator.cpp
+++ b/libs/hwui/Animator.cpp
@@ -90,6 +90,9 @@
doSetStartValue(getValue(mTarget));
}
if (mStagingPlayState > mPlayState) {
+ if (mStagingPlayState == PlayState::Restarted) {
+ mStagingPlayState = PlayState::Running;
+ }
mPlayState = mStagingPlayState;
// Oh boy, we're starting! Man the battle stations!
if (mPlayState == PlayState::Running) {
@@ -131,6 +134,11 @@
return true;
}
+ // This should be set before setValue() so animators can query this time when setValue
+ // is called.
+ nsecs_t currentFrameTime = context.frameTimeMs();
+ onPlayTimeChanged(currentFrameTime - mStartTime);
+
// If BaseRenderNodeAnimator is handling the delay (not typical), then
// because the staging properties reflect the final value, we always need
// to call setValue even if the animation isn't yet running or is still
@@ -141,8 +149,9 @@
}
float fraction = 1.0f;
+
if (mPlayState == PlayState::Running && mDuration > 0) {
- fraction = (float)(context.frameTimeMs() - mStartTime) / mDuration;
+ fraction = (float)(currentFrameTime - mStartTime) / mDuration;
}
if (fraction >= 1.0f) {
fraction = 1.0f;
diff --git a/libs/hwui/Animator.h b/libs/hwui/Animator.h
index aea95bf..2c9c9c3 100644
--- a/libs/hwui/Animator.h
+++ b/libs/hwui/Animator.h
@@ -59,7 +59,13 @@
mMayRunAsync = mayRunAsync;
}
bool mayRunAsync() { return mMayRunAsync; }
- ANDROID_API void start() { mStagingPlayState = PlayState::Running; onStagingPlayStateChanged(); }
+ ANDROID_API void start() {
+ if (mStagingPlayState == PlayState::NotStarted) {
+ mStagingPlayState = PlayState::Running;
+ } else {
+ mStagingPlayState = PlayState::Restarted;
+ }
+ onStagingPlayStateChanged(); }
ANDROID_API void end() { mStagingPlayState = PlayState::Finished; onStagingPlayStateChanged(); }
void attach(RenderNode* target);
@@ -77,10 +83,27 @@
void forceEndNow(AnimationContext& context);
protected:
+ // PlayState is used by mStagingPlayState and mPlayState to track the state initiated from UI
+ // thread and Render Thread animation state, respectively.
+ // From the UI thread, mStagingPlayState transition looks like
+ // NotStarted -> Running -> Finished
+ // ^ |
+ // | |
+ // Restarted <------
+ // Note: For mStagingState, the Finished state (optional) is only set when the animation is
+ // terminated by user.
+ //
+ // On Render Thread, mPlayState transition:
+ // NotStart -> Running -> Finished
+ // ^ |
+ // | |
+ // -------------
+
enum class PlayState {
NotStarted,
Running,
Finished,
+ Restarted,
};
BaseRenderNodeAnimator(float finalValue);
@@ -93,6 +116,7 @@
void callOnFinishedListener(AnimationContext& context);
virtual void onStagingPlayStateChanged() {}
+ virtual void onPlayTimeChanged(nsecs_t playTime) {}
RenderNode* mTarget;
diff --git a/libs/hwui/Canvas.h b/libs/hwui/Canvas.h
index dbd502d..27facdf 100644
--- a/libs/hwui/Canvas.h
+++ b/libs/hwui/Canvas.h
@@ -52,6 +52,13 @@
} // namespace SaveFlags
+namespace uirenderer {
+namespace VectorDrawable {
+class Tree;
+};
+};
+typedef uirenderer::VectorDrawable::Tree VectorDrawableRoot;
+
class ANDROID_API Canvas {
public:
virtual ~Canvas() {};
@@ -217,6 +224,11 @@
*/
virtual bool drawTextAbsolutePos() const = 0;
+ /**
+ * Draws a VectorDrawable onto the canvas.
+ */
+ virtual void drawVectorDrawable(VectorDrawableRoot* tree);
+
protected:
void drawTextDecorations(float x, float y, float length, const SkPaint& paint);
};
diff --git a/libs/hwui/DisplayListCanvas.cpp b/libs/hwui/DisplayListCanvas.cpp
index 7eaa785..3db14b5 100644
--- a/libs/hwui/DisplayListCanvas.cpp
+++ b/libs/hwui/DisplayListCanvas.cpp
@@ -16,11 +16,12 @@
#include "DisplayListCanvas.h"
-#include "ResourceCache.h"
#include "DeferredDisplayList.h"
#include "DeferredLayerUpdater.h"
#include "DisplayListOp.h"
+#include "ResourceCache.h"
#include "RenderNode.h"
+#include "VectorDrawable.h"
#include "utils/PaintUtils.h"
#include <SkCamera.h>
@@ -412,6 +413,16 @@
addDrawOp(new (alloc()) DrawPointsOp(points, count, refPaint(&paint)));
}
+void DisplayListCanvas::drawVectorDrawable(VectorDrawableRoot* tree) {
+ mDisplayList->ref(tree);
+ const SkBitmap& bitmap = tree->getBitmapUpdateIfDirty();
+ SkPaint* paint = tree->getPaint();
+ const SkRect bounds = tree->getBounds();
+ addDrawOp(new (alloc()) DrawBitmapRectOp(refBitmap(bitmap),
+ 0, 0, bitmap.width(), bitmap.height(),
+ bounds.left(), bounds.top(), bounds.right(), bounds.bottom(), refPaint(paint)));
+}
+
void DisplayListCanvas::drawTextOnPath(const uint16_t* glyphs, int count,
const SkPath& path, float hOffset, float vOffset, const SkPaint& paint) {
if (!glyphs || count <= 0) return;
diff --git a/libs/hwui/DisplayListCanvas.h b/libs/hwui/DisplayListCanvas.h
index ad93960..06e72a0 100644
--- a/libs/hwui/DisplayListCanvas.h
+++ b/libs/hwui/DisplayListCanvas.h
@@ -206,6 +206,8 @@
float dstLeft, float dstTop, float dstRight, float dstBottom,
const SkPaint* paint) override;
+ virtual void drawVectorDrawable(VectorDrawableRoot* tree) override;
+
// Text
virtual void drawText(const uint16_t* glyphs, const float* positions, int count,
const SkPaint& paint, float x, float y, float boundsLeft, float boundsTop,
@@ -214,7 +216,6 @@
float hOffset, float vOffset, const SkPaint& paint) override;
virtual bool drawTextAbsolutePos() const override { return false; }
-
private:
CanvasState mState;
diff --git a/libs/hwui/FboCache.cpp b/libs/hwui/FboCache.cpp
index cca3cb7..b2181b6 100644
--- a/libs/hwui/FboCache.cpp
+++ b/libs/hwui/FboCache.cpp
@@ -27,15 +27,8 @@
// Constructors/destructor
///////////////////////////////////////////////////////////////////////////////
-FboCache::FboCache(): mMaxSize(DEFAULT_FBO_CACHE_SIZE) {
- char property[PROPERTY_VALUE_MAX];
- if (property_get(PROPERTY_FBO_CACHE_SIZE, property, nullptr) > 0) {
- INIT_LOGD(" Setting fbo cache size to %s", property);
- mMaxSize = atoi(property);
- } else {
- INIT_LOGD(" Using default fbo cache size of %d", DEFAULT_FBO_CACHE_SIZE);
- }
-}
+FboCache::FboCache()
+ : mMaxSize(Properties::fboCacheSize) {}
FboCache::~FboCache() {
clear();
diff --git a/libs/hwui/FrameBuilder.cpp b/libs/hwui/FrameBuilder.cpp
index c8910cb..185acce 100644
--- a/libs/hwui/FrameBuilder.cpp
+++ b/libs/hwui/FrameBuilder.cpp
@@ -19,6 +19,7 @@
#include "Canvas.h"
#include "LayerUpdateQueue.h"
#include "RenderNode.h"
+#include "VectorDrawable.h"
#include "renderstate/OffscreenBufferPool.h"
#include "utils/FatVector.h"
#include "utils/PaintUtils.h"
@@ -545,6 +546,18 @@
currentLayer().deferUnmergeableOp(mAllocator, bakedState, OpBatchType::Bitmap);
}
+void FrameBuilder::deferVectorDrawableOp(const VectorDrawableOp& op) {
+ const SkBitmap& bitmap = op.vectorDrawable->getBitmapUpdateIfDirty();
+ SkPaint* paint = op.vectorDrawable->getPaint();
+ const BitmapRectOp* resolvedOp = new (mAllocator) BitmapRectOp(op.unmappedBounds,
+ op.localMatrix,
+ op.localClip,
+ paint,
+ &bitmap,
+ Rect(bitmap.width(), bitmap.height()));
+ deferBitmapRectOp(*resolvedOp);
+}
+
void FrameBuilder::deferCirclePropsOp(const CirclePropsOp& op) {
// allocate a temporary oval op (with mAllocator, so it persists until render), so the
// renderer doesn't have to handle the RoundRectPropsOp type, and so state baking is simple.
diff --git a/libs/hwui/GradientCache.cpp b/libs/hwui/GradientCache.cpp
index e899ac7..11293d6 100644
--- a/libs/hwui/GradientCache.cpp
+++ b/libs/hwui/GradientCache.cpp
@@ -65,17 +65,9 @@
GradientCache::GradientCache(Extensions& extensions)
: mCache(LruCache<GradientCacheEntry, Texture*>::kUnlimitedCapacity)
, mSize(0)
- , mMaxSize(MB(DEFAULT_GRADIENT_CACHE_SIZE))
+ , mMaxSize(Properties::gradientCacheSize)
, mUseFloatTexture(extensions.hasFloatTextures())
, mHasNpot(extensions.hasNPot()){
- char property[PROPERTY_VALUE_MAX];
- if (property_get(PROPERTY_GRADIENT_CACHE_SIZE, property, nullptr) > 0) {
- INIT_LOGD(" Setting gradient cache size to %sMB", property);
- setMaxSize(MB(atof(property)));
- } else {
- INIT_LOGD(" Using default gradient cache size of %.2fMB", DEFAULT_GRADIENT_CACHE_SIZE);
- }
-
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &mMaxTextureSize);
mCache.setOnEntryRemovedListener(this);
@@ -97,13 +89,6 @@
return mMaxSize;
}
-void GradientCache::setMaxSize(uint32_t maxSize) {
- mMaxSize = maxSize;
- while (mSize > mMaxSize) {
- mCache.removeOldest();
- }
-}
-
///////////////////////////////////////////////////////////////////////////////
// Callbacks
///////////////////////////////////////////////////////////////////////////////
@@ -168,10 +153,13 @@
texture->blend = info.hasAlpha;
texture->generation = 1;
- // Asume the cache is always big enough
+ // Assume the cache is always big enough
const uint32_t size = info.width * 2 * bytesPerPixel();
while (getSize() + size > mMaxSize) {
- mCache.removeOldest();
+ LOG_ALWAYS_FATAL_IF(!mCache.removeOldest(),
+ "Ran out of things to remove from the cache? getSize() = %" PRIu32
+ ", size = %" PRIu32 ", mMaxSize = %" PRIu32 ", width = %" PRIu32,
+ getSize(), size, mMaxSize, info.width);
}
generateTexture(colors, positions, info.width, 2, texture);
diff --git a/libs/hwui/GradientCache.h b/libs/hwui/GradientCache.h
index b762ca7..dccd450 100644
--- a/libs/hwui/GradientCache.h
+++ b/libs/hwui/GradientCache.h
@@ -123,10 +123,6 @@
void clear();
/**
- * Sets the maximum size of the cache in bytes.
- */
- void setMaxSize(uint32_t maxSize);
- /**
* Returns the maximum size of the cache in bytes.
*/
uint32_t getMaxSize();
@@ -177,7 +173,7 @@
LruCache<GradientCacheEntry, Texture*> mCache;
uint32_t mSize;
- uint32_t mMaxSize;
+ const uint32_t mMaxSize;
GLint mMaxTextureSize;
bool mUseFloatTexture;
diff --git a/libs/hwui/PatchCache.cpp b/libs/hwui/PatchCache.cpp
index 9881280..bd6feb9 100644
--- a/libs/hwui/PatchCache.cpp
+++ b/libs/hwui/PatchCache.cpp
@@ -32,20 +32,12 @@
PatchCache::PatchCache(RenderState& renderState)
: mRenderState(renderState)
+ , mMaxSize(Properties::patchCacheSize)
, mSize(0)
, mCache(LruCache<PatchDescription, Patch*>::kUnlimitedCapacity)
, mMeshBuffer(0)
, mFreeBlocks(nullptr)
- , mGenerationId(0) {
- char property[PROPERTY_VALUE_MAX];
- if (property_get(PROPERTY_PATCH_CACHE_SIZE, property, nullptr) > 0) {
- INIT_LOGD(" Setting patch cache size to %skB", property);
- mMaxSize = KB(atoi(property));
- } else {
- INIT_LOGD(" Using default patch cache size of %.2fkB", DEFAULT_PATCH_CACHE_SIZE);
- mMaxSize = KB(DEFAULT_PATCH_CACHE_SIZE);
- }
-}
+ , mGenerationId(0) {}
PatchCache::~PatchCache() {
clear();
diff --git a/libs/hwui/PatchCache.h b/libs/hwui/PatchCache.h
index 387f79a..66ef6a0 100644
--- a/libs/hwui/PatchCache.h
+++ b/libs/hwui/PatchCache.h
@@ -169,7 +169,7 @@
#endif
RenderState& mRenderState;
- uint32_t mMaxSize;
+ const uint32_t mMaxSize;
uint32_t mSize;
LruCache<PatchDescription, Patch*> mCache;
diff --git a/libs/hwui/PathCache.cpp b/libs/hwui/PathCache.cpp
index bfabc1d..8f914ac 100644
--- a/libs/hwui/PathCache.cpp
+++ b/libs/hwui/PathCache.cpp
@@ -135,17 +135,10 @@
// Cache constructor/destructor
///////////////////////////////////////////////////////////////////////////////
-PathCache::PathCache():
- mCache(LruCache<PathDescription, PathTexture*>::kUnlimitedCapacity),
- mSize(0), mMaxSize(MB(DEFAULT_PATH_CACHE_SIZE)) {
- char property[PROPERTY_VALUE_MAX];
- if (property_get(PROPERTY_PATH_CACHE_SIZE, property, nullptr) > 0) {
- INIT_LOGD(" Setting path cache size to %sMB", property);
- mMaxSize = MB(atof(property));
- } else {
- INIT_LOGD(" Using default path cache size of %.2fMB", DEFAULT_PATH_CACHE_SIZE);
- }
-
+PathCache::PathCache()
+ : mCache(LruCache<PathDescription, PathTexture*>::kUnlimitedCapacity)
+ , mSize(0)
+ , mMaxSize(Properties::pathCacheSize) {
mCache.setOnEntryRemovedListener(this);
GLint maxTextureSize;
diff --git a/libs/hwui/PathCache.h b/libs/hwui/PathCache.h
index 18f380f..d2633aa 100644
--- a/libs/hwui/PathCache.h
+++ b/libs/hwui/PathCache.h
@@ -300,7 +300,7 @@
LruCache<PathDescription, PathTexture*> mCache;
uint32_t mSize;
- uint32_t mMaxSize;
+ const uint32_t mMaxSize;
GLuint mMaxTextureSize;
bool mDebugEnabled;
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 083aeb7..bbd8c72 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -37,7 +37,18 @@
bool Properties::enablePartialUpdates = true;
float Properties::textGamma = DEFAULT_TEXT_GAMMA;
-int Properties::layerPoolSize = DEFAULT_LAYER_CACHE_SIZE;
+
+int Properties::fboCacheSize = DEFAULT_FBO_CACHE_SIZE;
+int Properties::gradientCacheSize = MB(DEFAULT_GRADIENT_CACHE_SIZE);
+int Properties::layerPoolSize = MB(DEFAULT_LAYER_CACHE_SIZE);
+int Properties::patchCacheSize = KB(DEFAULT_PATCH_CACHE_SIZE);
+int Properties::pathCacheSize = MB(DEFAULT_PATH_CACHE_SIZE);
+int Properties::renderBufferCacheSize = MB(DEFAULT_RENDER_BUFFER_CACHE_SIZE);
+int Properties::tessellationCacheSize = MB(DEFAULT_VERTEX_CACHE_SIZE);
+int Properties::textDropShadowCacheSize = MB(DEFAULT_DROP_SHADOW_CACHE_SIZE);
+int Properties::textureCacheSize = MB(DEFAULT_TEXTURE_CACHE_SIZE);
+
+float Properties::textureCacheFlushRate = DEFAULT_TEXTURE_CACHE_FLUSH_RATE;
DebugLevel Properties::debugLevel = kDebugDisabled;
OverdrawColorSet Properties::overdrawColorSet = OverdrawColorSet::Default;
@@ -79,7 +90,6 @@
bool prevDebugOverdraw = debugOverdraw;
StencilClipDebug prevDebugStencilClip = debugStencilClip;
-
debugOverdraw = false;
if (property_get(PROPERTY_DEBUG_OVERDRAW, property, nullptr) > 0) {
INIT_LOGD(" Overdraw debug enabled: %s", property);
@@ -133,7 +143,18 @@
enablePartialUpdates = property_get_bool(PROPERTY_ENABLE_PARTIAL_UPDATES, true);
textGamma = property_get_float(PROPERTY_TEXT_GAMMA, DEFAULT_TEXT_GAMMA);
+
+ fboCacheSize = property_get_int(PROPERTY_FBO_CACHE_SIZE, DEFAULT_FBO_CACHE_SIZE);
+ gradientCacheSize = MB(property_get_float(PROPERTY_GRADIENT_CACHE_SIZE, DEFAULT_GRADIENT_CACHE_SIZE));
layerPoolSize = MB(property_get_float(PROPERTY_LAYER_CACHE_SIZE, DEFAULT_LAYER_CACHE_SIZE));
+ patchCacheSize = KB(property_get_float(PROPERTY_PATCH_CACHE_SIZE, DEFAULT_PATCH_CACHE_SIZE));
+ pathCacheSize = MB(property_get_float(PROPERTY_PATH_CACHE_SIZE, DEFAULT_PATH_CACHE_SIZE));
+ renderBufferCacheSize = MB(property_get_float(PROPERTY_RENDER_BUFFER_CACHE_SIZE, DEFAULT_RENDER_BUFFER_CACHE_SIZE));
+ tessellationCacheSize = MB(property_get_float(PROPERTY_VERTEX_CACHE_SIZE, DEFAULT_VERTEX_CACHE_SIZE));
+ textDropShadowCacheSize = MB(property_get_float(PROPERTY_DROP_SHADOW_CACHE_SIZE, DEFAULT_DROP_SHADOW_CACHE_SIZE));
+ textureCacheSize = MB(property_get_float(PROPERTY_TEXTURE_CACHE_SIZE, DEFAULT_TEXTURE_CACHE_SIZE));
+ textureCacheFlushRate = std::max(0.0f, std::min(1.0f,
+ property_get_float(PROPERTY_TEXTURE_CACHE_FLUSH_RATE, DEFAULT_TEXTURE_CACHE_FLUSH_RATE)));
return (prevDebugLayersUpdates != debugLayersUpdates)
|| (prevDebugOverdraw != debugOverdraw)
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index 88f1dbc..3e11151 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -267,7 +267,16 @@
static float textGamma;
+ static int fboCacheSize;
+ static int gradientCacheSize;
static int layerPoolSize;
+ static int patchCacheSize;
+ static int pathCacheSize;
+ static int renderBufferCacheSize;
+ static int tessellationCacheSize;
+ static int textDropShadowCacheSize;
+ static int textureCacheSize;
+ static float textureCacheFlushRate;
static DebugLevel debugLevel;
static OverdrawColorSet overdrawColorSet;
diff --git a/libs/hwui/PropertyValuesAnimatorSet.cpp b/libs/hwui/PropertyValuesAnimatorSet.cpp
new file mode 100644
index 0000000..eca1afcc
--- /dev/null
+++ b/libs/hwui/PropertyValuesAnimatorSet.cpp
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "PropertyValuesAnimatorSet.h"
+#include "RenderNode.h"
+
+namespace android {
+namespace uirenderer {
+
+void PropertyValuesAnimatorSet::addPropertyAnimator(PropertyValuesHolder* propertyValuesHolder,
+ Interpolator* interpolator, nsecs_t startDelay,
+ nsecs_t duration, int repeatCount) {
+
+ PropertyAnimator* animator = new PropertyAnimator(propertyValuesHolder,
+ interpolator, startDelay, duration, repeatCount);
+ mAnimators.emplace_back(animator);
+ setListener(new PropertyAnimatorSetListener(this));
+}
+
+PropertyValuesAnimatorSet::PropertyValuesAnimatorSet()
+ : BaseRenderNodeAnimator(1.0f) {
+ setStartValue(0);
+ mLastFraction = 0.0f;
+ setInterpolator(new LinearInterpolator());
+}
+
+void PropertyValuesAnimatorSet::onFinished(BaseRenderNodeAnimator* animator) {
+ if (mOneShotListener.get()) {
+ mOneShotListener->onAnimationFinished(animator);
+ mOneShotListener = nullptr;
+ }
+}
+
+float PropertyValuesAnimatorSet::getValue(RenderNode* target) const {
+ return mLastFraction;
+}
+
+void PropertyValuesAnimatorSet::setValue(RenderNode* target, float value) {
+ mLastFraction = value;
+}
+
+void PropertyValuesAnimatorSet::onPlayTimeChanged(nsecs_t playTime) {
+ for (size_t i = 0; i < mAnimators.size(); i++) {
+ mAnimators[i]->setCurrentPlayTime(playTime);
+ }
+}
+
+void PropertyValuesAnimatorSet::reset() {
+ // TODO: implement reset through adding a play state because we need to support reset() even
+ // during an animation run.
+}
+
+void PropertyValuesAnimatorSet::start(AnimationListener* listener) {
+ init();
+ mOneShotListener = listener;
+ BaseRenderNodeAnimator::start();
+}
+
+void PropertyValuesAnimatorSet::reverse(AnimationListener* listener) {
+// TODO: implement reverse
+}
+
+void PropertyValuesAnimatorSet::init() {
+ if (mInitialized) {
+ return;
+ }
+ nsecs_t maxDuration = 0;
+ for (size_t i = 0; i < mAnimators.size(); i++) {
+ if (maxDuration < mAnimators[i]->getTotalDuration()) {
+ maxDuration = mAnimators[i]->getTotalDuration();
+ }
+ }
+ mDuration = maxDuration;
+ mInitialized = true;
+}
+
+uint32_t PropertyValuesAnimatorSet::dirtyMask() {
+ return RenderNode::DISPLAY_LIST;
+}
+
+PropertyAnimator::PropertyAnimator(PropertyValuesHolder* holder, Interpolator* interpolator,
+ nsecs_t startDelay, nsecs_t duration, int repeatCount)
+ : mPropertyValuesHolder(holder), mInterpolator(interpolator), mStartDelay(startDelay),
+ mDuration(duration) {
+ if (repeatCount < 0) {
+ mRepeatCount = UINT32_MAX;
+ } else {
+ mRepeatCount = repeatCount;
+ }
+ mTotalDuration = ((nsecs_t) mRepeatCount + 1) * mDuration + mStartDelay;
+}
+
+void PropertyAnimator::setCurrentPlayTime(nsecs_t playTime) {
+ if (playTime >= mStartDelay && playTime < mTotalDuration) {
+ nsecs_t currentIterationPlayTime = (playTime - mStartDelay) % mDuration;
+ mLatestFraction = currentIterationPlayTime / (float) mDuration;
+ } else if (mLatestFraction < 1.0f && playTime >= mTotalDuration) {
+ mLatestFraction = 1.0f;
+ } else {
+ return;
+ }
+
+ setFraction(mLatestFraction);
+}
+
+void PropertyAnimator::setFraction(float fraction) {
+ float interpolatedFraction = mInterpolator->interpolate(mLatestFraction);
+ mPropertyValuesHolder->setFraction(interpolatedFraction);
+}
+
+void PropertyAnimatorSetListener::onAnimationFinished(BaseRenderNodeAnimator* animator) {
+ mSet->onFinished(animator);
+}
+
+}
+}
diff --git a/libs/hwui/PropertyValuesAnimatorSet.h b/libs/hwui/PropertyValuesAnimatorSet.h
new file mode 100644
index 0000000..4c7ce52
--- /dev/null
+++ b/libs/hwui/PropertyValuesAnimatorSet.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "Animator.h"
+#include "PropertyValuesHolder.h"
+#include "Interpolator.h"
+
+namespace android {
+namespace uirenderer {
+
+class PropertyAnimator {
+public:
+ PropertyAnimator(PropertyValuesHolder* holder, Interpolator* interpolator, nsecs_t startDelay,
+ nsecs_t duration, int repeatCount);
+ void setCurrentPlayTime(nsecs_t playTime);
+ nsecs_t getTotalDuration() {
+ return mTotalDuration;
+ }
+ void setFraction(float fraction);
+
+private:
+ std::unique_ptr<PropertyValuesHolder> mPropertyValuesHolder;
+ std::unique_ptr<Interpolator> mInterpolator;
+ nsecs_t mStartDelay;
+ nsecs_t mDuration;
+ uint32_t mRepeatCount;
+ nsecs_t mTotalDuration;
+ float mLatestFraction = 0.0f;
+};
+
+class ANDROID_API PropertyValuesAnimatorSet : public BaseRenderNodeAnimator {
+public:
+ friend class PropertyAnimatorSetListener;
+ PropertyValuesAnimatorSet();
+
+ void start(AnimationListener* listener);
+ void reverse(AnimationListener* listener);
+ void reset();
+
+ void addPropertyAnimator(PropertyValuesHolder* propertyValuesHolder,
+ Interpolator* interpolators, int64_t startDelays,
+ nsecs_t durations, int repeatCount);
+ virtual uint32_t dirtyMask();
+
+protected:
+ virtual float getValue(RenderNode* target) const override;
+ virtual void setValue(RenderNode* target, float value) override;
+ virtual void onPlayTimeChanged(nsecs_t playTime) override;
+
+private:
+ void init();
+ void onFinished(BaseRenderNodeAnimator* animator);
+ // Listener set from outside
+ sp<AnimationListener> mOneShotListener;
+ std::vector< std::unique_ptr<PropertyAnimator> > mAnimators;
+ float mLastFraction = 0.0f;
+ bool mInitialized = false;
+};
+
+class PropertyAnimatorSetListener : public AnimationListener {
+public:
+ PropertyAnimatorSetListener(PropertyValuesAnimatorSet* set) : mSet(set) {}
+ virtual void onAnimationFinished(BaseRenderNodeAnimator* animator) override;
+
+private:
+ PropertyValuesAnimatorSet* mSet;
+};
+
+} // namespace uirenderer
+} // namespace android
diff --git a/libs/hwui/PropertyValuesHolder.cpp b/libs/hwui/PropertyValuesHolder.cpp
new file mode 100644
index 0000000..8f837f6
--- /dev/null
+++ b/libs/hwui/PropertyValuesHolder.cpp
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "PropertyValuesHolder.h"
+
+#include "utils/VectorDrawableUtils.h"
+
+#include <utils/Log.h>
+
+namespace android {
+namespace uirenderer {
+
+using namespace VectorDrawable;
+
+float PropertyValuesHolder::getValueFromData(float fraction) {
+ if (mDataSource.size() == 0) {
+ LOG_ALWAYS_FATAL("No data source is defined");
+ return 0;
+ }
+ if (fraction <= 0.0f) {
+ return mDataSource.front();
+ }
+ if (fraction >= 1.0f) {
+ return mDataSource.back();
+ }
+
+ fraction *= mDataSource.size() - 1;
+ int lowIndex = floor(fraction);
+ fraction -= lowIndex;
+
+ float value = mDataSource[lowIndex] * (1.0f - fraction)
+ + mDataSource[lowIndex + 1] * fraction;
+ return value;
+}
+
+void GroupPropertyValuesHolder::setFraction(float fraction) {
+ float animatedValue;
+ if (mDataSource.size() > 0) {
+ animatedValue = getValueFromData(fraction);
+ } else {
+ animatedValue = mStartValue * (1 - fraction) + mEndValue * fraction;
+ }
+ mGroup->setPropertyValue(mPropertyId, animatedValue);
+}
+
+inline U8CPU lerp(U8CPU fromValue, U8CPU toValue, float fraction) {
+ return (U8CPU) (fromValue * (1 - fraction) + toValue * fraction);
+}
+
+// TODO: Add a test for this
+SkColor FullPathColorPropertyValuesHolder::interpolateColors(SkColor fromColor, SkColor toColor,
+ float fraction) {
+ U8CPU alpha = lerp(SkColorGetA(fromColor), SkColorGetA(toColor), fraction);
+ U8CPU red = lerp(SkColorGetR(fromColor), SkColorGetR(toColor), fraction);
+ U8CPU green = lerp(SkColorGetG(fromColor), SkColorGetG(toColor), fraction);
+ U8CPU blue = lerp(SkColorGetB(fromColor), SkColorGetB(toColor), fraction);
+ return SkColorSetARGB(alpha, red, green, blue);
+}
+
+void FullPathColorPropertyValuesHolder::setFraction(float fraction) {
+ SkColor animatedValue = interpolateColors(mStartValue, mEndValue, fraction);
+ mFullPath->setColorPropertyValue(mPropertyId, animatedValue);
+}
+
+void FullPathPropertyValuesHolder::setFraction(float fraction) {
+ float animatedValue;
+ if (mDataSource.size() > 0) {
+ animatedValue = getValueFromData(fraction);
+ } else {
+ animatedValue = mStartValue * (1 - fraction) + mEndValue * fraction;
+ }
+ mFullPath->setPropertyValue(mPropertyId, animatedValue);
+}
+
+void PathDataPropertyValuesHolder::setFraction(float fraction) {
+ VectorDrawableUtils::interpolatePaths(&mPathData, mStartValue, mEndValue, fraction);
+ mPath->setPathData(mPathData);
+}
+
+void RootAlphaPropertyValuesHolder::setFraction(float fraction) {
+ float animatedValue = mStartValue * (1 - fraction) + mEndValue * fraction;
+ mTree->setRootAlpha(animatedValue);
+}
+
+} // namepace uirenderer
+} // namespace android
diff --git a/libs/hwui/PropertyValuesHolder.h b/libs/hwui/PropertyValuesHolder.h
new file mode 100644
index 0000000..b905fae
--- /dev/null
+++ b/libs/hwui/PropertyValuesHolder.h
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "VectorDrawable.h"
+
+#include <SkColor.h>
+
+namespace android {
+namespace uirenderer {
+
+/**
+ * PropertyValues holder contains data needed to change a property of a Vector Drawable object.
+ * When a fraction in [0f, 1f] is provided, the holder will calculate an interpolated value based
+ * on its start and end value, and set the new value on the VectorDrawble's corresponding property.
+ */
+class ANDROID_API PropertyValuesHolder {
+public:
+ virtual void setFraction(float fraction) = 0;
+ void setPropertyDataSource(float* dataSource, int length) {
+ mDataSource.insert(mDataSource.begin(), dataSource, dataSource + length);
+ }
+ float getValueFromData(float fraction);
+ virtual ~PropertyValuesHolder() {}
+protected:
+ std::vector<float> mDataSource;
+};
+
+class ANDROID_API GroupPropertyValuesHolder : public PropertyValuesHolder {
+public:
+ GroupPropertyValuesHolder(VectorDrawable::Group* ptr, int propertyId, float startValue,
+ float endValue)
+ : mGroup(ptr)
+ , mPropertyId(propertyId)
+ , mStartValue(startValue)
+ , mEndValue(endValue){
+ }
+ void setFraction(float fraction) override;
+private:
+ VectorDrawable::Group* mGroup;
+ int mPropertyId;
+ float mStartValue;
+ float mEndValue;
+};
+
+class ANDROID_API FullPathColorPropertyValuesHolder : public PropertyValuesHolder {
+public:
+ FullPathColorPropertyValuesHolder(VectorDrawable::FullPath* ptr, int propertyId, int32_t startValue,
+ int32_t endValue)
+ : mFullPath(ptr)
+ , mPropertyId(propertyId)
+ , mStartValue(startValue)
+ , mEndValue(endValue) {};
+ void setFraction(float fraction) override;
+ static SkColor interpolateColors(SkColor fromColor, SkColor toColor, float fraction);
+private:
+ VectorDrawable::FullPath* mFullPath;
+ int mPropertyId;
+ int32_t mStartValue;
+ int32_t mEndValue;
+};
+
+class ANDROID_API FullPathPropertyValuesHolder : public PropertyValuesHolder {
+public:
+ FullPathPropertyValuesHolder(VectorDrawable::FullPath* ptr, int propertyId, float startValue,
+ float endValue)
+ : mFullPath(ptr)
+ , mPropertyId(propertyId)
+ , mStartValue(startValue)
+ , mEndValue(endValue) {};
+ void setFraction(float fraction) override;
+private:
+ VectorDrawable::FullPath* mFullPath;
+ int mPropertyId;
+ float mStartValue;
+ float mEndValue;
+};
+
+class ANDROID_API PathDataPropertyValuesHolder : public PropertyValuesHolder {
+public:
+ PathDataPropertyValuesHolder(VectorDrawable::Path* ptr, PathData* startValue,
+ PathData* endValue)
+ : mPath(ptr)
+ , mStartValue(*startValue)
+ , mEndValue(*endValue) {};
+ void setFraction(float fraction) override;
+private:
+ VectorDrawable::Path* mPath;
+ PathData mPathData;
+ PathData mStartValue;
+ PathData mEndValue;
+};
+
+class ANDROID_API RootAlphaPropertyValuesHolder : public PropertyValuesHolder {
+public:
+ RootAlphaPropertyValuesHolder(VectorDrawable::Tree* tree, float startValue, float endValue)
+ : mTree(tree)
+ , mStartValue(startValue)
+ , mEndValue(endValue) {}
+ void setFraction(float fraction) override;
+private:
+ VectorDrawable::Tree* mTree;
+ float mStartValue;
+ float mEndValue;
+};
+}
+}
diff --git a/libs/hwui/RecordedOp.h b/libs/hwui/RecordedOp.h
index 593d690..bb26e2e 100644
--- a/libs/hwui/RecordedOp.h
+++ b/libs/hwui/RecordedOp.h
@@ -17,6 +17,7 @@
#ifndef ANDROID_HWUI_RECORDED_OP_H
#define ANDROID_HWUI_RECORDED_OP_H
+#include "RecordedOp.h"
#include "font/FontUtil.h"
#include "Matrix.h"
#include "Rect.h"
@@ -39,6 +40,10 @@
class RenderNode;
struct Vertex;
+namespace VectorDrawable {
+class Tree;
+}
+
/**
* Authoritative op list, used for generating the op ID enum, ID based LUTS, and
* the functions to which they dispatch. Parameter macros are executed for each op,
@@ -75,6 +80,7 @@
PRE_RENDER_OP_FN(EndLayerOp) \
PRE_RENDER_OP_FN(BeginUnclippedLayerOp) \
PRE_RENDER_OP_FN(EndUnclippedLayerOp) \
+ PRE_RENDER_OP_FN(VectorDrawableOp) \
\
RENDER_ONLY_OP_FN(ShadowOp) \
RENDER_ONLY_OP_FN(LayerOp) \
@@ -325,6 +331,13 @@
const float* ry;
};
+struct VectorDrawableOp : RecordedOp {
+ VectorDrawableOp(VectorDrawable::Tree* tree, BASE_PARAMS_PAINTLESS)
+ : SUPER_PAINTLESS(VectorDrawableOp)
+ , vectorDrawable(tree) {}
+ VectorDrawable::Tree* vectorDrawable;
+};
+
/**
* Real-time, dynamic-lit shadow.
*
diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp
index 2387962..16929b8 100644
--- a/libs/hwui/RecordingCanvas.cpp
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -19,6 +19,7 @@
#include "DeferredLayerUpdater.h"
#include "RecordedOp.h"
#include "RenderNode.h"
+#include "VectorDrawable.h"
namespace android {
namespace uirenderer {
@@ -395,7 +396,6 @@
&x->value, &y->value, &radius->value));
}
-
void RecordingCanvas::drawOval(float left, float top, float right, float bottom, const SkPaint& paint) {
addOp(new (alloc()) OvalOp(
Rect(left, top, right, bottom),
@@ -422,6 +422,15 @@
refPaint(&paint), refPath(&path)));
}
+void RecordingCanvas::drawVectorDrawable(VectorDrawableRoot* tree) {
+ mDisplayList->ref(tree);
+ addOp(new (alloc()) VectorDrawableOp(
+ tree,
+ Rect(tree->getBounds()),
+ *(mState.currentSnapshot()->transform),
+ getRecordedClip()));
+}
+
// Bitmap-based
void RecordingCanvas::drawBitmap(const SkBitmap& bitmap, float left, float top, const SkPaint* paint) {
save(SaveFlags::Matrix);
diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h
index 375760f..cc14e61 100644
--- a/libs/hwui/RecordingCanvas.h
+++ b/libs/hwui/RecordingCanvas.h
@@ -175,6 +175,8 @@
const uint16_t* indices, int indexCount, const SkPaint& paint) override
{ /* RecordingCanvas does not support drawVertices(); ignore */ }
+ virtual void drawVectorDrawable(VectorDrawableRoot* tree) override;
+
// Bitmap-based
virtual void drawBitmap(const SkBitmap& bitmap, float left, float top, const SkPaint* paint) override;
virtual void drawBitmap(const SkBitmap& bitmap, const SkMatrix& matrix,
diff --git a/libs/hwui/RenderBufferCache.cpp b/libs/hwui/RenderBufferCache.cpp
index 11d7a6a..1ac57cd 100644
--- a/libs/hwui/RenderBufferCache.cpp
+++ b/libs/hwui/RenderBufferCache.cpp
@@ -40,16 +40,9 @@
// Constructors/destructor
///////////////////////////////////////////////////////////////////////////////
-RenderBufferCache::RenderBufferCache(): mSize(0), mMaxSize(MB(DEFAULT_RENDER_BUFFER_CACHE_SIZE)) {
- char property[PROPERTY_VALUE_MAX];
- if (property_get(PROPERTY_RENDER_BUFFER_CACHE_SIZE, property, nullptr) > 0) {
- INIT_LOGD(" Setting render buffer cache size to %sMB", property);
- setMaxSize(MB(atof(property)));
- } else {
- INIT_LOGD(" Using default render buffer cache size of %.2fMB",
- DEFAULT_RENDER_BUFFER_CACHE_SIZE);
- }
-}
+RenderBufferCache::RenderBufferCache()
+ : mSize(0)
+ , mMaxSize(Properties::renderBufferCacheSize) {}
RenderBufferCache::~RenderBufferCache() {
clear();
@@ -67,11 +60,6 @@
return mMaxSize;
}
-void RenderBufferCache::setMaxSize(uint32_t maxSize) {
- clear();
- mMaxSize = maxSize;
-}
-
///////////////////////////////////////////////////////////////////////////////
// Caching
///////////////////////////////////////////////////////////////////////////////
diff --git a/libs/hwui/RenderBufferCache.h b/libs/hwui/RenderBufferCache.h
index 7f59ec1..f77f4c9 100644
--- a/libs/hwui/RenderBufferCache.h
+++ b/libs/hwui/RenderBufferCache.h
@@ -64,10 +64,6 @@
void clear();
/**
- * Sets the maximum size of the cache in bytes.
- */
- void setMaxSize(uint32_t maxSize);
- /**
* Returns the maximum size of the cache in bytes.
*/
uint32_t getMaxSize();
diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp
index d320a41..bd4442d 100644
--- a/libs/hwui/SkiaCanvas.cpp
+++ b/libs/hwui/SkiaCanvas.cpp
@@ -32,6 +32,8 @@
#include <SkTLazy.h>
#include <SkTemplates.h>
+#include "VectorDrawable.h"
+
#include <memory>
namespace android {
@@ -153,6 +155,7 @@
float hOffset, float vOffset, const SkPaint& paint) override;
virtual bool drawTextAbsolutePos() const override { return true; }
+ virtual void drawVectorDrawable(VectorDrawableRoot* vectorDrawable) override;
virtual void drawRoundRect(uirenderer::CanvasPropertyPrimitive* left,
uirenderer::CanvasPropertyPrimitive* top, uirenderer::CanvasPropertyPrimitive* right,
@@ -742,6 +745,14 @@
NinePatch::Draw(mCanvas, bounds, bitmap, chunk, paint, nullptr);
}
+void SkiaCanvas::drawVectorDrawable(VectorDrawableRoot* vectorDrawable) {
+ const SkBitmap& bitmap = vectorDrawable->getBitmapUpdateIfDirty();
+ SkRect bounds = vectorDrawable->getBounds();
+ drawBitmap(bitmap, 0, 0, bitmap.width(), bitmap.height(),
+ bounds.fLeft, bounds.fTop, bounds.fRight, bounds.fBottom,
+ vectorDrawable->getPaint());
+}
+
// ----------------------------------------------------------------------------
// Canvas draw operations: Text
// ----------------------------------------------------------------------------
diff --git a/libs/hwui/TessellationCache.cpp b/libs/hwui/TessellationCache.cpp
index fd9fb852..14c8f392 100644
--- a/libs/hwui/TessellationCache.cpp
+++ b/libs/hwui/TessellationCache.cpp
@@ -265,18 +265,9 @@
///////////////////////////////////////////////////////////////////////////////
TessellationCache::TessellationCache()
- : mSize(0)
- , mMaxSize(MB(DEFAULT_VERTEX_CACHE_SIZE))
+ : mMaxSize(Properties::tessellationCacheSize)
, mCache(LruCache<Description, Buffer*>::kUnlimitedCapacity)
, mShadowCache(LruCache<ShadowDescription, Task<vertexBuffer_pair_t*>*>::kUnlimitedCapacity) {
- char property[PROPERTY_VALUE_MAX];
- if (property_get(PROPERTY_VERTEX_CACHE_SIZE, property, nullptr) > 0) {
- INIT_LOGD(" Setting tessellation cache size to %sMB", property);
- setMaxSize(MB(atof(property)));
- } else {
- INIT_LOGD(" Using default tessellation cache size of %.2fMB", DEFAULT_VERTEX_CACHE_SIZE);
- }
-
mCache.setOnEntryRemovedListener(&mBufferRemovedListener);
mShadowCache.setOnEntryRemovedListener(&mBufferPairRemovedListener);
mDebugEnabled = Properties::debugLevel & kDebugCaches;
@@ -303,13 +294,6 @@
return mMaxSize;
}
-void TessellationCache::setMaxSize(uint32_t maxSize) {
- mMaxSize = maxSize;
- while (mSize > mMaxSize) {
- mCache.removeOldest();
- }
-}
-
///////////////////////////////////////////////////////////////////////////////
// Caching
///////////////////////////////////////////////////////////////////////////////
diff --git a/libs/hwui/TessellationCache.h b/libs/hwui/TessellationCache.h
index 6dcc8120..0bd6365 100644
--- a/libs/hwui/TessellationCache.h
+++ b/libs/hwui/TessellationCache.h
@@ -131,11 +131,6 @@
* Clears the cache. This causes all TessellationBuffers to be deleted.
*/
void clear();
-
- /**
- * Sets the maximum size of the cache in bytes.
- */
- void setMaxSize(uint32_t maxSize);
/**
* Returns the maximum size of the cache in bytes.
*/
@@ -198,8 +193,7 @@
Buffer* getOrCreateBuffer(const Description& entry, Tessellator tessellator);
- uint32_t mSize;
- uint32_t mMaxSize;
+ const uint32_t mMaxSize;
bool mDebugEnabled;
diff --git a/libs/hwui/TextDropShadowCache.cpp b/libs/hwui/TextDropShadowCache.cpp
index 1707468..fe4b3d75 100644
--- a/libs/hwui/TextDropShadowCache.cpp
+++ b/libs/hwui/TextDropShadowCache.cpp
@@ -93,36 +93,21 @@
// Constructors/destructor
///////////////////////////////////////////////////////////////////////////////
-TextDropShadowCache::TextDropShadowCache():
- mCache(LruCache<ShadowText, ShadowTexture*>::kUnlimitedCapacity),
- mSize(0), mMaxSize(MB(DEFAULT_DROP_SHADOW_CACHE_SIZE)) {
- char property[PROPERTY_VALUE_MAX];
- if (property_get(PROPERTY_DROP_SHADOW_CACHE_SIZE, property, nullptr) > 0) {
- INIT_LOGD(" Setting drop shadow cache size to %sMB", property);
- setMaxSize(MB(atof(property)));
- } else {
- INIT_LOGD(" Using default drop shadow cache size of %.2fMB",
- DEFAULT_DROP_SHADOW_CACHE_SIZE);
- }
+TextDropShadowCache::TextDropShadowCache()
+ : TextDropShadowCache(Properties::textDropShadowCacheSize) {}
- init();
-}
-
-TextDropShadowCache::TextDropShadowCache(uint32_t maxByteSize):
- mCache(LruCache<ShadowText, ShadowTexture*>::kUnlimitedCapacity),
- mSize(0), mMaxSize(maxByteSize) {
- init();
+TextDropShadowCache::TextDropShadowCache(uint32_t maxByteSize)
+ : mCache(LruCache<ShadowText, ShadowTexture*>::kUnlimitedCapacity)
+ , mSize(0)
+ , mMaxSize(maxByteSize) {
+ mCache.setOnEntryRemovedListener(this);
+ mDebugEnabled = Properties::debugLevel & kDebugMoreCaches;
}
TextDropShadowCache::~TextDropShadowCache() {
mCache.clear();
}
-void TextDropShadowCache::init() {
- mCache.setOnEntryRemovedListener(this);
- mDebugEnabled = Properties::debugLevel & kDebugMoreCaches;
-}
-
///////////////////////////////////////////////////////////////////////////////
// Size management
///////////////////////////////////////////////////////////////////////////////
@@ -135,13 +120,6 @@
return mMaxSize;
}
-void TextDropShadowCache::setMaxSize(uint32_t maxSize) {
- mMaxSize = maxSize;
- while (mSize > mMaxSize) {
- mCache.removeOldest();
- }
-}
-
///////////////////////////////////////////////////////////////////////////////
// Callbacks
///////////////////////////////////////////////////////////////////////////////
diff --git a/libs/hwui/TextDropShadowCache.h b/libs/hwui/TextDropShadowCache.h
index c4f3c5d..cf64788 100644
--- a/libs/hwui/TextDropShadowCache.h
+++ b/libs/hwui/TextDropShadowCache.h
@@ -148,10 +148,6 @@
}
/**
- * Sets the maximum size of the cache in bytes.
- */
- void setMaxSize(uint32_t maxSize);
- /**
* Returns the maximum size of the cache in bytes.
*/
uint32_t getMaxSize();
@@ -161,13 +157,11 @@
uint32_t getSize();
private:
- void init();
-
LruCache<ShadowText, ShadowTexture*> mCache;
uint32_t mSize;
- uint32_t mMaxSize;
- FontRenderer* mRenderer;
+ const uint32_t mMaxSize;
+ FontRenderer* mRenderer = nullptr;
bool mDebugEnabled;
}; // class TextDropShadowCache
diff --git a/libs/hwui/TextureCache.cpp b/libs/hwui/TextureCache.cpp
index 31bfa3a..ade8600a 100644
--- a/libs/hwui/TextureCache.cpp
+++ b/libs/hwui/TextureCache.cpp
@@ -35,26 +35,9 @@
TextureCache::TextureCache()
: mCache(LruCache<uint32_t, Texture*>::kUnlimitedCapacity)
, mSize(0)
- , mMaxSize(MB(DEFAULT_TEXTURE_CACHE_SIZE))
- , mFlushRate(DEFAULT_TEXTURE_CACHE_FLUSH_RATE)
+ , mMaxSize(Properties::textureCacheSize)
+ , mFlushRate(Properties::textureCacheFlushRate)
, mAssetAtlas(nullptr) {
- char property[PROPERTY_VALUE_MAX];
- if (property_get(PROPERTY_TEXTURE_CACHE_SIZE, property, nullptr) > 0) {
- INIT_LOGD(" Setting texture cache size to %sMB", property);
- setMaxSize(MB(atof(property)));
- } else {
- INIT_LOGD(" Using default texture cache size of %.2fMB", DEFAULT_TEXTURE_CACHE_SIZE);
- }
-
- if (property_get(PROPERTY_TEXTURE_CACHE_FLUSH_RATE, property, nullptr) > 0) {
- float flushRate = atof(property);
- INIT_LOGD(" Setting texture cache flush rate to %.2f%%", flushRate * 100.0f);
- setFlushRate(flushRate);
- } else {
- INIT_LOGD(" Using default texture cache flush rate of %.2f%%",
- DEFAULT_TEXTURE_CACHE_FLUSH_RATE * 100.0f);
- }
-
mCache.setOnEntryRemovedListener(this);
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &mMaxTextureSize);
@@ -79,17 +62,6 @@
return mMaxSize;
}
-void TextureCache::setMaxSize(uint32_t maxSize) {
- mMaxSize = maxSize;
- while (mSize > mMaxSize) {
- mCache.removeOldest();
- }
-}
-
-void TextureCache::setFlushRate(float flushRate) {
- mFlushRate = std::max(0.0f, std::min(1.0f, flushRate));
-}
-
///////////////////////////////////////////////////////////////////////////////
// Callbacks
///////////////////////////////////////////////////////////////////////////////
diff --git a/libs/hwui/TextureCache.h b/libs/hwui/TextureCache.h
index 463450c..a4317ce 100644
--- a/libs/hwui/TextureCache.h
+++ b/libs/hwui/TextureCache.h
@@ -109,10 +109,6 @@
void clear();
/**
- * Sets the maximum size of the cache in bytes.
- */
- void setMaxSize(uint32_t maxSize);
- /**
* Returns the maximum size of the cache in bytes.
*/
uint32_t getMaxSize();
@@ -126,11 +122,6 @@
* is defined by the flush rate.
*/
void flush();
- /**
- * Indicates the percentage of the cache to retain when a
- * memory trim is requested (see Caches::flush).
- */
- void setFlushRate(float flushRate);
void setAssetAtlas(AssetAtlas* assetAtlas);
@@ -148,10 +139,10 @@
LruCache<uint32_t, Texture*> mCache;
uint32_t mSize;
- uint32_t mMaxSize;
+ const uint32_t mMaxSize;
GLint mMaxTextureSize;
- float mFlushRate;
+ const float mFlushRate;
bool mDebugEnabled;
diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp
index 1cf15ac..2e3856f 100644
--- a/libs/hwui/VectorDrawable.cpp
+++ b/libs/hwui/VectorDrawable.cpp
@@ -138,18 +138,7 @@
}
FullPath::FullPath(const FullPath& path) : Path(path) {
- mStrokeWidth = path.mStrokeWidth;
- mStrokeColor = path.mStrokeColor;
- mStrokeAlpha = path.mStrokeAlpha;
- mFillColor = path.mFillColor;
- mFillAlpha = path.mFillAlpha;
- mTrimPathStart = path.mTrimPathStart;
- mTrimPathEnd = path.mTrimPathEnd;
- mTrimPathOffset = path.mTrimPathOffset;
- mStrokeMiterLimit = path.mStrokeMiterLimit;
- mStrokeLineCap = path.mStrokeLineCap;
- mStrokeLineJoin = path.mStrokeLineJoin;
-
+ mProperties = path.mProperties;
SkRefCnt_SafeAssign(mStrokeGradient, path.mStrokeGradient);
SkRefCnt_SafeAssign(mFillGradient, path.mFillGradient);
}
@@ -159,7 +148,7 @@
return mTrimmedSkPath;
}
Path::getUpdatedPath();
- if (mTrimPathStart != 0.0f || mTrimPathEnd != 1.0f) {
+ if (mProperties.trimPathStart != 0.0f || mProperties.trimPathEnd != 1.0f) {
applyTrim();
return mTrimmedSkPath;
} else {
@@ -170,14 +159,14 @@
void FullPath::updateProperties(float strokeWidth, SkColor strokeColor, float strokeAlpha,
SkColor fillColor, float fillAlpha, float trimPathStart, float trimPathEnd,
float trimPathOffset, float strokeMiterLimit, int strokeLineCap, int strokeLineJoin) {
- mStrokeWidth = strokeWidth;
- mStrokeColor = strokeColor;
- mStrokeAlpha = strokeAlpha;
- mFillColor = fillColor;
- mFillAlpha = fillAlpha;
- mStrokeMiterLimit = strokeMiterLimit;
- mStrokeLineCap = SkPaint::Cap(strokeLineCap);
- mStrokeLineJoin = SkPaint::Join(strokeLineJoin);
+ mProperties.strokeWidth = strokeWidth;
+ mProperties.strokeColor = strokeColor;
+ mProperties.strokeAlpha = strokeAlpha;
+ mProperties.fillColor = fillColor;
+ mProperties.fillAlpha = fillAlpha;
+ mProperties.strokeMiterLimit = strokeMiterLimit;
+ mProperties.strokeLineCap = strokeLineCap;
+ mProperties.strokeLineJoin = strokeLineJoin;
// If any trim property changes, mark trim dirty and update the trim path
setTrimPathStart(trimPathStart);
@@ -195,12 +184,12 @@
// Draw path's fill, if fill color or gradient is valid
bool needsFill = false;
if (mFillGradient != nullptr) {
- mPaint.setColor(applyAlpha(SK_ColorBLACK, mFillAlpha));
+ mPaint.setColor(applyAlpha(SK_ColorBLACK, mProperties.fillAlpha));
SkShader* newShader = mFillGradient->newWithLocalMatrix(matrix);
mPaint.setShader(newShader);
needsFill = true;
- } else if (mFillColor != SK_ColorTRANSPARENT) {
- mPaint.setColor(applyAlpha(mFillColor, mFillAlpha));
+ } else if (mProperties.fillColor != SK_ColorTRANSPARENT) {
+ mPaint.setColor(applyAlpha(mProperties.fillColor, mProperties.fillAlpha));
needsFill = true;
}
@@ -213,21 +202,21 @@
// Draw path's stroke, if stroke color or gradient is valid
bool needsStroke = false;
if (mStrokeGradient != nullptr) {
- mPaint.setColor(applyAlpha(SK_ColorBLACK, mStrokeAlpha));
+ mPaint.setColor(applyAlpha(SK_ColorBLACK, mProperties.strokeAlpha));
SkShader* newShader = mStrokeGradient->newWithLocalMatrix(matrix);
mPaint.setShader(newShader);
needsStroke = true;
- } else if (mStrokeColor != SK_ColorTRANSPARENT) {
- mPaint.setColor(applyAlpha(mStrokeColor, mStrokeAlpha));
+ } else if (mProperties.strokeColor != SK_ColorTRANSPARENT) {
+ mPaint.setColor(applyAlpha(mProperties.strokeColor, mProperties.strokeAlpha));
needsStroke = true;
}
if (needsStroke) {
mPaint.setStyle(SkPaint::Style::kStroke_Style);
mPaint.setAntiAlias(true);
- mPaint.setStrokeJoin(mStrokeLineJoin);
- mPaint.setStrokeCap(mStrokeLineCap);
- mPaint.setStrokeMiter(mStrokeMiterLimit);
- mPaint.setStrokeWidth(mStrokeWidth * strokeScale);
+ mPaint.setStrokeJoin(SkPaint::Join(mProperties.strokeLineJoin));
+ mPaint.setStrokeCap(SkPaint::Cap(mProperties.strokeLineCap));
+ mPaint.setStrokeMiter(mProperties.strokeMiterLimit);
+ mPaint.setStrokeWidth(mProperties.strokeWidth * strokeScale);
outCanvas->drawPath(renderPath, mPaint);
}
}
@@ -236,14 +225,14 @@
* Applies trimming to the specified path.
*/
void FullPath::applyTrim() {
- if (mTrimPathStart == 0.0f && mTrimPathEnd == 1.0f) {
+ if (mProperties.trimPathStart == 0.0f && mProperties.trimPathEnd == 1.0f) {
// No trimming necessary.
return;
}
SkPathMeasure measure(mSkPath, false);
float len = SkScalarToFloat(measure.getLength());
- float start = len * fmod((mTrimPathStart + mTrimPathOffset), 1.0f);
- float end = len * fmod((mTrimPathEnd + mTrimPathOffset), 1.0f);
+ float start = len * fmod((mProperties.trimPathStart + mProperties.trimPathOffset), 1.0f);
+ float end = len * fmod((mProperties.trimPathEnd + mProperties.trimPathOffset), 1.0f);
mTrimmedSkPath.reset();
if (start > end) {
@@ -255,76 +244,69 @@
mTrimDirty = false;
}
-inline int putData(int8_t* outBytes, int startIndex, float value) {
- int size = sizeof(float);
- memcpy(&outBytes[startIndex], &value, size);
- return size;
-}
-
-inline int putData(int8_t* outBytes, int startIndex, int value) {
- int size = sizeof(int);
- memcpy(&outBytes[startIndex], &value, size);
- return size;
-}
-
-struct FullPathProperties {
- // TODO: Consider storing full path properties in this struct instead of the fields.
- float strokeWidth;
- SkColor strokeColor;
- float strokeAlpha;
- SkColor fillColor;
- float fillAlpha;
- float trimPathStart;
- float trimPathEnd;
- float trimPathOffset;
- int32_t strokeLineCap;
- int32_t strokeLineJoin;
- float strokeMiterLimit;
-};
-
-REQUIRE_COMPATIBLE_LAYOUT(FullPathProperties);
+REQUIRE_COMPATIBLE_LAYOUT(FullPath::Properties);
static_assert(sizeof(float) == sizeof(int32_t), "float is not the same size as int32_t");
static_assert(sizeof(SkColor) == sizeof(int32_t), "SkColor is not the same size as int32_t");
bool FullPath::getProperties(int8_t* outProperties, int length) {
- int propertyDataSize = sizeof(FullPathProperties);
+ int propertyDataSize = sizeof(Properties);
if (length != propertyDataSize) {
LOG_ALWAYS_FATAL("Properties needs exactly %d bytes, a byte array of size %d is provided",
propertyDataSize, length);
return false;
}
- // TODO: consider replacing the property fields with a FullPathProperties struct.
- FullPathProperties properties;
- properties.strokeWidth = mStrokeWidth;
- properties.strokeColor = mStrokeColor;
- properties.strokeAlpha = mStrokeAlpha;
- properties.fillColor = mFillColor;
- properties.fillAlpha = mFillAlpha;
- properties.trimPathStart = mTrimPathStart;
- properties.trimPathEnd = mTrimPathEnd;
- properties.trimPathOffset = mTrimPathOffset;
- properties.strokeLineCap = mStrokeLineCap;
- properties.strokeLineJoin = mStrokeLineJoin;
- properties.strokeMiterLimit = mStrokeMiterLimit;
-
- memcpy(outProperties, &properties, length);
+ Properties* out = reinterpret_cast<Properties*>(outProperties);
+ *out = mProperties;
return true;
}
+void FullPath::setColorPropertyValue(int propertyId, int32_t value) {
+ Property currentProperty = static_cast<Property>(propertyId);
+ if (currentProperty == Property::StrokeColor) {
+ mProperties.strokeColor = value;
+ } else if (currentProperty == Property::FillColor) {
+ mProperties.fillColor = value;
+ } else {
+ LOG_ALWAYS_FATAL("Error setting color property on FullPath: No valid property with id: %d",
+ propertyId);
+ }
+}
+
+void FullPath::setPropertyValue(int propertyId, float value) {
+ Property property = static_cast<Property>(propertyId);
+ switch (property) {
+ case Property::StrokeWidth:
+ setStrokeWidth(value);
+ break;
+ case Property::StrokeAlpha:
+ setStrokeAlpha(value);
+ break;
+ case Property::FillAlpha:
+ setFillAlpha(value);
+ break;
+ case Property::TrimPathStart:
+ setTrimPathStart(value);
+ break;
+ case Property::TrimPathEnd:
+ setTrimPathEnd(value);
+ break;
+ case Property::TrimPathOffset:
+ setTrimPathOffset(value);
+ break;
+ default:
+ LOG_ALWAYS_FATAL("Invalid property id: %d for animation", propertyId);
+ break;
+ }
+}
+
void ClipPath::drawPath(SkCanvas* outCanvas, const SkPath& renderPath,
float strokeScale, const SkMatrix& matrix){
outCanvas->clipPath(renderPath, SkRegion::kIntersect_Op);
}
Group::Group(const Group& group) : Node(group) {
- mRotate = group.mRotate;
- mPivotX = group.mPivotX;
- mPivotY = group.mPivotY;
- mScaleX = group.mScaleX;
- mScaleY = group.mScaleY;
- mTranslateX = group.mTranslateX;
- mTranslateY = group.mTranslateY;
+ mProperties = group.mProperties;
}
void Group::draw(SkCanvas* outCanvas, const SkMatrix& currentMatrix, float scaleX,
@@ -342,7 +324,7 @@
// Save the current clip information, which is local to this group.
outCanvas->save();
// Draw the group tree in the same order as the XML file.
- for (Node* child : mChildren) {
+ for (auto& child : mChildren) {
child->draw(outCanvas, stackedMatrix, scaleX, scaleY);
}
// Restore the previous clip information.
@@ -371,14 +353,15 @@
outMatrix->reset();
// TODO: use rotate(mRotate, mPivotX, mPivotY) and scale with pivot point, instead of
// translating to pivot for rotating and scaling, then translating back.
- outMatrix->postTranslate(-mPivotX, -mPivotY);
- outMatrix->postScale(mScaleX, mScaleY);
- outMatrix->postRotate(mRotate, 0, 0);
- outMatrix->postTranslate(mTranslateX + mPivotX, mTranslateY + mPivotY);
+ outMatrix->postTranslate(-mProperties.pivotX, -mProperties.pivotY);
+ outMatrix->postScale(mProperties.scaleX, mProperties.scaleY);
+ outMatrix->postRotate(mProperties.rotate, 0, 0);
+ outMatrix->postTranslate(mProperties.translateX + mProperties.pivotX,
+ mProperties.translateY + mProperties.pivotY);
}
void Group::addChild(Node* child) {
- mChildren.push_back(child);
+ mChildren.emplace_back(child);
}
bool Group::getProperties(float* outProperties, int length) {
@@ -388,38 +371,68 @@
propertyCount, length);
return false;
}
- for (int i = 0; i < propertyCount; i++) {
- Property currentProperty = static_cast<Property>(i);
- switch (currentProperty) {
- case Property::Rotate_Property:
- outProperties[i] = mRotate;
- break;
- case Property::PivotX_Property:
- outProperties[i] = mPivotX;
- break;
- case Property::PivotY_Property:
- outProperties[i] = mPivotY;
- break;
- case Property::ScaleX_Property:
- outProperties[i] = mScaleX;
- break;
- case Property::ScaleY_Property:
- outProperties[i] = mScaleY;
- break;
- case Property::TranslateX_Property:
- outProperties[i] = mTranslateX;
- break;
- case Property::TranslateY_Property:
- outProperties[i] = mTranslateY;
- break;
- default:
- LOG_ALWAYS_FATAL("Invalid input index: %d", i);
- return false;
- }
- }
+ Properties* out = reinterpret_cast<Properties*>(outProperties);
+ *out = mProperties;
return true;
}
+// TODO: Consider animating the properties as float pointers
+float Group::getPropertyValue(int propertyId) const {
+ Property currentProperty = static_cast<Property>(propertyId);
+ switch (currentProperty) {
+ case Property::Rotate:
+ return mProperties.rotate;
+ case Property::PivotX:
+ return mProperties.pivotX;
+ case Property::PivotY:
+ return mProperties.pivotY;
+ case Property::ScaleX:
+ return mProperties.scaleX;
+ case Property::ScaleY:
+ return mProperties.scaleY;
+ case Property::TranslateX:
+ return mProperties.translateX;
+ case Property::TranslateY:
+ return mProperties.translateY;
+ default:
+ LOG_ALWAYS_FATAL("Invalid property index: %d", propertyId);
+ return 0;
+ }
+}
+
+void Group::setPropertyValue(int propertyId, float value) {
+ Property currentProperty = static_cast<Property>(propertyId);
+ switch (currentProperty) {
+ case Property::Rotate:
+ mProperties.rotate = value;
+ break;
+ case Property::PivotX:
+ mProperties.pivotX = value;
+ break;
+ case Property::PivotY:
+ mProperties.pivotY = value;
+ break;
+ case Property::ScaleX:
+ mProperties.scaleX = value;
+ break;
+ case Property::ScaleY:
+ mProperties.scaleY = value;
+ break;
+ case Property::TranslateX:
+ mProperties.translateX = value;
+ break;
+ case Property::TranslateY:
+ mProperties.translateY = value;
+ break;
+ default:
+ LOG_ALWAYS_FATAL("Invalid property index: %d", propertyId);
+ }
+}
+
+bool Group::isValidProperty(int propertyId) {
+ return propertyId >= 0 && propertyId < static_cast<int>(Property::Count);
+}
+
void Tree::draw(Canvas* outCanvas, SkColorFilter* colorFilter,
const SkRect& bounds, bool needsMirroring, bool canReuseCache) {
// The imageView can scale the canvas in different ways, in order to
@@ -445,6 +458,8 @@
return;
}
+ mPaint.setColorFilter(colorFilter);
+
int saveCount = outCanvas->save(SaveFlags::MatrixClip);
outCanvas->translate(mBounds.fLeft, mBounds.fTop);
@@ -458,43 +473,33 @@
// And we use this bound for the destination rect for the drawBitmap, so
// we offset to (0, 0);
mBounds.offsetTo(0, 0);
-
createCachedBitmapIfNeeded(scaledWidth, scaledHeight);
- if (!mAllowCaching) {
- updateCachedBitmap(scaledWidth, scaledHeight);
- } else {
- if (!canReuseCache || mCacheDirty) {
- updateCachedBitmap(scaledWidth, scaledHeight);
- }
- }
- drawCachedBitmapWithRootAlpha(outCanvas, colorFilter, mBounds);
+
+ outCanvas->drawVectorDrawable(this);
outCanvas->restoreToCount(saveCount);
}
-void Tree::drawCachedBitmapWithRootAlpha(Canvas* outCanvas, SkColorFilter* filter,
- const SkRect& originalBounds) {
+SkPaint* Tree::getPaint() {
SkPaint* paint;
- if (mRootAlpha == 1.0f && filter == NULL) {
+ if (mRootAlpha == 1.0f && mPaint.getColorFilter() == NULL) {
paint = NULL;
} else {
mPaint.setFilterQuality(kLow_SkFilterQuality);
mPaint.setAlpha(mRootAlpha * 255);
- mPaint.setColorFilter(filter);
paint = &mPaint;
}
- outCanvas->drawBitmap(mCachedBitmap, 0, 0, mCachedBitmap.width(), mCachedBitmap.height(),
- originalBounds.fLeft, originalBounds.fTop, originalBounds.fRight,
- originalBounds.fBottom, paint);
+ return paint;
}
-void Tree::updateCachedBitmap(int width, int height) {
+const SkBitmap& Tree::getBitmapUpdateIfDirty() {
mCachedBitmap.eraseColor(SK_ColorTRANSPARENT);
SkCanvas outCanvas(mCachedBitmap);
- float scaleX = width / mViewportWidth;
- float scaleY = height / mViewportHeight;
+ float scaleX = (float) mCachedBitmap.width() / mViewportWidth;
+ float scaleY = (float) mCachedBitmap.height() / mViewportHeight;
mRootNode->draw(&outCanvas, SkMatrix::I(), scaleX, scaleY);
mCacheDirty = false;
+ return mCachedBitmap;
}
void Tree::createCachedBitmapIfNeeded(int width, int height) {
diff --git a/libs/hwui/VectorDrawable.h b/libs/hwui/VectorDrawable.h
index 09bdce5..36a8aeb 100644
--- a/libs/hwui/VectorDrawable.h
+++ b/libs/hwui/VectorDrawable.h
@@ -18,6 +18,7 @@
#define ANDROID_HWUI_VPATH_H
#include "Canvas.h"
+
#include <SkBitmap.h>
#include <SkColor.h>
#include <SkCanvas.h>
@@ -104,6 +105,21 @@
class ANDROID_API FullPath: public Path {
public:
+
+struct Properties {
+ float strokeWidth = 0;
+ SkColor strokeColor = SK_ColorTRANSPARENT;
+ float strokeAlpha = 1;
+ SkColor fillColor = SK_ColorTRANSPARENT;
+ float fillAlpha = 1;
+ float trimPathStart = 0;
+ float trimPathEnd = 1;
+ float trimPathOffset = 0;
+ int32_t strokeLineCap = SkPaint::Cap::kButt_Cap;
+ int32_t strokeLineJoin = SkPaint::Join::kMiter_Join;
+ float strokeMiterLimit = 4;
+};
+
FullPath(const FullPath& path); // for cloning
FullPath(const char* path, size_t strLength) : Path(path, strLength) {}
FullPath() : Path() {}
@@ -118,55 +134,58 @@
float strokeAlpha, SkColor fillColor, float fillAlpha,
float trimPathStart, float trimPathEnd, float trimPathOffset,
float strokeMiterLimit, int strokeLineCap, int strokeLineJoin);
+ // TODO: Cleanup: Remove the setter and getters below, and their counterparts in java and JNI
float getStrokeWidth() {
- return mStrokeWidth;
+ return mProperties.strokeWidth;
}
void setStrokeWidth(float strokeWidth) {
- mStrokeWidth = strokeWidth;
+ mProperties.strokeWidth = strokeWidth;
}
SkColor getStrokeColor() {
- return mStrokeColor;
+ return mProperties.strokeColor;
}
void setStrokeColor(SkColor strokeColor) {
- mStrokeColor = strokeColor;
+ mProperties.strokeColor = strokeColor;
}
float getStrokeAlpha() {
- return mStrokeAlpha;
+ return mProperties.strokeAlpha;
}
void setStrokeAlpha(float strokeAlpha) {
- mStrokeAlpha = strokeAlpha;
+ mProperties.strokeAlpha = strokeAlpha;
}
SkColor getFillColor() {
- return mFillColor;
+ return mProperties.fillColor;
}
void setFillColor(SkColor fillColor) {
- mFillColor = fillColor;
+ mProperties.fillColor = fillColor;
}
float getFillAlpha() {
- return mFillAlpha;
+ return mProperties.fillAlpha;
}
void setFillAlpha(float fillAlpha) {
- mFillAlpha = fillAlpha;
+ mProperties.fillAlpha = fillAlpha;
}
float getTrimPathStart() {
- return mTrimPathStart;
+ return mProperties.trimPathStart;
}
void setTrimPathStart(float trimPathStart) {
- VD_SET_PROP_WITH_FLAG(mTrimPathStart, trimPathStart, mTrimDirty);
+ VD_SET_PROP_WITH_FLAG(mProperties.trimPathStart, trimPathStart, mTrimDirty);
}
float getTrimPathEnd() {
- return mTrimPathEnd;
+ return mProperties.trimPathEnd;
}
void setTrimPathEnd(float trimPathEnd) {
- VD_SET_PROP_WITH_FLAG(mTrimPathEnd, trimPathEnd, mTrimDirty);
+ VD_SET_PROP_WITH_FLAG(mProperties.trimPathEnd, trimPathEnd, mTrimDirty);
}
float getTrimPathOffset() {
- return mTrimPathOffset;
+ return mProperties.trimPathOffset;
}
void setTrimPathOffset(float trimPathOffset) {
- VD_SET_PROP_WITH_FLAG(mTrimPathOffset, trimPathOffset, mTrimDirty);
+ VD_SET_PROP_WITH_FLAG(mProperties.trimPathOffset, trimPathOffset, mTrimDirty);
}
bool getProperties(int8_t* outProperties, int length);
+ void setColorPropertyValue(int propertyId, int32_t value);
+ void setPropertyValue(int propertyId, float value);
void setFillGradient(SkShader* fillGradient) {
SkRefCnt_SafeAssign(mFillGradient, fillGradient);
@@ -182,24 +201,28 @@
float strokeScale, const SkMatrix& matrix) override;
private:
+ enum class Property {
+ StrokeWidth = 0,
+ StrokeColor,
+ StrokeAlpha,
+ FillColor,
+ FillAlpha,
+ TrimPathStart,
+ TrimPathEnd,
+ TrimPathOffset,
+ StrokeLineCap,
+ StrokeLineJoin,
+ StrokeMiterLimit,
+ Count,
+ };
// Applies trimming to the specified path.
void applyTrim();
- float mStrokeWidth = 0;
- SkColor mStrokeColor = SK_ColorTRANSPARENT;
- float mStrokeAlpha = 1;
- SkColor mFillColor = SK_ColorTRANSPARENT;
- SkShader* mStrokeGradient = nullptr;
- SkShader* mFillGradient = nullptr;
- float mFillAlpha = 1;
- float mTrimPathStart = 0;
- float mTrimPathEnd = 1;
- float mTrimPathOffset = 0;
+ Properties mProperties;
bool mTrimDirty = true;
- SkPaint::Cap mStrokeLineCap = SkPaint::Cap::kButt_Cap;
- SkPaint::Join mStrokeLineJoin = SkPaint::Join::kMiter_Join;
- float mStrokeMiterLimit = 4;
SkPath mTrimmedSkPath;
SkPaint mPaint;
+ SkShader* mStrokeGradient = nullptr;
+ SkShader* mFillGradient = nullptr;
};
class ANDROID_API ClipPath: public Path {
@@ -216,49 +239,58 @@
class ANDROID_API Group: public Node {
public:
+ struct Properties {
+ float rotate = 0;
+ float pivotX = 0;
+ float pivotY = 0;
+ float scaleX = 1;
+ float scaleY = 1;
+ float translateX = 0;
+ float translateY = 0;
+ };
Group(const Group& group);
Group() {}
float getRotation() {
- return mRotate;
+ return mProperties.rotate;
}
void setRotation(float rotation) {
- mRotate = rotation;
+ mProperties.rotate = rotation;
}
float getPivotX() {
- return mPivotX;
+ return mProperties.pivotX;
}
void setPivotX(float pivotX) {
- mPivotX = pivotX;
+ mProperties.pivotX = pivotX;
}
float getPivotY() {
- return mPivotY;
+ return mProperties.pivotY;
}
void setPivotY(float pivotY) {
- mPivotY = pivotY;
+ mProperties.pivotY = pivotY;
}
float getScaleX() {
- return mScaleX;
+ return mProperties.scaleX;
}
void setScaleX(float scaleX) {
- mScaleX = scaleX;
+ mProperties.scaleX = scaleX;
}
float getScaleY() {
- return mScaleY;
+ return mProperties.scaleY;
}
void setScaleY(float scaleY) {
- mScaleY = scaleY;
+ mProperties.scaleY = scaleY;
}
float getTranslateX() {
- return mTranslateX;
+ return mProperties.translateX;
}
void setTranslateX(float translateX) {
- mTranslateX = translateX;
+ mProperties.translateX = translateX;
}
float getTranslateY() {
- return mTranslateY;
+ return mProperties.translateY;
}
void setTranslateY(float translateY) {
- mTranslateY = translateY;
+ mProperties.translateY = translateY;
}
virtual void draw(SkCanvas* outCanvas, const SkMatrix& currentMatrix,
float scaleX, float scaleY) override;
@@ -268,38 +300,33 @@
void addChild(Node* child);
void dump() override;
bool getProperties(float* outProperties, int length);
+ float getPropertyValue(int propertyId) const;
+ void setPropertyValue(int propertyId, float value);
+ static bool isValidProperty(int propertyId);
private:
enum class Property {
- Rotate_Property = 0,
- PivotX_Property,
- PivotY_Property,
- ScaleX_Property,
- ScaleY_Property,
- TranslateX_Property,
- TranslateY_Property,
+ Rotate = 0,
+ PivotX,
+ PivotY,
+ ScaleX,
+ ScaleY,
+ TranslateX,
+ TranslateY,
// Count of the properties, must be at the end.
Count,
};
- float mRotate = 0;
- float mPivotX = 0;
- float mPivotY = 0;
- float mScaleX = 1;
- float mScaleY = 1;
- float mTranslateX = 0;
- float mTranslateY = 0;
- std::vector<Node*> mChildren;
+ std::vector< std::unique_ptr<Node> > mChildren;
+ Properties mProperties;
};
-class ANDROID_API Tree {
+class ANDROID_API Tree : public VirtualLightRefBase {
public:
Tree(Group* rootNode) : mRootNode(rootNode) {}
void draw(Canvas* outCanvas, SkColorFilter* colorFilter,
const SkRect& bounds, bool needsMirroring, bool canReuseCache);
- void drawCachedBitmapWithRootAlpha(Canvas* outCanvas, SkColorFilter* filter,
- const SkRect& originalBounds);
- void updateCachedBitmap(int width, int height);
+ const SkBitmap& getBitmapUpdateIfDirty();
void createCachedBitmapIfNeeded(int width, int height);
bool canReuseBitmap(int width, int height);
void setAllowCaching(bool allowCaching) {
@@ -316,6 +343,10 @@
mViewportWidth = viewportWidth;
mViewportHeight = viewportHeight;
}
+ SkPaint* getPaint();
+ const SkRect& getBounds() const {
+ return mBounds;
+ }
private:
// Cap the bitmap size, such that it won't hurt the performance too much
@@ -329,7 +360,7 @@
float mViewportHeight = 0;
float mRootAlpha = 1.0f;
- Group* mRootNode;
+ std::unique_ptr<Group> mRootNode;
SkRect mBounds;
SkMatrix mCanvasMatrix;
SkPaint mPaint;
diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp
index 466fef9..364d4dd 100644
--- a/libs/hwui/renderthread/EglManager.cpp
+++ b/libs/hwui/renderthread/EglManager.cpp
@@ -228,6 +228,13 @@
LOG_ALWAYS_FATAL_IF(surface == EGL_NO_SURFACE,
"Failed to create EGLSurface for window %p, eglErr = %s",
(void*) window, egl_error_str());
+
+ if (mSwapBehavior != SwapBehavior::Preserved) {
+ LOG_ALWAYS_FATAL_IF(eglSurfaceAttrib(mEglDisplay, surface, EGL_SWAP_BEHAVIOR, EGL_BUFFER_DESTROYED) == EGL_FALSE,
+ "Failed to set swap behavior to destroyed for window %p, eglErr = %s",
+ (void*) window, egl_error_str());
+ }
+
return surface;
}
@@ -337,8 +344,8 @@
// For some reason our surface was destroyed out from under us
// This really shouldn't happen, but if it does we can recover easily
// by just not trying to use the surface anymore
- ALOGW("swapBuffers encountered EGL_BAD_SURFACE on %p, halting rendering...",
- frame.mSurface);
+ ALOGW("swapBuffers encountered EGL error %d on %p, halting rendering...",
+ err, frame.mSurface);
return false;
}
LOG_ALWAYS_FATAL("Encountered EGL error %d %s during rendering",
diff --git a/libs/hwui/tests/unit/GradientCacheTests.cpp b/libs/hwui/tests/unit/GradientCacheTests.cpp
new file mode 100644
index 0000000..0ee9647
--- /dev/null
+++ b/libs/hwui/tests/unit/GradientCacheTests.cpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gtest/gtest.h>
+
+#include "Extensions.h"
+#include "GradientCache.h"
+#include "tests/common/TestUtils.h"
+
+using namespace android;
+using namespace android::uirenderer;
+
+RENDERTHREAD_TEST(GradientCache, addRemove) {
+ Extensions extensions;
+ GradientCache cache(extensions);
+ ASSERT_LT(1000u, cache.getMaxSize()) << "Expect non-trivial size";
+
+ SkColor colors[] = { 0xFF00FF00, 0xFFFF0000, 0xFF0000FF };
+ float positions[] = { 1, 2, 3 };
+ Texture* texture = cache.get(colors, positions, 3);
+ ASSERT_TRUE(texture);
+ ASSERT_FALSE(texture->cleanup);
+ ASSERT_EQ((uint32_t) texture->objectSize(), cache.getSize());
+ ASSERT_TRUE(cache.getSize());
+ cache.clear();
+ ASSERT_EQ(cache.getSize(), 0u);
+}
diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.java b/media/java/android/media/audiopolicy/AudioMixingRule.java
index f9fdd8d..54543ec 100644
--- a/media/java/android/media/audiopolicy/AudioMixingRule.java
+++ b/media/java/android/media/audiopolicy/AudioMixingRule.java
@@ -428,8 +428,17 @@
}
}
// rule didn't exist, add it
- // FIXME doesn't work with RULE_MATCH_UID yet
- mCriteria.add(new AttributeMatchCriterion(attrToMatch, rule));
+ switch (match_rule) {
+ case RULE_MATCH_ATTRIBUTE_USAGE:
+ case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
+ mCriteria.add(new AttributeMatchCriterion(attrToMatch, rule));
+ break;
+ case RULE_MATCH_UID:
+ mCriteria.add(new AttributeMatchCriterion(intProp, rule));
+ break;
+ default:
+ throw new IllegalStateException("Unreachable code in addRuleInternal()");
+ }
}
return this;
}
diff --git a/packages/Osu/Android.mk b/packages/Osu/Android.mk
new file mode 100644
index 0000000..15744c5
--- /dev/null
+++ b/packages/Osu/Android.mk
@@ -0,0 +1,19 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_JAVA_LIBRARIES := telephony-common ims-common bouncycastle conscrypt
+
+LOCAL_PACKAGE_NAME := Osu
+LOCAL_CERTIFICATE := platform
+LOCAL_PRIVILEGED_MODULE := true
+
+include $(BUILD_PACKAGE)
+
+########################
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/packages/Osu/AndroidManifest.xml b/packages/Osu/AndroidManifest.xml
new file mode 100644
index 0000000..288f1a4
--- /dev/null
+++ b/packages/Osu/AndroidManifest.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android">
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+ <uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL" />
+ <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+ <uses-permission android:name="android.permission.CONNECTIVITY_INTERNAL" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application
+ android:enabled="false"
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:persistent="true"
+ android:supportsRtl="true">
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="com.android.hotspot2.OSU_NOTIFICATION" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <receiver android:name="com.android.MainActivity$WifiReceiver" >
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" android:enabled="true"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.net.wifi.SCAN_RESULTS" android:enabled="true"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.net.wifi.PASSPOINT_WNM_FRAME_RECEIVED" android:enabled="true"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.net.wifi.PASSPOINT_ICON_RECEIVED" android:enabled="true"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.net.wifi.CONFIGURED_NETWORKS_CHANGE" android:enabled="true"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.net.wifi.WIFI_STATE_CHANGED" android:enabled="true"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.net.wifi.STATE_CHANGE" android:enabled="true"/>
+ </intent-filter>
+ </receiver>
+ <service android:name="com.android.MainActivity$OSUService" />
+ </application>
+
+</manifest>
diff --git a/packages/Osu/res/layout/activity_main.xml b/packages/Osu/res/layout/activity_main.xml
new file mode 100644
index 0000000..7e33537
--- /dev/null
+++ b/packages/Osu/res/layout/activity_main.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/no_osu"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/no_osu"/>
+ <ListView
+ android:id="@+id/profile_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:divider="#1F000000"
+ android:dividerHeight="1dp"
+ android:padding="16dp" />
+
+</LinearLayout>
diff --git a/packages/Osu/res/layout/list_item.xml b/packages/Osu/res/layout/list_item.xml
new file mode 100644
index 0000000..eb431d3
--- /dev/null
+++ b/packages/Osu/res/layout/list_item.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:gravity="top"
+ android:layout_marginTop="10sp"
+ android:layout_marginBottom="10sp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <ImageView
+ android:id="@+id/profile_logo"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_weight="1"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/profile_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"/>
+
+ <TextView
+ android:id="@+id/profile_detail"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="10sp"
+ android:textSize="12sp"/>
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/packages/Osu/res/mipmap-hdpi/ic_launcher.png b/packages/Osu/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/packages/Osu/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/Osu/res/mipmap-mdpi/ic_launcher.png b/packages/Osu/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/packages/Osu/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/Osu/res/mipmap-xhdpi/ic_launcher.png b/packages/Osu/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/packages/Osu/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/Osu/res/mipmap-xxhdpi/ic_launcher.png b/packages/Osu/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/packages/Osu/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/Osu/res/mipmap-xxxhdpi/ic_launcher.png b/packages/Osu/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..aee44e1
--- /dev/null
+++ b/packages/Osu/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/Osu/res/values-w820dp/dimens.xml b/packages/Osu/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/packages/Osu/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+ <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+ (such as screen margins) for screens with more than 820dp of available width. This
+ would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+ <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/packages/Osu/res/values/colors.xml b/packages/Osu/res/values/colors.xml
new file mode 100644
index 0000000..3ab3e9c
--- /dev/null
+++ b/packages/Osu/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#3F51B5</color>
+ <color name="colorPrimaryDark">#303F9F</color>
+ <color name="colorAccent">#FF4081</color>
+</resources>
diff --git a/packages/Osu/res/values/dimens.xml b/packages/Osu/res/values/dimens.xml
new file mode 100644
index 0000000..47c8224
--- /dev/null
+++ b/packages/Osu/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<resources>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/packages/Osu/res/values/strings.xml b/packages/Osu/res/values/strings.xml
new file mode 100644
index 0000000..93593dc
--- /dev/null
+++ b/packages/Osu/res/values/strings.xml
@@ -0,0 +1,4 @@
+<resources>
+ <string name="app_name">OSU</string>
+ <string name="no_osu">No OSU available</string>
+</resources>
diff --git a/packages/Osu/src/com/android/MainActivity.java b/packages/Osu/src/com/android/MainActivity.java
new file mode 100644
index 0000000..7e7d49a
--- /dev/null
+++ b/packages/Osu/src/com/android/MainActivity.java
@@ -0,0 +1,419 @@
+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<OSUInfo> 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.<OSUInfo>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<OsuData> osus = printOsuDataList(bundle.getParcelableArray(AppBridge.OSU_INFO));
+ showOsuList(osus);
+ }
+ */
+ }
+ }
+
+ private void showOsuCount(int osuCount, List<OSUInfo> 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) {
+ 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:
+ mOsuManager.networkConfigChange((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<OSUInfo> 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<OSUInfo> {
+ private Activity activity;
+
+ public OsuListAdapter2(Activity activity, List<OSUInfo> 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<OSUInfo> 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/ANQPElement.java b/packages/Osu/src/com/android/anqp/ANQPElement.java
new file mode 100644
index 0000000..58aee29
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/ANQPElement.java
@@ -0,0 +1,16 @@
+package com.android.anqp;
+
+/**
+ * Base class for an IEEE802.11u ANQP element.
+ */
+public abstract class ANQPElement {
+ private final Constants.ANQPElementType mID;
+
+ protected ANQPElement(Constants.ANQPElementType id) {
+ mID = id;
+ }
+
+ public Constants.ANQPElementType getID() {
+ return mID;
+ }
+}
diff --git a/packages/Osu/src/com/android/anqp/Constants.java b/packages/Osu/src/com/android/anqp/Constants.java
new file mode 100644
index 0000000..214bb93
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/Constants.java
@@ -0,0 +1,233 @@
+package com.android.anqp;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ANQP related constants (802.11-2012)
+ */
+public class Constants {
+
+ public static final int NIBBLE_MASK = 0x0f;
+ public static final int BYTE_MASK = 0xff;
+ public static final int SHORT_MASK = 0xffff;
+ public static final long INT_MASK = 0xffffffffL;
+ public static final int BYTES_IN_SHORT = 2;
+ public static final int BYTES_IN_INT = 4;
+ public static final int BYTES_IN_EUI48 = 6;
+ public static final long MILLIS_IN_A_SEC = 1000L;
+
+ public static final int HS20_PREFIX = 0x119a6f50; // Note that this is represented as a LE int
+ public static final int HS20_FRAME_PREFIX = 0x109a6f50;
+ public static final int UTF8_INDICATOR = 1;
+
+ public static final int LANG_CODE_LENGTH = 3;
+
+ public static final int ANQP_QUERY_LIST = 256;
+ public static final int ANQP_CAPABILITY_LIST = 257;
+ public static final int ANQP_VENUE_NAME = 258;
+ public static final int ANQP_EMERGENCY_NUMBER = 259;
+ public static final int ANQP_NWK_AUTH_TYPE = 260;
+ public static final int ANQP_ROAMING_CONSORTIUM = 261;
+ public static final int ANQP_IP_ADDR_AVAILABILITY = 262;
+ public static final int ANQP_NAI_REALM = 263;
+ public static final int ANQP_3GPP_NETWORK = 264;
+ public static final int ANQP_GEO_LOC = 265;
+ public static final int ANQP_CIVIC_LOC = 266;
+ public static final int ANQP_LOC_URI = 267;
+ public static final int ANQP_DOM_NAME = 268;
+ public static final int ANQP_EMERGENCY_ALERT = 269;
+ public static final int ANQP_TDLS_CAP = 270;
+ public static final int ANQP_EMERGENCY_NAI = 271;
+ public static final int ANQP_NEIGHBOR_REPORT = 272;
+ public static final int ANQP_VENDOR_SPEC = 56797;
+
+ public static final int HS_QUERY_LIST = 1;
+ public static final int HS_CAPABILITY_LIST = 2;
+ public static final int HS_FRIENDLY_NAME = 3;
+ public static final int HS_WAN_METRICS = 4;
+ public static final int HS_CONN_CAPABILITY = 5;
+ public static final int HS_NAI_HOME_REALM_QUERY = 6;
+ public static final int HS_OPERATING_CLASS = 7;
+ public static final int HS_OSU_PROVIDERS = 8;
+ public static final int HS_ICON_REQUEST = 10;
+ public static final int HS_ICON_FILE = 11;
+
+ public enum ANQPElementType {
+ ANQPQueryList,
+ ANQPCapabilityList,
+ ANQPVenueName,
+ ANQPEmergencyNumber,
+ ANQPNwkAuthType,
+ ANQPRoamingConsortium,
+ ANQPIPAddrAvailability,
+ ANQPNAIRealm,
+ ANQP3GPPNetwork,
+ ANQPGeoLoc,
+ ANQPCivicLoc,
+ ANQPLocURI,
+ ANQPDomName,
+ ANQPEmergencyAlert,
+ ANQPTDLSCap,
+ ANQPEmergencyNAI,
+ ANQPNeighborReport,
+ ANQPVendorSpec,
+ HSQueryList,
+ HSCapabilityList,
+ HSFriendlyName,
+ HSWANMetrics,
+ HSConnCapability,
+ HSNAIHomeRealmQuery,
+ HSOperatingclass,
+ HSOSUProviders,
+ HSIconRequest,
+ HSIconFile
+ }
+
+ private static final Map<Integer, ANQPElementType> sAnqpMap = new HashMap<>();
+ private static final Map<Integer, ANQPElementType> sHs20Map = new HashMap<>();
+ private static final Map<ANQPElementType, Integer> sRevAnqpmap =
+ new EnumMap<>(ANQPElementType.class);
+ private static final Map<ANQPElementType, Integer> sRevHs20map =
+ new EnumMap<>(ANQPElementType.class);
+
+ static {
+ sAnqpMap.put(ANQP_QUERY_LIST, ANQPElementType.ANQPQueryList);
+ sAnqpMap.put(ANQP_CAPABILITY_LIST, ANQPElementType.ANQPCapabilityList);
+ sAnqpMap.put(ANQP_VENUE_NAME, ANQPElementType.ANQPVenueName);
+ sAnqpMap.put(ANQP_EMERGENCY_NUMBER, ANQPElementType.ANQPEmergencyNumber);
+ sAnqpMap.put(ANQP_NWK_AUTH_TYPE, ANQPElementType.ANQPNwkAuthType);
+ sAnqpMap.put(ANQP_ROAMING_CONSORTIUM, ANQPElementType.ANQPRoamingConsortium);
+ sAnqpMap.put(ANQP_IP_ADDR_AVAILABILITY, ANQPElementType.ANQPIPAddrAvailability);
+ sAnqpMap.put(ANQP_NAI_REALM, ANQPElementType.ANQPNAIRealm);
+ sAnqpMap.put(ANQP_3GPP_NETWORK, ANQPElementType.ANQP3GPPNetwork);
+ sAnqpMap.put(ANQP_GEO_LOC, ANQPElementType.ANQPGeoLoc);
+ sAnqpMap.put(ANQP_CIVIC_LOC, ANQPElementType.ANQPCivicLoc);
+ sAnqpMap.put(ANQP_LOC_URI, ANQPElementType.ANQPLocURI);
+ sAnqpMap.put(ANQP_DOM_NAME, ANQPElementType.ANQPDomName);
+ sAnqpMap.put(ANQP_EMERGENCY_ALERT, ANQPElementType.ANQPEmergencyAlert);
+ sAnqpMap.put(ANQP_TDLS_CAP, ANQPElementType.ANQPTDLSCap);
+ sAnqpMap.put(ANQP_EMERGENCY_NAI, ANQPElementType.ANQPEmergencyNAI);
+ sAnqpMap.put(ANQP_NEIGHBOR_REPORT, ANQPElementType.ANQPNeighborReport);
+ sAnqpMap.put(ANQP_VENDOR_SPEC, ANQPElementType.ANQPVendorSpec);
+
+ sHs20Map.put(HS_QUERY_LIST, ANQPElementType.HSQueryList);
+ sHs20Map.put(HS_CAPABILITY_LIST, ANQPElementType.HSCapabilityList);
+ sHs20Map.put(HS_FRIENDLY_NAME, ANQPElementType.HSFriendlyName);
+ sHs20Map.put(HS_WAN_METRICS, ANQPElementType.HSWANMetrics);
+ sHs20Map.put(HS_CONN_CAPABILITY, ANQPElementType.HSConnCapability);
+ sHs20Map.put(HS_NAI_HOME_REALM_QUERY, ANQPElementType.HSNAIHomeRealmQuery);
+ sHs20Map.put(HS_OPERATING_CLASS, ANQPElementType.HSOperatingclass);
+ sHs20Map.put(HS_OSU_PROVIDERS, ANQPElementType.HSOSUProviders);
+ sHs20Map.put(HS_ICON_REQUEST, ANQPElementType.HSIconRequest);
+ sHs20Map.put(HS_ICON_FILE, ANQPElementType.HSIconFile);
+
+ for (Map.Entry<Integer, ANQPElementType> entry : sAnqpMap.entrySet()) {
+ sRevAnqpmap.put(entry.getValue(), entry.getKey());
+ }
+ for (Map.Entry<Integer, ANQPElementType> entry : sHs20Map.entrySet()) {
+ sRevHs20map.put(entry.getValue(), entry.getKey());
+ }
+ }
+
+ public static ANQPElementType mapANQPElement(int id) {
+ return sAnqpMap.get(id);
+ }
+
+ public static ANQPElementType mapHS20Element(int id) {
+ return sHs20Map.get(id);
+ }
+
+ public static Integer getANQPElementID(ANQPElementType elementType) {
+ return sRevAnqpmap.get(elementType);
+ }
+
+ public static Integer getHS20ElementID(ANQPElementType elementType) {
+ return sRevHs20map.get(elementType);
+ }
+
+ public static boolean hasBaseANQPElements(Collection<ANQPElementType> elements) {
+ if (elements == null) {
+ return false;
+ }
+ for (ANQPElementType element : elements) {
+ if (sRevAnqpmap.containsKey(element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean hasR2Elements(List<ANQPElementType> elements) {
+ return elements.contains(ANQPElementType.HSOSUProviders);
+ }
+
+ public static long getInteger(ByteBuffer payload, ByteOrder bo, int size) {
+ byte[] octets = new byte[size];
+ payload.get(octets);
+ long value = 0;
+ if (bo == ByteOrder.LITTLE_ENDIAN) {
+ for (int n = octets.length - 1; n >= 0; n--) {
+ value = (value << Byte.SIZE) | (octets[n] & BYTE_MASK);
+ }
+ }
+ else {
+ for (byte octet : octets) {
+ value = (value << Byte.SIZE) | (octet & BYTE_MASK);
+ }
+ }
+ return value;
+ }
+
+ public static String getPrefixedString(ByteBuffer payload, int lengthLength, Charset charset)
+ throws ProtocolException {
+ return getPrefixedString(payload, lengthLength, charset, false);
+ }
+
+ public static String getPrefixedString(ByteBuffer payload, int lengthLength, Charset charset,
+ boolean useNull) throws ProtocolException {
+ if (payload.remaining() < lengthLength) {
+ throw new ProtocolException("Runt string: " + payload.remaining());
+ }
+ return getString(payload, (int) getInteger(payload, ByteOrder.LITTLE_ENDIAN,
+ lengthLength), charset, useNull);
+ }
+
+ public static String getTrimmedString(ByteBuffer payload, int length, Charset charset)
+ throws ProtocolException {
+ String s = getString(payload, length, charset, false);
+ int zero = length - 1;
+ while (zero >= 0) {
+ if (s.charAt(zero) != 0) {
+ break;
+ }
+ zero--;
+ }
+ return zero < length - 1 ? s.substring(0, zero + 1) : s;
+ }
+
+ public static String getString(ByteBuffer payload, int length, Charset charset)
+ throws ProtocolException {
+ return getString(payload, length, charset, false);
+ }
+
+ public static String getString(ByteBuffer payload, int length, Charset charset, boolean useNull)
+ throws ProtocolException {
+ if (length > payload.remaining()) {
+ throw new ProtocolException("Bad string length: " + length);
+ }
+ if (useNull && length == 0) {
+ return null;
+ }
+ byte[] octets = new byte[length];
+ payload.get(octets);
+ return new String(octets, charset);
+ }
+}
diff --git a/packages/Osu/src/com/android/anqp/HSIconFileElement.java b/packages/Osu/src/com/android/anqp/HSIconFileElement.java
new file mode 100644
index 0000000..bd14e3f
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/HSIconFileElement.java
@@ -0,0 +1,59 @@
+package com.android.anqp;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+import static com.android.anqp.Constants.SHORT_MASK;
+
+/**
+ * The Icon Binary File vendor specific ANQP Element,
+ * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00,
+ * section 4.11
+ */
+public class HSIconFileElement extends ANQPElement {
+
+ public enum StatusCode {Success, FileNotFound, Unspecified}
+
+ private final StatusCode mStatusCode;
+ private final String mType;
+ private final byte[] mIconData;
+
+ public HSIconFileElement(Constants.ANQPElementType infoID, ByteBuffer payload)
+ throws ProtocolException {
+ super(infoID);
+
+ if (payload.remaining() < 4) {
+ throw new ProtocolException("Truncated icon file: " + payload.remaining());
+ }
+
+ int statusID = payload.get() & BYTE_MASK;
+ mStatusCode = statusID < StatusCode.values().length ? StatusCode.values()[statusID] : null;
+ mType = Constants.getPrefixedString(payload, 1, StandardCharsets.US_ASCII);
+
+ int dataLength = payload.getShort() & SHORT_MASK;
+ mIconData = new byte[dataLength];
+ payload.get(mIconData);
+ }
+
+ public StatusCode getStatusCode() {
+ return mStatusCode;
+ }
+
+ public String getType() {
+ return mType;
+ }
+
+ public byte[] getIconData() {
+ return mIconData;
+ }
+
+ @Override
+ public String toString() {
+ return "HSIconFile{" +
+ "statusCode=" + mStatusCode +
+ ", type='" + mType + '\'' +
+ ", iconData=" + mIconData.length + " bytes }";
+ }
+}
diff --git a/packages/Osu/src/com/android/anqp/HSOsuProvidersElement.java b/packages/Osu/src/com/android/anqp/HSOsuProvidersElement.java
new file mode 100644
index 0000000..646e003
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/HSOsuProvidersElement.java
@@ -0,0 +1,49 @@
+package com.android.anqp;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * The OSU Providers List vendor specific ANQP Element,
+ * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00,
+ * section 4.8
+ */
+public class HSOsuProvidersElement extends ANQPElement {
+ private final String mSSID;
+ private final List<OSUProvider> mProviders;
+
+ public HSOsuProvidersElement(Constants.ANQPElementType infoID, ByteBuffer payload)
+ throws ProtocolException {
+ super(infoID);
+
+ mSSID = Constants.getPrefixedString(payload, 1, StandardCharsets.UTF_8);
+ int providerCount = payload.get() & Constants.BYTE_MASK;
+
+ mProviders = new ArrayList<>(providerCount);
+
+ while (providerCount > 0) {
+ mProviders.add(new OSUProvider(mSSID, payload));
+ providerCount--;
+ }
+ }
+
+ public String getSSID() {
+ return mSSID;
+ }
+
+ public List<OSUProvider> getProviders() {
+ return Collections.unmodifiableList(mProviders);
+ }
+
+ @Override
+ public String toString() {
+ return "HSOsuProviders{" +
+ "SSID='" + mSSID + '\'' +
+ ", providers=" + mProviders +
+ '}';
+ }
+}
diff --git a/packages/Osu/src/com/android/anqp/I18Name.java b/packages/Osu/src/com/android/anqp/I18Name.java
new file mode 100644
index 0000000..0a16db7
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/I18Name.java
@@ -0,0 +1,80 @@
+package com.android.anqp;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+
+/**
+ * A generic Internationalized name used in ANQP elements as specified in 802.11-2012 and
+ * "Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00"
+ */
+public class I18Name {
+ private final String mLanguage;
+ private final Locale mLocale;
+ private final String mText;
+
+ public I18Name(ByteBuffer payload) throws ProtocolException {
+ if (payload.remaining() < Constants.LANG_CODE_LENGTH + 1) {
+ throw new ProtocolException("Truncated I18Name: " + payload.remaining());
+ }
+ int nameLength = payload.get() & BYTE_MASK;
+ if (nameLength < Constants.LANG_CODE_LENGTH) {
+ throw new ProtocolException("Runt I18Name: " + nameLength);
+ }
+ mLanguage = Constants.getTrimmedString(payload,
+ Constants.LANG_CODE_LENGTH, StandardCharsets.US_ASCII);
+ mLocale = Locale.forLanguageTag(mLanguage);
+ mText = Constants.getString(payload, nameLength -
+ Constants.LANG_CODE_LENGTH, StandardCharsets.UTF_8);
+ }
+
+ public I18Name(String compoundString) throws IOException {
+ if (compoundString.length() < Constants.LANG_CODE_LENGTH) {
+ throw new IOException("I18String too short: '" + compoundString + "'");
+ }
+ mLanguage = compoundString.substring(0, Constants.LANG_CODE_LENGTH);
+ mText = compoundString.substring(Constants.LANG_CODE_LENGTH);
+ mLocale = Locale.forLanguageTag(mLanguage);
+ }
+
+ public String getLanguage() {
+ return mLanguage;
+ }
+
+ public Locale getLocale() {
+ return mLocale;
+ }
+
+ public String getText() {
+ return mText;
+ }
+
+ @Override
+ public boolean equals(Object thatObject) {
+ if (this == thatObject) {
+ return true;
+ }
+ if (thatObject == null || getClass() != thatObject.getClass()) {
+ return false;
+ }
+
+ I18Name that = (I18Name) thatObject;
+ return mLanguage.equals(that.mLanguage) && mText.equals(that.mText);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mLanguage.hashCode();
+ result = 31 * result + mText.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return mText + ':' + mLocale.getLanguage();
+ }
+}
diff --git a/packages/Osu/src/com/android/anqp/IconInfo.java b/packages/Osu/src/com/android/anqp/IconInfo.java
new file mode 100644
index 0000000..9e9f1ee
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/IconInfo.java
@@ -0,0 +1,91 @@
+package com.android.anqp;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import static com.android.anqp.Constants.SHORT_MASK;
+
+/**
+ * The Icons available OSU Providers sub field, as specified in
+ * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00,
+ * section 4.8.1.4
+ */
+public class IconInfo {
+ private final int mWidth;
+ private final int mHeight;
+ private final String mLanguage;
+ private final String mIconType;
+ private final String mFileName;
+
+ public IconInfo(ByteBuffer payload) throws ProtocolException {
+ if (payload.remaining() < 9) {
+ throw new ProtocolException("Truncated icon meta data");
+ }
+
+ mWidth = payload.getShort() & SHORT_MASK;
+ mHeight = payload.getShort() & SHORT_MASK;
+ mLanguage = Constants.getTrimmedString(payload,
+ Constants.LANG_CODE_LENGTH, StandardCharsets.US_ASCII);
+ mIconType = Constants.getPrefixedString(payload, 1, StandardCharsets.US_ASCII);
+ mFileName = Constants.getPrefixedString(payload, 1, StandardCharsets.UTF_8);
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public String getLanguage() {
+ return mLanguage;
+ }
+
+ public String getIconType() {
+ return mIconType;
+ }
+
+ public String getFileName() {
+ return mFileName;
+ }
+
+ @Override
+ public boolean equals(Object thatObject) {
+ if (this == thatObject) {
+ return true;
+ }
+ if (thatObject == null || getClass() != thatObject.getClass()) {
+ return false;
+ }
+
+ IconInfo that = (IconInfo) thatObject;
+ return mHeight == that.mHeight &&
+ mWidth == that.mWidth &&
+ mFileName.equals(that.mFileName) &&
+ mIconType.equals(that.mIconType) &&
+ mLanguage.equals(that.mLanguage);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mWidth;
+ result = 31 * result + mHeight;
+ result = 31 * result + mLanguage.hashCode();
+ result = 31 * result + mIconType.hashCode();
+ result = 31 * result + mFileName.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "IconInfo{" +
+ "Width=" + mWidth +
+ ", Height=" + mHeight +
+ ", Language=" + mLanguage +
+ ", IconType='" + mIconType + '\'' +
+ ", FileName='" + mFileName + '\'' +
+ '}';
+ }
+}
diff --git a/packages/Osu/src/com/android/anqp/OSUProvider.java b/packages/Osu/src/com/android/anqp/OSUProvider.java
new file mode 100644
index 0000000..e2669d4
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/OSUProvider.java
@@ -0,0 +1,158 @@
+package com.android.anqp;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+import static com.android.anqp.Constants.SHORT_MASK;
+
+/**
+ * An OSU Provider, as specified in
+ * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00,
+ * section 4.8.1
+ */
+public class OSUProvider {
+
+ public enum OSUMethod {OmaDm, SoapXml}
+
+ private final String mSSID;
+ private final List<I18Name> mNames;
+ private final String mOSUServer;
+ private final List<OSUMethod> mOSUMethods;
+ private final List<IconInfo> mIcons;
+ private final String mOsuNai;
+ private final List<I18Name> mServiceDescriptions;
+ private final int mHashCode;
+
+ public OSUProvider(String ssid, ByteBuffer payload) throws ProtocolException {
+ if (payload.remaining() < 11) {
+ throw new ProtocolException("Truncated OSU provider: " + payload.remaining());
+ }
+
+ mSSID = ssid;
+
+ int length = payload.getShort() & SHORT_MASK;
+ int namesLength = payload.getShort() & SHORT_MASK;
+
+ ByteBuffer namesBuffer = payload.duplicate().order(ByteOrder.LITTLE_ENDIAN);
+ namesBuffer.limit(namesBuffer.position() + namesLength);
+ payload.position(payload.position() + namesLength);
+
+ mNames = new ArrayList<>();
+
+ while (namesBuffer.hasRemaining()) {
+ mNames.add(new I18Name(namesBuffer));
+ }
+
+ mOSUServer = Constants.getPrefixedString(payload, 1, StandardCharsets.UTF_8);
+ int methodLength = payload.get() & BYTE_MASK;
+ mOSUMethods = new ArrayList<>(methodLength);
+ while (methodLength > 0) {
+ int methodID = payload.get() & BYTE_MASK;
+ mOSUMethods.add(methodID < OSUMethod.values().length ?
+ OSUMethod.values()[methodID] :
+ null);
+ methodLength--;
+ }
+
+ int iconsLength = payload.getShort() & SHORT_MASK;
+ ByteBuffer iconsBuffer = payload.duplicate().order(ByteOrder.LITTLE_ENDIAN);
+ iconsBuffer.limit(iconsBuffer.position() + iconsLength);
+ payload.position(payload.position() + iconsLength);
+
+ mIcons = new ArrayList<>();
+
+ while (iconsBuffer.hasRemaining()) {
+ mIcons.add(new IconInfo(iconsBuffer));
+ }
+
+ mOsuNai = Constants.getPrefixedString(payload, 1, StandardCharsets.UTF_8, true);
+
+ int descriptionsLength = payload.getShort() & SHORT_MASK;
+ ByteBuffer descriptionsBuffer = payload.duplicate().order(ByteOrder.LITTLE_ENDIAN);
+ descriptionsBuffer.limit(descriptionsBuffer.position() + descriptionsLength);
+ payload.position(payload.position() + descriptionsLength);
+
+ mServiceDescriptions = new ArrayList<>();
+
+ while (descriptionsBuffer.hasRemaining()) {
+ mServiceDescriptions.add(new I18Name(descriptionsBuffer));
+ }
+
+ int result = mNames.hashCode();
+ result = 31 * result + mSSID.hashCode();
+ result = 31 * result + mOSUServer.hashCode();
+ result = 31 * result + mOSUMethods.hashCode();
+ result = 31 * result + mIcons.hashCode();
+ result = 31 * result + (mOsuNai != null ? mOsuNai.hashCode() : 0);
+ result = 31 * result + mServiceDescriptions.hashCode();
+ mHashCode = result;
+ }
+
+ public String getSSID() {
+ return mSSID;
+ }
+
+ public List<I18Name> getNames() {
+ return mNames;
+ }
+
+ public String getOSUServer() {
+ return mOSUServer;
+ }
+
+ public List<OSUMethod> getOSUMethods() {
+ return mOSUMethods;
+ }
+
+ public List<IconInfo> getIcons() {
+ return mIcons;
+ }
+
+ public String getOsuNai() {
+ return mOsuNai;
+ }
+
+ public List<I18Name> getServiceDescriptions() {
+ return mServiceDescriptions;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ OSUProvider that = (OSUProvider) o;
+
+ if (!mSSID.equals(that.mSSID)) return false;
+ if (!mOSUServer.equals(that.mOSUServer)) return false;
+ if (!mNames.equals(that.mNames)) return false;
+ if (!mServiceDescriptions.equals(that.mServiceDescriptions)) return false;
+ if (!mIcons.equals(that.mIcons)) return false;
+ if (!mOSUMethods.equals(that.mOSUMethods)) return false;
+ if (mOsuNai != null ? !mOsuNai.equals(that.mOsuNai) : that.mOsuNai != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return mHashCode;
+ }
+
+ @Override
+ public String toString() {
+ return "OSUProvider{" +
+ "names=" + mNames +
+ ", OSUServer='" + mOSUServer + '\'' +
+ ", OSUMethods=" + mOSUMethods +
+ ", icons=" + mIcons +
+ ", NAI='" + mOsuNai + '\'' +
+ ", serviceDescriptions=" + mServiceDescriptions +
+ '}';
+ }
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/AuthParam.java b/packages/Osu/src/com/android/anqp/eap/AuthParam.java
new file mode 100644
index 0000000..4243954
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/AuthParam.java
@@ -0,0 +1,9 @@
+package com.android.anqp.eap;
+
+/**
+ * An Authentication parameter, part of the NAI Realm ANQP element, specified in
+ * IEEE802.11-2012 section 8.4.4.10, table 8-188
+ */
+public interface AuthParam {
+ public EAP.AuthInfoID getAuthInfoID();
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/Credential.java b/packages/Osu/src/com/android/anqp/eap/Credential.java
new file mode 100644
index 0000000..0a89f4f
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/Credential.java
@@ -0,0 +1,72 @@
+package com.android.anqp.eap;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+
+/**
+ * An EAP authentication parameter, IEEE802.11-2012, table 8-188
+ */
+public class Credential implements AuthParam {
+
+ public enum CredType {
+ Reserved,
+ SIM,
+ USIM,
+ NFC,
+ HWToken,
+ Softoken,
+ Certificate,
+ Username,
+ None,
+ Anonymous,
+ VendorSpecific}
+
+ private final EAP.AuthInfoID mAuthInfoID;
+ private final CredType mCredType;
+
+ public Credential(EAP.AuthInfoID infoID, int length, ByteBuffer payload)
+ throws ProtocolException {
+ if (length != 1) {
+ throw new ProtocolException("Bad length: " + length);
+ }
+
+ mAuthInfoID = infoID;
+ int typeID = payload.get() & BYTE_MASK;
+
+ mCredType = typeID < CredType.values().length ?
+ CredType.values()[typeID] :
+ CredType.Reserved;
+ }
+
+ @Override
+ public EAP.AuthInfoID getAuthInfoID() {
+ return mAuthInfoID;
+ }
+
+ @Override
+ public int hashCode() {
+ return mAuthInfoID.hashCode() * 31 + mCredType.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object thatObject) {
+ if (thatObject == this) {
+ return true;
+ } else if (thatObject == null || thatObject.getClass() != Credential.class) {
+ return false;
+ } else {
+ return ((Credential) thatObject).getCredType() == getCredType();
+ }
+ }
+
+ public CredType getCredType() {
+ return mCredType;
+ }
+
+ @Override
+ public String toString() {
+ return "Auth method " + mAuthInfoID + " = " + mCredType + "\n";
+ }
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/EAP.java b/packages/Osu/src/com/android/anqp/eap/EAP.java
new file mode 100644
index 0000000..4b968b6
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/EAP.java
@@ -0,0 +1,155 @@
+package com.android.anqp.eap;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * EAP Related constants for the ANQP NAIRealm element, IEEE802.11-2012 section 8.4.4.10
+ */
+public abstract class EAP {
+
+ private static final Map<Integer, EAPMethodID> sEapIds = new HashMap<>();
+ private static final Map<EAPMethodID, Integer> sRevEapIds = new HashMap<>();
+ private static final Map<Integer, AuthInfoID> sAuthIds = new HashMap<>();
+
+ public static final int EAP_MD5 = 4;
+ public static final int EAP_OTP = 5;
+ public static final int EAP_RSA = 9;
+ public static final int EAP_KEA = 11;
+ public static final int EAP_KEA_VALIDATE = 12;
+ public static final int EAP_TLS = 13;
+ public static final int EAP_LEAP = 17;
+ public static final int EAP_SIM = 18;
+ public static final int EAP_TTLS = 21;
+ public static final int EAP_AKA = 23;
+ public static final int EAP_3Com = 24;
+ public static final int EAP_MSCHAPv2 = 26;
+ public static final int EAP_PEAP = 29;
+ public static final int EAP_POTP = 32;
+ public static final int EAP_ActiontecWireless = 35;
+ public static final int EAP_HTTPDigest = 38;
+ public static final int EAP_SPEKE = 41;
+ public static final int EAP_MOBAC = 42;
+ public static final int EAP_FAST = 43;
+ public static final int EAP_ZLXEAP = 44;
+ public static final int EAP_Link = 45;
+ public static final int EAP_PAX = 46;
+ public static final int EAP_PSK = 47;
+ public static final int EAP_SAKE = 48;
+ public static final int EAP_IKEv2 = 49;
+ public static final int EAP_AKAPrim = 50;
+ public static final int EAP_GPSK = 51;
+ public static final int EAP_PWD = 52;
+ public static final int EAP_EKE = 53;
+ public static final int EAP_TEAP = 55;
+
+ public enum EAPMethodID {
+ EAP_MD5,
+ EAP_OTP,
+ EAP_RSA,
+ EAP_KEA,
+ EAP_KEA_VALIDATE,
+ EAP_TLS,
+ EAP_LEAP,
+ EAP_SIM,
+ EAP_TTLS,
+ EAP_AKA,
+ EAP_3Com,
+ EAP_MSCHAPv2,
+ EAP_PEAP,
+ EAP_POTP,
+ EAP_ActiontecWireless,
+ EAP_HTTPDigest,
+ EAP_SPEKE,
+ EAP_MOBAC,
+ EAP_FAST,
+ EAP_ZLXEAP,
+ EAP_Link,
+ EAP_PAX,
+ EAP_PSK,
+ EAP_SAKE,
+ EAP_IKEv2,
+ EAP_AKAPrim,
+ EAP_GPSK,
+ EAP_PWD,
+ EAP_EKE,
+ EAP_TEAP
+ }
+
+ public static final int ExpandedEAPMethod = 1;
+ public static final int NonEAPInnerAuthType = 2;
+ public static final int InnerAuthEAPMethodType = 3;
+ public static final int ExpandedInnerEAPMethod = 4;
+ public static final int CredentialType = 5;
+ public static final int TunneledEAPMethodCredType = 6;
+ public static final int VendorSpecific = 221;
+
+ public enum AuthInfoID {
+ Undefined,
+ ExpandedEAPMethod,
+ NonEAPInnerAuthType,
+ InnerAuthEAPMethodType,
+ ExpandedInnerEAPMethod,
+ CredentialType,
+ TunneledEAPMethodCredType,
+ VendorSpecific
+ }
+
+ static {
+ sEapIds.put(EAP_MD5, EAPMethodID.EAP_MD5);
+ sEapIds.put(EAP_OTP, EAPMethodID.EAP_OTP);
+ sEapIds.put(EAP_RSA, EAPMethodID.EAP_RSA);
+ sEapIds.put(EAP_KEA, EAPMethodID.EAP_KEA);
+ sEapIds.put(EAP_KEA_VALIDATE, EAPMethodID.EAP_KEA_VALIDATE);
+ sEapIds.put(EAP_TLS, EAPMethodID.EAP_TLS);
+ sEapIds.put(EAP_LEAP, EAPMethodID.EAP_LEAP);
+ sEapIds.put(EAP_SIM, EAPMethodID.EAP_SIM);
+ sEapIds.put(EAP_TTLS, EAPMethodID.EAP_TTLS);
+ sEapIds.put(EAP_AKA, EAPMethodID.EAP_AKA);
+ sEapIds.put(EAP_3Com, EAPMethodID.EAP_3Com);
+ sEapIds.put(EAP_MSCHAPv2, EAPMethodID.EAP_MSCHAPv2);
+ sEapIds.put(EAP_PEAP, EAPMethodID.EAP_PEAP);
+ sEapIds.put(EAP_POTP, EAPMethodID.EAP_POTP);
+ sEapIds.put(EAP_ActiontecWireless, EAPMethodID.EAP_ActiontecWireless);
+ sEapIds.put(EAP_HTTPDigest, EAPMethodID.EAP_HTTPDigest);
+ sEapIds.put(EAP_SPEKE, EAPMethodID.EAP_SPEKE);
+ sEapIds.put(EAP_MOBAC, EAPMethodID.EAP_MOBAC);
+ sEapIds.put(EAP_FAST, EAPMethodID.EAP_FAST);
+ sEapIds.put(EAP_ZLXEAP, EAPMethodID.EAP_ZLXEAP);
+ sEapIds.put(EAP_Link, EAPMethodID.EAP_Link);
+ sEapIds.put(EAP_PAX, EAPMethodID.EAP_PAX);
+ sEapIds.put(EAP_PSK, EAPMethodID.EAP_PSK);
+ sEapIds.put(EAP_SAKE, EAPMethodID.EAP_SAKE);
+ sEapIds.put(EAP_IKEv2, EAPMethodID.EAP_IKEv2);
+ sEapIds.put(EAP_AKAPrim, EAPMethodID.EAP_AKAPrim);
+ sEapIds.put(EAP_GPSK, EAPMethodID.EAP_GPSK);
+ sEapIds.put(EAP_PWD, EAPMethodID.EAP_PWD);
+ sEapIds.put(EAP_EKE, EAPMethodID.EAP_EKE);
+ sEapIds.put(EAP_TEAP, EAPMethodID.EAP_TEAP);
+
+ for (Map.Entry<Integer, EAPMethodID> entry : sEapIds.entrySet()) {
+ sRevEapIds.put(entry.getValue(), entry.getKey());
+ }
+
+ sAuthIds.put(ExpandedEAPMethod, AuthInfoID.ExpandedEAPMethod);
+ sAuthIds.put(NonEAPInnerAuthType, AuthInfoID.NonEAPInnerAuthType);
+ sAuthIds.put(InnerAuthEAPMethodType, AuthInfoID.InnerAuthEAPMethodType);
+ sAuthIds.put(ExpandedInnerEAPMethod, AuthInfoID.ExpandedInnerEAPMethod);
+ sAuthIds.put(CredentialType, AuthInfoID.CredentialType);
+ sAuthIds.put(TunneledEAPMethodCredType, AuthInfoID.TunneledEAPMethodCredType);
+ sAuthIds.put(VendorSpecific, AuthInfoID.VendorSpecific);
+ }
+
+ public static EAPMethodID mapEAPMethod(int methodID) {
+ return sEapIds.get(methodID);
+ }
+
+ public static Integer mapEAPMethod(EAPMethodID methodID) {
+ return sRevEapIds.get(methodID);
+ }
+
+ public static AuthInfoID mapAuthMethod(int methodID) {
+ return sAuthIds.get(methodID);
+ }
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/EAPMethod.java b/packages/Osu/src/com/android/anqp/eap/EAPMethod.java
new file mode 100644
index 0000000..fa6c2f9
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/EAPMethod.java
@@ -0,0 +1,191 @@
+package com.android.anqp.eap;
+
+import com.android.anqp.Constants;
+import com.android.hotspot2.AuthMatch;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An EAP Method, part of the NAI Realm ANQP element, specified in
+ * IEEE802.11-2012 section 8.4.4.10, figure 8-420
+ */
+public class EAPMethod {
+ private final EAP.EAPMethodID mEAPMethodID;
+ private final Map<EAP.AuthInfoID, Set<AuthParam>> mAuthParams;
+
+ public EAPMethod(ByteBuffer payload) throws ProtocolException {
+ if (payload.remaining() < 3) {
+ throw new ProtocolException("Runt EAP Method: " + payload.remaining());
+ }
+
+ int length = payload.get() & Constants.BYTE_MASK;
+ int methodID = payload.get() & Constants.BYTE_MASK;
+ int count = payload.get() & Constants.BYTE_MASK;
+
+ mEAPMethodID = EAP.mapEAPMethod(methodID);
+ mAuthParams = new EnumMap<>(EAP.AuthInfoID.class);
+
+ int realCount = 0;
+
+ ByteBuffer paramPayload = payload.duplicate().order(ByteOrder.LITTLE_ENDIAN);
+ paramPayload.limit(paramPayload.position() + length - 2);
+ payload.position(payload.position() + length - 2);
+ while (paramPayload.hasRemaining()) {
+ int id = paramPayload.get() & Constants.BYTE_MASK;
+
+ EAP.AuthInfoID authInfoID = EAP.mapAuthMethod(id);
+ if (authInfoID == null) {
+ throw new ProtocolException("Unknown auth parameter ID: " + id);
+ }
+
+ int len = paramPayload.get() & Constants.BYTE_MASK;
+ if (len == 0 || len > paramPayload.remaining()) {
+ throw new ProtocolException("Bad auth method length: " + len);
+ }
+
+ switch (authInfoID) {
+ case ExpandedEAPMethod:
+ addAuthParam(new ExpandedEAPMethod(authInfoID, len, paramPayload));
+ break;
+ case NonEAPInnerAuthType:
+ addAuthParam(new NonEAPInnerAuth(len, paramPayload));
+ break;
+ case InnerAuthEAPMethodType:
+ addAuthParam(new InnerAuthEAP(len, paramPayload));
+ break;
+ case ExpandedInnerEAPMethod:
+ addAuthParam(new ExpandedEAPMethod(authInfoID, len, paramPayload));
+ break;
+ case CredentialType:
+ addAuthParam(new Credential(authInfoID, len, paramPayload));
+ break;
+ case TunneledEAPMethodCredType:
+ addAuthParam(new Credential(authInfoID, len, paramPayload));
+ break;
+ case VendorSpecific:
+ addAuthParam(new VendorSpecificAuth(len, paramPayload));
+ break;
+ }
+
+ realCount++;
+ }
+ if (realCount != count)
+ throw new ProtocolException("Invalid parameter count: " + realCount +
+ ", expected " + count);
+ }
+
+ public EAPMethod(EAP.EAPMethodID eapMethodID, AuthParam authParam) {
+ mEAPMethodID = eapMethodID;
+ mAuthParams = new HashMap<>(1);
+ if (authParam != null) {
+ Set<AuthParam> authParams = new HashSet<>();
+ authParams.add(authParam);
+ mAuthParams.put(authParam.getAuthInfoID(), authParams);
+ }
+ }
+
+ private void addAuthParam(AuthParam param) {
+ Set<AuthParam> authParams = mAuthParams.get(param.getAuthInfoID());
+ if (authParams == null) {
+ authParams = new HashSet<>();
+ mAuthParams.put(param.getAuthInfoID(), authParams);
+ }
+ authParams.add(param);
+ }
+
+ public Map<EAP.AuthInfoID, Set<AuthParam>> getAuthParams() {
+ return Collections.unmodifiableMap(mAuthParams);
+ }
+
+ public EAP.EAPMethodID getEAPMethodID() {
+ return mEAPMethodID;
+ }
+
+ public int match(com.android.hotspot2.pps.Credential credential) {
+
+ EAPMethod credMethod = credential.getEAPMethod();
+ if (mEAPMethodID != credMethod.getEAPMethodID()) {
+ return AuthMatch.None;
+ }
+
+ switch (mEAPMethodID) {
+ case EAP_TTLS:
+ if (mAuthParams.isEmpty()) {
+ return AuthMatch.Method;
+ }
+ int paramCount = 0;
+ for (Map.Entry<EAP.AuthInfoID, Set<AuthParam>> entry :
+ credMethod.getAuthParams().entrySet()) {
+ Set<AuthParam> params = mAuthParams.get(entry.getKey());
+ if (params == null) {
+ continue;
+ }
+
+ if (!Collections.disjoint(params, entry.getValue())) {
+ return AuthMatch.MethodParam;
+ }
+ paramCount += params.size();
+ }
+ return paramCount > 0 ? AuthMatch.None : AuthMatch.Method;
+ case EAP_TLS:
+ return AuthMatch.MethodParam;
+ case EAP_SIM:
+ case EAP_AKA:
+ case EAP_AKAPrim:
+ return AuthMatch.Method;
+ default:
+ return AuthMatch.Method;
+ }
+ }
+
+ public AuthParam getAuthParam() {
+ if (mAuthParams.isEmpty()) {
+ return null;
+ }
+ Set<AuthParam> params = mAuthParams.values().iterator().next();
+ if (params.isEmpty()) {
+ return null;
+ }
+ return params.iterator().next();
+ }
+
+ @Override
+ public boolean equals(Object thatObject) {
+ if (this == thatObject) {
+ return true;
+ }
+ else if (thatObject == null || getClass() != thatObject.getClass()) {
+ return false;
+ }
+
+ EAPMethod that = (EAPMethod) thatObject;
+ return mEAPMethodID == that.mEAPMethodID && mAuthParams.equals(that.mAuthParams);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mEAPMethodID.hashCode();
+ result = 31 * result + mAuthParams.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("EAP Method ").append(mEAPMethodID).append('\n');
+ for (Set<AuthParam> paramSet : mAuthParams.values()) {
+ for (AuthParam param : paramSet) {
+ sb.append(" ").append(param.toString());
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/ExpandedEAPMethod.java b/packages/Osu/src/com/android/anqp/eap/ExpandedEAPMethod.java
new file mode 100644
index 0000000..1358c09
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/ExpandedEAPMethod.java
@@ -0,0 +1,78 @@
+package com.android.anqp.eap;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+import static com.android.anqp.Constants.INT_MASK;
+import static com.android.anqp.Constants.SHORT_MASK;
+
+/**
+ * An EAP authentication parameter, IEEE802.11-2012, table 8-188
+ */
+public class ExpandedEAPMethod implements AuthParam {
+
+ private final EAP.AuthInfoID mAuthInfoID;
+ private final int mVendorID;
+ private final long mVendorType;
+
+ public ExpandedEAPMethod(EAP.AuthInfoID authInfoID, int length, ByteBuffer payload)
+ throws ProtocolException {
+ if (length != 7) {
+ throw new ProtocolException("Bad length: " + payload.remaining());
+ }
+
+ mAuthInfoID = authInfoID;
+
+ ByteBuffer vndBuffer = payload.duplicate().order(ByteOrder.BIG_ENDIAN);
+
+ int id = vndBuffer.getShort() & SHORT_MASK;
+ id = (id << Byte.SIZE) | (vndBuffer.get() & BYTE_MASK);
+ mVendorID = id;
+ mVendorType = vndBuffer.getInt() & INT_MASK;
+
+ payload.position(payload.position()+7);
+ }
+
+ public ExpandedEAPMethod(EAP.AuthInfoID authInfoID, int vendorID, long vendorType) {
+ mAuthInfoID = authInfoID;
+ mVendorID = vendorID;
+ mVendorType = vendorType;
+ }
+
+ @Override
+ public EAP.AuthInfoID getAuthInfoID() {
+ return mAuthInfoID;
+ }
+
+ @Override
+ public int hashCode() {
+ return (mAuthInfoID.hashCode() * 31 + mVendorID) * 31 + (int) mVendorType;
+ }
+
+ @Override
+ public boolean equals(Object thatObject) {
+ if (thatObject == this) {
+ return true;
+ } else if (thatObject == null || thatObject.getClass() != ExpandedEAPMethod.class) {
+ return false;
+ } else {
+ ExpandedEAPMethod that = (ExpandedEAPMethod) thatObject;
+ return that.getVendorID() == getVendorID() && that.getVendorType() == getVendorType();
+ }
+ }
+
+ public int getVendorID() {
+ return mVendorID;
+ }
+
+ public long getVendorType() {
+ return mVendorType;
+ }
+
+ @Override
+ public String toString() {
+ return "Auth method " + mAuthInfoID + ", id " + mVendorID + ", type " + mVendorType + "\n";
+ }
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/InnerAuthEAP.java b/packages/Osu/src/com/android/anqp/eap/InnerAuthEAP.java
new file mode 100644
index 0000000..571cf26
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/InnerAuthEAP.java
@@ -0,0 +1,56 @@
+package com.android.anqp.eap;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+
+/**
+ * An EAP authentication parameter, IEEE802.11-2012, table 8-188
+ */
+public class InnerAuthEAP implements AuthParam {
+
+ private final EAP.EAPMethodID mEapMethodID;
+
+ public InnerAuthEAP(int length, ByteBuffer payload) throws ProtocolException {
+ if (length != 1) {
+ throw new ProtocolException("Bad length: " + length);
+ }
+ int typeID = payload.get() & BYTE_MASK;
+ mEapMethodID = EAP.mapEAPMethod(typeID);
+ }
+
+ public InnerAuthEAP(EAP.EAPMethodID eapMethodID) {
+ mEapMethodID = eapMethodID;
+ }
+
+ @Override
+ public EAP.AuthInfoID getAuthInfoID() {
+ return EAP.AuthInfoID.InnerAuthEAPMethodType;
+ }
+
+ public EAP.EAPMethodID getEAPMethodID() {
+ return mEapMethodID;
+ }
+
+ @Override
+ public int hashCode() {
+ return mEapMethodID != null ? mEapMethodID.hashCode() : 0;
+ }
+
+ @Override
+ public boolean equals(Object thatObject) {
+ if (thatObject == this) {
+ return true;
+ } else if (thatObject == null || thatObject.getClass() != InnerAuthEAP.class) {
+ return false;
+ } else {
+ return ((InnerAuthEAP) thatObject).getEAPMethodID() == getEAPMethodID();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Auth method InnerAuthEAP, inner = " + mEapMethodID + '\n';
+ }
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/NonEAPInnerAuth.java b/packages/Osu/src/com/android/anqp/eap/NonEAPInnerAuth.java
new file mode 100644
index 0000000..9d37b4d
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/NonEAPInnerAuth.java
@@ -0,0 +1,93 @@
+package com.android.anqp.eap;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+
+/**
+ * An EAP authentication parameter, IEEE802.11-2012, table 8-188
+ */
+public class NonEAPInnerAuth implements AuthParam {
+
+ public enum NonEAPType {Reserved, PAP, CHAP, MSCHAP, MSCHAPv2}
+ private static final Map<NonEAPType, String> sOmaMap = new EnumMap<>(NonEAPType.class);
+ private static final Map<String, NonEAPType> sRevOmaMap = new HashMap<>();
+
+ private final NonEAPType mType;
+
+ static {
+ sOmaMap.put(NonEAPType.PAP, "PAP");
+ sOmaMap.put(NonEAPType.CHAP, "CHAP");
+ sOmaMap.put(NonEAPType.MSCHAP, "MS-CHAP");
+ sOmaMap.put(NonEAPType.MSCHAPv2, "MS-CHAP-V2");
+
+ for (Map.Entry<NonEAPType, String> entry : sOmaMap.entrySet()) {
+ sRevOmaMap.put(entry.getValue(), entry.getKey());
+ }
+ }
+
+ public NonEAPInnerAuth(int length, ByteBuffer payload) throws ProtocolException {
+ if (length != 1) {
+ throw new ProtocolException("Bad length: " + payload.remaining());
+ }
+
+ int typeID = payload.get() & BYTE_MASK;
+ mType = typeID < NonEAPType.values().length ?
+ NonEAPType.values()[typeID] :
+ NonEAPType.Reserved;
+ }
+
+ public NonEAPInnerAuth(NonEAPType type) {
+ mType = type;
+ }
+
+ /**
+ * Construct from the OMA-DM PPS data
+ * @param eapType as defined in the HS2.0 spec.
+ */
+ public NonEAPInnerAuth(String eapType) {
+ mType = sRevOmaMap.get(eapType);
+ }
+
+ @Override
+ public EAP.AuthInfoID getAuthInfoID() {
+ return EAP.AuthInfoID.NonEAPInnerAuthType;
+ }
+
+ public NonEAPType getType() {
+ return mType;
+ }
+
+ public String getOMAtype() {
+ return sOmaMap.get(mType);
+ }
+
+ public static String mapInnerType(NonEAPType type) {
+ return sOmaMap.get(type);
+ }
+
+ @Override
+ public int hashCode() {
+ return mType.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object thatObject) {
+ if (thatObject == this) {
+ return true;
+ } else if (thatObject == null || thatObject.getClass() != NonEAPInnerAuth.class) {
+ return false;
+ } else {
+ return ((NonEAPInnerAuth) thatObject).getType() == getType();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Auth method NonEAPInnerAuthEAP, inner = " + mType + '\n';
+ }
+}
diff --git a/packages/Osu/src/com/android/anqp/eap/VendorSpecificAuth.java b/packages/Osu/src/com/android/anqp/eap/VendorSpecificAuth.java
new file mode 100644
index 0000000..04a315d
--- /dev/null
+++ b/packages/Osu/src/com/android/anqp/eap/VendorSpecificAuth.java
@@ -0,0 +1,47 @@
+package com.android.anqp.eap;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * An EAP authentication parameter, IEEE802.11-2012, table 8-188
+ */
+public class VendorSpecificAuth implements AuthParam {
+
+ private final byte[] mData;
+
+ public VendorSpecificAuth(int length, ByteBuffer payload) throws ProtocolException {
+ mData = new byte[length];
+ payload.get(mData);
+ }
+
+ @Override
+ public EAP.AuthInfoID getAuthInfoID() {
+ return EAP.AuthInfoID.VendorSpecific;
+ }
+
+ public int hashCode() {
+ return Arrays.hashCode(mData);
+ }
+
+ @Override
+ public boolean equals(Object thatObject) {
+ if (thatObject == this) {
+ return true;
+ } else if (thatObject == null || thatObject.getClass() != VendorSpecificAuth.class) {
+ return false;
+ } else {
+ return Arrays.equals(((VendorSpecificAuth) thatObject).getData(), getData());
+ }
+ }
+
+ public byte[] getData() {
+ return mData;
+ }
+
+ @Override
+ public String toString() {
+ return "Auth method VendorSpecificAuth, data = " + Arrays.toString(mData) + '\n';
+ }
+}
diff --git a/packages/Osu/src/com/android/configparse/ConfigBuilder.java b/packages/Osu/src/com/android/configparse/ConfigBuilder.java
new file mode 100644
index 0000000..b760ade
--- /dev/null
+++ b/packages/Osu/src/com/android/configparse/ConfigBuilder.java
@@ -0,0 +1,258 @@
+package com.android.configparse;
+
+import android.content.Context;
+import android.net.Uri;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiEnterpriseConfig;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.anqp.eap.AuthParam;
+import com.android.anqp.eap.EAP;
+import com.android.anqp.eap.EAPMethod;
+import com.android.anqp.eap.NonEAPInnerAuth;
+import com.android.hotspot2.IMSIParameter;
+import com.android.hotspot2.pps.Credential;
+import com.android.hotspot2.pps.HomeSP;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+public class ConfigBuilder {
+ private static final String TAG = "WCFG";
+
+ private static void dropFile(Uri uri, Context context) {
+ context.getContentResolver().delete(uri, null, null);
+ }
+
+ public static WifiConfiguration buildConfig(HomeSP homeSP, X509Certificate caCert,
+ List<X509Certificate> clientChain, PrivateKey key)
+ throws IOException, GeneralSecurityException {
+
+ Credential credential = homeSP.getCredential();
+
+ WifiConfiguration config;
+
+ EAP.EAPMethodID eapMethodID = credential.getEAPMethod().getEAPMethodID();
+ switch (eapMethodID) {
+ case EAP_TTLS:
+ if (key != null || clientChain != null) {
+ Log.w(TAG, "Client cert and/or key included with EAP-TTLS profile");
+ }
+ config = buildTTLSConfig(homeSP);
+ break;
+ case EAP_TLS:
+ config = buildTLSConfig(homeSP, clientChain, key);
+ break;
+ case EAP_AKA:
+ case EAP_AKAPrim:
+ case EAP_SIM:
+ if (key != null || clientChain != null || caCert != null) {
+ Log.i(TAG, "Client/CA cert and/or key included with " +
+ eapMethodID + " profile");
+ }
+ config = buildSIMConfig(homeSP);
+ break;
+ default:
+ throw new IOException("Unsupported EAP Method: " + eapMethodID);
+ }
+
+ WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig;
+
+ enterpriseConfig.setCaCertificate(caCert);
+ enterpriseConfig.setAnonymousIdentity("anonymous@" + credential.getRealm());
+
+ return config;
+ }
+
+ // Retain for debugging purposes
+ /*
+ private static void xIterateCerts(KeyStore ks, X509Certificate caCert)
+ throws GeneralSecurityException {
+ Enumeration<String> aliases = ks.aliases();
+ while (aliases.hasMoreElements()) {
+ String alias = aliases.nextElement();
+ Certificate cert = ks.getCertificate(alias);
+ Log.d("HS2J", "Checking " + alias);
+ if (cert instanceof X509Certificate) {
+ X509Certificate x509Certificate = (X509Certificate) cert;
+ boolean sm = x509Certificate.getSubjectX500Principal().equals(
+ caCert.getSubjectX500Principal());
+ boolean eq = false;
+ if (sm) {
+ eq = Arrays.equals(x509Certificate.getEncoded(), caCert.getEncoded());
+ }
+ Log.d("HS2J", "Subject: " + x509Certificate.getSubjectX500Principal() +
+ ": " + sm + "/" + eq);
+ }
+ }
+ }
+ */
+
+ private static WifiConfiguration buildTTLSConfig(HomeSP homeSP)
+ throws IOException {
+ Credential credential = homeSP.getCredential();
+
+ if (credential.getUserName() == null || credential.getPassword() == null) {
+ throw new IOException("EAP-TTLS provisioned without user name or password");
+ }
+
+ EAPMethod eapMethod = credential.getEAPMethod();
+
+ AuthParam authParam = eapMethod.getAuthParam();
+ if (authParam == null ||
+ authParam.getAuthInfoID() != EAP.AuthInfoID.NonEAPInnerAuthType) {
+ throw new IOException("Bad auth parameter for EAP-TTLS: " + authParam);
+ }
+
+ WifiConfiguration config = buildBaseConfiguration(homeSP);
+ NonEAPInnerAuth ttlsParam = (NonEAPInnerAuth) authParam;
+ WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig;
+ enterpriseConfig.setPhase2Method(remapInnerMethod(ttlsParam.getType()));
+ enterpriseConfig.setIdentity(credential.getUserName());
+ enterpriseConfig.setPassword(credential.getPassword());
+
+ return config;
+ }
+
+ private static WifiConfiguration buildTLSConfig(HomeSP homeSP,
+ List<X509Certificate> clientChain,
+ PrivateKey clientKey)
+ throws IOException, GeneralSecurityException {
+
+ Credential credential = homeSP.getCredential();
+
+ X509Certificate clientCertificate = null;
+
+ if (clientKey == null || clientChain == null) {
+ throw new IOException("No key and/or cert passed for EAP-TLS");
+ }
+ if (credential.getCertType() != Credential.CertType.x509v3) {
+ throw new IOException("Invalid certificate type for TLS: " +
+ credential.getCertType());
+ }
+
+ byte[] reference = credential.getFingerPrint();
+ MessageDigest digester = MessageDigest.getInstance("SHA-256");
+ for (X509Certificate certificate : clientChain) {
+ digester.reset();
+ byte[] fingerprint = digester.digest(certificate.getEncoded());
+ if (Arrays.equals(reference, fingerprint)) {
+ clientCertificate = certificate;
+ break;
+ }
+ }
+ if (clientCertificate == null) {
+ throw new IOException("No certificate in chain matches supplied fingerprint");
+ }
+
+ String alias = Base64.encodeToString(reference, Base64.DEFAULT);
+
+ WifiConfiguration config = buildBaseConfiguration(homeSP);
+ WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig;
+ enterpriseConfig.setClientCertificateAlias(alias);
+ enterpriseConfig.setClientKeyEntry(clientKey, clientCertificate);
+
+ return config;
+ }
+
+ private static WifiConfiguration buildSIMConfig(HomeSP homeSP)
+ throws IOException {
+
+ Credential credential = homeSP.getCredential();
+ IMSIParameter credImsi = credential.getImsi();
+
+ /*
+ * Uncomment to enforce strict IMSI matching with currently installed SIM cards.
+ *
+ TelephonyManager tm = TelephonyManager.from(context);
+ SubscriptionManager sub = SubscriptionManager.from(context);
+ boolean match = false;
+
+ for (int subId : sub.getActiveSubscriptionIdList()) {
+ String imsi = tm.getSubscriberId(subId);
+ if (credImsi.matches(imsi)) {
+ match = true;
+ break;
+ }
+ }
+ if (!match) {
+ throw new IOException("Supplied IMSI does not match any SIM card");
+ }
+ */
+
+ WifiConfiguration config = buildBaseConfiguration(homeSP);
+ config.enterpriseConfig.setPlmn(credImsi.toString());
+ return config;
+ }
+
+ private static WifiConfiguration buildBaseConfiguration(HomeSP homeSP) throws IOException {
+ EAP.EAPMethodID eapMethodID = homeSP.getCredential().getEAPMethod().getEAPMethodID();
+
+ WifiConfiguration config = new WifiConfiguration();
+
+ config.FQDN = homeSP.getFQDN();
+
+ HashSet<Long> roamingConsortiumIds = homeSP.getRoamingConsortiums();
+ config.roamingConsortiumIds = new long[roamingConsortiumIds.size()];
+ int i = 0;
+ for (long id : roamingConsortiumIds) {
+ config.roamingConsortiumIds[i] = id;
+ i++;
+ }
+ config.providerFriendlyName = homeSP.getFriendlyName();
+
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP);
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X);
+
+ WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig();
+ enterpriseConfig.setEapMethod(remapEAPMethod(eapMethodID));
+ enterpriseConfig.setRealm(homeSP.getCredential().getRealm());
+ if (homeSP.getUpdateIdentifier() >= 0) {
+ config.updateIdentifier = Integer.toString(homeSP.getUpdateIdentifier());
+ }
+ config.enterpriseConfig = enterpriseConfig;
+ if (homeSP.getUpdateIdentifier() >= 0) {
+ config.updateIdentifier = Integer.toString(homeSP.getUpdateIdentifier());
+ }
+
+ return config;
+ }
+
+ private static int remapEAPMethod(EAP.EAPMethodID eapMethodID) throws IOException {
+ switch (eapMethodID) {
+ case EAP_TTLS:
+ return WifiEnterpriseConfig.Eap.TTLS;
+ case EAP_TLS:
+ return WifiEnterpriseConfig.Eap.TLS;
+ case EAP_SIM:
+ return WifiEnterpriseConfig.Eap.SIM;
+ case EAP_AKA:
+ return WifiEnterpriseConfig.Eap.AKA;
+ case EAP_AKAPrim:
+ return WifiEnterpriseConfig.Eap.AKA_PRIME;
+ default:
+ throw new IOException("Bad EAP method: " + eapMethodID);
+ }
+ }
+
+ private static int remapInnerMethod(NonEAPInnerAuth.NonEAPType type) throws IOException {
+ switch (type) {
+ case PAP:
+ return WifiEnterpriseConfig.Phase2.PAP;
+ case MSCHAP:
+ return WifiEnterpriseConfig.Phase2.MSCHAP;
+ case MSCHAPv2:
+ return WifiEnterpriseConfig.Phase2.MSCHAPV2;
+ case CHAP:
+ default:
+ throw new IOException("Inner method " + type + " not supported");
+ }
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/AppBridge.java b/packages/Osu/src/com/android/hotspot2/AppBridge.java
new file mode 100644
index 0000000..95f5970
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/AppBridge.java
@@ -0,0 +1,63 @@
+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";
+ public static final String SP_NAME = "sp-name";
+ public static final String PROV_SUCCESS = "prov-success";
+ public static final String DEAUTH = "deauth";
+ public static final String DEAUTH_DELAY = "deauth-delay";
+ public static final String DEAUTH_URL = "deauth-url";
+ public static final String PROV_MESSAGE = "prov-message";
+ public static final String OSU_INFO = "osu-info";
+
+ public static final String GET_OSUS_ACTION = "com.android.hotspot2.GET_OSUS";
+
+ private final Context mContext;
+
+ public AppBridge(Context context) {
+ mContext = context;
+ }
+
+ public void showOsuCount(int osuCount, List<OSUInfo> osus) {
+ Intent intent = new Intent(ACTION_OSU_NOTIFICATION);
+ intent.putExtra(OSU_COUNT, osuCount);
+ intent.setFlags(
+ Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ mContext.startActivity(intent);
+ }
+
+ public void showStatus(OSUOperationStatus status, String spName, String message,
+ String remoteStatus) {
+ Intent intent = new Intent(ACTION_OSU_NOTIFICATION);
+ intent.putExtra(SP_NAME, spName);
+ 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.startActivity(intent);
+ }
+
+ public void showDeauth(String spName, boolean ess, int delay, String url) {
+ Intent intent = new Intent(ACTION_OSU_NOTIFICATION);
+ intent.putExtra(SP_NAME, spName);
+ intent.putExtra(DEAUTH, ess);
+ intent.putExtra(DEAUTH_DELAY, delay);
+ intent.putExtra(DEAUTH_URL, url);
+ intent.setFlags(
+ Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivity(intent);
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/AuthMatch.java b/packages/Osu/src/com/android/hotspot2/AuthMatch.java
new file mode 100644
index 0000000..f9c1f42
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/AuthMatch.java
@@ -0,0 +1,39 @@
+package com.android.hotspot2;
+
+/**
+ * Match score for EAP credentials:
+ * None means that there is a distinct mismatch, i.e. realm, method or parameter is defined
+ * and mismatches that of the credential.
+ * Indeterminate means that there is no ANQP information to match against.
+ * Note: The numeric values given to the constants are used for preference comparison and
+ * must be maintained accordingly.
+ */
+public abstract class AuthMatch {
+ public static final int None = -1;
+ public static final int Indeterminate = 0;
+ public static final int Realm = 0x04;
+ public static final int Method = 0x02;
+ public static final int Param = 0x01;
+ public static final int MethodParam = Method | Param;
+ public static final int Exact = Realm | Method | Param;
+
+ public static String toString(int match) {
+ if (match < 0) {
+ return "None";
+ } else if (match == 0) {
+ return "Indeterminate";
+ }
+
+ StringBuilder sb = new StringBuilder();
+ if ((match & Realm) != 0) {
+ sb.append("Realm");
+ }
+ if ((match & Method) != 0) {
+ sb.append("Method");
+ }
+ if ((match & Param) != 0) {
+ sb.append("Param");
+ }
+ return sb.toString();
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/IMSIParameter.java b/packages/Osu/src/com/android/hotspot2/IMSIParameter.java
new file mode 100644
index 0000000..1d5d95d
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/IMSIParameter.java
@@ -0,0 +1,96 @@
+package com.android.hotspot2;
+
+import java.io.IOException;
+
+public class IMSIParameter {
+ private final String mImsi;
+ private final boolean mPrefix;
+
+ public IMSIParameter(String imsi, boolean prefix) {
+ mImsi = imsi;
+ mPrefix = prefix;
+ }
+
+ public IMSIParameter(String imsi) throws IOException {
+ if (imsi == null || imsi.length() == 0) {
+ throw new IOException("Bad IMSI: '" + imsi + "'");
+ }
+
+ int nonDigit;
+ char stopChar = '\0';
+ for (nonDigit = 0; nonDigit < imsi.length(); nonDigit++) {
+ stopChar = imsi.charAt(nonDigit);
+ if (stopChar < '0' || stopChar > '9') {
+ break;
+ }
+ }
+
+ if (nonDigit == imsi.length()) {
+ mImsi = imsi;
+ mPrefix = false;
+ } else if (nonDigit == imsi.length() - 1 && stopChar == '*') {
+ mImsi = imsi.substring(0, nonDigit);
+ mPrefix = true;
+ } else {
+ throw new IOException("Bad IMSI: '" + imsi + "'");
+ }
+ }
+
+ public boolean matches(String fullIMSI) {
+ if (mPrefix) {
+ return mImsi.regionMatches(false, 0, fullIMSI, 0, mImsi.length());
+ } else {
+ return mImsi.equals(fullIMSI);
+ }
+ }
+
+ public boolean matchesMccMnc(String mccMnc) {
+ if (mPrefix) {
+ // For a prefix match, the entire prefix must match the mcc+mnc
+ return mImsi.regionMatches(false, 0, mccMnc, 0, mImsi.length());
+ } else {
+ // For regular match, the entire length of mcc+mnc must match this IMSI
+ return mImsi.regionMatches(false, 0, mccMnc, 0, mccMnc.length());
+ }
+ }
+
+ public boolean isPrefix() {
+ return mPrefix;
+ }
+
+ public String getImsi() {
+ return mImsi;
+ }
+
+ public int prefixLength() {
+ return mImsi.length();
+ }
+
+ @Override
+ public boolean equals(Object thatObject) {
+ if (this == thatObject) {
+ return true;
+ } else if (thatObject == null || getClass() != thatObject.getClass()) {
+ return false;
+ }
+
+ IMSIParameter that = (IMSIParameter) thatObject;
+ return mPrefix == that.mPrefix && mImsi.equals(that.mImsi);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mImsi != null ? mImsi.hashCode() : 0;
+ result = 31 * result + (mPrefix ? 1 : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ if (mPrefix) {
+ return mImsi + '*';
+ } else {
+ return mImsi;
+ }
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/OMADMAdapter.java b/packages/Osu/src/com/android/hotspot2/OMADMAdapter.java
new file mode 100644
index 0000000..1429b0b
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/OMADMAdapter.java
@@ -0,0 +1,601 @@
+package com.android.hotspot2;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.wifi.WifiManager;
+import android.os.SystemProperties;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.anqp.eap.EAP;
+import com.android.hotspot2.omadm.MOTree;
+import com.android.hotspot2.omadm.OMAConstants;
+import com.android.hotspot2.omadm.OMAConstructed;
+import com.android.hotspot2.osu.OSUManager;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.android.anqp.eap.NonEAPInnerAuth.NonEAPType;
+import static com.android.anqp.eap.NonEAPInnerAuth.mapInnerType;
+
+public class OMADMAdapter {
+ private final Context mContext;
+ private final String mImei;
+ private final String mImsi;
+ private final String mDevID;
+ private final List<PathAccessor> mDevInfo;
+ private final List<PathAccessor> mDevDetail;
+
+ private static final int IMEI_Length = 14;
+
+ private static final String[] ExtWiFiPath = {"DevDetail", "Ext", "org.wi-fi", "Wi-Fi"};
+
+ private static final Map<String, String> RTProps = new HashMap<>();
+
+ private MOTree mDevInfoTree;
+ private MOTree mDevDetailTree;
+
+ private static OMADMAdapter sInstance;
+
+ static {
+ RTProps.put(ExtWiFiPath[2], "urn:wfa:mo-ext:hotspot2dot0-devdetail-ext:1.0");
+ }
+
+ private static abstract class PathAccessor {
+ private final String[] mPath;
+ private final int mHashCode;
+
+ protected PathAccessor(Object... path) {
+ int length = 0;
+ for (Object o : path) {
+ if (o.getClass() == String[].class) {
+ length += ((String[]) o).length;
+ } else {
+ length++;
+ }
+ }
+ mPath = new String[length];
+ int n = 0;
+ for (Object o : path) {
+ if (o.getClass() == String[].class) {
+ for (String element : (String[]) o) {
+ mPath[n++] = element;
+ }
+ } else if (o.getClass() == Integer.class) {
+ mPath[n++] = "x" + o.toString();
+ } else {
+ mPath[n++] = o.toString();
+ }
+ }
+ mHashCode = Arrays.hashCode(mPath);
+ }
+
+ @Override
+ public int hashCode() {
+ return mHashCode;
+ }
+
+ @Override
+ public boolean equals(Object thatObject) {
+ return thatObject == this || (thatObject instanceof ConstPathAccessor &&
+ Arrays.equals(mPath, ((PathAccessor) thatObject).mPath));
+ }
+
+ private String[] getPath() {
+ return mPath;
+ }
+
+ protected abstract Object getValue();
+ }
+
+ private static class ConstPathAccessor<T> extends PathAccessor {
+ private final T mValue;
+
+ protected ConstPathAccessor(T value, Object... path) {
+ super(path);
+ mValue = value;
+ }
+
+ protected Object getValue() {
+ return mValue;
+ }
+ }
+
+ public static OMADMAdapter getInstance(Context context) {
+ synchronized (OMADMAdapter.class) {
+ if (sInstance == null) {
+ sInstance = new OMADMAdapter(context);
+ }
+ return sInstance;
+ }
+ }
+
+ private OMADMAdapter(Context context) {
+ mContext = context;
+
+ TelephonyManager tm = (TelephonyManager) context
+ .getSystemService(Context.TELEPHONY_SERVICE);
+ String simOperator = tm.getSimOperator();
+ mImsi = tm.getSubscriberId();
+ mImei = tm.getImei();
+ String strDevId;
+
+ /* Use MEID for sprint */
+ if ("310120".equals(simOperator) || (mImsi != null && mImsi.startsWith("310120"))) {
+ /* MEID is 14 digits. If IMEI is returned as DevId, MEID can be extracted by taking
+ * first 14 characters. This is not always true but should be the case for sprint */
+ strDevId = tm.getDeviceId().toUpperCase(Locale.US);
+ if (strDevId != null && strDevId.length() >= IMEI_Length) {
+ strDevId = strDevId.substring(0, IMEI_Length);
+ } else {
+ Log.w(OSUManager.TAG, "MEID cannot be extracted from DeviceId " + strDevId);
+ }
+ } else {
+ if (isPhoneTypeLTE()) {
+ strDevId = mImei;
+ } else {
+ strDevId = tm.getDeviceId();
+ }
+ if (strDevId == null) {
+ strDevId = "unknown";
+ }
+ strDevId = strDevId.toUpperCase(Locale.US);
+
+ if (!isPhoneTypeLTE()) {
+ strDevId = strDevId.substring(0, IMEI_Length);
+ }
+ }
+ mDevID = strDevId;
+
+ mDevInfo = new ArrayList<>();
+ mDevInfo.add(new ConstPathAccessor<>(strDevId, "DevInfo", "DevID"));
+ mDevInfo.add(new ConstPathAccessor<>(getProperty(context,
+ "Man", "ro.product.manufacturer", "unknown"), "DevInfo", "Man"));
+ mDevInfo.add(new ConstPathAccessor<>(getProperty(context,
+ "Mod", "ro.product.model", "generic"), "DevInfo", "Mod"));
+ mDevInfo.add(new ConstPathAccessor<>(getLocale(context), "DevInfo", "Lang"));
+ mDevInfo.add(new ConstPathAccessor<>("1.2", "DevInfo", "DmV"));
+
+ mDevDetail = new ArrayList<>();
+ mDevDetail.add(new ConstPathAccessor<>(getDeviceType(), "DevDetail", "DevType"));
+ mDevDetail.add(new ConstPathAccessor<>(SystemProperties.get("ro.product.brand"),
+ "DevDetail", "OEM"));
+ mDevDetail.add(new ConstPathAccessor<>(getVersion(context, false), "DevDetail", "FwV"));
+ mDevDetail.add(new ConstPathAccessor<>(getVersion(context, true), "DevDetail", "SwV"));
+ mDevDetail.add(new ConstPathAccessor<>(getHwV(), "DevDetail", "HwV"));
+ mDevDetail.add(new ConstPathAccessor<>("TRUE", "DevDetail", "LrgObj"));
+
+ mDevDetail.add(new ConstPathAccessor<>(32, "DevDetail", "URI", "MaxDepth"));
+ mDevDetail.add(new ConstPathAccessor<>(2048, "DevDetail", "URI", "MaxTotLen"));
+ mDevDetail.add(new ConstPathAccessor<>(64, "DevDetail", "URI", "MaxSegLen"));
+
+ AtomicInteger index = new AtomicInteger(1);
+ mDevDetail.add(new ConstPathAccessor<>(EAP.EAP_TTLS, ExtWiFiPath,
+ "EAPMethodList", index, "EAPType"));
+ mDevDetail.add(new ConstPathAccessor<>(mapInnerType(NonEAPType.MSCHAPv2), ExtWiFiPath,
+ "EAPMethodList", index, "InnerMethod"));
+
+ index.incrementAndGet();
+ mDevDetail.add(new ConstPathAccessor<>(EAP.EAP_TTLS, ExtWiFiPath,
+ "EAPMethodList", index, "EAPType"));
+ mDevDetail.add(new ConstPathAccessor<>(mapInnerType(NonEAPType.PAP), ExtWiFiPath,
+ "EAPMethodList", index, "InnerMethod"));
+
+ index.incrementAndGet();
+ mDevDetail.add(new ConstPathAccessor<>(EAP.EAP_TTLS, ExtWiFiPath,
+ "EAPMethodList", index, "EAPType"));
+ mDevDetail.add(new ConstPathAccessor<>(mapInnerType(NonEAPType.MSCHAP), ExtWiFiPath,
+ "EAPMethodList", index, "InnerMethod"));
+
+ index.incrementAndGet();
+ mDevDetail.add(new ConstPathAccessor<>(EAP.EAP_TLS, ExtWiFiPath,
+ "EAPMethodList", index, "EAPType"));
+ index.incrementAndGet();
+ mDevDetail.add(new ConstPathAccessor<>(EAP.EAP_AKA, ExtWiFiPath,
+ "EAPMethodList", index, "EAPType"));
+ index.incrementAndGet();
+ mDevDetail.add(new ConstPathAccessor<>(EAP.EAP_AKAPrim, ExtWiFiPath,
+ "EAPMethodList", index, "EAPType"));
+ index.incrementAndGet();
+ mDevDetail.add(new ConstPathAccessor<>(EAP.EAP_SIM, ExtWiFiPath,
+ "EAPMethodList", index, "EAPType"));
+
+ mDevDetail.add(new ConstPathAccessor<>("FALSE", ExtWiFiPath, "ManufacturingCertificate"));
+ mDevDetail.add(new ConstPathAccessor<>(mImsi, ExtWiFiPath, "IMSI"));
+ mDevDetail.add(new ConstPathAccessor<>(mImei, ExtWiFiPath, "IMEI_MEID"));
+ mDevDetail.add(new PathAccessor(ExtWiFiPath, "Wi-FiMACAddress") {
+ @Override
+ protected String getValue() {
+ return getMAC();
+ }
+ });
+ }
+
+ private static void buildNode(PathAccessor pathAccessor, int depth, OMAConstructed parent)
+ throws IOException {
+ String[] path = pathAccessor.getPath();
+ String name = path[depth];
+ if (depth < path.length - 1) {
+ OMAConstructed node = (OMAConstructed) parent.getChild(name);
+ if (node == null) {
+ node = (OMAConstructed) parent.addChild(name, RTProps.get(name),
+ null, null);
+ }
+ buildNode(pathAccessor, depth + 1, node);
+ } else if (pathAccessor.getValue() != null) {
+ parent.addChild(name, null, pathAccessor.getValue().toString(), null);
+ }
+ }
+
+ public String getMAC() {
+ WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ return wifiManager != null ?
+ String.format("%012x",
+ Utils.parseMac(wifiManager.getConnectionInfo().getMacAddress())) :
+ null;
+ }
+
+ public String getImei() {
+ return mImei;
+ }
+
+ public byte[] getMeid() {
+ return Arrays.copyOf(mImei.getBytes(StandardCharsets.ISO_8859_1), IMEI_Length);
+ }
+
+ public String getDevID() {
+ return mDevID;
+ }
+
+ public MOTree getMO(String urn) {
+ try {
+ switch (urn) {
+ case OMAConstants.DevInfoURN:
+ if (mDevInfoTree == null) {
+ OMAConstructed root = new OMAConstructed(null, "DevInfo", urn);
+ for (PathAccessor pathAccessor : mDevInfo) {
+ buildNode(pathAccessor, 1, root);
+ }
+ mDevInfoTree = MOTree.buildMgmtTree(OMAConstants.DevInfoURN,
+ OMAConstants.OMAVersion, root);
+ }
+ return mDevInfoTree;
+ case OMAConstants.DevDetailURN:
+ if (mDevDetailTree == null) {
+ OMAConstructed root = new OMAConstructed(null, "DevDetail", urn);
+ for (PathAccessor pathAccessor : mDevDetail) {
+ buildNode(pathAccessor, 1, root);
+ }
+ mDevDetailTree = MOTree.buildMgmtTree(OMAConstants.DevDetailURN,
+ OMAConstants.OMAVersion, root);
+ }
+ return mDevDetailTree;
+ default:
+ throw new IllegalArgumentException(urn);
+ }
+ } catch (IOException ioe) {
+ Log.e(OSUManager.TAG, "Caught exception building OMA Tree: " + ioe, ioe);
+ return null;
+ }
+
+ /*
+ switch (urn) {
+ case DevInfoURN: return DevInfo;
+ case DevDetailURN: return DevDetail;
+ default: throw new IllegalArgumentException(urn);
+ }
+ */
+ }
+
+ // TODO: For now, assume the device supports LTE.
+ private static boolean isPhoneTypeLTE() {
+ return true;
+ }
+
+ private static String getHwV() {
+ try {
+ return SystemProperties.get("ro.hardware", "Unknown")
+ + "." + SystemProperties.get("ro.revision", "Unknown");
+ } catch (RuntimeException e) {
+ return "Unknown";
+ }
+ }
+
+ private static String getDeviceType() {
+ String devicetype = SystemProperties.get("ro.build.characteristics");
+ if ((((TextUtils.isEmpty(devicetype)) || (!devicetype.equals("tablet"))))) {
+ devicetype = "phone";
+ }
+ return devicetype;
+ }
+
+ private static String getVersion(Context context, boolean swv) {
+ String version;
+ try {
+ if (!isSprint(context) && swv) {
+ return "Android " + SystemProperties.get("ro.build.version.release");
+ } else {
+ version = SystemProperties.get("ro.build.version.full");
+ if (null == version || version.equals("")) {
+ return SystemProperties.get("ro.build.id", null) + "~"
+ + SystemProperties.get("ro.build.config.version", null) + "~"
+ + SystemProperties.get("gsm.version.baseband", null) + "~"
+ + SystemProperties.get("ro.gsm.flexversion", null);
+ }
+ }
+ } catch (RuntimeException e) {
+ return "Unknown";
+ }
+ return version;
+ }
+
+ private static boolean isSprint(Context context) {
+ TelephonyManager tm = (TelephonyManager) context
+ .getSystemService(Context.TELEPHONY_SERVICE);
+ String simOperator = tm.getSimOperator();
+ String imsi = tm.getSubscriberId();
+ /* Use MEID for sprint */
+ if ("310120".equals(simOperator) || (imsi != null && imsi.startsWith("310120"))) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private static String getLocale(Context context) {
+ String strLang = readValueFromFile(context, "Lang");
+ if (strLang == null) {
+ strLang = Locale.getDefault().toString();
+ }
+ return strLang;
+ }
+
+ private static String getProperty(Context context, String key, String propKey, String dflt) {
+ String strMan = readValueFromFile(context, key);
+ if (strMan == null) {
+ strMan = SystemProperties.get(propKey, dflt);
+ }
+ return strMan;
+ }
+
+ private static String readValueFromFile(Context context, String propName) {
+ String ret = null;
+ // use preference instead of the system property
+ SharedPreferences prefs = context.getSharedPreferences("dmconfig", 0);
+ if (prefs.contains(propName)) {
+ ret = prefs.getString(propName, "");
+ if (ret.length() == 0) {
+ ret = null;
+ }
+ }
+ return ret;
+ }
+
+ private static final String DevDetail =
+ "<MgmtTree>" +
+ "<VerDTD>1.2</VerDTD>" +
+ "<Node>" +
+ "<NodeName>DevDetail</NodeName>" +
+ "<RTProperties>" +
+ "<Type>" +
+ "<DDFName>urn:oma:mo:oma-dm-devdetail:1.0</DDFName>" +
+ "</Type>" +
+ "</RTProperties>" +
+ "<Node>" +
+ "<NodeName>Ext</NodeName>" +
+ "<Node>" +
+ "<NodeName>org.wi-fi</NodeName>" +
+ "<RTProperties>" +
+ "<Type>" +
+ "<DDFName>" +
+ "urn:wfa:mo-ext:hotspot2dot0-devdetail-ext :1.0" +
+ "</DDFName>" +
+ "</Type>" +
+ "</RTProperties>" +
+ "<Node>" +
+ "<NodeName>Wi-Fi</NodeName>" +
+ "<Node>" +
+ "<NodeName>EAPMethodList</NodeName>" +
+ "<Node>" +
+ "<NodeName>Method01</NodeName>" +
+ "<!-- EAP-TTLS/MS-CHAPv2 -->" +
+ "<Node>" +
+ "<NodeName>EAPType</NodeName>" +
+ "<Value>21</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>InnerMethod</NodeName>" +
+ "<Value>MS-CHAP-V2</Value>" +
+ "</Node>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>Method02</NodeName>" +
+ "<!-- EAP-TLS -->" +
+ "<Node>" +
+ "<NodeName>EAPType</NodeName>" +
+ "<Value>13</Value>" +
+ "</Node>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>Method03</NodeName>" +
+ "<!-- EAP-SIM -->" +
+ "<Node>" +
+ "<NodeName>EAPType</NodeName>" +
+ "<Value>18</Value>" +
+ "</Node>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>Method04</NodeName>" +
+ "<!-- EAP-AKA -->" +
+ "<Node>" +
+ "<NodeName>EAPType</NodeName>" +
+ "<Value>23</Value>" +
+ "</Node>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>Method05</NodeName>" +
+ "<!-- EAP-AKA' -->" +
+ "<Node>" +
+ "<NodeName>EAPType</NodeName>" +
+ "<Value>50</Value>" +
+ "</Node>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>Method06</NodeName>" +
+ "<!-- Supported method (EAP-TTLS/PAP) not mandated by Hotspot2.0-->" +
+ "<Node>" +
+ "<NodeName>EAPType</NodeName>" +
+ "<Value>21</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>InnerMethod</NodeName>" +
+ "<Value>PAP</Value>" +
+ "</Node>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>Method07</NodeName>" +
+ "<!-- Supported method (PEAP/EAP-GTC) not mandated by Hotspot 2.0-->" +
+ "<Node>" +
+ "<NodeName>EAPType</NodeName>" +
+ "<Value>25</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>InnerEAPType</NodeName>" +
+ "<Value>6</Value>" +
+ "</Node>" +
+ "</Node>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>SPCertificate</NodeName>" +
+ "<Node>" +
+ "<NodeName>Cert01</NodeName>" +
+ "<Node>" +
+ "<NodeName>CertificateIssuerName</NodeName>" +
+ "<Value>CN=RuckusCA</Value>" +
+ "</Node>" +
+ "</Node>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>ManufacturingCertificate</NodeName>" +
+ "<Value>FALSE</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>Wi-FiMACAddress</NodeName>" +
+ "<Value>001d2e112233</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>ClientTriggerRedirectURI</NodeName>" +
+ "<Value>http://127.0.0.1:12345/index.htm</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>Ops</NodeName>" +
+ "<Node>" +
+ "<NodeName>launchBrowserToURI</NodeName>" +
+ "<Value></Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>negotiateClientCertTLS</NodeName>" +
+ "<Value></Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>getCertificate</NodeName>" +
+ "<Value></Value>" +
+ "</Node>" +
+ "</Node>" +
+ "</Node>" +
+ "<!-- End of Wi-Fi node -->" +
+ "</Node>" +
+ "<!-- End of org.wi-fi node -->" +
+ "</Node>" +
+ "<!-- End of Ext node -->" +
+ "<Node>" +
+ "<NodeName>URI</NodeName>" +
+ "<Node>" +
+ "<NodeName>MaxDepth</NodeName>" +
+ "<Value>32</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>MaxTotLen</NodeName>" +
+ "<Value>2048</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>MaxSegLen</NodeName>" +
+ "<Value>64</Value>" +
+ "</Node>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>DevType</NodeName>" +
+ "<Value>Smartphone</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>OEM</NodeName>" +
+ "<Value>ACME</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>FwV</NodeName>" +
+ "<Value>1.2.100.5</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>SwV</NodeName>" +
+ "<Value>9.11.130</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>HwV</NodeName>" +
+ "<Value>1.0</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>LrgObj</NodeName>" +
+ "<Value>TRUE</Value>" +
+ "</Node>" +
+ "</Node>" +
+ "</MgmtTree>";
+
+
+ private static final String DevInfo =
+ "<MgmtTree>" +
+ "<VerDTD>1.2</VerDTD>" +
+ "<Node>" +
+ "<NodeName>DevInfo</NodeName>" +
+ "<RTProperties>" +
+ "<Type>" +
+ "<DDFName>urn:oma:mo:oma-dm-devinfo:1.0" +
+ "</DDFName>" +
+ "</Type>" +
+ "</RTProperties>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>DevID</NodeName>" +
+ "<Path>DevInfo</Path>" +
+ "<Value>urn:acme:00-11-22-33-44-55</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>Man</NodeName>" +
+ "<Path>DevInfo</Path>" +
+ "<Value>ACME</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>Mod</NodeName>" +
+ "<Path>DevInfo</Path>" +
+ "<Value>HS2.0-01</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>DmV</NodeName>" +
+ "<Path>DevInfo</Path>" +
+ "<Value>1.2</Value>" +
+ "</Node>" +
+ "<Node>" +
+ "<NodeName>Lang</NodeName>" +
+ "<Path>DevInfo</Path>" +
+ "<Value>en-US</Value>" +
+ "</Node>" +
+ "</MgmtTree>";
+}
diff --git a/packages/Osu/src/com/android/hotspot2/PasspointMatch.java b/packages/Osu/src/com/android/hotspot2/PasspointMatch.java
new file mode 100644
index 0000000..8330283
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/PasspointMatch.java
@@ -0,0 +1,9 @@
+package com.android.hotspot2;
+
+public enum PasspointMatch {
+ HomeProvider,
+ RoamingProvider,
+ Incomplete,
+ None,
+ Declined
+}
diff --git a/packages/Osu/src/com/android/hotspot2/Utils.java b/packages/Osu/src/com/android/hotspot2/Utils.java
new file mode 100644
index 0000000..100b967
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/Utils.java
@@ -0,0 +1,300 @@
+package com.android.hotspot2;
+
+import com.android.anqp.Constants;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.TimeZone;
+
+import static com.android.anqp.Constants.BYTE_MASK;
+import static com.android.anqp.Constants.NIBBLE_MASK;
+
+public abstract class Utils {
+
+ public static final long UNSET_TIME = -1;
+
+ private static final int EUI48Length = 6;
+ private static final int EUI64Length = 8;
+ private static final long EUI48Mask = 0xffffffffffffL;
+ private static final String[] PLMNText = {"org", "3gppnetwork", "mcc*", "mnc*", "wlan"};
+
+ public static List<String> splitDomain(String domain) {
+
+ if (domain.endsWith("."))
+ domain = domain.substring(0, domain.length() - 1);
+ int at = domain.indexOf('@');
+ if (at >= 0)
+ domain = domain.substring(at + 1);
+
+ String[] labels = domain.toLowerCase().split("\\.");
+ LinkedList<String> labelList = new LinkedList<String>();
+ for (String label : labels) {
+ labelList.addFirst(label);
+ }
+
+ return labelList;
+ }
+
+ public static long parseMac(String s) {
+
+ long mac = 0;
+ int count = 0;
+ for (int n = 0; n < s.length(); n++) {
+ int nibble = Utils.fromHex(s.charAt(n), true); // Set lenient to not blow up on ':'
+ if (nibble >= 0) { // ... and use only legit hex.
+ mac = (mac << 4) | nibble;
+ count++;
+ }
+ }
+ if (count < 12 || (count & 1) == 1) {
+ throw new IllegalArgumentException("Bad MAC address: '" + s + "'");
+ }
+ return mac;
+ }
+
+ public static String macToString(long mac) {
+ int len = (mac & ~EUI48Mask) != 0 ? EUI64Length : EUI48Length;
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for (int n = (len - 1) * Byte.SIZE; n >= 0; n -= Byte.SIZE) {
+ if (first) {
+ first = false;
+ } else {
+ sb.append(':');
+ }
+ sb.append(String.format("%02x", (mac >>> n) & Constants.BYTE_MASK));
+ }
+ return sb.toString();
+ }
+
+ public static String getMccMnc(List<String> domain) {
+ if (domain.size() != PLMNText.length) {
+ return null;
+ }
+
+ for (int n = 0; n < PLMNText.length; n++) {
+ String expect = PLMNText[n];
+ int len = expect.endsWith("*") ? expect.length() - 1 : expect.length();
+ if (!domain.get(n).regionMatches(0, expect, 0, len)) {
+ return null;
+ }
+ }
+
+ String prefix = domain.get(2).substring(3) + domain.get(3).substring(3);
+ for (int n = 0; n < prefix.length(); n++) {
+ char ch = prefix.charAt(n);
+ if (ch < '0' || ch > '9') {
+ return null;
+ }
+ }
+ return prefix;
+ }
+
+ public static String bssidsToString(Collection<Long> bssids) {
+ StringBuilder sb = new StringBuilder();
+ for (Long bssid : bssids) {
+ sb.append(String.format(" %012x", bssid));
+ }
+ return sb.toString();
+ }
+
+ public static String roamingConsortiumsToString(long[] ois) {
+ if (ois == null) {
+ return "null";
+ }
+ List<Long> list = new ArrayList<Long>(ois.length);
+ for (long oi : ois) {
+ list.add(oi);
+ }
+ return roamingConsortiumsToString(list);
+ }
+
+ public static String roamingConsortiumsToString(Collection<Long> ois) {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for (long oi : ois) {
+ if (first) {
+ first = false;
+ } else {
+ sb.append(", ");
+ }
+ if (Long.numberOfLeadingZeros(oi) > 40) {
+ sb.append(String.format("%06x", oi));
+ } else {
+ sb.append(String.format("%010x", oi));
+ }
+ }
+ return sb.toString();
+ }
+
+ public static String toUnicodeEscapedString(String s) {
+ StringBuilder sb = new StringBuilder(s.length());
+ for (int n = 0; n < s.length(); n++) {
+ char ch = s.charAt(n);
+ if (ch >= ' ' && ch < 127) {
+ sb.append(ch);
+ } else {
+ sb.append("\\u").append(String.format("%04x", (int) ch));
+ }
+ }
+ return sb.toString();
+ }
+
+ public static String toHexString(byte[] data) {
+ if (data == null) {
+ return "null";
+ }
+ StringBuilder sb = new StringBuilder(data.length * 3);
+
+ boolean first = true;
+ for (byte b : data) {
+ if (first) {
+ first = false;
+ } else {
+ sb.append(' ');
+ }
+ sb.append(String.format("%02x", b & BYTE_MASK));
+ }
+ return sb.toString();
+ }
+
+ public static String toHex(byte[] octets) {
+ StringBuilder sb = new StringBuilder(octets.length * 2);
+ for (byte o : octets) {
+ sb.append(String.format("%02x", o & BYTE_MASK));
+ }
+ return sb.toString();
+ }
+
+ public static byte[] hexToBytes(String text) {
+ if ((text.length() & 1) == 1) {
+ throw new NumberFormatException("Odd length hex string: " + text.length());
+ }
+ byte[] data = new byte[text.length() >> 1];
+ int position = 0;
+ for (int n = 0; n < text.length(); n += 2) {
+ data[position] =
+ (byte) (((fromHex(text.charAt(n), false) & NIBBLE_MASK) << 4) |
+ (fromHex(text.charAt(n + 1), false) & NIBBLE_MASK));
+ position++;
+ }
+ return data;
+ }
+
+ public static int fromHex(char ch, boolean lenient) throws NumberFormatException {
+ if (ch <= '9' && ch >= '0') {
+ return ch - '0';
+ } else if (ch >= 'a' && ch <= 'f') {
+ return ch + 10 - 'a';
+ } else if (ch <= 'F' && ch >= 'A') {
+ return ch + 10 - 'A';
+ } else if (lenient) {
+ return -1;
+ } else {
+ throw new NumberFormatException("Bad hex-character: " + ch);
+ }
+ }
+
+ private static char toAscii(int b) {
+ return b >= ' ' && b < 0x7f ? (char) b : '.';
+ }
+
+ static boolean isDecimal(String s) {
+ for (int n = 0; n < s.length(); n++) {
+ char ch = s.charAt(n);
+ if (ch < '0' || ch > '9') {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static <T extends Comparable> int compare(Comparable<T> c1, T c2) {
+ if (c1 == null) {
+ return c2 == null ? 0 : -1;
+ } else if (c2 == null) {
+ return 1;
+ } else {
+ return c1.compareTo(c2);
+ }
+ }
+
+ public static String bytesToBingoCard(ByteBuffer data, int len) {
+ ByteBuffer dup = data.duplicate();
+ dup.limit(dup.position() + len);
+ return bytesToBingoCard(dup);
+ }
+
+ public static String bytesToBingoCard(ByteBuffer data) {
+ ByteBuffer dup = data.duplicate();
+ StringBuilder sbx = new StringBuilder();
+ while (dup.hasRemaining()) {
+ sbx.append(String.format("%02x ", dup.get() & BYTE_MASK));
+ }
+ dup = data.duplicate();
+ sbx.append(' ');
+ while (dup.hasRemaining()) {
+ sbx.append(String.format("%c", toAscii(dup.get() & BYTE_MASK)));
+ }
+ return sbx.toString();
+ }
+
+ public static String toHMS(long millis) {
+ long time = millis >= 0 ? millis : -millis;
+ long tmp = time / 1000L;
+ long ms = time - tmp * 1000L;
+
+ time = tmp;
+ tmp /= 60L;
+ long s = time - tmp * 60L;
+
+ time = tmp;
+ tmp /= 60L;
+ long m = time - tmp * 60L;
+
+ return String.format("%s%d:%02d:%02d.%03d", millis < 0 ? "-" : "", tmp, m, s, ms);
+ }
+
+ public static String toUTCString(long ms) {
+ if (ms < 0) {
+ return "unset";
+ }
+ Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+ c.setTimeInMillis(ms);
+ return String.format("%4d/%02d/%02d %2d:%02d:%02dZ",
+ c.get(Calendar.YEAR),
+ c.get(Calendar.MONTH) + 1,
+ c.get(Calendar.DAY_OF_MONTH),
+ c.get(Calendar.HOUR_OF_DAY),
+ c.get(Calendar.MINUTE),
+ 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;
+ }
+ }
+
+
+ public static void delay(long ms) {
+ long until = System.currentTimeMillis() + ms;
+ for (; ; ) {
+ long remainder = until - System.currentTimeMillis();
+ if (remainder <= 0) {
+ break;
+ }
+ try {
+ Thread.sleep(remainder);
+ } catch (InterruptedException ie) { /**/ }
+ }
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/WifiNetworkAdapter.java b/packages/Osu/src/com/android/hotspot2/WifiNetworkAdapter.java
new file mode 100644
index 0000000..518e64e
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/WifiNetworkAdapter.java
@@ -0,0 +1,388 @@
+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.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<String, PasspointConfig> 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<HomeSP> 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) {
+ loadAllSps();
+ }
+
+ private void loadAllSps() {
+ Log.d(OSUManager.TAG, "Loading all SPs");
+ WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ for (WifiConfiguration config : wifiManager.getPrivilegedConfiguredNetworks()) {
+ String moTree = config.getMoTree();
+ if (moTree != null) {
+ try {
+ mPasspointConfigs.put(config.FQDN, new PasspointConfig(config));
+ } catch (IOException | SAXException e) {
+ Log.w(OSUManager.TAG, "Failed to parse MO: " + e);
+ }
+ }
+ }
+ }
+
+ public Collection<HomeSP> getLoadedSPs() {
+ List<HomeSP> 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 HomeSP addSP(MOTree instanceTree) throws IOException, SAXException {
+ WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ String xml = instanceTree.toXml();
+ wifiManager.addPasspointManagementObject(xml);
+ return MOManager.buildSP(xml);
+ }
+
+ public void removeSP(String fqdn) throws IOException {
+ WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ }
+
+ public HomeSP modifySP(HomeSP homeSP, Collection<MOData> mods)
+ throws IOException {
+ WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ return null;
+ }
+
+ 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 WifiConfiguration getActivePasspointNetwork() {
+ PasspointConfig passpointConfig = getActivePasspointConfig();
+ return passpointConfig != null ? passpointConfig.getWifiConfiguration() : 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 HomeSP getCurrentSP() {
+ PasspointConfig passpointConfig = getActivePasspointConfig();
+ return passpointConfig != null ? passpointConfig.getHomeSP() : 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<OSUCertType, List<X509Certificate>> certs,
+ PrivateKey privateKey, Network osuNetwork)
+ throws IOException, GeneralSecurityException {
+
+ List<X509Certificate> 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<X509Certificate> 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.getSSID() + '"';
+ if (osuInfo.getOSUBssid() != 0) {
+ config.BSSID = Utils.macToString(osuInfo.getOSUBssid());
+ Log.d(OSUManager.TAG, String.format("Setting BSSID of '%s' to %012x",
+ osuInfo.getSSID(), 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/asn1/Asn1Boolean.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Boolean.java
new file mode 100644
index 0000000..18af3b8
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Boolean.java
@@ -0,0 +1,31 @@
+package com.android.hotspot2.asn1;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+
+public class Asn1Boolean extends Asn1Object {
+ private final boolean mBoolean;
+
+ public Asn1Boolean(int tag, Asn1Class asn1Class, int length, ByteBuffer data)
+ throws DecodeException {
+ super(tag, asn1Class, false, length);
+ if (length != 1) {
+ throw new DecodeException("Boolean length != 1: " + length, data.position());
+ }
+ mBoolean = data.get() != 0;
+ }
+
+ public boolean getValue() {
+ return mBoolean;
+ }
+
+ @Override
+ public Collection<Asn1Object> getChildren() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + "=" + Boolean.toString(mBoolean);
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Class.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Class.java
new file mode 100644
index 0000000..8a4d8a8
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Class.java
@@ -0,0 +1,5 @@
+package com.android.hotspot2.asn1;
+
+public enum Asn1Class {
+ Universal, Application, Context, Private
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Constructed.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Constructed.java
new file mode 100644
index 0000000..69b65dc
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Constructed.java
@@ -0,0 +1,53 @@
+package com.android.hotspot2.asn1;
+
+import java.nio.ByteBuffer;
+import java.util.*;
+
+public class Asn1Constructed extends Asn1Object {
+ private final int mTagPosition;
+ private final List<Asn1Object> mChildren;
+
+ public Asn1Constructed(int tag, Asn1Class asn1Class, int length,
+ ByteBuffer payload, int tagPosition) {
+ super(tag, asn1Class, true, length, payload);
+ mTagPosition = tagPosition;
+ mChildren = new ArrayList<>();
+ }
+
+ public void addChild(Asn1Object object) {
+ mChildren.add(object);
+ }
+
+ @Override
+ public Collection<Asn1Object> getChildren() {
+ return Collections.unmodifiableCollection(mChildren);
+ }
+
+ public ByteBuffer getEncoding() {
+ return getPayload(mTagPosition);
+ }
+
+ private void toString(int level, StringBuilder sb) {
+ sb.append(indent(level)).append(super.toString()).append(":\n");
+ for (Asn1Object child : mChildren) {
+ if (child.isConstructed()) {
+ ((Asn1Constructed) child).toString(level + 1, sb);
+ } else {
+ sb.append(indent(level + 1)).append(child.toString()).append('\n');
+ }
+ }
+ }
+
+ public static String indent(int level) {
+ char[] indent = new char[level * 2];
+ Arrays.fill(indent, ' ');
+ return new String(indent);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ toString(0, sb);
+ return sb.toString();
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Decoder.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Decoder.java
new file mode 100644
index 0000000..53452e7
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Decoder.java
@@ -0,0 +1,211 @@
+package com.android.hotspot2.asn1;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+public class Asn1Decoder {
+ public static final int TAG_UNIVZERO = 0x00;
+ public static final int TAG_BOOLEAN = 0x01;
+ public static final int TAG_INTEGER = 0x02;
+ public static final int TAG_BITSTRING = 0x03;
+ public static final int TAG_OCTET_STRING = 0x04;
+ public static final int TAG_NULL = 0x05;
+ public static final int TAG_OID = 0x06;
+ public static final int TAG_ObjectDescriptor = 0x07;
+ public static final int TAG_EXTERNAL = 0x08;
+ public static final int TAG_REAL = 0x09;
+ public static final int TAG_ENUMERATED = 0x0a;
+ public static final int TAG_UTF8String = 0x0c; // * (*) are X.509 DirectoryString's
+ public static final int TAG_RelativeOID = 0x0d;
+ public static final int TAG_SEQ = 0x10; // 30 if constructed
+ public static final int TAG_SET = 0x11;
+ public static final int TAG_NumericString = 0x12; // [UNIVERSAL 18]
+ public static final int TAG_PrintableString = 0x13; // * [UNIVERSAL 19]
+ public static final int TAG_T61String = 0x14; // * TeletexString [UNIVERSAL 20]
+ public static final int TAG_VideotexString = 0x15; // [UNIVERSAL 21]
+ public static final int TAG_IA5String = 0x16; // [UNIVERSAL 22]
+ public static final int TAG_UTCTime = 0x17;
+ public static final int TAG_GeneralizedTime = 0x18;
+ public static final int TAG_GraphicString = 0x19; // [UNIVERSAL 25]
+ public static final int TAG_VisibleString = 0x1a; // ISO64String [UNIVERSAL 26]
+ public static final int TAG_GeneralString = 0x1b; // [UNIVERSAL 27]
+ public static final int TAG_UniversalString = 0x1c; // * [UNIVERSAL 28]
+ public static final int TAG_BMPString = 0x1e; // * [UNIVERSAL 30]
+
+ public static final int IntOverflow = 0xffff0000;
+ public static final int MoreBit = 0x80;
+ public static final int MoreData = 0x7f;
+ public static final int ConstructedBit = 0x20;
+ public static final int ClassShift = 6;
+ public static final int ClassMask = 0x3;
+ public static final int MoreWidth = 7;
+ public static final int ByteWidth = 8;
+ public static final int ByteMask = 0xff;
+ public static final int ContinuationTag = 31;
+
+ public static final int IndefiniteLength = -1;
+
+ private static final Map<Integer, Asn1Tag> sTagMap = new HashMap<>();
+
+ static {
+ sTagMap.put(TAG_UNIVZERO, Asn1Tag.UNIVZERO);
+ sTagMap.put(TAG_BOOLEAN, Asn1Tag.BOOLEAN);
+ sTagMap.put(TAG_INTEGER, Asn1Tag.INTEGER);
+ sTagMap.put(TAG_BITSTRING, Asn1Tag.BITSTRING);
+ sTagMap.put(TAG_OCTET_STRING, Asn1Tag.OCTET_STRING);
+ sTagMap.put(TAG_NULL, Asn1Tag.NULL);
+ sTagMap.put(TAG_OID, Asn1Tag.OID);
+ sTagMap.put(TAG_ObjectDescriptor, Asn1Tag.ObjectDescriptor);
+ sTagMap.put(TAG_EXTERNAL, Asn1Tag.EXTERNAL);
+ sTagMap.put(TAG_REAL, Asn1Tag.REAL);
+ sTagMap.put(TAG_ENUMERATED, Asn1Tag.ENUMERATED);
+ sTagMap.put(TAG_UTF8String, Asn1Tag.UTF8String);
+ sTagMap.put(TAG_RelativeOID, Asn1Tag.RelativeOID);
+ sTagMap.put(TAG_SEQ, Asn1Tag.SEQUENCE);
+ sTagMap.put(TAG_SET, Asn1Tag.SET);
+ sTagMap.put(TAG_NumericString, Asn1Tag.NumericString);
+ sTagMap.put(TAG_PrintableString, Asn1Tag.PrintableString);
+ sTagMap.put(TAG_T61String, Asn1Tag.T61String);
+ sTagMap.put(TAG_VideotexString, Asn1Tag.VideotexString);
+ sTagMap.put(TAG_IA5String, Asn1Tag.IA5String);
+ sTagMap.put(TAG_UTCTime, Asn1Tag.UTCTime);
+ sTagMap.put(TAG_GeneralizedTime, Asn1Tag.GeneralizedTime);
+ sTagMap.put(TAG_GraphicString, Asn1Tag.GraphicString);
+ sTagMap.put(TAG_VisibleString, Asn1Tag.VisibleString);
+ sTagMap.put(TAG_GeneralString, Asn1Tag.GeneralString);
+ sTagMap.put(TAG_UniversalString, Asn1Tag.UniversalString);
+ sTagMap.put(TAG_BMPString, Asn1Tag.BMPString);
+ }
+
+ public static Asn1Tag mapTag(int tag) {
+ return sTagMap.get(tag);
+ }
+
+ public static Collection<Asn1Object> decode(ByteBuffer data) throws DecodeException {
+ Asn1Constructed root =
+ new Asn1Constructed(0, null, data.remaining(), data, data.position());
+ decode(0, root);
+ return root.getChildren();
+ }
+
+ private static void decode(int level, Asn1Constructed parent) throws DecodeException {
+ ByteBuffer data = parent.getPayload();
+ while (data.hasRemaining()) {
+ int tagPosition = data.position();
+ int propMask = data.get(tagPosition) & ByteMask;
+ if (propMask == 0 && parent.isIndefiniteLength() && data.get(tagPosition + 1) == 0) {
+ parent.setEndOfData(tagPosition);
+ return;
+ }
+ Asn1Class asn1Class = Asn1Class.values()[(propMask >> ClassShift) & ClassMask];
+ boolean constructed = (propMask & ConstructedBit) != 0;
+
+ int tag = decodeTag(data);
+ int length = decodeLength(data);
+
+ if (constructed) {
+ ByteBuffer payload = peelOff(data, length);
+ Asn1Constructed root =
+ new Asn1Constructed(tag, asn1Class, length, payload, tagPosition);
+ decode(level + 1, root);
+ if (length == IndefiniteLength) {
+ data.position(root.getEndOfData() + 2); // advance past '00'
+ }
+ parent.addChild(root);
+ } else {
+ if (asn1Class != Asn1Class.Universal) {
+ parent.addChild(new Asn1Octets(tag, asn1Class, length, data));
+ } else {
+ parent.addChild(buildScalar(tag, asn1Class, length, data));
+ }
+ }
+ }
+ }
+
+ private static ByteBuffer peelOff(ByteBuffer base, int length) {
+ ByteBuffer copy = base.duplicate();
+ if (length == IndefiniteLength) {
+ return copy;
+ }
+ copy.limit(copy.position() + length);
+ base.position(base.position() + length);
+ return copy;
+ }
+
+ private static Asn1Object buildScalar(int tag, Asn1Class asn1Class, int length, ByteBuffer data)
+ throws DecodeException {
+ switch (tag) {
+ case TAG_BOOLEAN:
+ return new Asn1Boolean(tag, asn1Class, length, data);
+ case TAG_INTEGER:
+ case TAG_ENUMERATED:
+ return new Asn1Integer(tag, asn1Class, length, data);
+ case TAG_BITSTRING:
+ int bitResidual = data.get() & ByteMask;
+ return new Asn1Octets(tag, asn1Class, length, data, bitResidual);
+ case TAG_OCTET_STRING:
+ return new Asn1Octets(tag, asn1Class, length, data);
+ case TAG_OID:
+ return new Asn1Oid(tag, asn1Class, length, data);
+ case TAG_UTF8String:
+ case TAG_NumericString:
+ case TAG_PrintableString:
+ case TAG_T61String:
+ case TAG_VideotexString:
+ case TAG_IA5String:
+ case TAG_GraphicString:
+ case TAG_VisibleString:
+ case TAG_GeneralString:
+ case TAG_UniversalString:
+ case TAG_BMPString:
+ return new Asn1String(tag, asn1Class, length, data);
+ case TAG_GeneralizedTime:
+ case TAG_UTCTime:
+ // Should really be a dedicated time object
+ return new Asn1String(tag, asn1Class, length, data);
+ default:
+ return new Asn1Octets(tag, asn1Class, length, data);
+ }
+ }
+
+ private static int decodeTag(ByteBuffer data) throws DecodeException {
+ int tag;
+ byte tag0 = data.get();
+
+ if ((tag = (tag0 & ContinuationTag)) == ContinuationTag) {
+ int tagByte;
+ tag = 0;
+ while (((tagByte = data.get() & ByteMask) & MoreBit) != 0) {
+ tag = (tag << MoreWidth) | (tagByte & MoreData);
+ if ((tag & IntOverflow) != 0)
+ throw new DecodeException("Tag overflow", data.position());
+ }
+ tag = (tag << MoreWidth) | tagByte;
+ }
+ return tag;
+ }
+
+ private static int decodeLength(ByteBuffer data) throws DecodeException {
+ int length;
+ int lenlen = data.get() & ByteMask;
+
+ if ((lenlen & MoreBit) == 0) // One byte encoding
+ length = lenlen;
+ else {
+ lenlen &= MoreData;
+ if (lenlen == 0) {
+ return IndefiniteLength;
+ }
+ length = 0;
+ while (lenlen-- > 0) {
+ length = (length << ByteWidth) | (data.get() & ByteMask);
+ if ((length & IntOverflow) != 0 && lenlen > 0)
+ throw new DecodeException("Length overflow", data.position());
+ }
+ }
+ return length;
+ }
+
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1ID.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1ID.java
new file mode 100644
index 0000000..452d85c
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1ID.java
@@ -0,0 +1,19 @@
+package com.android.hotspot2.asn1;
+
+public class Asn1ID {
+ private final int mTag;
+ private final Asn1Class mClass;
+
+ public Asn1ID(int tag, Asn1Class asn1Class) {
+ mTag = tag;
+ mClass = asn1Class;
+ }
+
+ public int getTag() {
+ return mTag;
+ }
+
+ public Asn1Class getAsn1Class() {
+ return mClass;
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Integer.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Integer.java
new file mode 100644
index 0000000..5180a4d
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Integer.java
@@ -0,0 +1,56 @@
+package com.android.hotspot2.asn1;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+
+public class Asn1Integer extends Asn1Object {
+ private static final int SignBit = 0x80;
+
+ private final long mValue;
+ private final BigInteger mBigValue;
+
+ public Asn1Integer(int tag, Asn1Class asn1Class, int length, ByteBuffer data) {
+ super(tag, asn1Class, false, length);
+
+ if (length <= 8) {
+ long value = (data.get(data.position()) & SignBit) != 0 ? -1 : 0;
+ for (int n = 0; n < length; n++) {
+ value = (value << Byte.SIZE) | data.get();
+ }
+ mValue = value;
+ mBigValue = null;
+ } else {
+ byte[] payload = new byte[length];
+ data.get(payload);
+ mValue = 0;
+ mBigValue = new BigInteger(payload);
+ }
+ }
+
+ public boolean isBigValue() {
+ return mBigValue != null;
+ }
+
+ public long getValue() {
+ return mValue;
+ }
+
+ public BigInteger getBigValue() {
+ return mBigValue;
+ }
+
+ @Override
+ public Collection<Asn1Object> getChildren() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ if (isBigValue()) {
+ return super.toString() + '=' + mBigValue.toString(16);
+ } else {
+ return super.toString() + '=' + mValue;
+ }
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Object.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Object.java
new file mode 100644
index 0000000..8137583
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Object.java
@@ -0,0 +1,88 @@
+package com.android.hotspot2.asn1;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+
+public abstract class Asn1Object {
+ private final int mTag;
+ private final Asn1Class mClass;
+ private final boolean mConstructed;
+ private final int mLength;
+ private final ByteBuffer mPayload;
+
+ protected Asn1Object(int tag, Asn1Class asn1Class, boolean constructed, int length) {
+ this(tag, asn1Class, constructed, length, null);
+ }
+
+ protected Asn1Object(int tag, Asn1Class asn1Class, boolean constructed,
+ int length, ByteBuffer payload) {
+ mTag = tag;
+ mClass = asn1Class;
+ mConstructed = constructed;
+ mLength = length;
+ mPayload = payload != null ? payload.duplicate() : null;
+ }
+
+ public int getTag() {
+ return mTag;
+ }
+
+ public Asn1Class getAsn1Class() {
+ return mClass;
+ }
+
+ public boolean isConstructed() {
+ return mConstructed;
+ }
+
+ public boolean isIndefiniteLength() {
+ return mLength == Asn1Decoder.IndefiniteLength;
+ }
+
+ public int getLength() {
+ return mLength;
+ }
+
+ public ByteBuffer getPayload() {
+ return mPayload != null ? mPayload.duplicate() : null;
+ }
+
+ protected ByteBuffer getPayload(int position) {
+ if (mPayload == null) {
+ return null;
+ }
+ ByteBuffer encoding = mPayload.duplicate();
+ encoding.position(position);
+ return encoding;
+ }
+
+ protected void setEndOfData(int position) {
+ mPayload.limit(position);
+ }
+
+ protected int getEndOfData() {
+ return mPayload.limit();
+ }
+
+ public boolean matches(Asn1ID id) {
+ return mTag == id.getTag() && mClass == id.getAsn1Class();
+ }
+
+ public String toSimpleString() {
+ Asn1Tag tag = mClass == Asn1Class.Universal ? Asn1Decoder.mapTag(mTag) : null;
+ if (tag != null) {
+ return tag.name();
+ } else if (mClass == Asn1Class.Universal) {
+ return String.format("[%d]", mTag);
+ } else {
+ return String.format("[%s %d]", mClass, mTag);
+ }
+ }
+
+ public abstract Collection<Asn1Object> getChildren();
+
+ @Override
+ public String toString() {
+ return toSimpleString();
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Octets.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Octets.java
new file mode 100644
index 0000000..1e19953
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Octets.java
@@ -0,0 +1,47 @@
+package com.android.hotspot2.asn1;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+
+public class Asn1Octets extends Asn1Object {
+ private final byte[] mOctets;
+ private final int mBitResidual;
+
+ public Asn1Octets(int tag, Asn1Class asn1Class, int length, ByteBuffer data) {
+ super(tag, asn1Class, false, length);
+ mOctets = new byte[length];
+ data.get(mOctets);
+ mBitResidual = -1;
+ }
+
+ public Asn1Octets(int tag, Asn1Class asn1Class, int length, ByteBuffer data, int bitResidual) {
+ super(tag, asn1Class, false, length);
+ mOctets = new byte[length - 1];
+ data.get(mOctets);
+ mBitResidual = bitResidual;
+ }
+
+ public byte[] getOctets() {
+ return mOctets;
+ }
+
+ @Override
+ public Collection<Asn1Object> getChildren() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : mOctets) {
+ sb.append(String.format(" %02x", b & Asn1Decoder.ByteMask));
+ }
+ if (mBitResidual >= 0) {
+ return super.toString() + '=' + sb + '/' + mBitResidual;
+ } else if (getTag() == Asn1Decoder.TAG_NULL && getLength() == 0) {
+ return super.toString();
+ } else {
+ return super.toString() + '=' + sb;
+ }
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Oid.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Oid.java
new file mode 100644
index 0000000..50f0553
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Oid.java
@@ -0,0 +1,212 @@
+package com.android.hotspot2.asn1;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Asn1Oid extends Asn1Object {
+ public static final int OidMaxOctet1 = 2;
+ public static final int OidOctet1Modulus = 40;
+
+ private final List<Long> mArcs;
+ private final int mHashcode;
+
+ private static final Map<Asn1Oid, String> sOidMap = new HashMap<>();
+
+ public Asn1Oid(int tag, Asn1Class asn1Class, int length, ByteBuffer data)
+ throws DecodeException {
+ super(tag, asn1Class, false, length);
+
+ if (length == 0)
+ throw new DecodeException("oid-encoding length is zero", data.position());
+
+ mArcs = new ArrayList<>();
+
+ ByteBuffer payload = data.duplicate();
+ payload.limit(payload.position() + length);
+ data.position(data.position() + length);
+
+ byte current = payload.get();
+ long seg01 = current & Asn1Decoder.ByteMask;
+ long segValue = seg01 / OidOctet1Modulus;
+ int hashcode = (int) segValue;
+ mArcs.add(segValue);
+ segValue = seg01 - segValue * OidOctet1Modulus;
+ hashcode = hashcode * 31 + (int) segValue;
+ mArcs.add(segValue);
+
+ current = 0;
+ segValue = 0L;
+
+ while (payload.hasRemaining()) {
+ current = payload.get();
+ segValue |= current & Asn1Decoder.MoreData;
+ if ((current & Asn1Decoder.MoreBit) == 0) {
+ hashcode = hashcode * 31 + (int) segValue;
+ mArcs.add(segValue);
+ segValue = 0L;
+ } else
+ segValue <<= Asn1Decoder.MoreWidth;
+ }
+ if ((current & Asn1Decoder.MoreBit) != 0)
+ throw new DecodeException("Illegal (end of) oid-encoding", payload.position());
+ mHashcode = hashcode;
+ }
+
+ public Asn1Oid(Long... arcs) {
+ super(Asn1Decoder.TAG_OID, Asn1Class.Universal, false, -1);
+ mArcs = Arrays.asList(arcs);
+ int hashcode = 0;
+ for (long arc : arcs) {
+ hashcode = hashcode * 31 + (int) arc;
+ }
+ mHashcode = hashcode;
+ }
+
+ @Override
+ public int hashCode() {
+ return mHashcode;
+ }
+
+ @Override
+ public boolean equals(Object thatObject) {
+ return !(thatObject == null || thatObject.getClass() != Asn1Oid.class) &&
+ mArcs.equals(((Asn1Oid) thatObject).mArcs);
+ }
+
+ public String toOIDString() {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for (long arc : mArcs) {
+ if (first) {
+ first = false;
+ } else {
+ sb.append('.');
+ }
+ sb.append(arc);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(toOIDString());
+ String name = sOidMap.get(this);
+ if (name != null) {
+ sb.append(" (").append(name).append(')');
+ }
+ return super.toString() + '=' + sb.toString();
+ }
+
+ @Override
+ public Collection<Asn1Object> getChildren() {
+ throw new UnsupportedOperationException();
+ }
+
+ public static final Asn1Oid PKCS7Data = new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 7L, 1L);
+ public static final Asn1Oid PKCS7SignedData = new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 7L, 2L);
+ // encoded as an IA5STRING type
+ public static final Asn1Oid OidMacAddress = new Asn1Oid(1L, 3L, 6L, 1L, 1L, 1L, 1L, 22L);
+ // encoded as an IA5STRING type
+ public static final Asn1Oid OidImei = new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 3L);
+ // encoded as a BITSTRING type
+ public static final Asn1Oid OidMeid = new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 4L);
+ // encoded as a PRINTABLESTRING type
+ public static final Asn1Oid OidDevId = new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 5L);
+
+ //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10040L, 4L, 1L), "algo_id_dsa");
+ //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10040L, 4L, 3L), "algo_id_dsawithsha1");
+ //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10045L, 2L, 1L), "algo_id_ecPublicKey");
+ //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10045L, 4L, 3L, 3L), "eccdaWithSHA384");
+ //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 1L), "algo_id_rsaEncryption");
+ //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 2L), "algo_id_md2WithRSAEncryption");
+ //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 4L), "algo_id_md5WithRSAEncryption");
+ //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 5L), "algo_id_sha1WithRSAEncryption");
+ //sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 11L),
+ // "algo_id_sha256WithRSAEncryption");
+
+ static {
+ sOidMap.put(new Asn1Oid(0L, 0L), "NullOid");
+ sOidMap.put(new Asn1Oid(0L, 9L, 2342L, 19200300L, 100L, 1L, 25L), "domComp");
+
+ sOidMap.put(OidMacAddress, "mac-address");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10040L, 4L, 1L), "algo_id_dsa");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10040L, 4L, 3L), "algo_id_dsawithsha1");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10045L, 2L, 1L), "algo_id_ecPublicKey");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10045L, 4L, 3L, 3L), "eccdaWithSHA384");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 10046L, 2L, 1L), "algo_id_dhpublicnumber");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 1L), "algo_id_rsaEncryption");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 2L), "algo_id_md2WithRSAEncryption");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 4L), "algo_id_md5WithRSAEncryption");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 5L),
+ "algo_id_sha1WithRSAEncryption");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 11L),
+ "algo_id_sha256WithRSAEncryption");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 7L), "pkcs7");
+ sOidMap.put(PKCS7Data, "pkcs7-data");
+ sOidMap.put(PKCS7SignedData, "pkcs7-signedData");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 9L, 1L), "emailAddress");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 9L, 7L), "challengePassword");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 9L, 14L), "extensionRequest");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 3L, 2L), "algo_id_RC2_CBC");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 3L, 4L), "algo_id_RC4_ENC");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 3L, 7L), "algo_id_DES_EDE3_CBC");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 3L, 9L), "algo_id_RC5_CBC_PAD");
+ sOidMap.put(new Asn1Oid(1L, 2L, 840L, 113549L, 3L, 10L), "algo_id_desCDMF");
+ sOidMap.put(new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 2L), "id-kp-HS2.0Auth");
+ sOidMap.put(OidImei, "imei");
+ sOidMap.put(OidMeid, "meid");
+ sOidMap.put(OidDevId, "DevId");
+ sOidMap.put(new Asn1Oid(1L, 3L, 6L, 1L, 5L, 5L, 7L, 1L, 1L),
+ "certAuthorityInfoAccessSyntax");
+ sOidMap.put(new Asn1Oid(1L, 3L, 6L, 1L, 5L, 5L, 7L, 1L, 11L),
+ "certSubjectInfoAccessSyntax");
+ sOidMap.put(new Asn1Oid(1L, 3L, 14L, 3L, 2L, 26L), "algo_id_SHA1");
+ sOidMap.put(new Asn1Oid(1L, 3L, 132L, 0L, 34L), "secp384r1");
+
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 3L), "x500_CN");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 4L), "x500_SN");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 5L), "x500_serialNum");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 6L), "x500_C");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 7L), "x500_L");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 8L), "x500_ST");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 9L), "x500_STREET");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 10L), "x500_O");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 11L), "x500_OU");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 12L), "x500_title");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 13L), "x500_description");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 17L), "x500_postalCode");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 18L), "x500_poBox");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 20L), "x500_phone");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 41L), "x500_name");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 42L), "x500_givenName");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 44L), "x500_genQual");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 43L), "x500_initials");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 46L), "x500_dnQualifier");
+ sOidMap.put(new Asn1Oid(2L, 5L, 4L, 65L), "x500_pseudonym");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 9L), "certSubjectDirectoryAttributes");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 14L), "certSubjectKeyIdentifier ");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 15L), "certKeyUsage");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 16L), "certPrivateKeyUsagePeriod");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 17L), "certSubjectAltName");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 18L), "certIssuerAltName");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 19L), "certBasicConstraints");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 30L), "certNameConstraints");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 31L), "certCRLDistributionPoints");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 32L), "certificatePolicies");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 33L), "certPolicyMappings");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 35L), "certAuthorityKeyIdentifier ");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 36L), "certPolicyConstraints");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 37L), "certExtKeyUsageSyntax");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 46L), "certFreshestCRL");
+ sOidMap.put(new Asn1Oid(2L, 5L, 29L, 54L), "certInhibitAnyPolicy");
+ sOidMap.put(new Asn1Oid(2L, 16L, 840L, 1L, 101L, 3L, 4L, 1L, 2L), "algo_id_aes128");
+ sOidMap.put(new Asn1Oid(2L, 16L, 840L, 1L, 101L, 3L, 4L, 1L, 22L), "algo_id_aes192");
+ sOidMap.put(new Asn1Oid(2L, 16L, 840L, 1L, 101L, 3L, 4L, 1L, 42L), "algo_id_aes256");
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1String.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1String.java
new file mode 100644
index 0000000..37ed2b2
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1String.java
@@ -0,0 +1,34 @@
+package com.android.hotspot2.asn1;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+
+public class Asn1String extends Asn1Object {
+ private final String mString;
+
+ public Asn1String(int tag, Asn1Class asn1Class, int length, ByteBuffer data) {
+ super(tag, asn1Class, false, length);
+
+ byte[] octets = new byte[length];
+ data.get(octets);
+ Charset charset = tag == Asn1Decoder.TAG_UTF8String
+ ? StandardCharsets.UTF_8 : StandardCharsets.ISO_8859_1;
+ mString = new String(octets, charset);
+ }
+
+ public String getString() {
+ return mString;
+ }
+
+ @Override
+ public Collection<Asn1Object> getChildren() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + "='" + mString + '\'';
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/Asn1Tag.java b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Tag.java
new file mode 100644
index 0000000..8129481
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/Asn1Tag.java
@@ -0,0 +1,31 @@
+package com.android.hotspot2.asn1;
+
+public enum Asn1Tag {
+ UNIVZERO,
+ BOOLEAN,
+ INTEGER,
+ BITSTRING,
+ OCTET_STRING,
+ NULL,
+ OID,
+ ObjectDescriptor,
+ EXTERNAL,
+ REAL,
+ ENUMERATED,
+ UTF8String,
+ RelativeOID,
+ SEQUENCE,
+ SET,
+ NumericString,
+ PrintableString,
+ T61String,
+ VideotexString,
+ IA5String,
+ UTCTime,
+ GeneralizedTime,
+ GraphicString,
+ VisibleString,
+ GeneralString,
+ UniversalString,
+ BMPString
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/DecodeException.java b/packages/Osu/src/com/android/hotspot2/asn1/DecodeException.java
new file mode 100644
index 0000000..1f10ee4
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/DecodeException.java
@@ -0,0 +1,17 @@
+package com.android.hotspot2.asn1;
+
+import java.io.IOException;
+
+public class DecodeException extends IOException {
+ private final int mOffset;
+
+ public DecodeException(String message, int offset) {
+ super(message);
+ mOffset = offset;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + " at " + mOffset;
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/asn1/OidMappings.java b/packages/Osu/src/com/android/hotspot2/asn1/OidMappings.java
new file mode 100644
index 0000000..01a6fd6
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/asn1/OidMappings.java
@@ -0,0 +1,197 @@
+package com.android.hotspot2.asn1;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class OidMappings {
+ public static class SigEntry {
+ private final String mSigAlgo;
+ private final Asn1Oid mKeyAlgo;
+
+ private SigEntry(String sigAlgo, Asn1Oid keyAlgo) {
+ mSigAlgo = sigAlgo;
+ mKeyAlgo = keyAlgo;
+ }
+
+ public String getSigAlgo() {
+ return mSigAlgo;
+ }
+
+ public Asn1Oid getKeyAlgo() {
+ return mKeyAlgo;
+ }
+ }
+
+ public static final String IdPeLogotype = "1.3.6.1.5.5.7.1.12";
+ public static final String IdCeSubjectAltName = "2.5.29.17";
+
+ private static final Map<Asn1Oid, String> sCryptoMapping = new HashMap<>();
+ private static final Map<Asn1Oid, String> sNameMapping = new HashMap<>();
+ private static final Set<Asn1Oid> sIDMapping = new HashSet<>();
+ private static final Map<Asn1Oid, SigEntry> sSigAlgos = new HashMap<>();
+
+ // DSA
+ private static final Asn1Oid sAlgo_DSA = new Asn1Oid(1L, 2L, 840L, 10040L, 4L, 1L);
+ private static final Asn1Oid sAlgo_SHA1withDSA = new Asn1Oid(1L, 2L, 840L, 10040L, 4L, 3L);
+
+ // RSA
+ public static final Asn1Oid sAlgo_RSA = new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 1L);
+ private static final Asn1Oid sAlgo_MD2withRSA = new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 2L);
+ private static final Asn1Oid sAlgo_MD5withRSA = new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 4L);
+ private static final Asn1Oid sAlgo_SHA1withRSA = new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 5L);
+ private static final Asn1Oid sAlgo_SHA224withRSA =
+ new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 14L); // n/a
+ private static final Asn1Oid sAlgo_SHA256withRSA =
+ new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 11L);
+ private static final Asn1Oid sAlgo_SHA384withRSA =
+ new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 12L);
+ private static final Asn1Oid sAlgo_SHA512withRSA =
+ new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 1L, 13L);
+
+ // ECC
+ public static final Asn1Oid sAlgo_EC = new Asn1Oid(1L, 2L, 840L, 10045L, 2L, 1L);
+ private static final Asn1Oid sAlgo_SHA1withECDSA = new Asn1Oid(1L, 2L, 840L, 10045L, 4L, 1L);
+ private static final Asn1Oid sAlgo_SHA224withECDSA =
+ new Asn1Oid(1L, 2L, 840L, 10045L, 4L, 3L, 1L); // n/a
+ private static final Asn1Oid sAlgo_SHA256withECDSA =
+ new Asn1Oid(1L, 2L, 840L, 10045L, 4L, 3L, 2L);
+ private static final Asn1Oid sAlgo_SHA384withECDSA =
+ new Asn1Oid(1L, 2L, 840L, 10045L, 4L, 3L, 3L);
+ private static final Asn1Oid sAlgo_SHA512withECDSA =
+ new Asn1Oid(1L, 2L, 840L, 10045L, 4L, 3L, 4L);
+
+ private static final Asn1Oid sAlgo_MD2 = new Asn1Oid(1L, 2L, 840L, 113549L, 2L, 2L);
+ private static final Asn1Oid sAlgo_MD5 = new Asn1Oid(1L, 2L, 840L, 113549L, 2L, 5L);
+ private static final Asn1Oid sAlgo_SHA1 = new Asn1Oid(1L, 3L, 14L, 3L, 2L, 26L);
+ private static final Asn1Oid sAlgo_SHA256 =
+ new Asn1Oid(2L, 16L, 840L, 1L, 101L, 3L, 4L, 2L, 1L);
+ private static final Asn1Oid sAlgo_SHA384 =
+ new Asn1Oid(2L, 16L, 840L, 1L, 101L, 3L, 4L, 2L, 2L);
+ private static final Asn1Oid sAlgo_SHA512 =
+ new Asn1Oid(2L, 16L, 840L, 1L, 101L, 3L, 4L, 2L, 3L);
+
+ // HS2.0 stuff:
+ public static final Asn1Oid sPkcs9AtChallengePassword =
+ new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 9L, 7L);
+ public static final Asn1Oid sExtensionRequest = new Asn1Oid(1L, 2L, 840L, 113549L, 1L, 9L, 14L);
+
+ public static final Asn1Oid sMAC = new Asn1Oid(1L, 3L, 6L, 1L, 1L, 1L, 1L, 22L);
+ public static final Asn1Oid sIMEI = new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 3L);
+ public static final Asn1Oid sMEID = new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 4L);
+ public static final Asn1Oid sDevID = new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 5L);
+
+ public static final Asn1Oid sIdWfaHotspotFriendlyName =
+ new Asn1Oid(1L, 3L, 6L, 1L, 4L, 1L, 40808L, 1L, 1L, 1L);
+
+ static {
+ sCryptoMapping.put(sAlgo_DSA, "DSA");
+ sCryptoMapping.put(sAlgo_RSA, "RSA");
+ sCryptoMapping.put(sAlgo_EC, "EC");
+
+ sSigAlgos.put(sAlgo_SHA1withDSA, new SigEntry("SHA1withDSA", sAlgo_DSA));
+
+ sSigAlgos.put(sAlgo_MD2withRSA, new SigEntry("MD2withRSA", sAlgo_RSA));
+ sSigAlgos.put(sAlgo_MD5withRSA, new SigEntry("MD5withRSA", sAlgo_RSA));
+ sSigAlgos.put(sAlgo_SHA1withRSA, new SigEntry("SHA1withRSA", sAlgo_RSA));
+ sSigAlgos.put(sAlgo_SHA224withRSA, new SigEntry(null, sAlgo_RSA));
+ sSigAlgos.put(sAlgo_SHA256withRSA, new SigEntry("SHA256withRSA", sAlgo_RSA));
+ sSigAlgos.put(sAlgo_SHA384withRSA, new SigEntry("SHA384withRSA", sAlgo_RSA));
+ sSigAlgos.put(sAlgo_SHA512withRSA, new SigEntry("SHA512withRSA", sAlgo_RSA));
+
+ sSigAlgos.put(sAlgo_SHA1withECDSA, new SigEntry("SHA1withECDSA", sAlgo_EC));
+ sSigAlgos.put(sAlgo_SHA224withECDSA, new SigEntry(null, sAlgo_EC));
+ sSigAlgos.put(sAlgo_SHA256withECDSA, new SigEntry("SHA256withECDSA", sAlgo_EC));
+ sSigAlgos.put(sAlgo_SHA384withECDSA, new SigEntry("SHA384withECDSA", sAlgo_EC));
+ sSigAlgos.put(sAlgo_SHA512withECDSA, new SigEntry("SHA512withECDSA", sAlgo_EC));
+
+ sIDMapping.add(sMAC);
+ sIDMapping.add(sIMEI);
+ sIDMapping.add(sMEID);
+ sIDMapping.add(sDevID);
+
+ for (Map.Entry<Asn1Oid, String> entry : sCryptoMapping.entrySet()) {
+ sNameMapping.put(entry.getKey(), entry.getValue());
+ }
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 1L), "sect163k1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 2L), "sect163r1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 3L), "sect239k1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 4L), "sect113r1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 5L), "sect113r2");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 6L), "secp112r1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 7L), "secp112r2");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 8L), "secp160r1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 9L), "secp160k1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 10L), "secp256k1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 15L), "sect163r2");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 16L), "sect283k1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 17L), "sect283r1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 22L), "sect131r1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 23L), "sect131r2");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 24L), "sect193r1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 25L), "sect193r2");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 26L), "sect233k1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 27L), "sect233r1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 28L), "secp128r1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 29L), "secp128r2");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 30L), "secp160r2");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 31L), "secp192k1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 32L), "secp224k1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 33L), "secp224r1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 34L), "secp384r1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 35L), "secp521r1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 36L), "sect409k1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 37L), "sect409r1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 38L), "sect571k1");
+ sNameMapping.put(new Asn1Oid(1L, 3L, 132L, 0L, 39L), "sect571r1");
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 1L, 1L), "secp192r1");
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 1L, 7L), "secp256r1");
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 1L, 2L), "prime192v2"); // X9.62
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 1L, 3L), "prime192v3"); // X9.62
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 1L, 4L), "prime239v1"); // X9.62
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 1L, 5L), "prime239v2"); // X9.62
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 1L, 6L), "prime239v3"); // X9.62
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 5L), "c2tnb191v1"); // X9.62
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 6L), "c2tnb191v2"); // X9.62
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 7L), "c2tnb191v3"); // X9.62
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 11L), "c2tnb239v1"); // X9.62
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 12L), "c2tnb239v2"); // X9.62
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 13L), "c2tnb239v3"); // X9.62
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 18L), "c2tnb359v1"); // X9.62
+ sNameMapping.put(new Asn1Oid(1L, 2L, 840L, 10045L, 3L, 0L, 20L), "c2tnb431r1"); // X9.62
+
+ sNameMapping.put(sAlgo_MD2, "MD2");
+ sNameMapping.put(sAlgo_MD5, "MD5");
+ sNameMapping.put(sAlgo_SHA1, "SHA-1");
+ sNameMapping.put(sAlgo_SHA256, "SHA-256");
+ sNameMapping.put(sAlgo_SHA384, "SHA-384");
+ sNameMapping.put(sAlgo_SHA512, "SHA-512");
+ }
+
+ public static SigEntry getSigEntry(Asn1Oid oid) {
+ return sSigAlgos.get(oid);
+ }
+
+ public static String getCryptoID(Asn1Oid oid) {
+ return sCryptoMapping.get(oid);
+ }
+
+ public static String getJCEName(Asn1Oid oid) {
+ return sNameMapping.get(oid);
+ }
+
+ public static String getSigAlgoName(Asn1Oid oid) {
+ SigEntry sigEntry = sSigAlgos.get(oid);
+ return sigEntry != null ? sigEntry.getSigAlgo() : null;
+ }
+
+ public static String getKeyAlgoName(Asn1Oid oid) {
+ SigEntry sigEntry = sSigAlgos.get(oid);
+ return sigEntry != null ? sNameMapping.get(sigEntry.getKeyAlgo()) : null;
+ }
+
+ public static boolean isIDAttribute(Asn1Oid oid) {
+ return sIDMapping.contains(oid);
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/est/ESTHandler.java b/packages/Osu/src/com/android/hotspot2/est/ESTHandler.java
new file mode 100644
index 0000000..b305f4b
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/est/ESTHandler.java
@@ -0,0 +1,500 @@
+package com.android.hotspot2.est;
+
+import android.net.Network;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.hotspot2.OMADMAdapter;
+import com.android.hotspot2.asn1.Asn1Class;
+import com.android.hotspot2.asn1.Asn1Constructed;
+import com.android.hotspot2.asn1.Asn1Decoder;
+import com.android.hotspot2.asn1.Asn1ID;
+import com.android.hotspot2.asn1.Asn1Integer;
+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.OSUSocketFactory;
+import com.android.hotspot2.osu.commands.GetCertData;
+import com.android.hotspot2.pps.HomeSP;
+import com.android.hotspot2.utils.HTTPMessage;
+import com.android.hotspot2.utils.HTTPResponse;
+import com.android.org.bouncycastle.asn1.ASN1Encodable;
+import com.android.org.bouncycastle.asn1.ASN1EncodableVector;
+import com.android.org.bouncycastle.asn1.ASN1Set;
+import com.android.org.bouncycastle.asn1.DERBitString;
+import com.android.org.bouncycastle.asn1.DEREncodableVector;
+import com.android.org.bouncycastle.asn1.DERIA5String;
+import com.android.org.bouncycastle.asn1.DERObjectIdentifier;
+import com.android.org.bouncycastle.asn1.DERPrintableString;
+import com.android.org.bouncycastle.asn1.DERSet;
+import com.android.org.bouncycastle.asn1.x509.Attribute;
+import com.android.org.bouncycastle.jce.PKCS10CertificationRequest;
+import com.android.org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.AlgorithmParameters;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.net.ssl.KeyManager;
+import javax.security.auth.x500.X500Principal;
+
+//import com.android.org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+public class ESTHandler implements AutoCloseable {
+ private static final String TAG = "HS2EST";
+ private static final int MinRSAKeySize = 2048;
+
+ private static final String CACERT_PATH = "/cacerts";
+ private static final String CSR_PATH = "/csrattrs";
+ private static final String SIMPLE_ENROLL_PATH = "/simpleenroll";
+ private static final String SIMPLE_REENROLL_PATH = "/simplereenroll";
+
+ private final URL mURL;
+ private final String mUser;
+ private final byte[] mPassword;
+ private final OSUSocketFactory mSocketFactory;
+ private final OMADMAdapter mOMADMAdapter;
+
+ private final List<X509Certificate> mCACerts = new ArrayList<>();
+ private final List<X509Certificate> mClientCerts = new ArrayList<>();
+ private PrivateKey mClientKey;
+
+ public ESTHandler(GetCertData certData, Network network, OMADMAdapter omadmAdapter,
+ KeyManager km, KeyStore ks, HomeSP homeSP, int flowType)
+ throws IOException, GeneralSecurityException {
+ mURL = new URL(certData.getServer());
+ mUser = certData.getUserName();
+ mPassword = certData.getPassword();
+ mSocketFactory = OSUSocketFactory.getSocketFactory(ks, homeSP, flowType,
+ network, mURL, km, true);
+ mOMADMAdapter = omadmAdapter;
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+
+ public List<X509Certificate> getCACerts() {
+ return mCACerts;
+ }
+
+ public List<X509Certificate> getClientCerts() {
+ return mClientCerts;
+ }
+
+ public PrivateKey getClientKey() {
+ return mClientKey;
+ }
+
+ private static String indent(int amount) {
+ char[] indent = new char[amount * 2];
+ Arrays.fill(indent, ' ');
+ return new String(indent);
+ }
+
+ public void execute(boolean reenroll) throws IOException, GeneralSecurityException {
+ URL caURL = new URL(mURL.getProtocol(), mURL.getHost(), mURL.getPort(),
+ mURL.getFile() + CACERT_PATH);
+
+ HTTPResponse response;
+ try (HTTPHandler httpHandler = new HTTPHandler(StandardCharsets.ISO_8859_1, mSocketFactory,
+ mUser, mPassword)) {
+ response = httpHandler.doGetHTTP(caURL);
+
+ if (!"application/pkcs7-mime".equals(response.getHeaders().
+ get(HTTPMessage.ContentTypeHeader))) {
+ throw new IOException("Unexpected Content-Type: " +
+ response.getHeaders().get(HTTPMessage.ContentTypeHeader));
+ }
+ ByteBuffer octetBuffer = response.getBinaryPayload();
+ Collection<Asn1Object> pkcs7Content1 = Asn1Decoder.decode(octetBuffer);
+ for (Asn1Object asn1Object : pkcs7Content1) {
+ Log.d(TAG, "---");
+ Log.d(TAG, asn1Object.toString());
+ }
+ Log.d(TAG, CACERT_PATH);
+
+ mCACerts.addAll(unpackPkcs7(octetBuffer));
+ for (X509Certificate certificate : mCACerts) {
+ Log.d(TAG, "CA-Cert: " + certificate.getSubjectX500Principal());
+ }
+
+ /*
+ byte[] octets = new byte[octetBuffer.remaining()];
+ octetBuffer.duplicate().get(octets);
+ for (byte b : octets) {
+ System.out.printf("%02x ", b & 0xff);
+ }
+ Log.d(TAG, );
+ */
+
+ /* + BC
+ try {
+ byte[] octets = new byte[octetBuffer.remaining()];
+ octetBuffer.duplicate().get(octets);
+ ASN1InputStream asnin = new ASN1InputStream(octets);
+ for (int n = 0; n < 100; n++) {
+ ASN1Primitive object = asnin.readObject();
+ if (object == null) {
+ break;
+ }
+ parseObject(object, 0);
+ }
+ }
+ catch (Throwable t) {
+ t.printStackTrace();
+ }
+
+ Collection<Asn1Object> pkcs7Content = Asn1Decoder.decode(octetBuffer);
+ for (Asn1Object asn1Object : pkcs7Content) {
+ Log.d(TAG, asn1Object);
+ }
+
+ if (pkcs7Content.size() != 1) {
+ throw new IOException("Unexpected pkcs 7 container: " + pkcs7Content.size());
+ }
+
+ Asn1Constructed pkcs7Root = (Asn1Constructed) pkcs7Content.iterator().next();
+ Iterator<Asn1ID> certPath = Arrays.asList(Pkcs7CertPath).iterator();
+ Asn1Object certObject = pkcs7Root.findObject(certPath);
+ if (certObject == null || certPath.hasNext()) {
+ throw new IOException("Failed to find cert; returned object " + certObject +
+ ", path " + (certPath.hasNext() ? "short" : "exhausted"));
+ }
+
+ ByteBuffer certOctets = certObject.getPayload();
+ if (certOctets == null) {
+ throw new IOException("No cert payload in: " + certObject);
+ }
+
+ byte[] certBytes = new byte[certOctets.remaining()];
+ certOctets.get(certBytes);
+
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
+ Log.d(TAG, "EST Cert: " + cert);
+ */
+
+ URL csrURL = new URL(mURL.getProtocol(), mURL.getHost(), mURL.getPort(),
+ mURL.getFile() + CSR_PATH);
+ response = httpHandler.doGetHTTP(csrURL);
+
+ octetBuffer = response.getBinaryPayload();
+ byte[] csrData = buildCSR(octetBuffer, mOMADMAdapter, httpHandler);
+
+ /**/
+ Collection<Asn1Object> o = Asn1Decoder.decode(ByteBuffer.wrap(csrData));
+ Log.d(TAG, "CSR:");
+ Log.d(TAG, o.iterator().next().toString());
+ Log.d(TAG, "End CSR.");
+ /**/
+
+ URL enrollURL = new URL(mURL.getProtocol(), mURL.getHost(), mURL.getPort(),
+ mURL.getFile() + (reenroll ? SIMPLE_REENROLL_PATH : SIMPLE_ENROLL_PATH));
+ String data = Base64.encodeToString(csrData, Base64.DEFAULT);
+ octetBuffer = httpHandler.exchangeBinary(enrollURL, data, "application/pkcs10");
+
+ Collection<Asn1Object> pkcs7Content2 = Asn1Decoder.decode(octetBuffer);
+ for (Asn1Object asn1Object : pkcs7Content2) {
+ Log.d(TAG, "---");
+ Log.d(TAG, asn1Object.toString());
+ }
+ mClientCerts.addAll(unpackPkcs7(octetBuffer));
+ for (X509Certificate cert : mClientCerts) {
+ Log.d(TAG, cert.toString());
+ }
+ }
+ }
+
+ private static final Asn1ID sSEQUENCE = new Asn1ID(Asn1Decoder.TAG_SEQ, Asn1Class.Universal);
+ private static final Asn1ID sCTXT0 = new Asn1ID(0, Asn1Class.Context);
+ private static final int PKCS7DataVersion = 1;
+ private static final int PKCS7SignedDataVersion = 3;
+
+ private static List<X509Certificate> unpackPkcs7(ByteBuffer pkcs7)
+ throws IOException, GeneralSecurityException {
+ Collection<Asn1Object> pkcs7Content = Asn1Decoder.decode(pkcs7);
+
+ if (pkcs7Content.size() != 1) {
+ throw new IOException("Unexpected pkcs 7 container: " + pkcs7Content.size());
+ }
+
+ Asn1Object data = pkcs7Content.iterator().next();
+ if (!data.isConstructed() || !data.matches(sSEQUENCE)) {
+ throw new IOException("Expected SEQ OF, got " + data.toSimpleString());
+ } else if (data.getChildren().size() != 2) {
+ throw new IOException("Expected content info to have two children, got " +
+ data.getChildren().size());
+ }
+
+ Iterator<Asn1Object> children = data.getChildren().iterator();
+ Asn1Object contentType = children.next();
+ if (!contentType.equals(Asn1Oid.PKCS7SignedData)) {
+ throw new IOException("Content not PKCS7 signed data");
+ }
+ Asn1Object content = children.next();
+ if (!content.isConstructed() || !content.matches(sCTXT0)) {
+ throw new IOException("Expected [CONTEXT 0] with one child, got " +
+ content.toSimpleString() + ", " + content.getChildren().size());
+ }
+
+ Asn1Object signedData = content.getChildren().iterator().next();
+ Map<Integer, Asn1Object> itemMap = new HashMap<>();
+ for (Asn1Object item : signedData.getChildren()) {
+ if (itemMap.put(item.getTag(), item) != null && item.getTag() != Asn1Decoder.TAG_SET) {
+ throw new IOException("Duplicate item in SignedData: " + item.toSimpleString());
+ }
+ }
+
+ Asn1Object versionObject = itemMap.get(Asn1Decoder.TAG_INTEGER);
+ if (versionObject == null || !(versionObject instanceof Asn1Integer)) {
+ throw new IOException("Bad or missing PKCS7 version: " + versionObject);
+ }
+ int pkcs7version = (int) ((Asn1Integer) versionObject).getValue();
+ Asn1Object innerContentInfo = itemMap.get(Asn1Decoder.TAG_SEQ);
+ if (innerContentInfo == null ||
+ !innerContentInfo.isConstructed() ||
+ !innerContentInfo.matches(sSEQUENCE) ||
+ innerContentInfo.getChildren().size() != 1) {
+ throw new IOException("Bad or missing PKCS7 contentInfo");
+ }
+ Asn1Object contentID = innerContentInfo.getChildren().iterator().next();
+ if (pkcs7version == PKCS7DataVersion && !contentID.equals(Asn1Oid.PKCS7Data) ||
+ pkcs7version == PKCS7SignedDataVersion && !contentID.equals(Asn1Oid.PKCS7SignedData)) {
+ throw new IOException("Inner PKCS7 content (" + contentID +
+ ") not expected for version " + pkcs7version);
+ }
+ Asn1Object certWrapper = itemMap.get(0);
+ if (certWrapper == null || !certWrapper.isConstructed() || !certWrapper.matches(sCTXT0)) {
+ throw new IOException("Expected [CONTEXT 0], got: " + certWrapper);
+ }
+
+ List<X509Certificate> certList = new ArrayList<>(certWrapper.getChildren().size());
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ for (Asn1Object certObject : certWrapper.getChildren()) {
+ ByteBuffer certOctets = ((Asn1Constructed) certObject).getEncoding();
+ if (certOctets == null) {
+ throw new IOException("No cert payload in: " + certObject);
+ }
+ byte[] certBytes = new byte[certOctets.remaining()];
+ certOctets.get(certBytes);
+
+ certList.add((X509Certificate) certFactory.
+ generateCertificate(new ByteArrayInputStream(certBytes)));
+ }
+ return certList;
+ }
+
+ private byte[] buildCSR(ByteBuffer octetBuffer, OMADMAdapter omadmAdapter,
+ HTTPHandler httpHandler) throws IOException, GeneralSecurityException {
+
+ //Security.addProvider(new BouncyCastleProvider());
+
+ Log.d(TAG, "/csrattrs:");
+ /*
+ byte[] octets = new byte[octetBuffer.remaining()];
+ octetBuffer.duplicate().get(octets);
+ for (byte b : octets) {
+ System.out.printf("%02x ", b & 0xff);
+ }
+ */
+ Collection<Asn1Object> csrs = Asn1Decoder.decode(octetBuffer);
+ for (Asn1Object asn1Object : csrs) {
+ Log.d(TAG, asn1Object.toString());
+ }
+
+ if (csrs.size() != 1) {
+ throw new IOException("Unexpected object count in CSR attributes response: " +
+ csrs.size());
+ }
+ Asn1Object sequence = csrs.iterator().next();
+ if (sequence.getClass() != Asn1Constructed.class) {
+ throw new IOException("Unexpected CSR attribute container: " + sequence);
+ }
+
+ String keyAlgo = null;
+ Asn1Oid keyAlgoOID = null;
+ String sigAlgo = null;
+ String curveName = null;
+ Asn1Oid pubCrypto = null;
+ int keySize = -1;
+ Map<Asn1Oid, ASN1Encodable> idAttributes = new HashMap<>();
+
+ for (Asn1Object child : sequence.getChildren()) {
+ if (child.getTag() == Asn1Decoder.TAG_OID) {
+ Asn1Oid oid = (Asn1Oid) child;
+ OidMappings.SigEntry sigEntry = OidMappings.getSigEntry(oid);
+ if (sigEntry != null) {
+ sigAlgo = sigEntry.getSigAlgo();
+ keyAlgoOID = sigEntry.getKeyAlgo();
+ keyAlgo = OidMappings.getJCEName(keyAlgoOID);
+ } else if (oid.equals(OidMappings.sPkcs9AtChallengePassword)) {
+ byte[] tlsUnique = httpHandler.getTLSUnique();
+ if (tlsUnique != null) {
+ idAttributes.put(oid, new DERPrintableString(
+ Base64.encodeToString(tlsUnique, Base64.DEFAULT)));
+ } else {
+ Log.w(TAG, "Cannot retrieve TLS unique channel binding");
+ }
+ }
+ } else if (child.getTag() == Asn1Decoder.TAG_SEQ) {
+ Asn1Oid oid = null;
+ Set<Asn1Oid> oidValues = new HashSet<>();
+ List<Asn1Object> values = new ArrayList<>();
+
+ for (Asn1Object attributeSeq : child.getChildren()) {
+ if (attributeSeq.getTag() == Asn1Decoder.TAG_OID) {
+ oid = (Asn1Oid) attributeSeq;
+ } else if (attributeSeq.getTag() == Asn1Decoder.TAG_SET) {
+ for (Asn1Object value : attributeSeq.getChildren()) {
+ if (value.getTag() == Asn1Decoder.TAG_OID) {
+ oidValues.add((Asn1Oid) value);
+ } else {
+ values.add(value);
+ }
+ }
+ }
+ }
+ if (oid == null) {
+ throw new IOException("Invalid attribute, no OID");
+ }
+ if (oid.equals(OidMappings.sExtensionRequest)) {
+ for (Asn1Oid subOid : oidValues) {
+ if (OidMappings.isIDAttribute(subOid)) {
+ if (subOid.equals(OidMappings.sMAC)) {
+ idAttributes.put(subOid, new DERIA5String(omadmAdapter.getMAC()));
+ } else if (subOid.equals(OidMappings.sIMEI)) {
+ idAttributes.put(subOid, new DERIA5String(omadmAdapter.getImei()));
+ } else if (subOid.equals(OidMappings.sMEID)) {
+ idAttributes.put(subOid, new DERBitString(omadmAdapter.getMeid()));
+ } else if (subOid.equals(OidMappings.sDevID)) {
+ idAttributes.put(subOid,
+ new DERPrintableString(omadmAdapter.getDevID()));
+ }
+ }
+ }
+ } else if (OidMappings.getCryptoID(oid) != null) {
+ pubCrypto = oid;
+ if (!values.isEmpty()) {
+ for (Asn1Object value : values) {
+ if (value.getTag() == Asn1Decoder.TAG_INTEGER) {
+ keySize = (int) ((Asn1Integer) value).getValue();
+ }
+ }
+ }
+ if (oid.equals(OidMappings.sAlgo_EC)) {
+ if (oidValues.isEmpty()) {
+ throw new IOException("No ECC curve name provided");
+ }
+ for (Asn1Oid value : oidValues) {
+ curveName = OidMappings.getJCEName(value);
+ if (curveName != null) {
+ break;
+ }
+ }
+ if (curveName == null) {
+ throw new IOException("Found no ECC curve for " + oidValues);
+ }
+ }
+ }
+ }
+ }
+
+ if (keyAlgoOID == null) {
+ throw new IOException("No public key algorithm specified");
+ }
+ if (pubCrypto != null && !pubCrypto.equals(keyAlgoOID)) {
+ throw new IOException("Mismatching key algorithms");
+ }
+
+ if (keyAlgoOID.equals(OidMappings.sAlgo_RSA)) {
+ if (keySize < MinRSAKeySize) {
+ if (keySize >= 0) {
+ Log.i(TAG, "Upgrading suggested RSA key size from " +
+ keySize + " to " + MinRSAKeySize);
+ }
+ keySize = MinRSAKeySize;
+ }
+ }
+
+ Log.d(TAG, String.format("pub key '%s', signature '%s', ECC curve '%s', id-atts %s",
+ keyAlgo, sigAlgo, curveName, idAttributes));
+
+ /*
+ Ruckus:
+ SEQUENCE:
+ OID=1.2.840.113549.1.1.11 (algo_id_sha256WithRSAEncryption)
+
+ RFC-7030:
+ SEQUENCE:
+ OID=1.2.840.113549.1.9.7 (challengePassword)
+ SEQUENCE:
+ OID=1.2.840.10045.2.1 (algo_id_ecPublicKey)
+ SET:
+ OID=1.3.132.0.34 (secp384r1)
+ SEQUENCE:
+ OID=1.2.840.113549.1.9.14 (extensionRequest)
+ SET:
+ OID=1.3.6.1.1.1.1.22 (mac-address)
+ OID=1.2.840.10045.4.3.3 (eccdaWithSHA384)
+
+ 1L, 3L, 6L, 1L, 1L, 1L, 1L, 22
+ */
+
+ // ECC Does not appear to be supported currently
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance(keyAlgo);
+ if (curveName != null) {
+ AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(keyAlgo);
+ algorithmParameters.init(new ECNamedCurveGenParameterSpec(curveName));
+ kpg.initialize(algorithmParameters
+ .getParameterSpec(ECNamedCurveGenParameterSpec.class));
+ } else {
+ kpg.initialize(keySize);
+ }
+ KeyPair kp = kpg.generateKeyPair();
+
+ X500Principal subject = new X500Principal("CN=Android, O=Google, C=US");
+
+ mClientKey = kp.getPrivate();
+
+ // !!! Map the idAttributes into an ASN1Set of values to pass to
+ // the PKCS10CertificationRequest - this code is using outdated BC classes and
+ // has *not* been tested.
+ ASN1Set attributes;
+ if (!idAttributes.isEmpty()) {
+ ASN1EncodableVector payload = new DEREncodableVector();
+ for (Map.Entry<Asn1Oid, ASN1Encodable> entry : idAttributes.entrySet()) {
+ DERObjectIdentifier type = new DERObjectIdentifier(entry.getKey().toOIDString());
+ ASN1Set values = new DERSet(entry.getValue());
+ Attribute attribute = new Attribute(type, values);
+ payload.add(attribute);
+ }
+ attributes = new DERSet(payload);
+ } else {
+ attributes = null;
+ }
+
+ return new PKCS10CertificationRequest(sigAlgo, subject, kp.getPublic(),
+ attributes, mClientKey).getEncoded();
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/MOManager.java b/packages/Osu/src/com/android/hotspot2/omadm/MOManager.java
new file mode 100644
index 0000000..6a748cd
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/MOManager.java
@@ -0,0 +1,992 @@
+package com.android.hotspot2.omadm;
+
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.anqp.eap.EAP;
+import com.android.anqp.eap.EAPMethod;
+import com.android.anqp.eap.ExpandedEAPMethod;
+import com.android.anqp.eap.InnerAuthEAP;
+import com.android.anqp.eap.NonEAPInnerAuth;
+import com.android.hotspot2.IMSIParameter;
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.osu.OSUManager;
+import com.android.hotspot2.osu.commands.MOData;
+import com.android.hotspot2.pps.Credential;
+import com.android.hotspot2.pps.HomeSP;
+import com.android.hotspot2.pps.Policy;
+import com.android.hotspot2.pps.SubscriptionParameters;
+import com.android.hotspot2.pps.UpdateInfo;
+
+import org.xml.sax.SAXException;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+
+/**
+ * Handles provisioning of PerProviderSubscription data.
+ */
+public class MOManager {
+
+ public static final String TAG_AAAServerTrustRoot = "AAAServerTrustRoot";
+ public static final String TAG_AbleToShare = "AbleToShare";
+ public static final String TAG_CertificateType = "CertificateType";
+ public static final String TAG_CertSHA256Fingerprint = "CertSHA256Fingerprint";
+ public static final String TAG_CertURL = "CertURL";
+ public static final String TAG_CheckAAAServerCertStatus = "CheckAAAServerCertStatus";
+ public static final String TAG_Country = "Country";
+ public static final String TAG_CreationDate = "CreationDate";
+ public static final String TAG_Credential = "Credential";
+ public static final String TAG_CredentialPriority = "CredentialPriority";
+ public static final String TAG_DataLimit = "DataLimit";
+ public static final String TAG_DigitalCertificate = "DigitalCertificate";
+ public static final String TAG_DLBandwidth = "DLBandwidth";
+ public static final String TAG_EAPMethod = "EAPMethod";
+ public static final String TAG_EAPType = "EAPType";
+ public static final String TAG_ExpirationDate = "ExpirationDate";
+ public static final String TAG_Extension = "Extension";
+ public static final String TAG_FQDN = "FQDN";
+ public static final String TAG_FQDN_Match = "FQDN_Match";
+ public static final String TAG_FriendlyName = "FriendlyName";
+ public static final String TAG_HESSID = "HESSID";
+ public static final String TAG_HomeOI = "HomeOI";
+ public static final String TAG_HomeOIList = "HomeOIList";
+ public static final String TAG_HomeOIRequired = "HomeOIRequired";
+ public static final String TAG_HomeSP = "HomeSP";
+ public static final String TAG_IconURL = "IconURL";
+ public static final String TAG_IMSI = "IMSI";
+ public static final String TAG_InnerEAPType = "InnerEAPType";
+ public static final String TAG_InnerMethod = "InnerMethod";
+ public static final String TAG_InnerVendorID = "InnerVendorID";
+ public static final String TAG_InnerVendorType = "InnerVendorType";
+ public static final String TAG_IPProtocol = "IPProtocol";
+ public static final String TAG_MachineManaged = "MachineManaged";
+ public static final String TAG_MaximumBSSLoadValue = "MaximumBSSLoadValue";
+ public static final String TAG_MinBackhaulThreshold = "MinBackhaulThreshold";
+ public static final String TAG_NetworkID = "NetworkID";
+ public static final String TAG_NetworkType = "NetworkType";
+ public static final String TAG_Other = "Other";
+ public static final String TAG_OtherHomePartners = "OtherHomePartners";
+ public static final String TAG_Password = "Password";
+ public static final String TAG_PerProviderSubscription = "PerProviderSubscription";
+ public static final String TAG_Policy = "Policy";
+ public static final String TAG_PolicyUpdate = "PolicyUpdate";
+ public static final String TAG_PortNumber = "PortNumber";
+ public static final String TAG_PreferredRoamingPartnerList = "PreferredRoamingPartnerList";
+ public static final String TAG_Priority = "Priority";
+ public static final String TAG_Realm = "Realm";
+ public static final String TAG_RequiredProtoPortTuple = "RequiredProtoPortTuple";
+ public static final String TAG_Restriction = "Restriction";
+ public static final String TAG_RoamingConsortiumOI = "RoamingConsortiumOI";
+ public static final String TAG_SIM = "SIM";
+ public static final String TAG_SoftTokenApp = "SoftTokenApp";
+ public static final String TAG_SPExclusionList = "SPExclusionList";
+ public static final String TAG_SSID = "SSID";
+ public static final String TAG_StartDate = "StartDate";
+ public static final String TAG_SubscriptionParameters = "SubscriptionParameters";
+ public static final String TAG_SubscriptionUpdate = "SubscriptionUpdate";
+ public static final String TAG_TimeLimit = "TimeLimit";
+ public static final String TAG_TrustRoot = "TrustRoot";
+ public static final String TAG_TypeOfSubscription = "TypeOfSubscription";
+ public static final String TAG_ULBandwidth = "ULBandwidth";
+ public static final String TAG_UpdateIdentifier = "UpdateIdentifier";
+ public static final String TAG_UpdateInterval = "UpdateInterval";
+ public static final String TAG_UpdateMethod = "UpdateMethod";
+ public static final String TAG_URI = "URI";
+ public static final String TAG_UsageLimits = "UsageLimits";
+ public static final String TAG_UsageTimePeriod = "UsageTimePeriod";
+ public static final String TAG_Username = "Username";
+ public static final String TAG_UsernamePassword = "UsernamePassword";
+ public static final String TAG_VendorId = "VendorId";
+ public static final String TAG_VendorType = "VendorType";
+
+ public static final long IntervalFactor = 60000L; // All MO intervals are in minutes
+
+ private static final DateFormat DTFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+
+ private static final Map<String, Map<String, Object>> sSelectionMap;
+
+ static {
+ DTFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+ sSelectionMap = new HashMap<>();
+
+ setSelections(TAG_FQDN_Match,
+ "exactmatch", Boolean.FALSE,
+ "includesubdomains", Boolean.TRUE);
+ setSelections(TAG_UpdateMethod,
+ "oma-dm-clientinitiated", Boolean.FALSE,
+ "spp-clientinitiated", Boolean.TRUE);
+ setSelections(TAG_Restriction,
+ "homesp", UpdateInfo.UpdateRestriction.HomeSP,
+ "roamingpartner", UpdateInfo.UpdateRestriction.RoamingPartner,
+ "unrestricted", UpdateInfo.UpdateRestriction.Unrestricted);
+ }
+
+ private static void setSelections(String key, Object... pairs) {
+ Map<String, Object> kvp = new HashMap<>();
+ sSelectionMap.put(key, kvp);
+ for (int n = 0; n < pairs.length; n += 2) {
+ kvp.put(pairs[n].toString(), pairs[n + 1]);
+ }
+ }
+
+ private final File mPpsFile;
+ private final boolean mEnabled;
+ private final Map<String, HomeSP> mSPs;
+
+ public MOManager(File ppsFile, boolean hs2enabled) {
+ mPpsFile = ppsFile;
+ mEnabled = hs2enabled;
+ mSPs = new HashMap<>();
+ }
+
+ public File getPpsFile() {
+ return mPpsFile;
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public boolean isConfigured() {
+ return mEnabled && !mSPs.isEmpty();
+ }
+
+ public Map<String, HomeSP> getLoadedSPs() {
+ return Collections.unmodifiableMap(mSPs);
+ }
+
+ public List<HomeSP> loadAllSPs() throws IOException {
+
+ if (!mEnabled || !mPpsFile.exists()) {
+ return Collections.emptyList();
+ }
+
+ try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
+ MOTree moTree = MOTree.unmarshal(in);
+ mSPs.clear();
+ if (moTree == null) {
+ return Collections.emptyList(); // Empty file
+ }
+
+ List<HomeSP> sps = buildSPs(moTree);
+ if (sps != null) {
+ for (HomeSP sp : sps) {
+ if (mSPs.put(sp.getFQDN(), sp) != null) {
+ throw new OMAException("Multiple SPs for FQDN '" + sp.getFQDN() + "'");
+ } else {
+ Log.d(OSUManager.TAG, "retrieved " + sp.getFQDN() + " from PPS");
+ }
+ }
+ return sps;
+
+ } else {
+ throw new OMAException("Failed to build HomeSP");
+ }
+ }
+ }
+
+ public static HomeSP buildSP(String xml) throws IOException, SAXException {
+ OMAParser omaParser = new OMAParser();
+ MOTree tree = omaParser.parse(xml, OMAConstants.PPS_URN);
+ List<HomeSP> spList = buildSPs(tree);
+ if (spList.size() != 1) {
+ throw new OMAException("Expected exactly one HomeSP, got " + spList.size());
+ }
+ return spList.iterator().next();
+ }
+
+ public HomeSP addSP(String xml, OSUManager osuManager) throws IOException, SAXException {
+ OMAParser omaParser = new OMAParser();
+ return addSP(omaParser.parse(xml, OMAConstants.PPS_URN), osuManager);
+ }
+
+ private static final List<String> FQDNPath = Arrays.asList(TAG_HomeSP, TAG_FQDN);
+
+ /**
+ * R1 *only* addSP method.
+ *
+ * @param homeSP
+ * @throws IOException
+ */
+ public void addSP(HomeSP homeSP, OSUManager osuManager) throws IOException {
+ if (!mEnabled) {
+ throw new IOException("HS2.0 not enabled on this device");
+ }
+ if (mSPs.containsKey(homeSP.getFQDN())) {
+ Log.d(OSUManager.TAG, "HS20 profile for " +
+ homeSP.getFQDN() + " already exists");
+ return;
+ }
+ Log.d(OSUManager.TAG, "Adding new HS20 profile for " + homeSP.getFQDN());
+
+ OMAConstructed dummyRoot = new OMAConstructed(null, TAG_PerProviderSubscription, null);
+ buildHomeSPTree(homeSP, dummyRoot, mSPs.size() + 1);
+ try {
+ addSP(dummyRoot, osuManager);
+ } catch (FileNotFoundException fnfe) {
+ MOTree tree =
+ MOTree.buildMgmtTree(OMAConstants.PPS_URN, OMAConstants.OMAVersion, dummyRoot);
+ // No file to load a pre-build MO tree from, create a new one and save it.
+ //MOTree tree = new MOTree(OMAConstants.PPS_URN, OMAConstants.OMAVersion, dummyRoot);
+ writeMO(tree, mPpsFile, osuManager);
+ }
+ mSPs.put(homeSP.getFQDN(), homeSP);
+ }
+
+ public HomeSP addSP(MOTree instanceTree, OSUManager osuManager) throws IOException {
+ List<HomeSP> spList = buildSPs(instanceTree);
+ if (spList.size() != 1) {
+ throw new OMAException("Expected exactly one HomeSP, got " + spList.size());
+ }
+
+ HomeSP sp = spList.iterator().next();
+ String fqdn = sp.getFQDN();
+ if (mSPs.put(fqdn, sp) != null) {
+ throw new OMAException("SP " + fqdn + " already exists");
+ }
+
+ OMAConstructed pps = (OMAConstructed) instanceTree.getRoot().
+ getChild(TAG_PerProviderSubscription);
+
+ try {
+ addSP(pps, osuManager);
+ } catch (FileNotFoundException fnfe) {
+ MOTree tree = new MOTree(instanceTree.getUrn(), instanceTree.getDtdRev(),
+ instanceTree.getRoot());
+ writeMO(tree, mPpsFile, osuManager);
+ }
+
+ return sp;
+ }
+
+ /**
+ * Add an SP sub-tree. mo must be PPS with an immediate instance child (e.g. Cred01) and an
+ * optional UpdateIdentifier,
+ *
+ * @param mo The new MO
+ * @throws IOException
+ */
+ private void addSP(OMANode mo, OSUManager osuManager) throws IOException {
+ MOTree moTree;
+ try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
+ moTree = MOTree.unmarshal(in);
+ moTree.getRoot().addChild(mo);
+
+ /*
+ OMAConstructed ppsRoot = (OMAConstructed)
+ moTree.getRoot().addChild(TAG_PerProviderSubscription, "", null, null);
+ for (OMANode child : mo.getChildren()) {
+ ppsRoot.addChild(child);
+ if (!child.isLeaf()) {
+ moTree.getRoot().addChild(child);
+ }
+ else if (child.getName().equals(TAG_UpdateIdentifier)) {
+ OMANode currentUD = moTree.getRoot().getChild(TAG_UpdateIdentifier);
+ if (currentUD != null) {
+ moTree.getRoot().replaceNode(currentUD, child);
+ }
+ else {
+ moTree.getRoot().addChild(child);
+ }
+ }
+ }
+ */
+ }
+ writeMO(moTree, mPpsFile, osuManager);
+ }
+
+ private static OMAConstructed findTargetTree(MOTree moTree, String fqdn) throws OMAException {
+ OMANode pps = moTree.getRoot();
+ for (OMANode node : pps.getChildren()) {
+ OMANode instance = null;
+ if (node.getName().equals(TAG_PerProviderSubscription)) {
+ instance = getInstanceNode((OMAConstructed) node);
+ } else if (!node.isLeaf()) {
+ instance = node;
+ }
+ if (instance != null) {
+ String nodeFqdn = getString(instance.getListValue(FQDNPath.iterator()));
+ if (fqdn.equalsIgnoreCase(nodeFqdn)) {
+ return (OMAConstructed) node;
+ // targetTree is rooted at the PPS
+ }
+ }
+ }
+ return null;
+ }
+
+ private static OMAConstructed getInstanceNode(OMAConstructed root) throws OMAException {
+ for (OMANode child : root.getChildren()) {
+ if (!child.isLeaf()) {
+ return (OMAConstructed) child;
+ }
+ }
+ throw new OMAException("Cannot find instance node");
+ }
+
+ public HomeSP modifySP(HomeSP homeSP, Collection<MOData> mods, OSUManager osuManager)
+ throws IOException {
+
+ Log.d(OSUManager.TAG, "modifying SP: " + mods);
+ MOTree moTree;
+ int ppsMods = 0;
+ int updateIdentifier = 0;
+ try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
+ moTree = MOTree.unmarshal(in);
+ // moTree is PPS/?/provider-data
+
+ OMAConstructed targetTree = findTargetTree(moTree, homeSP.getFQDN());
+ if (targetTree == null) {
+ throw new IOException("Failed to find PPS tree for " + homeSP.getFQDN());
+ }
+ OMAConstructed instance = getInstanceNode(targetTree);
+
+ for (MOData mod : mods) {
+ LinkedList<String> tailPath =
+ getTailPath(mod.getBaseURI(), TAG_PerProviderSubscription);
+ OMAConstructed modRoot = mod.getMOTree().getRoot();
+ // modRoot is the MgmtTree with the actual object as a direct child
+ // (e.g. Credential)
+
+ if (tailPath.getFirst().equals(TAG_UpdateIdentifier)) {
+ updateIdentifier = getInteger(modRoot.getChildren().iterator().next());
+ OMANode oldUdi = targetTree.getChild(TAG_UpdateIdentifier);
+ if (getInteger(oldUdi) != updateIdentifier) {
+ ppsMods++;
+ }
+ if (oldUdi != null) {
+ targetTree.replaceNode(oldUdi, modRoot.getChild(TAG_UpdateIdentifier));
+ } else {
+ targetTree.addChild(modRoot.getChild(TAG_UpdateIdentifier));
+ }
+ } else {
+ tailPath.removeFirst(); // Drop the instance
+ OMANode current = instance.getListValue(tailPath.iterator());
+ if (current == null) {
+ throw new IOException("No previous node for " + tailPath + " in " +
+ homeSP.getFQDN());
+ }
+ for (OMANode newNode : modRoot.getChildren()) {
+ // newNode is something like Credential
+ // current is the same existing node
+ OMANode old = current.getParent().replaceNode(current, newNode);
+ ppsMods++;
+ }
+ }
+ }
+ }
+ writeMO(moTree, mPpsFile, osuManager);
+
+ if (ppsMods == 0) {
+ return null; // HomeSP not modified.
+ }
+
+ // Return a new rebuilt HomeSP
+ List<HomeSP> sps = buildSPs(moTree);
+ if (sps != null) {
+ for (HomeSP sp : sps) {
+ if (sp.getFQDN().equals(homeSP.getFQDN())) {
+ return sp;
+ }
+ }
+ } else {
+ throw new OMAException("Failed to build HomeSP");
+ }
+ return null;
+ }
+
+ private static LinkedList<String> getTailPath(String pathString, String rootName)
+ throws IOException {
+ String[] path = pathString.split("/");
+ int pathIndex;
+ for (pathIndex = 0; pathIndex < path.length; pathIndex++) {
+ if (path[pathIndex].equalsIgnoreCase(rootName)) {
+ pathIndex++;
+ break;
+ }
+ }
+ if (pathIndex >= path.length) {
+ throw new IOException("Bad node-path: " + pathString);
+ }
+ LinkedList<String> tailPath = new LinkedList<>();
+ while (pathIndex < path.length) {
+ tailPath.add(path[pathIndex]);
+ pathIndex++;
+ }
+ return tailPath;
+ }
+
+ public HomeSP getHomeSP(String fqdn) {
+ return mSPs.get(fqdn);
+ }
+
+ public void removeSP(String fqdn, OSUManager osuManager) throws IOException {
+ if (mSPs.remove(fqdn) == null) {
+ Log.d(OSUManager.TAG, "No HS20 profile to delete for " + fqdn);
+ return;
+ }
+
+ Log.d(OSUManager.TAG, "Deleting HS20 profile for " + fqdn);
+
+ MOTree moTree;
+ try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
+ moTree = MOTree.unmarshal(in);
+ OMAConstructed tbd = findTargetTree(moTree, fqdn);
+ if (tbd == null) {
+ throw new IOException("Node " + fqdn + " doesn't exist in MO tree");
+ }
+ OMAConstructed pps = moTree.getRoot();
+ OMANode removed = pps.removeNode("?", tbd);
+ if (removed == null) {
+ throw new IOException("Failed to remove " + fqdn + " out of MO tree");
+ }
+ }
+ writeMO(moTree, mPpsFile, osuManager);
+ osuManager.spDeleted(fqdn);
+ }
+
+ public MOTree getMOTree(HomeSP homeSP) throws IOException {
+ try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
+ MOTree moTree = MOTree.unmarshal(in);
+ OMAConstructed target = findTargetTree(moTree, homeSP.getFQDN());
+ if (target == null) {
+ throw new IOException("Can't find " + homeSP.getFQDN() + " in MO tree");
+ }
+ return MOTree.buildMgmtTree(OMAConstants.PPS_URN, OMAConstants.OMAVersion, target);
+ }
+ }
+
+ private static void writeMO(MOTree moTree, File f, OSUManager osuManager) throws IOException {
+ try (BufferedOutputStream out =
+ new BufferedOutputStream(new FileOutputStream(f, false))) {
+ moTree.marshal(out);
+ out.flush();
+ }
+ }
+
+ private static String fqdnList(Collection<HomeSP> sps) {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for (HomeSP sp : sps) {
+ if (first) {
+ first = false;
+ } else {
+ sb.append(", ");
+ }
+ sb.append(sp.getFQDN());
+ }
+ return sb.toString();
+ }
+
+ private static OMANode buildHomeSPTree(HomeSP homeSP, OMAConstructed root, int instanceID)
+ throws IOException {
+ OMANode providerSubNode = root.addChild(getInstanceString(instanceID),
+ null, null, null);
+
+ // The HomeSP:
+ OMANode homeSpNode = providerSubNode.addChild(TAG_HomeSP, null, null, null);
+ if (!homeSP.getSSIDs().isEmpty()) {
+ OMAConstructed nwkIDNode =
+ (OMAConstructed) homeSpNode.addChild(TAG_NetworkID, null, null, null);
+ int instance = 0;
+ for (Map.Entry<String, Long> entry : homeSP.getSSIDs().entrySet()) {
+ OMAConstructed inode =
+ (OMAConstructed) nwkIDNode
+ .addChild(getInstanceString(instance++), null, null, null);
+ inode.addChild(TAG_SSID, null, entry.getKey(), null);
+ if (entry.getValue() != null) {
+ inode.addChild(TAG_HESSID, null,
+ String.format("%012x", entry.getValue()), null);
+ }
+ }
+ }
+
+ homeSpNode.addChild(TAG_FriendlyName, null, homeSP.getFriendlyName(), null);
+
+ if (homeSP.getIconURL() != null) {
+ homeSpNode.addChild(TAG_IconURL, null, homeSP.getIconURL(), null);
+ }
+
+ homeSpNode.addChild(TAG_FQDN, null, homeSP.getFQDN(), null);
+
+ if (!homeSP.getMatchAllOIs().isEmpty() || !homeSP.getMatchAnyOIs().isEmpty()) {
+ OMAConstructed homeOIList =
+ (OMAConstructed) homeSpNode.addChild(TAG_HomeOIList, null, null, null);
+
+ int instance = 0;
+ for (Long oi : homeSP.getMatchAllOIs()) {
+ OMAConstructed inode =
+ (OMAConstructed) homeOIList.addChild(getInstanceString(instance++),
+ null, null, null);
+ inode.addChild(TAG_HomeOI, null, String.format("%x", oi), null);
+ inode.addChild(TAG_HomeOIRequired, null, "TRUE", null);
+ }
+ for (Long oi : homeSP.getMatchAnyOIs()) {
+ OMAConstructed inode =
+ (OMAConstructed) homeOIList.addChild(getInstanceString(instance++),
+ null, null, null);
+ inode.addChild(TAG_HomeOI, null, String.format("%x", oi), null);
+ inode.addChild(TAG_HomeOIRequired, null, "FALSE", null);
+ }
+ }
+
+ if (!homeSP.getOtherHomePartners().isEmpty()) {
+ OMAConstructed otherPartners =
+ (OMAConstructed) homeSpNode.addChild(TAG_OtherHomePartners, null, null, null);
+ int instance = 0;
+ for (String fqdn : homeSP.getOtherHomePartners()) {
+ OMAConstructed inode =
+ (OMAConstructed) otherPartners.addChild(getInstanceString(instance++),
+ null, null, null);
+ inode.addChild(TAG_FQDN, null, fqdn, null);
+ }
+ }
+
+ if (!homeSP.getRoamingConsortiums().isEmpty()) {
+ homeSpNode.addChild(TAG_RoamingConsortiumOI, null,
+ getRCList(homeSP.getRoamingConsortiums()), null);
+ }
+
+ // The Credential:
+ OMANode credentialNode = providerSubNode.addChild(TAG_Credential, null, null, null);
+ Credential cred = homeSP.getCredential();
+ EAPMethod method = cred.getEAPMethod();
+
+ if (cred.getCtime() > 0) {
+ credentialNode.addChild(TAG_CreationDate,
+ null, DTFormat.format(new Date(cred.getCtime())), null);
+ }
+ if (cred.getExpTime() > 0) {
+ credentialNode.addChild(TAG_ExpirationDate,
+ null, DTFormat.format(new Date(cred.getExpTime())), null);
+ }
+
+ if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_SIM
+ || method.getEAPMethodID() == EAP.EAPMethodID.EAP_AKA
+ || method.getEAPMethodID() == EAP.EAPMethodID.EAP_AKAPrim) {
+
+ OMANode simNode = credentialNode.addChild(TAG_SIM, null, null, null);
+ simNode.addChild(TAG_IMSI, null, cred.getImsi().toString(), null);
+ simNode.addChild(TAG_EAPType, null,
+ Integer.toString(EAP.mapEAPMethod(method.getEAPMethodID())), null);
+
+ } else if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_TTLS) {
+
+ OMANode unpNode = credentialNode.addChild(TAG_UsernamePassword, null, null, null);
+ unpNode.addChild(TAG_Username, null, cred.getUserName(), null);
+ unpNode.addChild(TAG_Password, null,
+ Base64.encodeToString(cred.getPassword().getBytes(StandardCharsets.UTF_8),
+ Base64.DEFAULT), null);
+ OMANode eapNode = unpNode.addChild(TAG_EAPMethod, null, null, null);
+ eapNode.addChild(TAG_EAPType, null,
+ Integer.toString(EAP.mapEAPMethod(method.getEAPMethodID())), null);
+ eapNode.addChild(TAG_InnerMethod, null,
+ ((NonEAPInnerAuth) method.getAuthParam()).getOMAtype(), null);
+
+ } else if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_TLS) {
+
+ OMANode certNode = credentialNode.addChild(TAG_DigitalCertificate, null, null, null);
+ certNode.addChild(TAG_CertificateType, null, Credential.CertTypeX509, null);
+ certNode.addChild(TAG_CertSHA256Fingerprint, null,
+ Utils.toHex(cred.getFingerPrint()), null);
+
+ } else {
+ throw new OMAException("Invalid credential on " + homeSP.getFQDN());
+ }
+
+ credentialNode.addChild(TAG_Realm, null, cred.getRealm(), null);
+
+ // !!! Note: This node defines CRL checking through OSCP, I suspect we won't be able
+ // to do that so it is commented out:
+ //credentialNode.addChild(TAG_CheckAAAServerCertStatus, null, "TRUE", null);
+ return providerSubNode;
+ }
+
+ private static String getInstanceString(int instance) {
+ return "r1i" + instance;
+ }
+
+ private static String getRCList(Collection<Long> rcs) {
+ StringBuilder builder = new StringBuilder();
+ boolean first = true;
+ for (Long roamingConsortium : rcs) {
+ if (first) {
+ first = false;
+ } else {
+ builder.append(',');
+ }
+ builder.append(String.format("%x", roamingConsortium));
+ }
+ return builder.toString();
+ }
+
+ public static List<HomeSP> buildSPs(MOTree moTree) throws OMAException {
+ OMAConstructed spList;
+ List<HomeSP> homeSPs = new ArrayList<>();
+ if (moTree.getRoot().getName().equals(TAG_PerProviderSubscription)) {
+ // The old PPS file was rooted at PPS instead of MgmtTree to conserve space
+ spList = moTree.getRoot();
+
+ if (spList == null) {
+ return homeSPs;
+ }
+
+ for (OMANode node : spList.getChildren()) {
+ if (!node.isLeaf()) {
+ homeSPs.add(buildHomeSP(node, 0));
+ }
+ }
+ } else {
+ for (OMANode ppsRoot : moTree.getRoot().getChildren()) {
+ if (ppsRoot.getName().equals(TAG_PerProviderSubscription)) {
+ Integer updateIdentifier = null;
+ OMANode instance = null;
+ for (OMANode child : ppsRoot.getChildren()) {
+ if (child.getName().equals(TAG_UpdateIdentifier)) {
+ updateIdentifier = getInteger(child);
+ } else if (!child.isLeaf()) {
+ instance = child;
+ }
+ }
+ if (instance == null) {
+ throw new OMAException("PPS node missing instance node");
+ }
+ homeSPs.add(buildHomeSP(instance,
+ updateIdentifier != null ? updateIdentifier : 0));
+ }
+ }
+ }
+
+ return homeSPs;
+ }
+
+ private static HomeSP buildHomeSP(OMANode ppsRoot, int updateIdentifier) throws OMAException {
+ OMANode spRoot = ppsRoot.getChild(TAG_HomeSP);
+
+ String fqdn = spRoot.getScalarValue(Arrays.asList(TAG_FQDN).iterator());
+ String friendlyName = spRoot.getScalarValue(Arrays.asList(TAG_FriendlyName).iterator());
+ String iconURL = spRoot.getScalarValue(Arrays.asList(TAG_IconURL).iterator());
+
+ HashSet<Long> roamingConsortiums = new HashSet<>();
+ String oiString = spRoot.getScalarValue(Arrays.asList(TAG_RoamingConsortiumOI).iterator());
+ if (oiString != null) {
+ for (String oi : oiString.split(",")) {
+ roamingConsortiums.add(Long.parseLong(oi.trim(), 16));
+ }
+ }
+
+ Map<String, Long> ssids = new HashMap<>();
+
+ OMANode ssidListNode = spRoot.getListValue(Arrays.asList(TAG_NetworkID).iterator());
+ if (ssidListNode != null) {
+ for (OMANode ssidRoot : ssidListNode.getChildren()) {
+ OMANode hessidNode = ssidRoot.getChild(TAG_HESSID);
+ ssids.put(ssidRoot.getChild(TAG_SSID).getValue(), getMac(hessidNode));
+ }
+ }
+
+ Set<Long> matchAnyOIs = new HashSet<>();
+ List<Long> matchAllOIs = new ArrayList<>();
+ OMANode homeOIListNode = spRoot.getListValue(Arrays.asList(TAG_HomeOIList).iterator());
+ if (homeOIListNode != null) {
+ for (OMANode homeOIRoot : homeOIListNode.getChildren()) {
+ String homeOI = homeOIRoot.getChild(TAG_HomeOI).getValue();
+ if (Boolean.parseBoolean(homeOIRoot.getChild(TAG_HomeOIRequired).getValue())) {
+ matchAllOIs.add(Long.parseLong(homeOI, 16));
+ } else {
+ matchAnyOIs.add(Long.parseLong(homeOI, 16));
+ }
+ }
+ }
+
+ Set<String> otherHomePartners = new HashSet<>();
+ OMANode otherListNode =
+ spRoot.getListValue(Arrays.asList(TAG_OtherHomePartners).iterator());
+ if (otherListNode != null) {
+ for (OMANode fqdnNode : otherListNode.getChildren()) {
+ otherHomePartners.add(fqdnNode.getChild(TAG_FQDN).getValue());
+ }
+ }
+
+ Credential credential = buildCredential(ppsRoot.getChild(TAG_Credential));
+
+ OMANode policyNode = ppsRoot.getChild(TAG_Policy);
+ Policy policy = policyNode != null ? new Policy(policyNode) : null;
+
+ Map<String, String> aaaTrustRoots;
+ OMANode aaaRootNode = ppsRoot.getChild(TAG_AAAServerTrustRoot);
+ if (aaaRootNode == null) {
+ aaaTrustRoots = null;
+ } else {
+ aaaTrustRoots = new HashMap<>(aaaRootNode.getChildren().size());
+ for (OMANode child : aaaRootNode.getChildren()) {
+ aaaTrustRoots.put(getString(child, TAG_CertURL),
+ getString(child, TAG_CertSHA256Fingerprint));
+ }
+ }
+
+ OMANode updateNode = ppsRoot.getChild(TAG_SubscriptionUpdate);
+ UpdateInfo subscriptionUpdate = updateNode != null ? new UpdateInfo(updateNode) : null;
+ OMANode subNode = ppsRoot.getChild(TAG_SubscriptionParameters);
+ SubscriptionParameters subscriptionParameters = subNode != null ?
+ new SubscriptionParameters(subNode) : null;
+
+ return new HomeSP(ssids, fqdn, roamingConsortiums, otherHomePartners,
+ matchAnyOIs, matchAllOIs, friendlyName, iconURL, credential,
+ policy, getInteger(ppsRoot.getChild(TAG_CredentialPriority), 0),
+ aaaTrustRoots, subscriptionUpdate, subscriptionParameters, updateIdentifier);
+ }
+
+ private static Credential buildCredential(OMANode credNode) throws OMAException {
+ long ctime = getTime(credNode.getChild(TAG_CreationDate));
+ long expTime = getTime(credNode.getChild(TAG_ExpirationDate));
+ String realm = getString(credNode.getChild(TAG_Realm));
+ boolean checkAAACert = getBoolean(credNode.getChild(TAG_CheckAAAServerCertStatus));
+
+ OMANode unNode = credNode.getChild(TAG_UsernamePassword);
+ OMANode certNode = credNode.getChild(TAG_DigitalCertificate);
+ OMANode simNode = credNode.getChild(TAG_SIM);
+
+ int alternatives = 0;
+ alternatives += unNode != null ? 1 : 0;
+ alternatives += certNode != null ? 1 : 0;
+ alternatives += simNode != null ? 1 : 0;
+ if (alternatives != 1) {
+ throw new OMAException("Expected exactly one credential type, got " + alternatives);
+ }
+
+ if (unNode != null) {
+ String userName = getString(unNode.getChild(TAG_Username));
+ String password = getString(unNode.getChild(TAG_Password));
+ boolean machineManaged = getBoolean(unNode.getChild(TAG_MachineManaged));
+ String softTokenApp = getString(unNode.getChild(TAG_SoftTokenApp));
+ boolean ableToShare = getBoolean(unNode.getChild(TAG_AbleToShare));
+
+ OMANode eapMethodNode = unNode.getChild(TAG_EAPMethod);
+ int eapID = getInteger(eapMethodNode.getChild(TAG_EAPType));
+
+ EAP.EAPMethodID eapMethodID = EAP.mapEAPMethod(eapID);
+ if (eapMethodID == null) {
+ throw new OMAException("Unknown EAP method: " + eapID);
+ }
+
+ Long vid = getOptionalInteger(eapMethodNode.getChild(TAG_VendorId));
+ Long vtype = getOptionalInteger(eapMethodNode.getChild(TAG_VendorType));
+ Long innerEAPType = getOptionalInteger(eapMethodNode.getChild(TAG_InnerEAPType));
+ EAP.EAPMethodID innerEAPMethod = null;
+ if (innerEAPType != null) {
+ innerEAPMethod = EAP.mapEAPMethod(innerEAPType.intValue());
+ if (innerEAPMethod == null) {
+ throw new OMAException("Bad inner EAP method: " + innerEAPType);
+ }
+ }
+
+ Long innerVid = getOptionalInteger(eapMethodNode.getChild(TAG_InnerVendorID));
+ Long innerVtype = getOptionalInteger(eapMethodNode.getChild(TAG_InnerVendorType));
+ String innerNonEAPMethod = getString(eapMethodNode.getChild(TAG_InnerMethod));
+
+ EAPMethod eapMethod;
+ if (innerEAPMethod != null) {
+ eapMethod = new EAPMethod(eapMethodID, new InnerAuthEAP(innerEAPMethod));
+ } else if (vid != null) {
+ eapMethod = new EAPMethod(eapMethodID,
+ new ExpandedEAPMethod(EAP.AuthInfoID.ExpandedEAPMethod,
+ vid.intValue(), vtype));
+ } else if (innerVid != null) {
+ eapMethod =
+ new EAPMethod(eapMethodID, new ExpandedEAPMethod(EAP.AuthInfoID
+ .ExpandedInnerEAPMethod, innerVid.intValue(), innerVtype));
+ } else if (innerNonEAPMethod != null) {
+ eapMethod = new EAPMethod(eapMethodID, new NonEAPInnerAuth(innerNonEAPMethod));
+ } else {
+ throw new OMAException("Incomplete set of EAP parameters");
+ }
+
+ return new Credential(ctime, expTime, realm, checkAAACert, eapMethod, userName,
+ password, machineManaged, softTokenApp, ableToShare);
+ }
+ if (certNode != null) {
+ try {
+ String certTypeString = getString(certNode.getChild(TAG_CertificateType));
+ byte[] fingerPrint = getOctets(certNode.getChild(TAG_CertSHA256Fingerprint));
+
+ EAPMethod eapMethod = new EAPMethod(EAP.EAPMethodID.EAP_TLS, null);
+
+ return new Credential(ctime, expTime, realm, checkAAACert, eapMethod,
+ Credential.mapCertType(certTypeString), fingerPrint);
+ } catch (NumberFormatException nfe) {
+ throw new OMAException("Bad hex string: " + nfe.toString());
+ }
+ }
+ if (simNode != null) {
+ try {
+ IMSIParameter imsi = new IMSIParameter(getString(simNode.getChild(TAG_IMSI)));
+
+ EAPMethod eapMethod =
+ new EAPMethod(EAP.mapEAPMethod(getInteger(simNode.getChild(TAG_EAPType))),
+ null);
+
+ return new Credential(ctime, expTime, realm, checkAAACert, eapMethod, imsi);
+ } catch (IOException ioe) {
+ throw new OMAException("Failed to parse IMSI: " + ioe);
+ }
+ }
+ throw new OMAException("Missing credential parameters");
+ }
+
+ public static OMANode getChild(OMANode node, String key) throws OMAException {
+ OMANode child = node.getChild(key);
+ if (child == null) {
+ throw new OMAException("No such node: " + key);
+ }
+ return child;
+ }
+
+ public static String getString(OMANode node, String key) throws OMAException {
+ OMANode child = node.getChild(key);
+ if (child == null) {
+ throw new OMAException("Missing value for " + key);
+ } else if (!child.isLeaf()) {
+ throw new OMAException(key + " is not a leaf node");
+ }
+ return child.getValue();
+ }
+
+ public static long getLong(OMANode node, String key, Long dflt) throws OMAException {
+ OMANode child = node.getChild(key);
+ if (child == null) {
+ if (dflt != null) {
+ return dflt;
+ } else {
+ throw new OMAException("Missing value for " + key);
+ }
+ } else {
+ if (!child.isLeaf()) {
+ throw new OMAException(key + " is not a leaf node");
+ }
+ String value = child.getValue();
+ try {
+ long result = Long.parseLong(value);
+ if (result < 0) {
+ throw new OMAException("Negative value for " + key);
+ }
+ return result;
+ } catch (NumberFormatException nfe) {
+ throw new OMAException("Value for " + key + " is non-numeric: " + value);
+ }
+ }
+ }
+
+ public static <T> T getSelection(OMANode node, String key) throws OMAException {
+ OMANode child = node.getChild(key);
+ if (child == null) {
+ throw new OMAException("Missing value for " + key);
+ } else if (!child.isLeaf()) {
+ throw new OMAException(key + " is not a leaf node");
+ }
+ return getSelection(key, child.getValue());
+ }
+
+ public static <T> T getSelection(String key, String value) throws OMAException {
+ if (value == null) {
+ throw new OMAException("No value for " + key);
+ }
+ Map<String, Object> kvp = sSelectionMap.get(key);
+ T result = (T) kvp.get(value.toLowerCase());
+ if (result == null) {
+ throw new OMAException("Invalid value '" + value + "' for " + key);
+ }
+ return result;
+ }
+
+ private static boolean getBoolean(OMANode boolNode) {
+ return boolNode != null && Boolean.parseBoolean(boolNode.getValue());
+ }
+
+ public static String getString(OMANode stringNode) {
+ return stringNode != null ? stringNode.getValue() : null;
+ }
+
+ private static int getInteger(OMANode intNode, int dflt) throws OMAException {
+ if (intNode == null) {
+ return dflt;
+ }
+ return getInteger(intNode);
+ }
+
+ private static int getInteger(OMANode intNode) throws OMAException {
+ if (intNode == null) {
+ throw new OMAException("Missing integer value");
+ }
+ try {
+ return Integer.parseInt(intNode.getValue());
+ } catch (NumberFormatException nfe) {
+ throw new OMAException("Invalid integer: " + intNode.getValue());
+ }
+ }
+
+ private static Long getMac(OMANode macNode) throws OMAException {
+ if (macNode == null) {
+ return null;
+ }
+ try {
+ return Long.parseLong(macNode.getValue(), 16);
+ } catch (NumberFormatException nfe) {
+ throw new OMAException("Invalid MAC: " + macNode.getValue());
+ }
+ }
+
+ private static Long getOptionalInteger(OMANode intNode) throws OMAException {
+ if (intNode == null) {
+ return null;
+ }
+ try {
+ return Long.parseLong(intNode.getValue());
+ } catch (NumberFormatException nfe) {
+ throw new OMAException("Invalid integer: " + intNode.getValue());
+ }
+ }
+
+ public static long getTime(OMANode timeNode) throws OMAException {
+ if (timeNode == null) {
+ return Utils.UNSET_TIME;
+ }
+ String timeText = timeNode.getValue();
+ try {
+ Date date = DTFormat.parse(timeText);
+ return date.getTime();
+ } catch (ParseException pe) {
+ throw new OMAException("Badly formatted time: " + timeText);
+ }
+ }
+
+ private static byte[] getOctets(OMANode octetNode) throws OMAException {
+ if (octetNode == null) {
+ throw new OMAException("Missing byte value");
+ }
+ return Utils.hexToBytes(octetNode.getValue());
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/MOTree.java b/packages/Osu/src/com/android/hotspot2/omadm/MOTree.java
new file mode 100644
index 0000000..93beaf4
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/MOTree.java
@@ -0,0 +1,269 @@
+package com.android.hotspot2.omadm;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class MOTree {
+ public static final String MgmtTreeTag = "MgmtTree";
+
+ public static final String NodeTag = "Node";
+ public static final String NodeNameTag = "NodeName";
+ public static final String PathTag = "Path";
+ public static final String ValueTag = "Value";
+ public static final String RTPropTag = "RTProperties";
+ public static final String TypeTag = "Type";
+ public static final String DDFNameTag = "DDFName";
+
+ private final String mUrn;
+ private final String mDtdRev;
+ private final OMAConstructed mRoot;
+
+ public MOTree(XMLNode node, String urn) throws IOException, SAXException {
+ Iterator<XMLNode> children = node.getChildren().iterator();
+
+ String dtdRev = null;
+
+ while (children.hasNext()) {
+ XMLNode child = children.next();
+ if (child.getTag().equals(OMAConstants.SyncMLVersionTag)) {
+ dtdRev = child.getText();
+ children.remove();
+ break;
+ }
+ }
+
+ mUrn = urn;
+ mDtdRev = dtdRev;
+
+ mRoot = new MgmtTreeRoot(node, dtdRev);
+
+ for (XMLNode child : node.getChildren()) {
+ buildNode(mRoot, child);
+ }
+ }
+
+ public MOTree(String urn, String rev, OMAConstructed root) throws IOException {
+ mUrn = urn;
+ mDtdRev = rev;
+ mRoot = root;
+ }
+
+ public static MOTree buildMgmtTree(String urn, String rev, OMAConstructed root)
+ throws IOException {
+ OMAConstructed realRoot;
+ switch (urn) {
+ case OMAConstants.PPS_URN:
+ case OMAConstants.DevInfoURN:
+ case OMAConstants.DevDetailURN:
+ case OMAConstants.DevDetailXURN:
+ realRoot = new OMAConstructed(null, MgmtTreeTag, urn, "xmlns", OMAConstants.SyncML);
+ realRoot.addChild(root);
+ return new MOTree(urn, rev, realRoot);
+ default:
+ return new MOTree(urn, rev, root);
+ }
+ }
+
+ public static boolean hasMgmtTreeTag(String text) {
+ for (int n = 0; n < text.length(); n++) {
+ char ch = text.charAt(n);
+ if (ch > ' ') {
+ return text.regionMatches(true, n, '<' + MgmtTreeTag + '>',
+ 0, MgmtTreeTag.length() + 2);
+ }
+ }
+ return false;
+ }
+
+ private static class NodeData {
+ private final String mName;
+ private String mPath;
+ private String mValue;
+
+ private NodeData(String name) {
+ mName = name;
+ }
+
+ private void setPath(String path) {
+ mPath = path;
+ }
+
+ private void setValue(String value) {
+ mValue = value;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getPath() {
+ return mPath;
+ }
+
+ public String getValue() {
+ return mValue;
+ }
+ }
+
+ private static void buildNode(OMANode parent, XMLNode node) throws IOException {
+ if (!node.getTag().equals(NodeTag))
+ throw new IOException("Node is a '" + node.getTag() + "' instead of a 'Node'");
+
+ Map<String, XMLNode> checkMap = new HashMap<>(3);
+ String context = null;
+ List<NodeData> values = new ArrayList<>();
+ List<XMLNode> children = new ArrayList<>();
+
+ NodeData curValue = null;
+
+ for (XMLNode child : node.getChildren()) {
+ XMLNode old = checkMap.put(child.getTag(), child);
+
+ switch (child.getTag()) {
+ case NodeNameTag:
+ if (curValue != null)
+ throw new IOException(NodeNameTag + " not expected");
+ curValue = new NodeData(child.getText());
+
+ break;
+ case PathTag:
+ if (curValue == null || curValue.getPath() != null)
+ throw new IOException(PathTag + " not expected");
+ curValue.setPath(child.getText());
+
+ break;
+ case ValueTag:
+ if (!children.isEmpty())
+ throw new IOException(ValueTag + " in constructed node");
+ if (curValue == null || curValue.getValue() != null)
+ throw new IOException(ValueTag + " not expected");
+ curValue.setValue(child.getText());
+ values.add(curValue);
+ curValue = null;
+
+ break;
+ case RTPropTag:
+ if (old != null)
+ throw new IOException("Duplicate " + RTPropTag);
+ XMLNode typeNode = getNextNode(child, TypeTag);
+ XMLNode ddfName = getNextNode(typeNode, DDFNameTag);
+ context = ddfName.getText();
+ if (context == null)
+ throw new IOException("No text in " + DDFNameTag);
+
+ break;
+ case NodeTag:
+ if (!values.isEmpty())
+ throw new IOException("Scalar node " + node.getText() + " has Node child");
+ children.add(child);
+
+ break;
+ }
+ }
+
+ if (values.isEmpty()) {
+ if (curValue == null)
+ throw new IOException("Missing name");
+
+ OMANode subNode = parent.addChild(curValue.getName(),
+ context, null, curValue.getPath());
+
+ for (XMLNode child : children) {
+ buildNode(subNode, child);
+ }
+ } else {
+ if (!children.isEmpty())
+ throw new IOException("Got both sub nodes and value(s)");
+
+ for (NodeData nodeData : values) {
+ parent.addChild(nodeData.getName(), context,
+ nodeData.getValue(), nodeData.getPath());
+ }
+ }
+ }
+
+ private static XMLNode getNextNode(XMLNode node, String tag) throws IOException {
+ if (node == null)
+ throw new IOException("No node for " + tag);
+ if (node.getChildren().size() != 1)
+ throw new IOException("Expected " + node.getTag() + " to have exactly one child");
+ XMLNode child = node.getChildren().iterator().next();
+ if (!child.getTag().equals(tag))
+ throw new IOException("Expected " + node.getTag() + " to have child '" + tag +
+ "' instead of '" + child.getTag() + "'");
+ return child;
+ }
+
+ public String getUrn() {
+ return mUrn;
+ }
+
+ public String getDtdRev() {
+ return mDtdRev;
+ }
+
+ public OMAConstructed getRoot() {
+ return mRoot;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("MO Tree v").append(mDtdRev).append(", urn ").append(mUrn).append(")\n");
+ sb.append(mRoot);
+
+ return sb.toString();
+ }
+
+ public void marshal(OutputStream out) throws IOException {
+ out.write("tree ".getBytes(StandardCharsets.UTF_8));
+ OMAConstants.serializeString(mDtdRev, out);
+ out.write(String.format("(%s)\n", mUrn).getBytes(StandardCharsets.UTF_8));
+ mRoot.marshal(out, 0);
+ }
+
+ public static MOTree unmarshal(InputStream in) throws IOException {
+ boolean strip = true;
+ StringBuilder tree = new StringBuilder();
+ for (; ; ) {
+ int octet = in.read();
+ if (octet < 0) {
+ return null;
+ } else if (octet > ' ') {
+ tree.append((char) octet);
+ strip = false;
+ } else if (!strip) {
+ break;
+ }
+ }
+ if (!tree.toString().equals("tree")) {
+ throw new IOException("Not a tree: " + tree);
+ }
+
+ String version = OMAConstants.deserializeString(in);
+ int next = in.read();
+ if (next != '(') {
+ throw new IOException("Expected URN in tree definition");
+ }
+ String urn = OMAConstants.readURN(in);
+
+ OMAConstructed root = OMANode.unmarshal(in);
+
+ return new MOTree(urn, version, root);
+ }
+
+ public String toXml() {
+ StringBuilder sb = new StringBuilder();
+ mRoot.toXml(sb);
+ return sb.toString();
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/MgmtTreeRoot.java b/packages/Osu/src/com/android/hotspot2/omadm/MgmtTreeRoot.java
new file mode 100644
index 0000000..97fb7cd
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/MgmtTreeRoot.java
@@ -0,0 +1,32 @@
+package com.android.hotspot2.omadm;
+
+import java.util.Map;
+
+public class MgmtTreeRoot extends OMAConstructed {
+ private final String mDtdRev;
+
+ public MgmtTreeRoot(XMLNode node, String dtdRev) {
+ super(null, MOTree.MgmtTreeTag, null, new MultiValueMap<OMANode>(),
+ node.getTextualAttributes());
+ mDtdRev = dtdRev;
+ }
+
+ @Override
+ public void toXml(StringBuilder sb) {
+ sb.append('<').append(MOTree.MgmtTreeTag);
+ if (getAttributes() != null && !getAttributes().isEmpty()) {
+ for (Map.Entry<String, String> avp : getAttributes().entrySet()) {
+ sb.append(' ').append(avp.getKey()).append("=\"")
+ .append(avp.getValue()).append('"');
+ }
+ }
+ sb.append(">\n");
+
+ sb.append('<').append(OMAConstants.SyncMLVersionTag).append('>').append(mDtdRev)
+ .append("</").append(OMAConstants.SyncMLVersionTag).append(">\n");
+ for (OMANode child : getChildren()) {
+ child.toXml(sb);
+ }
+ sb.append("</").append(MOTree.MgmtTreeTag).append(">\n");
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/MultiValueMap.java b/packages/Osu/src/com/android/hotspot2/omadm/MultiValueMap.java
new file mode 100644
index 0000000..ead0dbc
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/MultiValueMap.java
@@ -0,0 +1,117 @@
+package com.android.hotspot2.omadm;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MultiValueMap<T> {
+ private final Map<String, ArrayList<T>> mMap = new LinkedHashMap<>();
+
+ public void put(String key, T value) {
+ key = key.toLowerCase();
+ ArrayList<T> values = mMap.get(key);
+ if (values == null) {
+ values = new ArrayList<>();
+ mMap.put(key, values);
+ }
+ values.add(value);
+ }
+
+ public T get(String key) {
+ key = key.toLowerCase();
+ List<T> values = mMap.get(key);
+ if (values == null) {
+ return null;
+ } else if (values.size() == 1) {
+ return values.get(0);
+ } else {
+ throw new IllegalArgumentException("Cannot do get on multi-value");
+ }
+ }
+
+ public T replace(String key, T oldValue, T newValue) {
+ key = key.toLowerCase();
+ List<T> values = mMap.get(key);
+ if (values == null) {
+ return null;
+ }
+
+ for (int n = 0; n < values.size(); n++) {
+ T value = values.get(n);
+ if (value == oldValue) {
+ values.set(n, newValue);
+ return value;
+ }
+ }
+ return null;
+ }
+
+ public T remove(String key, T value) {
+ key = key.toLowerCase();
+ List<T> values = mMap.get(key);
+ if (values == null) {
+ return null;
+ }
+
+ T result = null;
+ Iterator<T> valueIterator = values.iterator();
+ while (valueIterator.hasNext()) {
+ if (valueIterator.next() == value) {
+ valueIterator.remove();
+ result = value;
+ break;
+ }
+ }
+ if (values.isEmpty()) {
+ mMap.remove(key);
+ }
+ return result;
+ }
+
+ public T remove(T value) {
+ T result = null;
+ Iterator<Map.Entry<String, ArrayList<T>>> iterator = mMap.entrySet().iterator();
+ while (iterator.hasNext()) {
+ ArrayList<T> values = iterator.next().getValue();
+ Iterator<T> valueIterator = values.iterator();
+ while (valueIterator.hasNext()) {
+ if (valueIterator.next() == value) {
+ valueIterator.remove();
+ result = value;
+ break;
+ }
+ }
+ if (result != null) {
+ if (values.isEmpty()) {
+ iterator.remove();
+ }
+ break;
+ }
+ }
+ return result;
+ }
+
+ public Collection<T> values() {
+ List<T> allValues = new ArrayList<>(mMap.size());
+ for (List<T> values : mMap.values()) {
+ for (T value : values) {
+ allValues.add(value);
+ }
+ }
+ return allValues;
+ }
+
+ public T getSingletonValue() {
+ if (mMap.size() != 1) {
+ throw new IllegalArgumentException("Map is not a single entry map");
+ }
+ List<T> values = mMap.values().iterator().next();
+ if (values.size() != 1) {
+ throw new IllegalArgumentException("Map is not a single entry map");
+ }
+ return values.iterator().next();
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/NodeAttribute.java b/packages/Osu/src/com/android/hotspot2/omadm/NodeAttribute.java
new file mode 100644
index 0000000..e4a08b3
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/NodeAttribute.java
@@ -0,0 +1,30 @@
+package com.android.hotspot2.omadm;
+
+public class NodeAttribute {
+ private final String mName;
+ private final String mType;
+ private final String mValue;
+
+ public NodeAttribute(String name, String type, String value) {
+ mName = name;
+ mType = type;
+ mValue = value;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getValue() {
+ return mValue;
+ }
+
+ public String getType() {
+ return mType;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s (%s) = '%s'", mName, mType, mValue);
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/OMAConstants.java b/packages/Osu/src/com/android/hotspot2/omadm/OMAConstants.java
new file mode 100644
index 0000000..92d8ed7
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/OMAConstants.java
@@ -0,0 +1,158 @@
+package com.android.hotspot2.omadm;
+
+import com.android.hotspot2.osu.OSUError;
+import com.android.hotspot2.osu.OSUStatus;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+
+public class OMAConstants {
+ private OMAConstants() {
+ }
+
+ public static final String MOVersion = "1.0";
+ public static final String PPS_URN = "urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0";
+ public static final String DevInfoURN = "urn:oma:mo:oma-dm-devinfo:1.0";
+ public static final String DevDetailURN = "urn:oma:mo:oma-dm-devdetail:1.0";
+ public static final String DevDetailXURN = "urn:wfa:mo-ext:hotspot2dot0-devdetail-ext:1.0";
+
+ public static final String[] SupportedMO_URNs = {
+ PPS_URN, DevInfoURN, DevDetailURN, DevDetailXURN
+ };
+
+ public static final String SppMOAttribute = "spp:moURN";
+ public static final String TAG_PostDevData = "spp:sppPostDevData";
+ public static final String TAG_SupportedVersions = "spp:supportedSPPVersions";
+ public static final String TAG_SupportedMOs = "spp:supportedMOList";
+ public static final String TAG_UpdateResponse = "spp:sppUpdateResponse";
+ public static final String TAG_MOContainer = "spp:moContainer";
+ public static final String TAG_Version = "spp:sppVersion";
+
+ public static final String TAG_SessionID = "spp:sessionID";
+ public static final String TAG_Status = "spp:sppStatus";
+ public static final String TAG_Error = "spp:sppError";
+
+ public static final String SyncMLVersionTag = "VerDTD";
+ public static final String OMAVersion = "1.2";
+ public static final String SyncML = "syncml:dmddf1.2";
+
+ private static final byte[] INDENT = new byte[1024];
+
+ private static final Map<OSUStatus, String> sStatusStrings = new EnumMap<>(OSUStatus.class);
+ private static final Map<String, OSUStatus> sStatusEnums = new HashMap<>();
+ private static final Map<OSUError, String> sErrorStrings = new EnumMap<>(OSUError.class);
+ private static final Map<String, OSUError> sErrorEnums = new HashMap<>();
+
+ static {
+ sStatusStrings.put(OSUStatus.OK, "OK");
+ sStatusStrings.put(OSUStatus.ProvComplete,
+ "Provisioning complete, request sppUpdateResponse");
+ sStatusStrings.put(OSUStatus.RemediationComplete,
+ "Remediation complete, request sppUpdateResponse");
+ sStatusStrings.put(OSUStatus.UpdateComplete, "Update complete, request sppUpdateResponse");
+ sStatusStrings.put(OSUStatus.ExchangeComplete, "Exchange complete, release TLS connection");
+ sStatusStrings.put(OSUStatus.Unknown, "No update available at this time");
+ sStatusStrings.put(OSUStatus.Error, "Error occurred");
+
+ for (Map.Entry<OSUStatus, String> entry : sStatusStrings.entrySet()) {
+ sStatusEnums.put(entry.getValue().toLowerCase(), entry.getKey());
+ }
+
+ sErrorStrings.put(OSUError.SPPversionNotSupported, "SPP version not supported");
+ sErrorStrings.put(OSUError.MOsNotSupported, "One or more mandatory MOs not supported");
+ sErrorStrings.put(OSUError.CredentialsFailure,
+ "Credentials cannot be provisioned at this time");
+ sErrorStrings.put(OSUError.RemediationFailure,
+ "Remediation cannot be completed at this time");
+ sErrorStrings.put(OSUError.ProvisioningFailed,
+ "Provisioning cannot be completed at this time");
+ sErrorStrings.put(OSUError.ExistingCertificate, "Continue to use existing certificate");
+ sErrorStrings.put(OSUError.CookieInvalid, "Cookie invalid");
+ sErrorStrings.put(OSUError.WebSessionID,
+ "No corresponding web-browser-connection Session ID");
+ sErrorStrings.put(OSUError.PermissionDenied, "Permission denied");
+ sErrorStrings.put(OSUError.CommandFailed, "Command failed");
+ sErrorStrings.put(OSUError.MOaddOrUpdateFailed, "MO addition or update failed");
+ sErrorStrings.put(OSUError.DeviceFull, "Device full");
+ sErrorStrings.put(OSUError.BadTreeURI, "Bad management tree URI");
+ sErrorStrings.put(OSUError.TooLarge, "Requested entity too large");
+ sErrorStrings.put(OSUError.CommandNotAllowed, "Command not allowed");
+ sErrorStrings.put(OSUError.UserAborted, "Command not executed due to user");
+ sErrorStrings.put(OSUError.NotFound, "Not found");
+ sErrorStrings.put(OSUError.Other, "Other");
+
+ for (Map.Entry<OSUError, String> entry : sErrorStrings.entrySet()) {
+ sErrorEnums.put(entry.getValue().toLowerCase(), entry.getKey());
+ }
+ Arrays.fill(INDENT, (byte) ' ');
+ }
+
+ public static String mapStatus(OSUStatus status) {
+ return sStatusStrings.get(status);
+ }
+
+ public static OSUStatus mapStatus(String status) {
+ return sStatusEnums.get(status.toLowerCase());
+ }
+
+ public static String mapError(OSUError error) {
+ return sErrorStrings.get(error);
+ }
+
+ public static OSUError mapError(String error) {
+ return sErrorEnums.get(error.toLowerCase());
+ }
+
+ public static void serializeString(String s, OutputStream out) throws IOException {
+ byte[] octets = s.getBytes(StandardCharsets.UTF_8);
+ byte[] prefix = String.format("%x:", octets.length).getBytes(StandardCharsets.UTF_8);
+ out.write(prefix);
+ out.write(octets);
+ }
+
+ public static void indent(int level, OutputStream out) throws IOException {
+ out.write(INDENT, 0, level);
+ }
+
+ public static String deserializeString(InputStream in) throws IOException {
+ StringBuilder prefix = new StringBuilder();
+ for (; ; ) {
+ byte b = (byte) in.read();
+ if (b == '.')
+ return null;
+ else if (b == ':')
+ break;
+ else if (b > ' ')
+ prefix.append((char) b);
+ }
+ int length = Integer.parseInt(prefix.toString(), 16);
+ byte[] octets = new byte[length];
+ int offset = 0;
+ while (offset < octets.length) {
+ int amount = in.read(octets, offset, octets.length - offset);
+ if (amount <= 0)
+ throw new EOFException();
+ offset += amount;
+ }
+ return new String(octets, StandardCharsets.UTF_8);
+ }
+
+ public static String readURN(InputStream in) throws IOException {
+ StringBuilder urn = new StringBuilder();
+
+ for (; ; ) {
+ byte b = (byte) in.read();
+ if (b == ')')
+ break;
+ urn.append((char) b);
+ }
+ return urn.toString();
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/OMAConstructed.java b/packages/Osu/src/com/android/hotspot2/omadm/OMAConstructed.java
new file mode 100644
index 0000000..e5285f2
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/OMAConstructed.java
@@ -0,0 +1,169 @@
+package com.android.hotspot2.omadm;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+
+public class OMAConstructed extends OMANode {
+ private final MultiValueMap<OMANode> mChildren;
+
+ public OMAConstructed(OMAConstructed parent, String name, String context, String... avps) {
+ this(parent, name, context, new MultiValueMap<OMANode>(), buildAttributes(avps));
+ }
+
+ protected OMAConstructed(OMAConstructed parent, String name, String context,
+ MultiValueMap<OMANode> children, Map<String, String> avps) {
+ super(parent, name, context, avps);
+ mChildren = children;
+ }
+
+ @Override
+ public OMANode addChild(String name, String context, String value, String pathString)
+ throws IOException {
+ if (pathString == null) {
+ OMANode child = value != null ?
+ new OMAScalar(this, name, context, value) :
+ new OMAConstructed(this, name, context);
+ mChildren.put(name, child);
+ return child;
+ } else {
+ OMANode target = this;
+ while (target.getParent() != null)
+ target = target.getParent();
+
+ for (String element : pathString.split("/")) {
+ target = target.getChild(element);
+ if (target == null)
+ throw new IOException("No child node '" + element + "' in " + getPathString());
+ else if (target.isLeaf())
+ throw new IOException("Cannot add child to leaf node: " + getPathString());
+ }
+ return target.addChild(name, context, value, null);
+ }
+ }
+
+ @Override
+ public OMAConstructed reparent(OMAConstructed parent) {
+ return new OMAConstructed(parent, getName(), getContext(), mChildren, getAttributes());
+ }
+
+ public void addChild(OMANode child) {
+ mChildren.put(child.getName(), child.reparent(this));
+ }
+
+ public String getScalarValue(Iterator<String> path) throws OMAException {
+ if (!path.hasNext()) {
+ throw new OMAException("Path too short for " + getPathString());
+ }
+ String tag = path.next();
+ OMANode child = mChildren.get(tag);
+ if (child != null) {
+ return child.getScalarValue(path);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public OMANode getListValue(Iterator<String> path) throws OMAException {
+ if (!path.hasNext()) {
+ return null;
+ }
+ String tag = path.next();
+ OMANode child;
+ if (tag.equals("?")) {
+ child = mChildren.getSingletonValue();
+ } else {
+ child = mChildren.get(tag);
+ }
+
+ if (child == null) {
+ return null;
+ } else if (path.hasNext()) {
+ return child.getListValue(path);
+ } else {
+ return child;
+ }
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+
+ @Override
+ public Collection<OMANode> getChildren() {
+ return Collections.unmodifiableCollection(mChildren.values());
+ }
+
+ public OMANode getChild(String name) {
+ return mChildren.get(name);
+ }
+
+ public OMANode replaceNode(OMANode oldNode, OMANode newNode) {
+ return mChildren.replace(oldNode.getName(), oldNode, newNode);
+ }
+
+ public OMANode removeNode(String key, OMANode node) {
+ if (key.equals("?")) {
+ return mChildren.remove(node);
+ } else {
+ return mChildren.remove(key, node);
+ }
+ }
+
+ @Override
+ public String getValue() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void toString(StringBuilder sb, int level) {
+ sb.append(getPathString());
+ if (getContext() != null) {
+ sb.append(" (").append(getContext()).append(')');
+ }
+ sb.append('\n');
+
+ for (OMANode node : mChildren.values()) {
+ node.toString(sb, level + 1);
+ }
+ }
+
+ @Override
+ public void marshal(OutputStream out, int level) throws IOException {
+ OMAConstants.indent(level, out);
+ OMAConstants.serializeString(getName(), out);
+ if (getContext() != null) {
+ out.write(String.format("(%s)", getContext()).getBytes(StandardCharsets.UTF_8));
+ }
+ out.write(new byte[]{'+', '\n'});
+
+ for (OMANode child : mChildren.values()) {
+ child.marshal(out, level + 1);
+ }
+ OMAConstants.indent(level, out);
+ out.write(".\n".getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public void fillPayload(StringBuilder sb) {
+ if (getContext() != null) {
+ sb.append('<').append(MOTree.RTPropTag).append(">\n");
+ sb.append('<').append(MOTree.TypeTag).append(">\n");
+ sb.append('<').append(MOTree.DDFNameTag).append(">");
+ sb.append(getContext());
+ sb.append("</").append(MOTree.DDFNameTag).append(">\n");
+ sb.append("</").append(MOTree.TypeTag).append(">\n");
+ sb.append("</").append(MOTree.RTPropTag).append(">\n");
+ }
+
+ for (OMANode child : getChildren()) {
+ child.toXml(sb);
+ }
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/OMAException.java b/packages/Osu/src/com/android/hotspot2/omadm/OMAException.java
new file mode 100644
index 0000000..33a6e37
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/OMAException.java
@@ -0,0 +1,9 @@
+package com.android.hotspot2.omadm;
+
+import java.io.IOException;
+
+public class OMAException extends IOException {
+ public OMAException(String message) {
+ super(message);
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/OMANode.java b/packages/Osu/src/com/android/hotspot2/omadm/OMANode.java
new file mode 100644
index 0000000..a428b2f
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/OMANode.java
@@ -0,0 +1,165 @@
+package com.android.hotspot2.omadm;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public abstract class OMANode {
+ private final OMAConstructed mParent;
+ private final String mName;
+ private final String mContext;
+ private final Map<String, String> mAttributes;
+
+ protected OMANode(OMAConstructed parent, String name, String context,
+ Map<String, String> avps) {
+ mParent = parent;
+ mName = name;
+ mContext = context;
+ mAttributes = avps;
+ }
+
+ protected static Map<String, String> buildAttributes(String[] avps) {
+ if (avps == null) {
+ return null;
+ }
+ Map<String, String> attributes = new HashMap<>();
+ for (int n = 0; n < avps.length; n += 2) {
+ attributes.put(avps[n], avps[n + 1]);
+ }
+ return attributes;
+ }
+
+ protected Map<String, String> getAttributes() {
+ return mAttributes;
+ }
+
+ public OMAConstructed getParent() {
+ return mParent;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getContext() {
+ return mContext;
+ }
+
+ public List<String> getPath() {
+ LinkedList<String> path = new LinkedList<>();
+ for (OMANode node = this; node != null; node = node.getParent()) {
+ path.addFirst(node.getName());
+ }
+ return path;
+ }
+
+ public String getPathString() {
+ StringBuilder sb = new StringBuilder();
+ for (String element : getPath()) {
+ sb.append('/').append(element);
+ }
+ return sb.toString();
+ }
+
+ public abstract OMANode reparent(OMAConstructed parent);
+
+ public abstract String getScalarValue(Iterator<String> path) throws OMAException;
+
+ public abstract OMANode getListValue(Iterator<String> path) throws OMAException;
+
+ public abstract boolean isLeaf();
+
+ public abstract Collection<OMANode> getChildren();
+
+ public abstract OMANode getChild(String name) throws OMAException;
+
+ public abstract String getValue();
+
+ public abstract OMANode addChild(String name, String context, String value, String path)
+ throws IOException;
+
+ public abstract void marshal(OutputStream out, int level) throws IOException;
+
+ public abstract void toString(StringBuilder sb, int level);
+
+ public abstract void fillPayload(StringBuilder sb);
+
+ public void toXml(StringBuilder sb) {
+ sb.append('<').append(MOTree.NodeTag);
+ if (mAttributes != null && !mAttributes.isEmpty()) {
+ for (Map.Entry<String, String> avp : mAttributes.entrySet()) {
+ sb.append(' ').append(avp.getKey()).append("=\"")
+ .append(avp.getValue()).append('"');
+ }
+ }
+ sb.append(">\n");
+
+ sb.append('<').append(MOTree.NodeNameTag).append('>');
+ sb.append(getName());
+ sb.append("</").append(MOTree.NodeNameTag).append(">\n");
+
+ fillPayload(sb);
+
+ sb.append("</").append(MOTree.NodeTag).append(">\n");
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ toString(sb, 0);
+ return sb.toString();
+ }
+
+ public static OMAConstructed unmarshal(InputStream in) throws IOException {
+ OMANode node = buildNode(in, null);
+ if (node == null || node.isLeaf()) {
+ throw new IOException("Bad OMA tree");
+ }
+ unmarshal(in, (OMAConstructed) node);
+ return (OMAConstructed) node;
+ }
+
+ private static void unmarshal(InputStream in, OMAConstructed parent) throws IOException {
+ for (; ; ) {
+ OMANode node = buildNode(in, parent);
+ if (node == null) {
+ return;
+ } else if (!node.isLeaf()) {
+ unmarshal(in, (OMAConstructed) node);
+ }
+ }
+ }
+
+ private static OMANode buildNode(InputStream in, OMAConstructed parent) throws IOException {
+ String name = OMAConstants.deserializeString(in);
+ if (name == null) {
+ return null;
+ }
+
+ String urn = null;
+ int next = in.read();
+ if (next == '(') {
+ urn = OMAConstants.readURN(in);
+ next = in.read();
+ }
+
+ if (next == '=') {
+ String value = OMAConstants.deserializeString(in);
+ return parent.addChild(name, urn, value, null);
+ } else if (next == '+') {
+ if (parent != null) {
+ return parent.addChild(name, urn, null, null);
+ } else {
+ return new OMAConstructed(null, name, urn);
+ }
+ } else {
+ throw new IOException("Parse error: expected = or + after node name");
+ }
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/OMAParser.java b/packages/Osu/src/com/android/hotspot2/omadm/OMAParser.java
new file mode 100644
index 0000000..21cc19a
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/OMAParser.java
@@ -0,0 +1,69 @@
+package com.android.hotspot2.omadm;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * Parses an OMA-DM XML tree.
+ */
+public class OMAParser extends DefaultHandler {
+ private XMLNode mRoot;
+ private XMLNode mCurrent;
+
+ public OMAParser() {
+ mRoot = null;
+ mCurrent = null;
+ }
+
+ public MOTree parse(String text, String urn) throws IOException, SAXException {
+ try {
+ SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
+ parser.parse(new InputSource(new StringReader(text)), this);
+ return new MOTree(mRoot, urn);
+ } catch (ParserConfigurationException pce) {
+ throw new SAXException(pce);
+ }
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attributes)
+ throws SAXException {
+ XMLNode parent = mCurrent;
+
+ mCurrent = new XMLNode(mCurrent, qName, attributes);
+
+ if (mRoot == null)
+ mRoot = mCurrent;
+ else
+ parent.addChild(mCurrent);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ if (!qName.equals(mCurrent.getTag()))
+ throw new SAXException("End tag '" + qName + "' doesn't match current node: " +
+ mCurrent);
+
+ try {
+ mCurrent.close();
+ } catch (IOException ioe) {
+ throw new SAXException("Failed to close element", ioe);
+ }
+
+ mCurrent = mCurrent.getParent();
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ mCurrent.addText(ch, start, length);
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/OMAScalar.java b/packages/Osu/src/com/android/hotspot2/omadm/OMAScalar.java
new file mode 100644
index 0000000..a971ac4
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/OMAScalar.java
@@ -0,0 +1,87 @@
+package com.android.hotspot2.omadm;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+public class OMAScalar extends OMANode {
+ private final String mValue;
+
+ public OMAScalar(OMAConstructed parent, String name, String context, String value,
+ String ... avps) {
+ this(parent, name, context, value, buildAttributes(avps));
+ }
+
+ public OMAScalar(OMAConstructed parent, String name, String context, String value,
+ Map<String, String> avps) {
+ super(parent, name, context, avps);
+ mValue = value;
+ }
+
+ @Override
+ public OMAScalar reparent(OMAConstructed parent) {
+ return new OMAScalar(parent, getName(), getContext(), mValue, getAttributes());
+ }
+
+ public String getScalarValue(Iterator<String> path) throws OMAException {
+ return mValue;
+ }
+
+ @Override
+ public OMANode getListValue(Iterator<String> path) throws OMAException {
+ throw new OMAException("Scalar encountered in list path: " + getPathString());
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+
+ @Override
+ public Collection<OMANode> getChildren() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getValue() {
+ return mValue;
+ }
+
+ @Override
+ public OMANode getChild(String name) throws OMAException {
+ throw new OMAException("'" + getName() + "' is a scalar node");
+ }
+
+ @Override
+ public OMANode addChild(String name, String context, String value, String path)
+ throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void toString(StringBuilder sb, int level) {
+ sb.append(getPathString()).append('=').append(mValue);
+ if (getContext() != null) {
+ sb.append(" (").append(getContext()).append(')');
+ }
+ sb.append('\n');
+ }
+
+ @Override
+ public void marshal(OutputStream out, int level) throws IOException {
+ OMAConstants.indent(level, out);
+ OMAConstants.serializeString(getName(), out);
+ out.write((byte) '=');
+ OMAConstants.serializeString(getValue(), out);
+ out.write((byte) '\n');
+ }
+
+ @Override
+ public void fillPayload(StringBuilder sb) {
+ sb.append('<').append(MOTree.ValueTag).append('>');
+ sb.append(mValue);
+ sb.append("</").append(MOTree.ValueTag).append(">\n");
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/omadm/XMLNode.java b/packages/Osu/src/com/android/hotspot2/omadm/XMLNode.java
new file mode 100644
index 0000000..b77c820
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/omadm/XMLNode.java
@@ -0,0 +1,240 @@
+package com.android.hotspot2.omadm;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class XMLNode {
+ private final String mTag;
+ private final Map<String, NodeAttribute> mAttributes;
+ private final List<XMLNode> mChildren;
+ private final XMLNode mParent;
+ private MOTree mMO;
+ private StringBuilder mTextBuilder;
+ private String mText;
+
+ private static final String XML_SPECIAL_CHARS = "\"'<>&";
+ private static final Set<Character> XML_SPECIAL = new HashSet<>();
+ private static final String CDATA_OPEN = "<![CDATA[";
+ private static final String CDATA_CLOSE = "]]>";
+
+ static {
+ for (int n = 0; n < XML_SPECIAL_CHARS.length(); n++) {
+ XML_SPECIAL.add(XML_SPECIAL_CHARS.charAt(n));
+ }
+ }
+
+ public XMLNode(XMLNode parent, String tag, Attributes attributes) throws SAXException {
+ mTag = tag;
+
+ mAttributes = new HashMap<>();
+
+ if (attributes.getLength() > 0) {
+ for (int n = 0; n < attributes.getLength(); n++)
+ mAttributes.put(attributes.getQName(n), new NodeAttribute(attributes.getQName(n),
+ attributes.getType(n), attributes.getValue(n)));
+ }
+
+ mParent = parent;
+ mChildren = new ArrayList<>();
+
+ mTextBuilder = new StringBuilder();
+ }
+
+ public XMLNode(XMLNode parent, String tag, Map<String, String> attributes) {
+ mTag = tag;
+
+ mAttributes = new HashMap<>(attributes == null ? 0 : attributes.size());
+
+ if (attributes != null) {
+ for (Map.Entry<String, String> entry : attributes.entrySet()) {
+ mAttributes.put(entry.getKey(),
+ new NodeAttribute(entry.getKey(), "", entry.getValue()));
+ }
+ }
+
+ mParent = parent;
+ mChildren = new ArrayList<>();
+
+ mTextBuilder = new StringBuilder();
+ }
+
+ public void setText(String text) {
+ mText = text;
+ mTextBuilder = null;
+ }
+
+ public void addText(char[] chs, int start, int length) {
+ String s = new String(chs, start, length);
+ String trimmed = s.trim();
+ if (trimmed.isEmpty())
+ return;
+
+ if (s.charAt(0) != trimmed.charAt(0))
+ mTextBuilder.append(' ');
+ mTextBuilder.append(trimmed);
+ if (s.charAt(s.length() - 1) != trimmed.charAt(trimmed.length() - 1))
+ mTextBuilder.append(' ');
+ }
+
+ public void addChild(XMLNode child) {
+ mChildren.add(child);
+ }
+
+ public void close() throws IOException, SAXException {
+ String text = mTextBuilder.toString().trim();
+ StringBuilder filtered = new StringBuilder(text.length());
+ for (int n = 0; n < text.length(); n++) {
+ char ch = text.charAt(n);
+ if (ch >= ' ')
+ filtered.append(ch);
+ }
+
+ mText = filtered.toString();
+ mTextBuilder = null;
+
+ if (MOTree.hasMgmtTreeTag(mText)) {
+ try {
+ NodeAttribute urn = mAttributes.get(OMAConstants.SppMOAttribute);
+ OMAParser omaParser = new OMAParser();
+ mMO = omaParser.parse(mText, urn != null ? urn.getValue() : null);
+ } catch (SAXException | IOException e) {
+ mMO = null;
+ }
+ }
+ }
+
+ public String getTag() {
+ return mTag;
+ }
+
+ public String getNameSpace() throws OMAException {
+ String[] nsn = mTag.split(":");
+ if (nsn.length != 2) {
+ throw new OMAException("Non-namespaced tag: '" + mTag + "'");
+ }
+ return nsn[0];
+ }
+
+ public String getStrippedTag() throws OMAException {
+ String[] nsn = mTag.split(":");
+ if (nsn.length != 2) {
+ throw new OMAException("Non-namespaced tag: '" + mTag + "'");
+ }
+ return nsn[1].toLowerCase();
+ }
+
+ public XMLNode getSoleChild() throws OMAException {
+ if (mChildren.size() != 1) {
+ throw new OMAException("Expected exactly one child to " + mTag);
+ }
+ return mChildren.get(0);
+ }
+
+ public XMLNode getParent() {
+ return mParent;
+ }
+
+ public String getText() {
+ return mText;
+ }
+
+ public Map<String, NodeAttribute> getAttributes() {
+ return Collections.unmodifiableMap(mAttributes);
+ }
+
+ public Map<String, String> getTextualAttributes() {
+ Map<String, String> map = new HashMap<>(mAttributes.size());
+ for (Map.Entry<String, NodeAttribute> entry : mAttributes.entrySet()) {
+ map.put(entry.getKey(), entry.getValue().getValue());
+ }
+ return map;
+ }
+
+ public String getAttributeValue(String name) {
+ NodeAttribute nodeAttribute = mAttributes.get(name);
+ return nodeAttribute != null ? nodeAttribute.getValue() : null;
+ }
+
+ public List<XMLNode> getChildren() {
+ return mChildren;
+ }
+
+ public MOTree getMOTree() {
+ return mMO;
+ }
+
+ private void toString(char[] indent, StringBuilder sb) {
+ Arrays.fill(indent, ' ');
+
+ sb.append(indent).append('<').append(mTag);
+ for (Map.Entry<String, NodeAttribute> entry : mAttributes.entrySet()) {
+ sb.append(' ').append(entry.getKey()).append("='")
+ .append(entry.getValue().getValue()).append('\'');
+ }
+
+ if (mText != null && !mText.isEmpty()) {
+ sb.append('>').append(escapeCdata(mText)).append("</").append(mTag).append(">\n");
+ } else if (mChildren.isEmpty()) {
+ sb.append("/>\n");
+ } else {
+ sb.append(">\n");
+ char[] subIndent = Arrays.copyOf(indent, indent.length + 2);
+ for (XMLNode child : mChildren) {
+ child.toString(subIndent, sb);
+ }
+ sb.append(indent).append("</").append(mTag).append(">\n");
+ }
+ }
+
+ private static String escapeCdata(String text) {
+ if (!escapable(text)) {
+ return text;
+ }
+
+ // Any appearance of ]]> in the text must be split into "]]" | "]]>" | <![CDATA[ | ">"
+ // i.e. "split the sequence by putting a close CDATA and a new open CDATA before the '>'
+ StringBuilder sb = new StringBuilder();
+ sb.append(CDATA_OPEN);
+ int start = 0;
+ for (; ; ) {
+ int etoken = text.indexOf(CDATA_CLOSE);
+ if (etoken >= 0) {
+ sb.append(text.substring(start, etoken + 2)).append(CDATA_CLOSE).append(CDATA_OPEN);
+ start = etoken + 2;
+ } else {
+ if (start < text.length() - 1) {
+ sb.append(text.substring(start));
+ }
+ break;
+ }
+ }
+ sb.append(CDATA_CLOSE);
+ return sb.toString();
+ }
+
+ private static boolean escapable(String s) {
+ for (int n = 0; n < s.length(); n++) {
+ if (XML_SPECIAL.contains(s.charAt(n))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ toString(new char[0], sb);
+ return sb.toString();
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/ClientKeyManager.java b/packages/Osu/src/com/android/hotspot2/osu/ClientKeyManager.java
new file mode 100644
index 0000000..f5d06d5
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/ClientKeyManager.java
@@ -0,0 +1,127 @@
+package com.android.hotspot2.osu;
+
+import android.util.Log;
+
+import com.android.hotspot2.pps.HomeSP;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.X509KeyManager;
+
+public class ClientKeyManager implements X509KeyManager {
+ private final KeyStore mKeyStore;
+ private final Map<OSUCertType, String> mAliasMap;
+ private final Map<OSUCertType, Object> mTempKeys;
+
+ private static final String sTempAlias = "client-alias";
+
+ 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());
+ mTempKeys = new HashMap<>();
+ }
+
+ public void reloadKeys(Map<OSUCertType, List<X509Certificate>> certs, PrivateKey key)
+ throws IOException {
+ List<X509Certificate> clientCerts = certs.get(OSUCertType.Client);
+ X509Certificate[] certArray = new X509Certificate[clientCerts.size()];
+ int n = 0;
+ for (X509Certificate cert : clientCerts) {
+ certArray[n++] = cert;
+ }
+ mTempKeys.put(OSUCertType.Client, certArray);
+ mTempKeys.put(OSUCertType.PrivateKey, key);
+ }
+
+ @Override
+ public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
+ if (mTempKeys.isEmpty()) {
+ return mAliasMap.get(OSUCertType.Client);
+ } else {
+ return sTempAlias;
+ }
+ }
+
+ @Override
+ public String[] getClientAliases(String keyType, Principal[] issuers) {
+ if (mTempKeys.isEmpty()) {
+ String alias = mAliasMap.get(OSUCertType.Client);
+ return alias != null ? new String[]{alias} : null;
+ } else {
+ return new String[]{sTempAlias};
+ }
+ }
+
+ @Override
+ public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String[] getServerAliases(String keyType, Principal[] issuers) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias) {
+ if (mTempKeys.isEmpty()) {
+ if (!mAliasMap.get(OSUCertType.Client).equals(alias)) {
+ Log.w(OSUManager.TAG, "Bad cert alias requested: '" + alias + "'");
+ return null;
+ }
+ try {
+ List<X509Certificate> certs = new ArrayList<>();
+ for (Certificate certificate :
+ mKeyStore.getCertificateChain(mAliasMap.get(OSUCertType.Client))) {
+ if (certificate instanceof X509Certificate) {
+ certs.add((X509Certificate) certificate);
+ }
+ }
+ return certs.toArray(new X509Certificate[certs.size()]);
+ } catch (KeyStoreException kse) {
+ Log.w(OSUManager.TAG, "Failed to retrieve certificates: " + kse);
+ return null;
+ }
+ } else if (sTempAlias.equals(alias)) {
+ return (X509Certificate[]) mTempKeys.get(OSUCertType.Client);
+ } else {
+ Log.w(OSUManager.TAG, "Bad cert alias requested: '" + alias + "'");
+ return null;
+ }
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias) {
+ if (mTempKeys.isEmpty()) {
+ if (!mAliasMap.get(OSUCertType.Client).equals(alias)) {
+ Log.w(OSUManager.TAG, "Bad key alias requested: '" + alias + "'");
+ }
+ try {
+ return (PrivateKey) mKeyStore.getKey(mAliasMap.get(OSUCertType.PrivateKey), null);
+ } catch (GeneralSecurityException gse) {
+ Log.w(OSUManager.TAG, "Failed to retrieve private key: " + gse);
+ return null;
+ }
+ } else if (sTempAlias.equals(alias)) {
+ return (PrivateKey) mTempKeys.get(OSUCertType.PrivateKey);
+ } else {
+ Log.w(OSUManager.TAG, "Bad cert alias requested: '" + alias + "'");
+ return null;
+ }
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/ExchangeCompleteResponse.java b/packages/Osu/src/com/android/hotspot2/osu/ExchangeCompleteResponse.java
new file mode 100644
index 0000000..fe23b5c
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/ExchangeCompleteResponse.java
@@ -0,0 +1,28 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+
+ /*
+ <xsd:element name="sppExchangeComplete">
+ <xsd:annotation>
+ <xsd:documentation>SOAP method used by SPP server to end session.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element ref="sppError" minOccurs="0"/>
+ <xsd:any namespace="##other" maxOccurs="unbounded" minOccurs="0"/>
+ </xsd:sequence>
+ <xsd:attribute ref="sppVersion" use="required"/>
+ <xsd:attribute ref="sppStatus" use="required"/>
+ <xsd:attribute ref="sessionID" use="required"/>
+ <xsd:anyAttribute namespace="##other"/>
+ </xsd:complexType>
+ </xsd:element>
+ */
+
+public class ExchangeCompleteResponse extends OSUResponse {
+ public ExchangeCompleteResponse(XMLNode root) throws OMAException {
+ super(root, OSUMessageType.ExchangeComplete);
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/ExecCommand.java b/packages/Osu/src/com/android/hotspot2/osu/ExecCommand.java
new file mode 100644
index 0000000..38a3947
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/ExecCommand.java
@@ -0,0 +1,3 @@
+package com.android.hotspot2.osu;
+
+public enum ExecCommand {Browser, GetCert, UseClientCertTLS, UploadMO}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/HTTPHandler.java b/packages/Osu/src/com/android/hotspot2/osu/HTTPHandler.java
new file mode 100644
index 0000000..1a66fcf
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/HTTPHandler.java
@@ -0,0 +1,178 @@
+package com.android.hotspot2.osu;
+
+import android.util.Log;
+
+import com.android.hotspot2.utils.HTTPMessage;
+import com.android.hotspot2.utils.HTTPRequest;
+import com.android.hotspot2.utils.HTTPResponse;
+
+import com.android.org.conscrypt.OpenSSLSocketImpl;
+
+import org.xml.sax.SAXException;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSocket;
+import javax.xml.parsers.ParserConfigurationException;
+
+public class HTTPHandler implements AutoCloseable {
+ private final Charset mCharset;
+ private final OSUSocketFactory mSocketFactory;
+ private Socket mSocket;
+ private BufferedOutputStream mOut;
+ private BufferedInputStream mIn;
+ private final String mUser;
+ private final byte[] mPassword;
+ private boolean mHTTPAuthPerformed;
+ private static final AtomicInteger sSequence = new AtomicInteger();
+
+ public HTTPHandler(Charset charset, OSUSocketFactory socketFactory) throws IOException {
+ this(charset, socketFactory, null, null);
+ }
+
+ public HTTPHandler(Charset charset, OSUSocketFactory socketFactory,
+ String user, byte[] password) throws IOException {
+ mCharset = charset;
+ mSocketFactory = socketFactory;
+ mSocket = mSocketFactory.createSocket();
+ mOut = new BufferedOutputStream(mSocket.getOutputStream());
+ mIn = new BufferedInputStream(mSocket.getInputStream());
+ mUser = user;
+ mPassword = password;
+ }
+
+ public boolean isHTTPAuthPerformed() {
+ return mHTTPAuthPerformed;
+ }
+
+ public X509Certificate getOSUCertificate(URL osu) throws GeneralSecurityException {
+ return mSocketFactory.getOSUCertificate(osu);
+ }
+
+ public void renegotiate(Map<OSUCertType, List<X509Certificate>> certs, PrivateKey key)
+ throws IOException {
+ if (!(mSocket instanceof SSLSocket)) {
+ throw new IOException("Not a TLS connection");
+ }
+ if (certs != null) {
+ mSocketFactory.reloadKeys(certs, key);
+ }
+ ((SSLSocket) mSocket).startHandshake();
+ }
+
+ public byte[] getTLSUnique() throws SSLException {
+ if (mSocket instanceof OpenSSLSocketImpl) {
+ return ((OpenSSLSocketImpl) mSocket).getChannelId();
+ }
+ return null;
+ }
+
+ public OSUResponse exchangeSOAP(URL url, String message) throws IOException {
+ HTTPResponse response = exchangeWithRetry(url, message, HTTPMessage.Method.POST,
+ HTTPMessage.ContentTypeSOAP);
+ if (response.getStatusCode() >= 300) {
+ throw new IOException("Bad HTTP status code " + response.getStatusCode());
+ }
+ try {
+ SOAPParser parser = new SOAPParser(response.getPayloadStream());
+ return parser.getResponse();
+ } catch (ParserConfigurationException | SAXException e) {
+ ByteBuffer x = response.getPayload();
+ byte[] b = new byte[x.remaining()];
+ x.get(b);
+ Log.w("XML", "Bad: '" + new String(b, StandardCharsets.ISO_8859_1));
+ throw new IOException(e);
+ }
+ }
+
+ public ByteBuffer exchangeBinary(URL url, String message, String contentType)
+ throws IOException {
+ HTTPResponse response =
+ exchangeWithRetry(url, message, HTTPMessage.Method.POST, contentType);
+ return response.getBinaryPayload();
+ }
+
+ public InputStream doGet(URL url) throws IOException {
+ HTTPResponse response = exchangeWithRetry(url, null, HTTPMessage.Method.GET, null);
+ return response.getPayloadStream();
+ }
+
+ public HTTPResponse doGetHTTP(URL url) throws IOException {
+ return exchangeWithRetry(url, null, HTTPMessage.Method.GET, null);
+ }
+
+ private HTTPResponse exchangeWithRetry(URL url, String message, HTTPMessage.Method method,
+ String contentType) throws IOException {
+ HTTPResponse response = null;
+ int retry = 0;
+ for (; ; ) {
+ try {
+ response = httpExchange(url, message, method, contentType);
+ break;
+ } catch (IOException ioe) {
+ close();
+ retry++;
+ if (retry > 3) {
+ break;
+ }
+ Log.d(OSUManager.TAG, "Failed HTTP exchange, retry " + retry);
+ mSocket = mSocketFactory.createSocket();
+ mOut = new BufferedOutputStream(mSocket.getOutputStream());
+ mIn = new BufferedInputStream(mSocket.getInputStream());
+ }
+ }
+ if (response == null) {
+ throw new IOException("Failed to establish connection to peer");
+ }
+ return response;
+ }
+
+ private HTTPResponse httpExchange(URL url, String message, HTTPMessage.Method method,
+ String contentType)
+ throws IOException {
+ HTTPRequest request = new HTTPRequest(message, mCharset, method, url, contentType, false);
+ request.send(mOut);
+ HTTPResponse response = new HTTPResponse(mIn);
+ Log.d(OSUManager.TAG, "HTTP code " + response.getStatusCode() + ", user " + mUser +
+ ", pw " + (mPassword != null ? '\'' + new String(mPassword) + '\'' : "-"));
+ if (response.getStatusCode() == 401) {
+ if (mUser == null) {
+ throw new IOException("Missing user name for HTTP authentication");
+ }
+ try {
+ request = new HTTPRequest(message, StandardCharsets.ISO_8859_1, method, url,
+ contentType, true);
+ request.doAuthenticate(response, mUser, mPassword, url,
+ sSequence.incrementAndGet());
+ request.send(mOut);
+ mHTTPAuthPerformed = true;
+ } catch (GeneralSecurityException gse) {
+ throw new IOException(gse);
+ }
+
+ response = new HTTPResponse(mIn);
+ }
+ return response;
+ }
+
+ public void close() throws IOException {
+ 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
new file mode 100644
index 0000000..b1580ac
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/IconCache.java
@@ -0,0 +1,392 @@
+package com.android.hotspot2.osu;
+
+import android.util.Log;
+
+import com.android.anqp.HSIconFileElement;
+import com.android.anqp.IconInfo;
+import com.android.hotspot2.Utils;
+
+import java.net.ProtocolException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import static com.android.anqp.Constants.ANQPElementType.HSIconFile;
+
+public class IconCache extends Thread {
+ private static final int CacheSize = 64;
+ private static final int RetryCount = 3;
+
+ private final OSUManager mOSUManager;
+ private final Map<Long, LinkedList<QuerySet>> mBssQueues = new HashMap<>();
+
+ private final Map<IconKey, HSIconFileElement> mCache =
+ new LinkedHashMap<IconKey, HSIconFileElement>() {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return size() > CacheSize;
+ }
+ };
+
+ private static class IconKey {
+ private final long mBSSID;
+ private final long mHESSID;
+ private final String mSSID;
+ private final int mAnqpDomID;
+ private final String mFileName;
+
+ private IconKey(OSUInfo osuInfo, String fileName) {
+ mBSSID = osuInfo.getBSSID();
+ mHESSID = osuInfo.getHESSID();
+ mSSID = osuInfo.getAdvertisingSSID();
+ mAnqpDomID = osuInfo.getAnqpDomID();
+ mFileName = fileName;
+ }
+
+ public String getFileName() {
+ return mFileName;
+ }
+
+ @Override
+ public boolean equals(Object thatObject) {
+ if (this == thatObject) {
+ return true;
+ }
+ if (thatObject == null || getClass() != thatObject.getClass()) {
+ return false;
+ }
+
+ IconKey that = (IconKey) thatObject;
+
+ return mFileName.equals(that.mFileName) && ((mBSSID == that.mBSSID) ||
+ ((mAnqpDomID == that.mAnqpDomID) && (mAnqpDomID != 0) &&
+ (mHESSID == that.mHESSID) && ((mHESSID != 0)
+ || mSSID.equals(that.mSSID))));
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (mBSSID ^ (mBSSID >>> 32));
+ result = 31 * result + (int) (mHESSID ^ (mHESSID >>> 32));
+ result = 31 * result + mSSID.hashCode();
+ result = 31 * result + mAnqpDomID;
+ result = 31 * result + mFileName.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%012x:%012x '%s' [%d] + '%s'",
+ mBSSID, mHESSID, mSSID, mAnqpDomID, mFileName);
+ }
+ }
+
+ private static class QueryEntry {
+ private final IconKey mKey;
+ private int mRetry;
+ private long mLastSent;
+
+ private QueryEntry(IconKey key) {
+ mKey = key;
+ mLastSent = System.currentTimeMillis();
+ }
+
+ private IconKey getKey() {
+ return mKey;
+ }
+
+ private int bumpRetry() {
+ mLastSent = System.currentTimeMillis();
+ return mRetry++;
+ }
+
+ private long age(long now) {
+ return now - mLastSent;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Entry %s, retry %d", mKey, mRetry);
+ }
+ }
+
+ private static class QuerySet {
+ private final OSUInfo mOsuInfo;
+ private final LinkedList<QueryEntry> mEntries;
+
+ private QuerySet(OSUInfo osuInfo, List<IconInfo> icons) {
+ mOsuInfo = osuInfo;
+ mEntries = new LinkedList<>();
+ for (IconInfo iconInfo : icons) {
+ mEntries.addLast(new QueryEntry(new IconKey(osuInfo, iconInfo.getFileName())));
+ }
+ }
+
+ private QueryEntry peek() {
+ return mEntries.getFirst();
+ }
+
+ private QueryEntry pop() {
+ mEntries.removeFirst();
+ return mEntries.isEmpty() ? null : mEntries.getFirst();
+ }
+
+ private boolean isEmpty() {
+ return mEntries.isEmpty();
+ }
+
+ private List<QueryEntry> getAllEntries() {
+ return Collections.unmodifiableList(mEntries);
+ }
+
+ private long getBssid() {
+ return mOsuInfo.getBSSID();
+ }
+
+ private OSUInfo getOsuInfo() {
+ return mOsuInfo;
+ }
+
+ private IconKey updateIcon(String fileName, HSIconFileElement iconFileElement) {
+ IconKey key = null;
+ for (QueryEntry queryEntry : mEntries) {
+ if (queryEntry.getKey().getFileName().equals(fileName)) {
+ key = queryEntry.getKey();
+ }
+ }
+ if (key == null) {
+ return null;
+ }
+
+ if (iconFileElement != null) {
+ mOsuInfo.setIconFileElement(iconFileElement, fileName);
+ } else {
+ mOsuInfo.setIconStatus(OSUInfo.IconStatus.NotAvailable);
+ }
+ return key;
+ }
+
+ private boolean updateIcon(IconKey key, HSIconFileElement iconFileElement) {
+ boolean match = false;
+ for (QueryEntry queryEntry : mEntries) {
+ if (queryEntry.getKey().equals(key)) {
+ match = true;
+ break;
+ }
+ }
+ if (!match) {
+ return false;
+ }
+
+ if (iconFileElement != null) {
+ mOsuInfo.setIconFileElement(iconFileElement, key.getFileName());
+ } else {
+ mOsuInfo.setIconStatus(OSUInfo.IconStatus.NotAvailable);
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "OSU " + mOsuInfo + ": " + mEntries;
+ }
+ }
+
+ public IconCache(OSUManager osuManager) {
+ mOSUManager = osuManager;
+ }
+
+ public void clear() {
+ mBssQueues.clear();
+ mCache.clear();
+ }
+
+ private boolean enqueue(QuerySet querySet) {
+ boolean newEntry = false;
+ LinkedList<QuerySet> queries = mBssQueues.get(querySet.getBssid());
+ if (queries == null) {
+ queries = new LinkedList<>();
+ mBssQueues.put(querySet.getBssid(), queries);
+ newEntry = true;
+ }
+ queries.addLast(querySet);
+ return newEntry;
+ }
+
+ public void startIconQuery(OSUInfo osuInfo, List<IconInfo> icons) {
+ Log.d("ZXZ", String.format("Icon query on %012x for %s", osuInfo.getBSSID(), icons));
+ if (icons == null || icons.isEmpty()) {
+ return;
+ }
+
+ QuerySet querySet = new QuerySet(osuInfo, icons);
+ for (QueryEntry entry : querySet.getAllEntries()) {
+ HSIconFileElement iconElement = mCache.get(entry.getKey());
+ if (iconElement != null) {
+ osuInfo.setIconFileElement(iconElement, entry.getKey().getFileName());
+ mOSUManager.iconResults(Arrays.asList(osuInfo));
+ return;
+ }
+ }
+ if (enqueue(querySet)) {
+ initiateQuery(querySet.getBssid());
+ }
+ }
+
+ private void initiateQuery(long bssid) {
+ LinkedList<QuerySet> queryEntries = mBssQueues.get(bssid);
+ if (queryEntries == null) {
+ return;
+ } else if (queryEntries.isEmpty()) {
+ mBssQueues.remove(bssid);
+ return;
+ }
+
+ QuerySet querySet = queryEntries.getFirst();
+ QueryEntry queryEntry = querySet.peek();
+ if (queryEntry.bumpRetry() >= RetryCount) {
+ QueryEntry newEntry = querySet.pop();
+ if (newEntry == null) {
+ // No more entries in this QuerySet, advance to the next set.
+ querySet.getOsuInfo().setIconStatus(OSUInfo.IconStatus.NotAvailable);
+ queryEntries.removeFirst();
+ if (queryEntries.isEmpty()) {
+ // No further QuerySet on this BSSID, drop the bucket and bail.
+ mBssQueues.remove(bssid);
+ return;
+ } else {
+ querySet = queryEntries.getFirst();
+ queryEntry = querySet.peek();
+ queryEntry.bumpRetry();
+ }
+ }
+ }
+ mOSUManager.doIconQuery(bssid, queryEntry.getKey().getFileName());
+ }
+
+ public void notifyIconReceived(long bssid, String fileName, byte[] iconData) {
+ Log.d("ZXZ", String.format("Icon '%s':%d received from %012x",
+ fileName, iconData != null ? iconData.length : -1, bssid));
+ IconKey key;
+ HSIconFileElement iconFileElement = null;
+ List<OSUInfo> updates = new ArrayList<>();
+
+ LinkedList<QuerySet> querySets = mBssQueues.get(bssid);
+ if (querySets == null || querySets.isEmpty()) {
+ Log.d(OSUManager.TAG,
+ String.format("Spurious icon response from %012x for '%s' (%d) bytes",
+ bssid, fileName, iconData != null ? iconData.length : -1));
+ Log.d("ZXZ", "query set: " + querySets
+ + ", BSS queues: " + Utils.bssidsToString(mBssQueues.keySet()));
+ return;
+ } else {
+ QuerySet querySet = querySets.removeFirst();
+ if (iconData != null) {
+ try {
+ iconFileElement = new HSIconFileElement(HSIconFile,
+ ByteBuffer.wrap(iconData).order(ByteOrder.LITTLE_ENDIAN));
+ } catch (ProtocolException | BufferUnderflowException e) {
+ Log.e(OSUManager.TAG, "Failed to parse ANQP icon file: " + e);
+ }
+ }
+ key = querySet.updateIcon(fileName, iconFileElement);
+ if (key == null) {
+ Log.d(OSUManager.TAG,
+ String.format("Spurious icon response from %012x for '%s' (%d) bytes",
+ bssid, fileName, iconData != null ? iconData.length : -1));
+ Log.d("ZXZ", "query set: " + querySets + ", BSS queues: "
+ + Utils.bssidsToString(mBssQueues.keySet()));
+ querySets.addFirst(querySet);
+ return;
+ }
+
+ if (iconFileElement != null) {
+ mCache.put(key, iconFileElement);
+ }
+
+ if (querySet.isEmpty()) {
+ mBssQueues.remove(bssid);
+ }
+ updates.add(querySet.getOsuInfo());
+ }
+
+ // Update any other pending entries that matches the ESS of the currently resolved icon
+ Iterator<Map.Entry<Long, LinkedList<QuerySet>>> bssIterator =
+ mBssQueues.entrySet().iterator();
+ while (bssIterator.hasNext()) {
+ Map.Entry<Long, LinkedList<QuerySet>> bssEntries = bssIterator.next();
+ Iterator<QuerySet> querySetIterator = bssEntries.getValue().iterator();
+ while (querySetIterator.hasNext()) {
+ QuerySet querySet = querySetIterator.next();
+ if (querySet.updateIcon(key, iconFileElement)) {
+ querySetIterator.remove();
+ updates.add(querySet.getOsuInfo());
+ }
+ }
+ if (bssEntries.getValue().isEmpty()) {
+ bssIterator.remove();
+ }
+ }
+
+ initiateQuery(bssid);
+
+ mOSUManager.iconResults(updates);
+ }
+
+ private static final long RequeryTimeLow = 6000L;
+ private static final long RequeryTimeHigh = 15000L;
+
+ public void tickle(boolean wifiOff) {
+ synchronized (mCache) {
+ if (wifiOff) {
+ mBssQueues.clear();
+ } else {
+ long now = System.currentTimeMillis();
+
+ Iterator<Map.Entry<Long, LinkedList<QuerySet>>> bssIterator =
+ mBssQueues.entrySet().iterator();
+ while (bssIterator.hasNext()) {
+ // Get the list of entries for this BSSID
+ Map.Entry<Long, LinkedList<QuerySet>> bssEntries = bssIterator.next();
+ Iterator<QuerySet> querySetIterator = bssEntries.getValue().iterator();
+ while (querySetIterator.hasNext()) {
+ QuerySet querySet = querySetIterator.next();
+ QueryEntry queryEntry = querySet.peek();
+ long age = queryEntry.age(now);
+ if (age > RequeryTimeHigh) {
+ // Timed out entry, move on to the next.
+ queryEntry = querySet.pop();
+ if (queryEntry == null) {
+ // Empty query set, update status and remove it.
+ querySet.getOsuInfo()
+ .setIconStatus(OSUInfo.IconStatus.NotAvailable);
+ querySetIterator.remove();
+ } else {
+ // Start a query on the next entry and bail out of the set iteration
+ initiateQuery(querySet.getBssid());
+ break;
+ }
+ } else if (age > RequeryTimeLow) {
+ // Re-issue queries for qualified entries and bail out of set iteration
+ initiateQuery(querySet.getBssid());
+ break;
+ }
+ }
+ if (bssEntries.getValue().isEmpty()) {
+ // Kill the whole bucket if the set list is empty
+ bssIterator.remove();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUCache.java b/packages/Osu/src/com/android/hotspot2/osu/OSUCache.java
new file mode 100644
index 0000000..dadda26
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUCache.java
@@ -0,0 +1,172 @@
+package com.android.hotspot2.osu;
+
+import android.net.wifi.AnqpInformationElement;
+import android.net.wifi.ScanResult;
+import android.util.Log;
+
+import com.android.anqp.Constants;
+import com.android.anqp.HSOsuProvidersElement;
+import com.android.anqp.OSUProvider;
+
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class holds a stable set of OSU information as well as scan results based on a trail of
+ * scan results.
+ * The purpose of this class is to provide a stable set of information over a a limited span of
+ * time (SCAN_BATCH_HISTORY_SIZE scan batches) so that OSU entries in the selection list does not
+ * come and go with temporarily lost scan results.
+ * The stable set of scan results are used by the remediation flow to retrieve ANQP information
+ * for the current network to determine whether the currently associated network is a roaming
+ * network for the Home SP whose timer has currently fired.
+ */
+public class OSUCache {
+ private static final int SCAN_BATCH_HISTORY_SIZE = 8;
+
+ private int mInstant;
+ private final Map<OSUProvider, ScanResult> mBatchedOSUs = new HashMap<>();
+ private final Map<OSUProvider, ScanInstance> mCache = new HashMap<>();
+
+ private static class ScanInstance {
+ private final ScanResult mScanResult;
+ private int mInstant;
+
+ private ScanInstance(ScanResult scanResult, int instant) {
+ mScanResult = scanResult;
+ mInstant = instant;
+ }
+
+ public ScanResult getScanResult() {
+ return mScanResult;
+ }
+
+ public int getInstant() {
+ return mInstant;
+ }
+
+ private boolean bssidEqual(ScanResult scanResult) {
+ return mScanResult.BSSID.equals(scanResult.BSSID);
+ }
+
+ private void updateInstant(int newInstant) {
+ mInstant = newInstant;
+ }
+
+ @Override
+ public String toString() {
+ return mScanResult.SSID + " @ " + mInstant;
+ }
+ }
+
+ public OSUCache() {
+ mInstant = 0;
+ }
+
+ private void clear() {
+ mBatchedOSUs.clear();
+ }
+
+ public void clearAll() {
+ clear();
+ mCache.clear();
+ }
+
+ public Map<OSUProvider, ScanResult> pushScanResults(Collection<ScanResult> scanResults) {
+ for (ScanResult scanResult : scanResults) {
+ AnqpInformationElement[] osuInfo = scanResult.anqpElements;
+ if (osuInfo != null && osuInfo.length > 0) {
+ putResult(scanResult, osuInfo);
+ }
+ }
+ return scanEnd();
+ }
+
+ private void putResult(ScanResult scanResult, AnqpInformationElement[] elements) {
+ for (AnqpInformationElement ie : elements) {
+ if (ie.getElementId() == AnqpInformationElement.HS_OSU_PROVIDERS
+ && ie.getVendorId() == AnqpInformationElement.HOTSPOT20_VENDOR_ID) {
+ try {
+ HSOsuProvidersElement providers = new HSOsuProvidersElement(
+ Constants.ANQPElementType.HSOSUProviders,
+ ByteBuffer.wrap(ie.getPayload()).order(ByteOrder.LITTLE_ENDIAN));
+
+ putProviders(scanResult, providers);
+ } catch (ProtocolException pe) {
+ Log.w(OSUManager.TAG,
+ "Failed to parse OSU element: " + pe);
+ }
+ }
+ }
+ }
+
+ private void putProviders(ScanResult scanResult, HSOsuProvidersElement osuProviders) {
+ for (OSUProvider provider : osuProviders.getProviders()) {
+ // Make a predictive put
+ ScanResult existing = mBatchedOSUs.put(provider, scanResult);
+ if (existing != null && existing.level > scanResult.level) {
+ // But undo it if the entry already held a better RSSI
+ mBatchedOSUs.put(provider, existing);
+ }
+ }
+ }
+
+ private Map<OSUProvider, ScanResult> scanEnd() {
+ // Update the trail of OSU Providers:
+ int changes = 0;
+ Map<OSUProvider, ScanInstance> aged = new HashMap<>(mCache);
+ for (Map.Entry<OSUProvider, ScanResult> entry : mBatchedOSUs.entrySet()) {
+ ScanInstance current = aged.remove(entry.getKey());
+ if (current == null || !current.bssidEqual(entry.getValue())) {
+ mCache.put(entry.getKey(), new ScanInstance(entry.getValue(), mInstant));
+ changes++;
+ if (current == null) {
+ Log.d("ZXZ", "Add OSU " + entry.getKey() + " from " + entry.getValue().SSID);
+ } else {
+ Log.d("ZXZ", "Update OSU " + entry.getKey() + " with " +
+ entry.getValue().SSID + " to " + current);
+ }
+ } else {
+ Log.d("ZXZ", "Existing OSU " + entry.getKey() + ", "
+ + current.getInstant() + " -> " + mInstant);
+ current.updateInstant(mInstant);
+ }
+ }
+
+ for (Map.Entry<OSUProvider, ScanInstance> entry : aged.entrySet()) {
+ if (mInstant - entry.getValue().getInstant() > SCAN_BATCH_HISTORY_SIZE) {
+ Log.d("ZXZ", "Remove OSU " + entry.getKey() + ", "
+ + entry.getValue().getInstant() + " @ " + mInstant);
+ mCache.remove(entry.getKey());
+ changes++;
+ }
+ }
+
+ mInstant++;
+ clear();
+
+ // Return the latest results if there were any changes from last batch
+ if (changes > 0) {
+ Map<OSUProvider, ScanResult> results = new HashMap<>(mCache.size());
+ for (Map.Entry<OSUProvider, ScanInstance> entry : mCache.entrySet()) {
+ results.put(entry.getKey(), entry.getValue().getScanResult());
+ }
+ return results;
+ } else {
+ return null;
+ }
+ }
+
+ private static String toBSSIDStrings(Set<Long> bssids) {
+ StringBuilder sb = new StringBuilder();
+ for (Long bssid : bssids) {
+ sb.append(String.format(" %012x", bssid));
+ }
+ return sb.toString();
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUCertType.java b/packages/Osu/src/com/android/hotspot2/osu/OSUCertType.java
new file mode 100644
index 0000000..91d7f72
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUCertType.java
@@ -0,0 +1,10 @@
+package com.android.hotspot2.osu;
+
+public enum OSUCertType {
+ CA,
+ Client,
+ AAA,
+ Remediation,
+ Policy,
+ PrivateKey
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUClient.java b/packages/Osu/src/com/android/hotspot2/osu/OSUClient.java
new file mode 100644
index 0000000..12dffe3
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUClient.java
@@ -0,0 +1,485 @@
+package com.android.hotspot2.osu;
+
+/*
+ * policy-server.r2-testbed IN A 10.123.107.107
+ * remediation-server.r2-testbed IN A 10.123.107.107
+ * subscription-server.r2-testbed IN A 10.123.107.107
+ * www.r2-testbed IN A 10.123.107.107
+ * osu-server.r2-testbed-rks IN A 10.123.107.107
+ * policy-server.r2-testbed-rks IN A 10.123.107.107
+ * remediation-server.r2-testbed-rks IN A 10.123.107.107
+ * subscription-server.r2-testbed-rks IN A 10.123.107.107
+ */
+
+import android.net.Network;
+import android.util.Log;
+
+import com.android.hotspot2.OMADMAdapter;
+import com.android.hotspot2.est.ESTHandler;
+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.pps.Credential;
+import com.android.hotspot2.pps.HomeSP;
+import com.android.hotspot2.pps.UpdateInfo;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+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;
+
+ public OSUClient(OSUInfo osuInfo, KeyStore ks) throws MalformedURLException {
+ mOSUInfo = osuInfo;
+ mURL = new URL(osuInfo.getOSUProvider().getOSUServer());
+ mKeyStore = ks;
+ }
+
+ public OSUClient(String osu, KeyStore ks) throws MalformedURLException {
+ mOSUInfo = null;
+ mURL = new URL(osu);
+ mKeyStore = ks;
+ }
+
+ public void provision(OSUManager osuManager, Network network, KeyManager km)
+ throws IOException, GeneralSecurityException {
+ try (HTTPHandler httpHandler = new HTTPHandler(StandardCharsets.UTF_8,
+ OSUSocketFactory.getSocketFactory(mKeyStore, null, OSUManager.FLOW_PROVISIONING,
+ network, mURL, km, true))) {
+
+ SPVerifier spVerifier = new SPVerifier(mOSUInfo);
+ spVerifier.verify(httpHandler.getOSUCertificate(mURL));
+
+ URL redirectURL = osuManager.prepareUserInput(mOSUInfo.getName(Locale.getDefault()));
+ OMADMAdapter omadmAdapter = osuManager.getOMADMAdapter();
+
+ String regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRegistration,
+ null,
+ redirectURL.toString(),
+ omadmAdapter.getMO(OMAConstants.DevInfoURN),
+ omadmAdapter.getMO(OMAConstants.DevDetailURN));
+ Log.d(TAG, "Registration request: " + regRequest);
+ OSUResponse osuResponse = httpHandler.exchangeSOAP(mURL, regRequest);
+
+ Log.d(TAG, "Response: " + osuResponse);
+ if (osuResponse.getMessageType() != OSUMessageType.PostDevData) {
+ throw new IOException("Expected a PostDevDataResponse");
+ }
+ PostDevDataResponse regResponse = (PostDevDataResponse) osuResponse;
+ String sessionID = regResponse.getSessionID();
+ if (regResponse.getExecCommand() == ExecCommand.UseClientCertTLS) {
+ ClientCertInfo ccInfo = (ClientCertInfo) regResponse.getCommandData();
+ if (ccInfo.doesAcceptMfgCerts()) {
+ throw new IOException("Mfg certs are not supported in Android");
+ } else if (ccInfo.doesAcceptProviderCerts()) {
+ ((WiFiKeyManager) km).enableClientAuth(ccInfo.getIssuerNames());
+ httpHandler.renegotiate(null, null);
+ } else {
+ throw new IOException("Neither manufacturer nor provider cert specified");
+ }
+ regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRegistration,
+ sessionID,
+ redirectURL.toString(),
+ omadmAdapter.getMO(OMAConstants.DevInfoURN),
+ omadmAdapter.getMO(OMAConstants.DevDetailURN));
+
+ osuResponse = httpHandler.exchangeSOAP(mURL, regRequest);
+ if (osuResponse.getMessageType() != OSUMessageType.PostDevData) {
+ throw new IOException("Expected a PostDevDataResponse");
+ }
+ regResponse = (PostDevDataResponse) osuResponse;
+ }
+
+ if (regResponse.getExecCommand() != ExecCommand.Browser) {
+ throw new IOException("Expected a launchBrowser command");
+ }
+ Log.d(TAG, "Exec: " + regResponse.getExecCommand() + ", for '" +
+ regResponse.getCommandData() + "'");
+
+ if (!osuResponse.getSessionID().equals(sessionID)) {
+ throw new IOException("Mismatching session IDs");
+ }
+ String webURL = ((BrowserURI) regResponse.getCommandData()).getURI();
+
+ if (webURL == null) {
+ throw new IOException("No web-url");
+ } else if (!webURL.contains(sessionID)) {
+ throw new IOException("Bad or missing session ID in webURL");
+ }
+
+ if (!osuManager.startUserInput(new URL(webURL), network)) {
+ throw new IOException("User session failed");
+ }
+
+ Log.d(TAG, " -- Sending user input complete:");
+ String userComplete = SOAPBuilder.buildPostDevDataResponse(RequestReason.InputComplete,
+ sessionID, null,
+ omadmAdapter.getMO(OMAConstants.DevInfoURN),
+ omadmAdapter.getMO(OMAConstants.DevDetailURN));
+ OSUResponse moResponse1 = httpHandler.exchangeSOAP(mURL, userComplete);
+ if (moResponse1.getMessageType() != OSUMessageType.PostDevData) {
+ throw new IOException("Bad user input complete response: " + moResponse1);
+ }
+ PostDevDataResponse provResponse = (PostDevDataResponse) moResponse1;
+ GetCertData estData = checkResponse(provResponse);
+
+ Map<OSUCertType, List<X509Certificate>> certs = new HashMap<>();
+ PrivateKey clientKey = null;
+
+ MOData moData;
+ if (estData == null) {
+ moData = (MOData) provResponse.getCommandData();
+ } else {
+ try (ESTHandler estHandler = new ESTHandler((GetCertData) provResponse.
+ getCommandData(), network, osuManager.getOMADMAdapter(),
+ km, mKeyStore, null, OSUManager.FLOW_PROVISIONING)) {
+ estHandler.execute(false);
+ certs.put(OSUCertType.CA, estHandler.getCACerts());
+ certs.put(OSUCertType.Client, estHandler.getClientCerts());
+ clientKey = estHandler.getClientKey();
+ }
+
+ Log.d(TAG, " -- Sending provisioning cert enrollment complete:");
+ String certComplete =
+ SOAPBuilder.buildPostDevDataResponse(RequestReason.CertEnrollmentComplete,
+ sessionID, null,
+ omadmAdapter.getMO(OMAConstants.DevInfoURN),
+ omadmAdapter.getMO(OMAConstants.DevDetailURN));
+ OSUResponse moResponse2 = httpHandler.exchangeSOAP(mURL, certComplete);
+ if (moResponse2.getMessageType() != OSUMessageType.PostDevData) {
+ throw new IOException("Bad cert enrollment complete response: " + moResponse2);
+ }
+ PostDevDataResponse provComplete = (PostDevDataResponse) moResponse2;
+ if (provComplete.getStatus() != OSUStatus.ProvComplete ||
+ provComplete.getOSUCommand() != OSUCommandID.AddMO) {
+ throw new IOException("Expected addMO: " + provComplete);
+ }
+ moData = (MOData) provComplete.getCommandData();
+ }
+
+ // !!! How can an ExchangeComplete be sent w/o knowing the fate of the certs???
+ String updateResponse = SOAPBuilder.buildUpdateResponse(sessionID, null);
+ Log.d(TAG, " -- Sending updateResponse:");
+ OSUResponse exComplete = httpHandler.exchangeSOAP(mURL, updateResponse);
+ Log.d(TAG, "exComplete response: " + exComplete);
+ if (exComplete.getMessageType() != OSUMessageType.ExchangeComplete) {
+ throw new IOException("Expected ExchangeComplete: " + exComplete);
+ } else if (exComplete.getStatus() != OSUStatus.ExchangeComplete) {
+ throw new IOException("Bad ExchangeComplete status: " + exComplete);
+ }
+
+ retrieveCerts(moData.getMOTree().getRoot(), certs, network, km, mKeyStore);
+ osuManager.provisioningComplete(mOSUInfo, moData, certs, clientKey, network);
+ }
+ }
+
+ public void remediate(OSUManager osuManager, Network network, KeyManager km, HomeSP homeSP,
+ int flowType)
+ throws IOException, GeneralSecurityException {
+ try (HTTPHandler httpHandler = createHandler(network, homeSP, km, flowType)) {
+ URL redirectURL = osuManager.prepareUserInput(homeSP.getFriendlyName());
+ OMADMAdapter omadmAdapter = osuManager.getOMADMAdapter();
+
+ String regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRemediation,
+ null,
+ redirectURL.toString(),
+ omadmAdapter.getMO(OMAConstants.DevInfoURN),
+ omadmAdapter.getMO(OMAConstants.DevDetailURN));
+
+ OSUResponse serverResponse = httpHandler.exchangeSOAP(mURL, regRequest);
+ if (serverResponse.getMessageType() != OSUMessageType.PostDevData) {
+ throw new IOException("Expected a PostDevDataResponse");
+ }
+ String sessionID = serverResponse.getSessionID();
+
+ PostDevDataResponse pddResponse = (PostDevDataResponse) serverResponse;
+ Log.d(TAG, "Remediation response: " + pddResponse);
+
+ Map<OSUCertType, List<X509Certificate>> certs = null;
+ PrivateKey clientKey = null;
+
+ if (pddResponse.getStatus() != OSUStatus.RemediationComplete) {
+ if (pddResponse.getExecCommand() == ExecCommand.UploadMO) {
+ String ulMessage = SOAPBuilder.buildPostDevDataResponse(RequestReason.MOUpload,
+ null,
+ redirectURL.toString(),
+ omadmAdapter.getMO(OMAConstants.DevInfoURN),
+ omadmAdapter.getMO(OMAConstants.DevDetailURN),
+ osuManager.getMOTree(homeSP));
+
+ Log.d(TAG, "Upload MO: " + ulMessage);
+
+ OSUResponse ulResponse = httpHandler.exchangeSOAP(mURL, ulMessage);
+ if (ulResponse.getMessageType() != OSUMessageType.PostDevData) {
+ throw new IOException("Expected a PostDevDataResponse to MOUpload");
+ }
+ pddResponse = (PostDevDataResponse) ulResponse;
+ }
+
+ if (pddResponse.getExecCommand() == ExecCommand.Browser) {
+ if (flowType == OSUManager.FLOW_POLICY) {
+ throw new IOException("Browser launch requested in policy flow");
+ }
+ String webURL = ((BrowserURI) pddResponse.getCommandData()).getURI();
+
+ if (webURL == null) {
+ throw new IOException("No web-url");
+ } else if (!webURL.contains(sessionID)) {
+ throw new IOException("Bad or missing session ID in webURL");
+ }
+
+ if (!osuManager.startUserInput(new URL(webURL), network)) {
+ throw new IOException("User session failed");
+ }
+
+ Log.d(TAG, " -- Sending user input complete:");
+ String userComplete =
+ SOAPBuilder.buildPostDevDataResponse(RequestReason.InputComplete,
+ sessionID, null,
+ omadmAdapter.getMO(OMAConstants.DevInfoURN),
+ omadmAdapter.getMO(OMAConstants.DevDetailURN));
+
+ OSUResponse udResponse = httpHandler.exchangeSOAP(mURL, userComplete);
+ if (udResponse.getMessageType() != OSUMessageType.PostDevData) {
+ throw new IOException("Bad user input complete response: " + udResponse);
+ }
+ pddResponse = (PostDevDataResponse) udResponse;
+ } else if (pddResponse.getExecCommand() == ExecCommand.GetCert) {
+ certs = new HashMap<>();
+ try (ESTHandler estHandler = new ESTHandler((GetCertData) pddResponse.
+ getCommandData(), network, osuManager.getOMADMAdapter(),
+ km, mKeyStore, homeSP, flowType)) {
+ estHandler.execute(true);
+ certs.put(OSUCertType.CA, estHandler.getCACerts());
+ certs.put(OSUCertType.Client, estHandler.getClientCerts());
+ clientKey = estHandler.getClientKey();
+ }
+
+ if (httpHandler.isHTTPAuthPerformed()) { // 8.4.3.6
+ httpHandler.renegotiate(certs, clientKey);
+ }
+
+ Log.d(TAG, " -- Sending remediation cert enrollment complete:");
+ // 8.4.3.5 in the spec actually prescribes that an update URI is sent here,
+ // but there is no remediation flow that defines user interaction after EST
+ // so for now a null is passed.
+ String certComplete =
+ SOAPBuilder
+ .buildPostDevDataResponse(RequestReason.CertEnrollmentComplete,
+ sessionID, null,
+ omadmAdapter.getMO(OMAConstants.DevInfoURN),
+ omadmAdapter.getMO(OMAConstants.DevDetailURN));
+ OSUResponse ceResponse = httpHandler.exchangeSOAP(mURL, certComplete);
+ if (ceResponse.getMessageType() != OSUMessageType.PostDevData) {
+ throw new IOException("Bad cert enrollment complete response: "
+ + ceResponse);
+ }
+ pddResponse = (PostDevDataResponse) ceResponse;
+ } else {
+ throw new IOException("Unexpected command: " + pddResponse.getExecCommand());
+ }
+ }
+
+ if (pddResponse.getStatus() != OSUStatus.RemediationComplete) {
+ throw new IOException("Expected a PostDevDataResponse to MOUpload");
+ }
+
+ Log.d(TAG, "Remediation response: " + pddResponse);
+
+ List<MOData> mods = new ArrayList<>();
+ for (OSUCommand command : pddResponse.getCommands()) {
+ if (command.getOSUCommand() == OSUCommandID.UpdateNode) {
+ mods.add((MOData) command.getCommandData());
+ } else if (command.getOSUCommand() != OSUCommandID.NoMOUpdate) {
+ throw new IOException("Unexpected OSU response: " + command);
+ }
+ }
+
+ // 1. Machine remediation: Remediation complete + replace node
+ // 2a. User remediation with upload: ExecCommand.UploadMO
+ // 2b. User remediation without upload: ExecCommand.Browser
+ // 3. User remediation only: -> sppPostDevData user input complete
+ //
+ // 4. Update node
+ // 5. -> Update response
+ // 6. Exchange complete
+
+ OSUError error = null;
+
+ String updateResponse = SOAPBuilder.buildUpdateResponse(sessionID, error);
+ Log.d(TAG, " -- Sending updateResponse:");
+ OSUResponse exComplete = httpHandler.exchangeSOAP(mURL, updateResponse);
+ Log.d(TAG, "exComplete response: " + exComplete);
+ if (exComplete.getMessageType() != OSUMessageType.ExchangeComplete) {
+ throw new IOException("Expected ExchangeComplete: " + exComplete);
+ } else if (exComplete.getStatus() != OSUStatus.ExchangeComplete) {
+ throw new IOException("Bad ExchangeComplete status: " + exComplete);
+ }
+
+ // 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);
+ } catch (IOException | GeneralSecurityException e) {
+ osuManager.provisioningFailed(homeSP.getFriendlyName(), e.getMessage(), homeSP,
+ OSUManager.FLOW_REMEDIATION);
+ error = OSUError.CommandFailed;
+ }
+ }
+ }
+
+ private HTTPHandler createHandler(Network network, HomeSP homeSP,
+ KeyManager km, int flowType) throws GeneralSecurityException, IOException {
+ Credential credential = homeSP.getCredential();
+
+ Log.d(TAG, "Credential method " + credential.getEAPMethod().getEAPMethodID());
+ switch (credential.getEAPMethod().getEAPMethodID()) {
+ case EAP_TTLS:
+ String user;
+ byte[] password;
+ UpdateInfo subscriptionUpdate;
+ if (flowType == OSUManager.FLOW_POLICY) {
+ subscriptionUpdate = homeSP.getPolicy() != null ?
+ homeSP.getPolicy().getPolicyUpdate() : null;
+ } else {
+ subscriptionUpdate = homeSP.getSubscriptionUpdate();
+ }
+ if (subscriptionUpdate != null && subscriptionUpdate.getUsername() != null) {
+ user = subscriptionUpdate.getUsername();
+ password = subscriptionUpdate.getPassword() != null ?
+ subscriptionUpdate.getPassword().getBytes(StandardCharsets.UTF_8) :
+ new byte[0];
+ } else {
+ user = credential.getUserName();
+ password = credential.getPassword().getBytes(StandardCharsets.UTF_8);
+ }
+ return new HTTPHandler(StandardCharsets.UTF_8,
+ OSUSocketFactory.getSocketFactory(mKeyStore, homeSP, flowType, network,
+ mURL, km, true), user, password);
+ case EAP_TLS:
+ return new HTTPHandler(StandardCharsets.UTF_8,
+ OSUSocketFactory.getSocketFactory(mKeyStore, homeSP, flowType, network,
+ mURL, km, true));
+ default:
+ throw new IOException("Cannot remediate account with " +
+ credential.getEAPMethod().getEAPMethodID());
+ }
+ }
+
+ private static GetCertData checkResponse(PostDevDataResponse response) throws IOException {
+ if (response.getStatus() == OSUStatus.ProvComplete &&
+ response.getOSUCommand() == OSUCommandID.AddMO) {
+ return null;
+ }
+
+ if (response.getOSUCommand() == OSUCommandID.Exec &&
+ response.getExecCommand() == ExecCommand.GetCert) {
+ return (GetCertData) response.getCommandData();
+ } else {
+ throw new IOException("Unexpected command: " + response);
+ }
+ }
+
+ private static final String[] AAACertPath =
+ {"PerProviderSubscription", "?", "AAAServerTrustRoot", "*", "CertURL"};
+ private static final String[] RemdCertPath =
+ {"PerProviderSubscription", "?", "SubscriptionUpdate", "TrustRoot", "CertURL"};
+ private static final String[] PolicyCertPath =
+ {"PerProviderSubscription", "?", "Policy", "PolicyUpdate", "TrustRoot", "CertURL"};
+
+ private static void retrieveCerts(OMANode ppsRoot,
+ Map<OSUCertType, List<X509Certificate>> certs,
+ Network network, KeyManager km, KeyStore ks)
+ throws GeneralSecurityException, IOException {
+
+ List<X509Certificate> aaaCerts = getCerts(ppsRoot, AAACertPath, network, km, ks);
+ certs.put(OSUCertType.AAA, aaaCerts);
+ certs.put(OSUCertType.Remediation, getCerts(ppsRoot, RemdCertPath, network, km, ks));
+ certs.put(OSUCertType.Policy, getCerts(ppsRoot, PolicyCertPath, network, km, ks));
+ }
+
+ private static List<X509Certificate> getCerts(OMANode ppsRoot, String[] path, Network network,
+ KeyManager km, KeyStore ks)
+ throws GeneralSecurityException, IOException {
+ List<String> urls = new ArrayList<>();
+ getCertURLs(ppsRoot, Arrays.asList(path).iterator(), urls);
+ Log.d(TAG, Arrays.toString(path) + ": " + urls);
+
+ List<X509Certificate> certs = new ArrayList<>(urls.size());
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ for (String urlString : urls) {
+ URL url = new URL(urlString);
+ HTTPHandler httpHandler = new HTTPHandler(StandardCharsets.UTF_8,
+ OSUSocketFactory.getSocketFactory(ks, null, OSUManager.FLOW_PROVISIONING,
+ network, url, km, false));
+
+ certs.add((X509Certificate) certFactory.generateCertificate(httpHandler.doGet(url)));
+ }
+ return certs;
+ }
+
+ private static void getCertURLs(OMANode root, Iterator<String> path, List<String> urls)
+ throws IOException {
+
+ String name = path.next();
+ // Log.d(TAG, "Pulling '" + name + "' out of '" + root.getName() + "'");
+ Collection<OMANode> nodes = null;
+ switch (name) {
+ case "?":
+ for (OMANode node : root.getChildren()) {
+ if (!node.isLeaf()) {
+ nodes = Arrays.asList(node);
+ break;
+ }
+ }
+ break;
+ case "*":
+ nodes = root.getChildren();
+ break;
+ default:
+ nodes = Arrays.asList(root.getChild(name));
+ break;
+ }
+
+ if (nodes == null) {
+ throw new IllegalArgumentException("No matching node in " + root.getName()
+ + " for " + name);
+ }
+
+ for (OMANode node : nodes) {
+ if (path.hasNext()) {
+ getCertURLs(node, path, urls);
+ } else {
+ urls.add(node.getValue());
+ }
+ }
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUCommand.java b/packages/Osu/src/com/android/hotspot2/osu/OSUCommand.java
new file mode 100644
index 0000000..4730377
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUCommand.java
@@ -0,0 +1,130 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+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.commands.MOURN;
+import com.android.hotspot2.osu.commands.OSUCommandData;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class OSUCommand {
+ private final OSUCommandID mOSUCommand;
+ private final ExecCommand mExecCommand;
+ private final OSUCommandData mCommandData;
+
+ private static final Map<String, OSUCommandID> sCommands = new HashMap<>();
+ private static final Map<String, ExecCommand> sExecs = new HashMap<>();
+
+ static {
+ sCommands.put("exec", OSUCommandID.Exec);
+ sCommands.put("addmo", OSUCommandID.AddMO);
+ sCommands.put("updatenode", OSUCommandID.UpdateNode); // Multi
+ sCommands.put("nomoupdate", OSUCommandID.NoMOUpdate);
+
+ sExecs.put("launchbrowsertouri", ExecCommand.Browser);
+ sExecs.put("getcertificate", ExecCommand.GetCert);
+ sExecs.put("useclientcerttls", ExecCommand.UseClientCertTLS);
+ sExecs.put("uploadmo", ExecCommand.UploadMO);
+ }
+
+ public OSUCommand(XMLNode child) throws OMAException {
+ mOSUCommand = sCommands.get(child.getStrippedTag());
+
+ switch (mOSUCommand) {
+ case Exec:
+ /*
+ * Receipt of this element by a mobile device causes the following command
+ * to be executed.
+ */
+ child = child.getSoleChild();
+ mExecCommand = sExecs.get(child.getStrippedTag());
+ if (mExecCommand == null) {
+ throw new OMAException("Unrecognized exec command: " + child.getStrippedTag());
+ }
+ switch (mExecCommand) {
+ case Browser:
+ /*
+ * When the mobile device receives this command, it launches its default
+ * browser to the URI contained in this element. The URI must use HTTPS as
+ * the protocol and must contain an FQDN.
+ */
+ mCommandData = new BrowserURI(child);
+ break;
+ case GetCert:
+ mCommandData = new GetCertData(child);
+ break;
+ case UploadMO:
+ mCommandData = new MOURN(child);
+ break;
+ case UseClientCertTLS:
+ /*
+ * Command to mobile to re-negotiate the TLS connection using a client
+ * certificate of the accepted type or Issuer to authenticate with the
+ * Subscription server.
+ */
+ mCommandData = new ClientCertInfo(child);
+ break;
+ default:
+ mCommandData = null;
+ break;
+ }
+ break;
+ case AddMO:
+ /*
+ * This command causes an management object in the mobile devices management tree
+ * at the specified location to be added.
+ * If there is already a management object at that location, the object is replaced.
+ */
+ mExecCommand = null;
+ mCommandData = new MOData(child);
+ break;
+ case UpdateNode:
+ /*
+ * This command causes the update of an interior node and its child nodes (if any)
+ * at the location specified in the management tree URI attribute. The content of
+ * this element is the MO node XML.
+ */
+ mExecCommand = null;
+ mCommandData = new MOData(child);
+ break;
+ case NoMOUpdate:
+ /*
+ * This response is used when there is no command to be executed nor update of
+ * any MO required.
+ */
+ mExecCommand = null;
+ mCommandData = null;
+ break;
+ default:
+ mExecCommand = null;
+ mCommandData = null;
+ break;
+ }
+ }
+
+ public OSUCommandID getOSUCommand() {
+ return mOSUCommand;
+ }
+
+ public ExecCommand getExecCommand() {
+ return mExecCommand;
+ }
+
+ public OSUCommandData getCommandData() {
+ return mCommandData;
+ }
+
+ @Override
+ public String toString() {
+ return "OSUCommand{" +
+ "OSUCommand=" + mOSUCommand +
+ ", execCommand=" + mExecCommand +
+ ", commandData=" + mCommandData +
+ '}';
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUCommandID.java b/packages/Osu/src/com/android/hotspot2/osu/OSUCommandID.java
new file mode 100644
index 0000000..eca953f
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUCommandID.java
@@ -0,0 +1,5 @@
+package com.android.hotspot2.osu;
+
+public enum OSUCommandID {
+ Exec, AddMO, UpdateNode, NoMOUpdate
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUError.java b/packages/Osu/src/com/android/hotspot2/osu/OSUError.java
new file mode 100644
index 0000000..2fa7de0
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUError.java
@@ -0,0 +1,22 @@
+package com.android.hotspot2.osu;
+
+public enum OSUError {
+ SPPversionNotSupported,
+ MOsNotSupported,
+ CredentialsFailure,
+ RemediationFailure,
+ ProvisioningFailed,
+ ExistingCertificate,
+ CookieInvalid,
+ WebSessionID,
+ PermissionDenied,
+ CommandFailed,
+ MOaddOrUpdateFailed,
+ DeviceFull,
+ BadTreeURI,
+ TooLarge,
+ CommandNotAllowed,
+ UserAborted,
+ NotFound,
+ Other
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUInfo.java b/packages/Osu/src/com/android/hotspot2/osu/OSUInfo.java
new file mode 100644
index 0000000..86c0be9
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUInfo.java
@@ -0,0 +1,252 @@
+package com.android.hotspot2.osu;
+
+import android.net.wifi.ScanResult;
+import android.util.Log;
+
+import com.android.anqp.HSIconFileElement;
+import com.android.anqp.I18Name;
+import com.android.anqp.IconInfo;
+import com.android.anqp.OSUProvider;
+import com.android.hotspot2.Utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+public class OSUInfo {
+ public static final String GenericLocale = "zxx";
+
+ public enum IconStatus {
+ NotQueried, //
+ InProgress, // Query pending
+ NotAvailable, // Deterministically unavailable
+ Available // Icon data retrieved
+ }
+
+ private final long mBSSID;
+ private final long mHESSID;
+ private final int mAnqpDomID;
+ private final String mSSID;
+ private final String mAdvertisingSSID;
+ private final OSUProvider mOSUProvider;
+ private final int mOsuID;
+ private long mOSUBssid;
+ private IconStatus mIconStatus = IconStatus.NotQueried;
+ private HSIconFileElement mIconFileElement;
+ private IconInfo mIconInfo;
+
+ public OSUInfo(ScanResult scanResult, String ssid, OSUProvider osuProvider, int osuID) {
+ mOsuID = osuID;
+ mBSSID = Utils.parseMac(scanResult.BSSID);
+ mHESSID = scanResult.hessid;
+ mAnqpDomID = scanResult.anqpDomainId;
+ mAdvertisingSSID = scanResult.SSID;
+ mSSID = ssid;
+ mOSUProvider = osuProvider;
+ }
+
+ public long getOSUBssid() {
+ return mOSUBssid;
+ }
+
+ public void setOSUBssid(long OSUBssid) {
+ mOSUBssid = OSUBssid;
+ }
+
+ public long getHESSID() {
+ return mHESSID;
+ }
+
+ public int getAnqpDomID() {
+ return mAnqpDomID;
+ }
+
+ public String getAdvertisingSSID() {
+ return mAdvertisingSSID;
+ }
+
+ public Set<Locale> getNameLocales() {
+ Set<Locale> locales = new HashSet<>(mOSUProvider.getNames().size());
+ for (I18Name name : mOSUProvider.getNames()) {
+ locales.add(name.getLocale());
+ }
+ return locales;
+ }
+
+ public Set<Locale> getServiceLocales() {
+ Set<Locale> locales = new HashSet<>(mOSUProvider.getServiceDescriptions().size());
+ for (I18Name name : mOSUProvider.getServiceDescriptions()) {
+ locales.add(name.getLocale());
+ }
+ return locales;
+ }
+
+ public Set<String> getIconLanguages() {
+ Set<String> locales = new HashSet<>(mOSUProvider.getIcons().size());
+ for (IconInfo iconInfo : mOSUProvider.getIcons()) {
+ locales.add(iconInfo.getLanguage());
+ }
+ return locales;
+ }
+
+ public String getName(Locale locale) {
+ List<ScoreEntry<String>> scoreList = new ArrayList<>();
+ for (I18Name name : mOSUProvider.getNames()) {
+ if (locale == null || name.getLocale().equals(locale)) {
+ return name.getText();
+ }
+ scoreList.add(new ScoreEntry<String>(name.getText(),
+ languageScore(name.getLanguage(), locale)));
+ }
+ Collections.sort(scoreList);
+ return scoreList.isEmpty() ? null : scoreList.iterator().next().getData();
+ }
+
+ public String getServiceDescription(Locale locale) {
+ List<ScoreEntry<String>> scoreList = new ArrayList<>();
+ for (I18Name service : mOSUProvider.getServiceDescriptions()) {
+ if (locale == null || service.getLocale().equals(locale)) {
+ return service.getText();
+ }
+ scoreList.add(new ScoreEntry<>(service.getText(),
+ languageScore(service.getLanguage(), locale)));
+ }
+ Collections.sort(scoreList);
+ return scoreList.isEmpty() ? null : scoreList.iterator().next().getData();
+ }
+
+ public int getOsuID() {
+ return mOsuID;
+ }
+
+ public void setIconStatus(IconStatus iconStatus) {
+ synchronized (mOSUProvider) {
+ mIconStatus = iconStatus;
+ }
+ }
+
+ public IconStatus getIconStatus() {
+ synchronized (mOSUProvider) {
+ return mIconStatus;
+ }
+ }
+
+ public HSIconFileElement getIconFileElement() {
+ synchronized (mOSUProvider) {
+ return mIconFileElement;
+ }
+ }
+
+ public IconInfo getIconInfo() {
+ synchronized (mOSUProvider) {
+ return mIconInfo;
+ }
+ }
+
+ public void setIconFileElement(HSIconFileElement iconFileElement, String fileName) {
+ synchronized (mOSUProvider) {
+ mIconFileElement = iconFileElement;
+ for (IconInfo iconInfo : mOSUProvider.getIcons()) {
+ if (iconInfo.getFileName().equals(fileName)) {
+ mIconInfo = iconInfo;
+ break;
+ }
+ }
+ mIconStatus = IconStatus.Available;
+ }
+ }
+
+ private static class ScoreEntry<T> implements Comparable<ScoreEntry> {
+ private final T mData;
+ private final int mScore;
+
+ private ScoreEntry(T data, int score) {
+ mData = data;
+ mScore = score;
+ }
+
+ public T getData() {
+ return mData;
+ }
+
+ @Override
+ public int compareTo(ScoreEntry other) {
+ return Integer.compare(mScore, other.mScore);
+ }
+ }
+
+ public List<IconInfo> getIconInfo(Locale locale, Set<String> types, int width, int height) {
+ if (mOSUProvider.getIcons().isEmpty()) {
+ return null;
+ }
+ Log.d(OSUManager.TAG, "Matching icons against " + locale
+ + ", types " + types + ", " + width + "*" + height);
+
+ List<ScoreEntry<IconInfo>> matches = new ArrayList<>();
+ for (IconInfo iconInfo : mOSUProvider.getIcons()) {
+ Log.d(OSUManager.TAG, "Checking icon " + iconInfo.toString());
+ if (!types.contains(iconInfo.getIconType())) {
+ continue;
+ }
+
+ int score = languageScore(iconInfo.getLanguage(), locale);
+ int delta = iconInfo.getWidth() - width;
+ // Best size score is 1024 for a exact match, i.e. 2048 if both sides match
+ if (delta >= 0) {
+ score += (256 - delta) * 4; // Prefer down-scaling
+ } else {
+ score += 256 + delta; // Before up-scaling
+ }
+ delta = iconInfo.getHeight() - height;
+ if (delta >= 0) {
+ score += (256 - delta) * 4;
+ } else {
+ score += 256 + delta;
+ }
+ matches.add(new ScoreEntry<>(iconInfo, score));
+ }
+ if (matches.isEmpty()) {
+ return Collections.emptyList();
+ }
+ Collections.sort(matches);
+ List<IconInfo> icons = new ArrayList<>(matches.size());
+ for (ScoreEntry<IconInfo> scoredIcon : matches) {
+ icons.add(scoredIcon.getData());
+ }
+ return icons;
+ }
+
+ private static int languageScore(String language, Locale locale) {
+ if (language.length() == 3 && language.equalsIgnoreCase(locale.getISO3Language()) ||
+ language.length() == 2 && language.equalsIgnoreCase(locale.getLanguage())) {
+ return 4096;
+ } else if (language.equalsIgnoreCase(GenericLocale)) {
+ return 3072;
+ } else if (language.equalsIgnoreCase("eng")) {
+ return 2048;
+ } else {
+ return 1024;
+ }
+ }
+
+ public long getBSSID() {
+ return mBSSID;
+ }
+
+ public String getSSID() {
+ return mSSID;
+ }
+
+ public OSUProvider getOSUProvider() {
+ return mOSUProvider;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("OSU Info '%s' %012x -> %s, icon %s",
+ mSSID, mBSSID, getServiceDescription(null), mIconStatus);
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUListener.java b/packages/Osu/src/com/android/hotspot2/osu/OSUListener.java
new file mode 100644
index 0000000..9197620
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUListener.java
@@ -0,0 +1,5 @@
+package com.android.hotspot2.osu;
+
+public interface OSUListener {
+ public void osuNotification(int count);
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUManager.java b/packages/Osu/src/com/android/hotspot2/osu/OSUManager.java
new file mode 100644
index 0000000..c90e96b
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUManager.java
@@ -0,0 +1,977 @@
+package com.android.hotspot2.osu;
+
+import android.content.Context;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiInfo;
+import android.util.Log;
+
+import com.android.anqp.Constants;
+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 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.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+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.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 WFA_CA_LOC = "/etc/security/wfa";
+
+ 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 static final int FLOW_PROVISIONING = 1;
+ public static final int FLOW_REMEDIATION = 2;
+ public static final int FLOW_POLICY = 3;
+
+ 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-";
+
+ // Preferred icon parameters
+ private static final Set<String> ICON_TYPES =
+ new HashSet<>(Arrays.asList("image/png", "image/jpeg"));
+ private static final int ICON_WIDTH = 64;
+ private static final int ICON_HEIGHT = 64;
+ public static final Locale LOCALE = java.util.Locale.getDefault();
+
+ private final WifiNetworkAdapter mWifiNetworkAdapter;
+
+ private final AppBridge mAppBridge;
+ private final Context mContext;
+ private final IconCache mIconCache;
+ private final SubscriptionTimer mSubscriptionTimer;
+ private final Set<String> mOSUSSIDs = new HashSet<>();
+ private final Map<OSUProvider, OSUInfo> mOSUMap = new HashMap<>();
+ private final KeyStore mKeyStore;
+ private RedirectListener mRedirectListener;
+ private final AtomicInteger mOSUSequence = new AtomicInteger();
+ private OSUThread mProvisioningThread;
+ private final Map<String, OSUThread> mServiceThreads = new HashMap<>();
+ private volatile OSUInfo mPendingOSU;
+ private volatile Integer mOSUNwkID;
+
+ private final OSUCache mOSUCache;
+
+ public OSUManager(Context context) {
+ mContext = context;
+ mAppBridge = new AppBridge(context);
+ mIconCache = new IconCache(this);
+ mWifiNetworkAdapter = new WifiNetworkAdapter(context, this);
+ mSubscriptionTimer = new SubscriptionTimer(this, mWifiNetworkAdapter, context);
+ mOSUCache = new OSUCache();
+ KeyStore ks = null;
+ try {
+ //ks = loadKeyStore(KEYSTORE_FILE, readCertsFromDisk(WFA_CA_LOC));
+ ks = loadKeyStore(new File(context.getFilesDir(), KEYSTORE_FILE),
+ 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<X509Certificate> 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<X509Certificate> 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<String> 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 Set<X509Certificate> readCertsFromDisk(String dir) throws CertificateException {
+ Set<X509Certificate> certs = new HashSet<>();
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ File caDir = new File(dir);
+ File[] certFiles = caDir.listFiles();
+ if (certFiles != null) {
+ for (File certFile : certFiles) {
+ try {
+ try (FileInputStream in = new FileInputStream(certFile)) {
+ Certificate cert = certFactory.generateCertificate(in);
+ if (cert instanceof X509Certificate) {
+ certs.add((X509Certificate) cert);
+ }
+ }
+ } catch (CertificateException | IOException e) {
+ /* Ignore */
+ }
+ }
+ }
+ return certs;
+ }
+
+ public KeyStore getKeyStore() {
+ return mKeyStore;
+ }
+
+ private static class OSUThread extends Thread {
+ private final OSUClient mOSUClient;
+ private final OSUManager mOSUManager;
+ private final HomeSP mHomeSP;
+ private final String mSpName;
+ private final int mFlowType;
+ private final KeyManager mKeyManager;
+ private final long mLaunchTime;
+ private final Object mLock = new Object();
+ private boolean mLocalAddressSet;
+ private Network mNetwork;
+
+ private OSUThread(OSUInfo osuInfo, OSUManager osuManager, KeyManager km)
+ throws MalformedURLException {
+ mOSUClient = new OSUClient(osuInfo, osuManager.getKeyStore());
+ mOSUManager = osuManager;
+ mHomeSP = null;
+ mSpName = osuInfo.getName(LOCALE);
+ mFlowType = FLOW_PROVISIONING;
+ mKeyManager = km;
+ mLaunchTime = System.currentTimeMillis();
+
+ setDaemon(true);
+ setName("OSU Client Thread");
+ }
+
+ private OSUThread(String osuURL, OSUManager osuManager, KeyManager km, HomeSP homeSP,
+ int flowType) throws MalformedURLException {
+ mOSUClient = new OSUClient(osuURL, osuManager.getKeyStore());
+ mOSUManager = osuManager;
+ mHomeSP = homeSP;
+ mSpName = homeSP.getFriendlyName();
+ mFlowType = flowType;
+ mKeyManager = km;
+ mLaunchTime = System.currentTimeMillis();
+
+ setDaemon(true);
+ setName("OSU Client Thread");
+ }
+
+ public long getLaunchTime() {
+ return mLaunchTime;
+ }
+
+ private void connect(Network network) {
+ synchronized (mLock) {
+ mNetwork = network;
+ mLocalAddressSet = true;
+ mLock.notifyAll();
+ }
+ Log.d(TAG, "Client notified...");
+ }
+
+ @Override
+ public void run() {
+ Log.d(TAG, mFlowType + "-" + getName() + " running.");
+ Network network;
+ synchronized (mLock) {
+ while (!mLocalAddressSet) {
+ try {
+ mLock.wait();
+ } catch (InterruptedException ie) {
+ /**/
+ }
+ Log.d(TAG, "OSU Thread running...");
+ }
+ network = mNetwork;
+ }
+
+ if (network == null) {
+ Log.d(TAG, "Association failed, exiting OSU flow");
+ mOSUManager.provisioningFailed(mSpName, "Network cannot be reached",
+ mHomeSP, mFlowType);
+ return;
+ }
+
+ Log.d(TAG, "OSU SSID Associated at " + network.toString());
+ try {
+ if (mFlowType == FLOW_PROVISIONING) {
+ mOSUClient.provision(mOSUManager, network, mKeyManager);
+ } else {
+ mOSUClient.remediate(mOSUManager, network, mKeyManager, mHomeSP, mFlowType);
+ }
+ } catch (Throwable t) {
+ Log.w(TAG, "OSU flow failed: " + t, t);
+ mOSUManager.provisioningFailed(mSpName, t.getMessage(), mHomeSP, mFlowType);
+ }
+ }
+ }
+
+ /*
+ 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<OSUInfo> getAvailableOSUs() {
+ synchronized (mOSUMap) {
+ List<OSUInfo> completeOSUs = new ArrayList<>();
+ for (OSUInfo osuInfo : mOSUMap.values()) {
+ if (osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) {
+ completeOSUs.add(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;
+ break;
+ }
+ }
+
+ Log.d(TAG, "Selected OSU ID " + osuID + ", matches " + selection);
+
+ if (selection == null) {
+ mPendingOSU = null;
+ return;
+ }
+
+ mPendingOSU = selection;
+ WifiConfiguration config = mWifiNetworkAdapter.getActiveWifiConfig();
+
+ if (config != null &&
+ bssidMatch(selection) &&
+ Utils.unquote(config.SSID).equals(selection.getSSID())) {
+
+ 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;
+ }
+ } else {
+ try {
+ mOSUNwkID = mWifiNetworkAdapter.connect(selection, mPendingOSU.getName(LOCALE));
+ } catch (IOException ioe) {
+ notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(),
+ selection.getName(LOCALE));
+ }
+ }
+ }
+
+ public void networkConfigChange(WifiConfiguration configuration) {
+ mWifiNetworkAdapter.networkConfigChange(configuration);
+ }
+
+ public void networkConnectEvent(WifiInfo wifiInfo) {
+ if (wifiInfo != null) {
+ setActiveNetwork(mWifiNetworkAdapter.getActiveWifiConfig(),
+ mWifiNetworkAdapter.getCurrentNetwork());
+ }
+ }
+
+ public void wifiStateChange(boolean on) {
+ if (!on) {
+ int current = mOSUMap.size();
+ mOSUMap.clear();
+ mOSUCache.clearAll();
+ mIconCache.clear();
+ if (current > 0) {
+ notifyOSUCount(0);
+ }
+ }
+ }
+
+ 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);
+ if (mPendingOSU != null &&
+ wifiConfiguration != null &&
+ network != null &&
+ bssidMatch(mPendingOSU) &&
+ Utils.unquote(wifiConfiguration.SSID).equals(mPendingOSU.getSSID())) {
+
+ 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;
+ }
+ return;
+ }
+
+ /*
+ // !!! Hack to force start remediation at connection time
+ else if (wifiConfiguration != null && wifiConfiguration.isPasspoint()) {
+ HomeSP homeSP = mWifiConfigStore.getHomeSPForConfig(wifiConfiguration);
+ if (homeSP != null && homeSP.getSubscriptionUpdate() != null) {
+ if (!mServiceThreads.containsKey(homeSP.getFQDN())) {
+ try {
+ remediate(homeSP);
+ } catch (IOException ioe) {
+ Log.w(TAG, "Failed to remediate: " + ioe);
+ }
+ }
+ }
+ }
+ */
+ else if (wifiConfiguration == null) {
+ mServiceThreads.clear();
+ }
+ }
+
+
+ /**
+ * 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 {
+ synchronized (mWifiNetworkAdapter) {
+ if (mProvisioningThread != null) {
+ mProvisioningThread.connect(null);
+ mProvisioningThread = null;
+ }
+ if (mRedirectListener != null) {
+ mRedirectListener.abort();
+ mRedirectListener = null;
+ }
+ if (osuInfo != null) {
+ //new ConnMonitor().start();
+ mProvisioningThread = new OSUThread(osuInfo, this, getKeyManager(null, mKeyStore));
+ mProvisioningThread.start();
+ //mWifiNetworkAdapter.associate(osuInfo.getSSID(),
+ // osuInfo.getBSSID(), osuInfo.getOSUProvider().getOsuNai());
+ mProvisioningThread.connect(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).
+ */
+ 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);
+ }
+ }
+
+ public void tickleIconCache(boolean all) {
+ mIconCache.tickle(all);
+
+ if (all) {
+ synchronized (mOSUMap) {
+ int current = mOSUMap.size();
+ mOSUMap.clear();
+ mOSUCache.clearAll();
+ mIconCache.clear();
+ if (current > 0) {
+ notifyOSUCount(0);
+ }
+ }
+ }
+ }
+
+ public void pushScanResults(Collection<ScanResult> scanResults) {
+ Map<OSUProvider, ScanResult> results = mOSUCache.pushScanResults(scanResults);
+ if (results != null) {
+ updateOSUInfoCache(results);
+ }
+ }
+
+ private void updateOSUInfoCache(Map<OSUProvider, ScanResult> results) {
+ Map<OSUProvider, OSUInfo> osus = new HashMap<>();
+ for (Map.Entry<OSUProvider, ScanResult> entry : results.entrySet()) {
+ OSUInfo existing = mOSUMap.get(entry.getKey());
+ long bssid = Utils.parseMac(entry.getValue().BSSID);
+
+ if (existing == null || existing.getBSSID() != bssid) {
+ osus.put(entry.getKey(), new OSUInfo(entry.getValue(), entry.getKey().getSSID(),
+ entry.getKey(), mOSUSequence.getAndIncrement()));
+ } else {
+ // Maintain existing entries.
+ osus.put(entry.getKey(), existing);
+ }
+ }
+
+ mOSUMap.clear();
+ mOSUMap.putAll(osus);
+
+ mOSUSSIDs.clear();
+ for (OSUInfo osuInfo : mOSUMap.values()) {
+ mOSUSSIDs.add(osuInfo.getSSID());
+ }
+
+ if (mOSUMap.isEmpty()) {
+ notifyOSUCount(0);
+ }
+ initiateIconQueries();
+ Log.d(TAG, "Latest (app) OSU info: " + mOSUMap);
+ }
+
+ public void iconResults(List<OSUInfo> osuInfos) {
+ int newIcons = 0;
+ for (OSUInfo osuInfo : osuInfos) {
+ if (osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) {
+ newIcons++;
+ }
+ }
+ if (newIcons > 0) {
+ int count = 0;
+ for (OSUInfo existing : mOSUMap.values()) {
+ if (existing.getIconStatus() == OSUInfo.IconStatus.Available) {
+ count++;
+ }
+ }
+ Log.d(TAG, "Icon results for " + count + " OSUs");
+ notifyOSUCount(count);
+ }
+ }
+
+ private void notifyOSUCount(int count) {
+ mAppBridge.showOsuCount(count, getAvailableOSUs());
+ }
+
+ private void initiateIconQueries() {
+ for (OSUInfo osuInfo : mOSUMap.values()) {
+ if (osuInfo.getIconStatus() == OSUInfo.IconStatus.NotQueried) {
+ mIconCache.startIconQuery(osuInfo,
+ osuInfo.getIconInfo(LOCALE, ICON_TYPES, ICON_WIDTH, ICON_HEIGHT));
+ }
+ }
+ }
+
+ 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";
+ mAppBridge.showDeauth(spName, ess, delay, url);
+ }
+
+ // !!! Consistently check passpoint match.
+ // !!! Convert to a one-thread thread-pool
+ public void wnmRemediate(long bssid, String url, PasspointMatch match)
+ throws IOException, SAXException {
+ WifiConfiguration config = mWifiNetworkAdapter.getActiveWifiConfig();
+ HomeSP homeSP = MOManager.buildSP(config.getMoTree());
+ if (homeSP == null) {
+ throw new IOException("Remediation request for unidentified Passpoint network " +
+ config.networkId);
+ }
+ 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());
+
+ doRemediate(url, network, homeSP, false);
+ }
+
+ 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");
+ }
+
+ WifiConfiguration config = mWifiNetworkAdapter.getActivePasspointNetwork();
+ HomeSP activeSP = MOManager.buildSP(config.getMoTree());
+
+ 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 {
+ synchronized (mWifiNetworkAdapter) {
+ OSUThread existing = mServiceThreads.get(homeSP.getFQDN());
+ if (existing != null) {
+ if (System.currentTimeMillis() - existing.getLaunchTime() > REMEDIATION_TIMEOUT) {
+ throw new IOException("Ignoring recurring remediation request");
+ } else {
+ existing.connect(null);
+ }
+ }
+
+ try {
+ OSUThread osuThread = new OSUThread(url, this,
+ getKeyManager(homeSP, mKeyStore),
+ homeSP, policy ? FLOW_POLICY : FLOW_REMEDIATION);
+ osuThread.start();
+ osuThread.connect(network);
+ mServiceThreads.put(homeSP.getFQDN(), osuThread);
+ } catch (MalformedURLException me) {
+ throw new IOException("Failed to start remediation: " + me);
+ }
+ }
+ }
+
+ public MOTree getMOTree(HomeSP homeSP) throws IOException {
+ return mWifiNetworkAdapter.getMOTree(homeSP);
+ }
+
+ public void notifyIconReceived(long bssid, String fileName, byte[] data) {
+ mIconCache.notifyIconReceived(bssid, fileName, data);
+ }
+
+ public void doIconQuery(long bssid, String fileName) {
+ mWifiNetworkAdapter.doIconQuery(bssid, fileName);
+ }
+
+ 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,
+ int flowType) {
+ synchronized (mWifiNetworkAdapter) {
+ switch (flowType) {
+ case FLOW_PROVISIONING:
+ mProvisioningThread = null;
+ if (mRedirectListener != null) {
+ mRedirectListener.abort();
+ mRedirectListener = null;
+ }
+ break;
+ case FLOW_REMEDIATION:
+ case FLOW_POLICY:
+ mServiceThreads.remove(homeSP.getFQDN());
+ if (mServiceThreads.isEmpty() && mRedirectListener != null) {
+ mRedirectListener.abort();
+ mRedirectListener = null;
+ }
+ break;
+ }
+ }
+ notifyUser(OSUOperationStatus.ProvisioningFailure, message, spName);
+ }
+
+ public void provisioningComplete(OSUInfo osuInfo,
+ MOData moData, Map<OSUCertType, List<X509Certificate>> certs,
+ PrivateKey privateKey, Network osuNetwork) {
+ synchronized (mWifiNetworkAdapter) {
+ mProvisioningThread = null;
+ }
+ try {
+ Log.d("ZXZ", "MOTree.toXML: " + moData.getMOTree().toXml());
+ HomeSP homeSP = mWifiNetworkAdapter.addSP(moData.getMOTree());
+
+ Integer spNwk = mWifiNetworkAdapter.addNetwork(homeSP, certs, privateKey, osuNetwork);
+ if (spNwk == null) {
+ notifyUser(OSUOperationStatus.ProvisioningFailure,
+ "Failed to save network configuration", osuInfo.getName(LOCALE));
+ mWifiNetworkAdapter.removeSP(homeSP.getFQDN());
+ } else {
+ Set<X509Certificate> rootCerts = OSUSocketFactory.getRootCerts(mKeyStore);
+ X509Certificate remCert = getCert(certs, OSUCertType.Remediation);
+ X509Certificate polCert = getCert(certs, OSUCertType.Policy);
+ if (privateKey != null) {
+ X509Certificate cltCert = getCert(certs, OSUCertType.Client);
+ mKeyStore.setKeyEntry(CERT_CLT_KEY_ALIAS + homeSP,
+ privateKey.getEncoded(),
+ new X509Certificate[]{cltCert});
+ mKeyStore.setCertificateEntry(CERT_CLT_CERT_ALIAS, cltCert);
+ }
+ boolean usingShared = false;
+ int newCerts = 0;
+ 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(KEYSTORE_FILE)) {
+ 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<OSUCertType, List<X509Certificate>> certMap,
+ OSUCertType certType) {
+ List<X509Certificate> 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);
+
+ if (count > 0) {
+ try (FileOutputStream out = new FileOutputStream(KEYSTORE_FILE)) {
+ 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<MOData> mods,
+ Map<OSUCertType, List<X509Certificate>> certs,
+ PrivateKey privateKey)
+ throws IOException, GeneralSecurityException {
+
+ HomeSP altSP = mWifiNetworkAdapter.modifySP(homeSP, mods);
+ X509Certificate caCert = null;
+ List<X509Certificate> clientCerts = null;
+ if (certs != null) {
+ List<X509Certificate> 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; // No MO mods, only certs and key
+ }
+ mWifiNetworkAdapter.updateNetwork(altSP, caCert, clientCerts, privateKey);
+ }
+ notifyUser(OSUOperationStatus.ProvisioningSuccess, null, homeSP.getFriendlyName());
+ }
+
+ protected OMADMAdapter getOMADMAdapter() {
+ return OMADMAdapter.getInstance(mContext);
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUMessageType.java b/packages/Osu/src/com/android/hotspot2/osu/OSUMessageType.java
new file mode 100644
index 0000000..8c1b50a
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUMessageType.java
@@ -0,0 +1,5 @@
+package com.android.hotspot2.osu;
+
+public enum OSUMessageType {
+ PostDevData, ExchangeComplete, GetCertificate, Error
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUOperationStatus.java b/packages/Osu/src/com/android/hotspot2/osu/OSUOperationStatus.java
new file mode 100644
index 0000000..ddda89c
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUOperationStatus.java
@@ -0,0 +1,8 @@
+package com.android.hotspot2.osu;
+
+public enum OSUOperationStatus {
+ UserInputComplete,
+ UserInputAborted,
+ ProvisioningSuccess,
+ ProvisioningFailure
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUResponse.java b/packages/Osu/src/com/android/hotspot2/osu/OSUResponse.java
new file mode 100644
index 0000000..1e4398d
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUResponse.java
@@ -0,0 +1,97 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.OMAConstants;
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class OSUResponse {
+ private static final String SPPVersionAttribute = "sppVersion";
+ private static final String SPPStatusAttribute = "sppStatus";
+ private static final String SPPSessionIDAttribute = "sessionID";
+
+ private final OSUMessageType mMessageType;
+ private final String mVersion;
+ private final String mSessionID;
+ private final OSUStatus mStatus;
+ private final OSUError mError;
+ private final Map<String, String> mAttributes;
+
+ protected OSUResponse(XMLNode root, OSUMessageType messageType, String... attributes)
+ throws OMAException {
+ mMessageType = messageType;
+ String ns = root.getNameSpace() + ":";
+ mVersion = root.getAttributeValue(ns + SPPVersionAttribute);
+ mSessionID = root.getAttributeValue(ns + SPPSessionIDAttribute);
+
+ String status = root.getAttributeValue(ns + SPPStatusAttribute);
+ if (status == null) {
+ throw new OMAException("Missing status");
+ }
+ mStatus = OMAConstants.mapStatus(status);
+
+ if (mVersion == null || mSessionID == null || mStatus == null) {
+ throw new OMAException("Incomplete request: " + root.getAttributes());
+ }
+
+ if (attributes != null) {
+ mAttributes = new HashMap<>();
+ for (String attribute : attributes) {
+ String value = root.getAttributeValue(ns + attribute);
+ if (value == null) {
+ throw new OMAException("Missing attribute: " + attribute);
+ }
+ mAttributes.put(attribute, value);
+ }
+ } else {
+ mAttributes = null;
+ }
+
+ if (mStatus == OSUStatus.Error) {
+ OSUError error = null;
+ String errorTag = ns + "sppError";
+ for (XMLNode child : root.getChildren()) {
+ if (child.getTag().equals(errorTag)) {
+ error = OMAConstants.mapError(child.getAttributeValue("errorCode"));
+ break;
+ }
+ }
+ mError = error;
+ } else {
+ mError = null;
+ }
+ }
+
+ public OSUMessageType getMessageType() {
+ return mMessageType;
+ }
+
+ public String getVersion() {
+ return mVersion;
+ }
+
+ public String getSessionID() {
+ return mSessionID;
+ }
+
+ public OSUStatus getStatus() {
+ return mStatus;
+ }
+
+ public OSUError getError() {
+ return mError;
+ }
+
+ protected Map<String, String> getAttributes() {
+ return mAttributes;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s version '%s', status %s, session-id '%s'%s",
+ mMessageType, mVersion, mStatus, mSessionID, mError != null
+ ? (" (" + mError + ")") : "");
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUSocketFactory.java b/packages/Osu/src/com/android/hotspot2/osu/OSUSocketFactory.java
new file mode 100644
index 0000000..ef22f643
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUSocketFactory.java
@@ -0,0 +1,447 @@
+package com.android.hotspot2.osu;
+
+import android.net.Network;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.pps.HomeSP;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PrivateKey;
+import java.security.cert.CertPath;
+import java.security.cert.CertPathValidator;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.PKIXCertPathChecker;
+import java.security.cert.PKIXParameters;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+public class OSUSocketFactory {
+ private static final long ConnectionTimeout = 10000L;
+ private static final long ReconnectWait = 2000L;
+
+ private static final String SecureHTTP = "https";
+ private static final String UnsecureHTTP = "http";
+ private static final String EKU_ID = "2.5.29.37";
+ private static final Set<String> EKU_ID_SET = new HashSet<>(Arrays.asList(EKU_ID));
+ private static final EKUChecker sEKUChecker = new EKUChecker();
+
+ private final Network mNetwork;
+ private final SocketFactory mSocketFactory;
+ private final KeyManager mKeyManager;
+ private final WFATrustManager mTrustManager;
+ private final List<InetSocketAddress> mRemotes;
+
+ public static Set<X509Certificate> buildCertSet() {
+ try {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ Set<X509Certificate> set = new HashSet<>();
+ for (String b64 : WFACerts) {
+ ByteArrayInputStream bis = new ByteArrayInputStream(
+ Base64.decode(b64, Base64.DEFAULT));
+ X509Certificate cert = (X509Certificate) certFactory.generateCertificate(bis);
+ set.add(cert);
+ }
+ return set;
+ } catch (CertificateException ce) {
+ Log.e(OSUManager.TAG, "Cannot build CA cert set");
+ return null;
+ }
+ }
+
+ public static OSUSocketFactory getSocketFactory(KeyStore ks, HomeSP homeSP, int flowType,
+ Network network, URL url, KeyManager km,
+ boolean enforceSecurity)
+ throws GeneralSecurityException, IOException {
+
+ if (enforceSecurity && !url.getProtocol().equalsIgnoreCase(SecureHTTP)) {
+ throw new IOException("Protocol '" + url.getProtocol() + "' is not secure");
+ }
+ return new OSUSocketFactory(ks, homeSP, flowType, network, url, km);
+ }
+
+ private OSUSocketFactory(KeyStore ks, HomeSP homeSP, int flowType, Network network,
+ URL url, KeyManager km) throws GeneralSecurityException, IOException {
+ mNetwork = network;
+ mKeyManager = km;
+ mTrustManager = new WFATrustManager(ks, homeSP, flowType);
+ int port;
+ switch (url.getProtocol()) {
+ case UnsecureHTTP:
+ mSocketFactory = new DefaultSocketFactory();
+ port = url.getPort() > 0 ? url.getPort() : 80;
+ break;
+ case SecureHTTP:
+ SSLContext tlsContext = SSLContext.getInstance("TLSv1");
+ tlsContext.init(km != null ? new KeyManager[]{km} : null,
+ new TrustManager[]{mTrustManager}, null);
+ mSocketFactory = tlsContext.getSocketFactory();
+ port = url.getPort() > 0 ? url.getPort() : 443;
+ break;
+ default:
+ throw new IOException("Bad URL: " + url);
+ }
+ if (OSUManager.R2_MOCK && url.getHost().endsWith(".wi-fi.org")) {
+ // !!! Warning: Ruckus hack!
+ mRemotes = new ArrayList<>(1);
+ mRemotes.add(new InetSocketAddress(InetAddress.getByName("10.123.107.107"), port));
+ } else {
+ InetAddress[] remotes = mNetwork.getAllByName(url.getHost());
+ android.util.Log.d(OSUManager.TAG, "'" + url.getHost() + "' resolves to " +
+ Arrays.toString(remotes));
+ if (remotes == null || remotes.length == 0) {
+ throw new IOException("Failed to look up host from " + url);
+ }
+ mRemotes = new ArrayList<>(remotes.length);
+ for (InetAddress remote : remotes) {
+ mRemotes.add(new InetSocketAddress(remote, port));
+ }
+ }
+ Collections.shuffle(mRemotes);
+ }
+
+ public void reloadKeys(Map<OSUCertType, List<X509Certificate>> certs, PrivateKey key)
+ throws IOException {
+ if (mKeyManager instanceof ClientKeyManager) {
+ ((ClientKeyManager) mKeyManager).reloadKeys(certs, key);
+ }
+ }
+
+ public Socket createSocket() throws IOException {
+ Socket socket = mSocketFactory.createSocket();
+ mNetwork.bindSocket(socket);
+
+ long bail = System.currentTimeMillis() + ConnectionTimeout;
+ boolean success = false;
+
+ while (System.currentTimeMillis() < bail) {
+ for (InetSocketAddress remote : mRemotes) {
+ try {
+ socket.connect(remote);
+ Log.d(OSUManager.TAG, "Connection " + socket.getLocalSocketAddress() +
+ " to " + socket.getRemoteSocketAddress());
+ success = true;
+ break;
+ } catch (IOException ioe) {
+ Log.d(OSUManager.TAG, "Failed to connect to " + remote + ": " + ioe);
+ socket = mSocketFactory.createSocket();
+ mNetwork.bindSocket(socket);
+ }
+ }
+ if (success) {
+ break;
+ }
+ Utils.delay(ReconnectWait);
+ }
+ if (!success) {
+ throw new IOException("No available network");
+ }
+ return socket;
+ }
+
+ public X509Certificate getOSUCertificate(URL url) throws GeneralSecurityException {
+ String fqdn = url.getHost();
+ for (X509Certificate certificate : mTrustManager.getTrustChain()) {
+ for (List<?> name : certificate.getSubjectAlternativeNames()) {
+ if (name.size() >= SPVerifier.DNSName &&
+ name.get(0).getClass() == Integer.class &&
+ name.get(1).toString().equals(fqdn)) {
+ return certificate;
+ }
+ }
+ }
+ return null;
+ }
+
+ final class DefaultSocketFactory extends SocketFactory {
+
+ DefaultSocketFactory() {
+ }
+
+ @Override
+ public Socket createSocket() throws IOException {
+ return new Socket();
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException {
+ return new Socket(host, port);
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
+ throws IOException {
+ return new Socket(host, port, localHost, localPort);
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ return new Socket(host, port);
+ }
+
+ @Override
+ public Socket createSocket(InetAddress address, int port, InetAddress localAddress,
+ int localPort) throws IOException {
+ return new Socket(address, port, localAddress, localPort);
+ }
+ }
+
+ private static class WFATrustManager implements X509TrustManager {
+ private final KeyStore mKeyStore;
+ private final HomeSP mHomeSP;
+ private final int mFlowType;
+ private X509Certificate[] mTrustChain;
+
+ private WFATrustManager(KeyStore ks, HomeSP homeSP, int flowType)
+ throws CertificateException {
+ mKeyStore = ks;
+ mHomeSP = homeSP;
+ mFlowType = flowType;
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ // N/A
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ Log.d("TLSOSU", "Checking " + chain.length + " certs.");
+
+ try {
+ CertPathValidator validator =
+ CertPathValidator.getInstance(CertPathValidator.getDefaultType());
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ CertPath path = certFactory.generateCertPath(
+ Arrays.asList(chain));
+ Set<TrustAnchor> trustAnchors = new HashSet<>();
+ if (mHomeSP == null) {
+ for (X509Certificate cert : getRootCerts(mKeyStore)) {
+ trustAnchors.add(new TrustAnchor(cert, null));
+ }
+ } else {
+ String prefix = mFlowType == OSUManager.FLOW_REMEDIATION ?
+ OSUManager.CERT_REM_ALIAS : OSUManager.CERT_POLICY_ALIAS;
+
+ X509Certificate cert = getCert(mKeyStore, prefix + mHomeSP.getFQDN());
+ if (cert == null) {
+ cert = getCert(mKeyStore, OSUManager.CERT_SHARED_ALIAS + mHomeSP.getFQDN());
+ }
+ if (cert == null) {
+ for (X509Certificate root : getRootCerts(mKeyStore)) {
+ trustAnchors.add(new TrustAnchor(root, null));
+ }
+ } else {
+ trustAnchors.add(new TrustAnchor(cert, null));
+ }
+ }
+ PKIXParameters params = new PKIXParameters(trustAnchors);
+ params.setRevocationEnabled(false);
+ params.addCertPathChecker(sEKUChecker);
+ validator.validate(path, params);
+ mTrustChain = chain;
+ } catch (GeneralSecurityException gse) {
+ throw new SecurityException(gse);
+ }
+ mTrustChain = chain;
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+
+ public X509Certificate[] getTrustChain() {
+ return mTrustChain != null ? mTrustChain : new X509Certificate[0];
+ }
+ }
+
+ private static X509Certificate getCert(KeyStore keyStore, String alias)
+ throws KeyStoreException {
+ Certificate cert = keyStore.getCertificate(alias);
+ if (cert != null && cert instanceof X509Certificate) {
+ return (X509Certificate) cert;
+ }
+ return null;
+ }
+
+ public static Set<X509Certificate> getRootCerts(KeyStore keyStore) throws KeyStoreException {
+ Set<X509Certificate> certSet = new HashSet<>();
+ int index = 0;
+ for (int n = 0; n < 1000; n++) {
+ Certificate cert = keyStore.getCertificate(
+ String.format("%s%d", OSUManager.CERT_WFA_ALIAS, index));
+ if (cert == null) {
+ break;
+ } else if (cert instanceof X509Certificate) {
+ certSet.add((X509Certificate) cert);
+ }
+ index++;
+ }
+ return certSet;
+ }
+
+ private static class EKUChecker extends PKIXCertPathChecker {
+ @Override
+ public void init(boolean forward) throws CertPathValidatorException {
+
+ }
+
+ @Override
+ public boolean isForwardCheckingSupported() {
+ return true;
+ }
+
+ @Override
+ public Set<String> getSupportedExtensions() {
+ return EKU_ID_SET;
+ }
+
+ @Override
+ public void check(Certificate cert, Collection<String> unresolvedCritExts)
+ throws CertPathValidatorException {
+ Log.d(OSUManager.TAG, "Checking EKU " + unresolvedCritExts);
+ unresolvedCritExts.remove(EKU_ID);
+ }
+ }
+
+ /*
+ *
+ Subject: CN=osu-server.r2-testbed-rks.wi-fi.org, O=Intel Corporation CCG DRD, C=US
+ Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
+ Validity: [From: Wed Jan 28 16:00:00 PST 2015,
+ To: Sat Jan 28 15:59:59 PST 2017]
+ Issuer: CN="NetworkFX, Inc. Hotspot 2.0 Intermediate CA", OU=OSU CA - 01, O="NetworkFX, Inc.", C=US
+ SerialNumber: [ 312af3db 138eae19 1defbce2 e2b88b55]
+ *
+ *
+ Subject: CN="NetworkFX, Inc. Hotspot 2.0 Intermediate CA", OU=OSU CA - 01, O="NetworkFX, Inc.", C=US
+ Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
+ Validity: [From: Tue Nov 19 16:00:00 PST 2013,
+ To: Sun Nov 19 15:59:59 PST 2023]
+ Issuer: CN=Hotspot 2.0 Trust Root CA - 01, O=WFA Hotspot 2.0, C=US
+ SerialNumber: [ 4152b1b0 301495f3 8fa76428 2ef41046]
+ */
+
+ public static final String[] WFACerts = {
+ "MIIFbDCCA1SgAwIBAgIQDLMPcPKGpDPguQmJ3gHttzANBgkqhkiG9w0BAQsFADBQ" +
+ "MQswCQYDVQQGEwJVUzEYMBYGA1UEChMPV0ZBIEhvdHNwb3QgMi4wMScwJQYDVQQD" +
+ "Ex5Ib3RzcG90IDIuMCBUcnVzdCBSb290IENBIC0gMDMwHhcNMTMxMjA4MTIwMDAw" +
+ "WhcNNDMxMjA4MTIwMDAwWjBQMQswCQYDVQQGEwJVUzEYMBYGA1UEChMPV0ZBIEhv" +
+ "dHNwb3QgMi4wMScwJQYDVQQDEx5Ib3RzcG90IDIuMCBUcnVzdCBSb290IENBIC0g" +
+ "MDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsdEtReIUbMlO+hR6b" +
+ "yQk4nGVITv3meYTaDeVwZnQVal8EjHuu4Kd89g8yRYVTv3J1kq9ukE7CDrDehrXK" +
+ "ym+8VlR7ro0lB/lwRyNk3W7yNccg3AknQ0x5fKVwcFznwD/FYg37owGmhGFtpMTB" +
+ "cxzreQaLXvLta8YNlJU10ZkfputBpzi9bLPWsLOkIrQw7KH1Wc+Oiy4hUMUbTlSi" +
+ "cjqacKPR188mVIoxxUoICHyVV1KvMmYZrVdc/b5dbmd0haMHxC0VSqbydXxxS7vv" +
+ "/lCrC2d5qbKE66PiuBPkhzyU7SI9C8GU/S7akYm1MMSTn5W7lSp2AWRDnf9LQg51" +
+ "dLvDxJ7t2fruXtSkkqG/cwY1yQI8O+WZYPDThKPcDmNbaxVE9lOizAHXFVsfYrXA" +
+ "PbbMOkzKehYwaIikmNgcpxtQNw+wikJiZb9N8VwwtwHK71XEFi+n5DGlPa9VDYgB" +
+ "YkBcxvVo2rbE3i3teQgHm+pWZNP08aFNWwMk9yQkm/SOGdLq1jLbQA9yd7fyR1Ct" +
+ "W1GLzKi1Ojr/6XiB9/noL3oxP/+gb8OSgcqVfkZp4QLvrGdlKiOI2fE7Bslmzn6l" +
+ "B3UTpApjab7BQ99rCXzDwt3Xd7IrCtAJNkxi302J7k6hnGlW8S4oPQBElkOtoH9y" +
+ "XEhp9rNS0lZiuwtFmWW2q50fkQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G" +
+ "A1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUZw5JLGEXnuvt4FTnhNmbrWRgc2UwDQYJ" +
+ "KoZIhvcNAQELBQADggIBAFPoGFDyzFg9B9+jJUPGW32omftBhChVcgjllI07RCie" +
+ "KTMBi47+auuLgiMox3xRyP7/dX7YaUeMXEQ1BMv6nlrsXWv1lH4yu+RNuehPlqRs" +
+ "fY351mAfPtQ654SBUi0Wg++9iyTOfgF5a9IWEDt4lnSZMvA4vlw8pUCz6zpKXHnA" +
+ "RXKrpY3bU+2dnrFDKR0XQhmAQdo7UvdsT1elVoFIxHhLpwfzx+kpEhtrXw3nGgt+" +
+ "M4jNp684XoWpxVGaQ4Vvv00Sm2DQ8jq2sf9F+kRWszZpQOTiMGKZr0lX2CI5cww1" +
+ "dfmd1BkAjI9cIWLkD8YSeaggZzvYe1o9d7e7lKfdJmjDlSQ0uBiG77keUK4tF2fi" +
+ "xFTxibtPux56p3GYQ2GdRsBaKjH3A3HMJSKXwIGR+wb1sgz/bBdlyJSylG8hYD//" +
+ "0Hyo+UrMUszAdszoPhMY+4Ol3QE3QRWzXi+W/NtKeYD2K8xUzjZM10wMdxCfoFOa" +
+ "8bzzWnxZQlnu880ULUSHIxDPeE+DDZYYOaN1hV2Rh/hrFKvvV+gJj2eXHF5G7y9u" +
+ "Yg7nHYCCf7Hy8UTIXDtAAeDCQNon1ReN8G+XOqhLQ9TalmnJ5U5ARtC0MdQDht7T" +
+ "DZpWeEVv+pQHARX9GDV/T85MV2RPJWKqfZ6kK0gvQDkunADdg8IhZAjwMMx3k6B/",
+
+ "MIIFbDCCA1SgAwIBAgIQaAV8NQv/Xdusi4IU+tpUfjANBgkqhkiG9w0BAQsFADBQ" +
+ "MQswCQYDVQQGEwJVUzEYMBYGA1UEChMPV0ZBIEhvdHNwb3QgMi4wMScwJQYDVQQD" +
+ "Ex5Ib3RzcG90IDIuMCBUcnVzdCBSb290IENBIC0gMDEwHhcNMTMxMTIwMDAwMDAw" +
+ "WhcNNDMxMTE5MjM1OTU5WjBQMQswCQYDVQQGEwJVUzEYMBYGA1UEChMPV0ZBIEhv" +
+ "dHNwb3QgMi4wMScwJQYDVQQDEx5Ib3RzcG90IDIuMCBUcnVzdCBSb290IENBIC0g" +
+ "MDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/gf4CHxWjr2EcktAZ" +
+ "pHT4z1yFYZILD3ZVqvzzXBK+YKjWhjsgZ28Z1VwXqu51JvVzwTGDalPf5m7zMcJW" +
+ "CpPtPBdxxwQ/cBDPK4w+/sCuYYSddlMLzwZ/IgwFike12tKTR7Kk7Nk6ghrYaxCG" +
+ "R+QEZDVrxITj79vGpgk2otVnMI4d3H9mWt1o6Lx+hVioyBgOvmo2OWHR2uKkbg5h" +
+ "tktXqmBEtzK+qDqIIUY4WRRZHxlOaF2/EdIIGhXlf+Vlr13aPqOPiDiE08o+GARz" +
+ "TIp8BrW2boo0+2kpEFUKiqc427vOYEkUdSMfwu4aGOcuOewc8sk6ztquL/JcPROL" +
+ "VSFSSFR3HKhUto8EJcHEEG9wzcOi1OO/OOSVxjNwiaV/hB9Ed1wvoBhiJ+C+Q8/K" +
+ "HXmoH/ankXDaB06yjt2Ojemt0nO45qlarRj8tO7zbpghJuJxztur47U7PJta7Zcg" +
+ "z7kOPJPTAbzmOU2TXt1pXO1hVnSlV+M1rRwe7qivnSMMrTnkX15YWmyK27/tgJeu" +
+ "muR2YzvPwPtF/m1N0bRKI7FW05NYg3smItFq0E/eyf/orgolcXTZ7zNRyRGnjWNs" +
+ "/w9SDbdby0uVUfdN4V/5uC4HBmA1rikoBbGZ+nzCtesY4yW8eEwMfguVpNT3ueaU" +
+ "q30nufeY2VnA3Rv1WH8TaeZU+wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G" +
+ "A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU+RjGVZbebjpzEPfthaTLqbvXMiEwDQYJ" +
+ "KoZIhvcNAQELBQADggIBABj3LP1UXVa16HYeXC1+GU1dX/cla1n1bwpIlxRnCZ5/" +
+ "3I3zGw/nRnsLUTkGf8q3XCgin+jX22kyzzQNrgepn0zqBsmAj+pjUUwWzYQUzphc" +
+ "Uzmg4PJRWaEaGG3kvD+wJEC0pWvIhe48qcq8FZCCmjbvecEVn5mM0smPzPyUjf/o" +
+ "fjUMQvVWqug/Ff5HT6kbyDWhC3nD+8IZ5PjyO85OnoBnQkr8WYwr24XJgO2HS2rs" +
+ "W40CzQe3Kdg7HHyef+/iyLYTBJH7EUJPCHGVQtZ3q0aNqURkutXJ/CxKJYMcNTEB" +
+ "x+a09EhZ6DOHQDqsdTuAqGh3VyrxhFk+3suNsxoh6XaRK10VslvdNB/1YKfU8DWe" +
+ "V6XfDH/TR0NIL04exUp3rER8sERulpJGBOnaG6OQKh4bFYDB406+QfusQnvO0aYR" +
+ "UXJzf01B15HRJgpZsggpIuex0UDcJhTTpkRfTj8L4ayUce2ZRsGn3dBaT9ZMx4o9" +
+ "E/YsQyOpfw28gM5u+zZt4BJz4gAaRGbp4r4sk5Vm/P1/0EXJ70Du6K9d0HAHtpEv" +
+ "Y94Ww5W6fpMDdyAKYTXZBgTX3cqtikNkLX/kHH8l4o/XW2sXqU3X7vOYqgeVYoD9" +
+ "NnhZXYCerH4Se5Lgj8/KhXxRWtcn3XduMdkC6UTApMooA64Vs508173Z3lJn2SeQ",
+
+ "MIIFXTCCA0WgAwIBAgIBATANBgkqhkiG9w0BAQsFADBQMQswCQYDVQQGEwJVUzEY" +
+ "MBYGA1UECgwPV0ZBIEhvdHNwb3QgMi4wMScwJQYDVQQDDB5Ib3RzcG90IDIuMCBU" +
+ "cnVzdCBSb290IENBIC0gMDIwHhcNMTMxMjAyMjA1NzU3WhcNNDMxMjAyMjA1NTAz" +
+ "WjBQMQswCQYDVQQGEwJVUzEYMBYGA1UECgwPV0ZBIEhvdHNwb3QgMi4wMScwJQYD" +
+ "VQQDDB5Ib3RzcG90IDIuMCBUcnVzdCBSb290IENBIC0gMDIwggIiMA0GCSqGSIb3" +
+ "DQEBAQUAA4ICDwAwggIKAoICAQDCSoMqNhtTwbnIsINp6nUhx5UFuq9ZQoTv+KDk" +
+ "vAajT0di6+cQG3sAVvZLySmJoiBAv3PizYYLOD4eGMrFQRqi7PmSJ83WqNv23ZYF" +
+ "ryFFJiy/URXc/ALDuB3dgElPt24Mx7n2xDPAh9t82HTmuskpQRrsyg9QPoi5rRRS" +
+ "Djm5mjFJjKChq99RWcweNV/KGH1sTwcmlDmNMScK16A+BBNiSvmZlsGJgAlP369k" +
+ "lnNqt6UiDhepcktuKpHmSvNel+c/xqzR0gURfUnXcZhzjzS94Rx5O+CNWL4EGiJq" +
+ "qKAfk99j/lbD0MWYo7Rh0UKQlXSdohWDiV93hxvvfugej8KUOIb+1wmd1Fi+lwDZ" +
+ "bR2yg2f0qyxbC/tAV4JJNnuDLFb19leD78x+68eAnlbMi+xMH5lINs15+26s2H5d" +
+ "lx9kwRDBJq02LuHnen6FLafWjejnnBQ/PuGD0ACvBegSsDKDaCuTAnTNS6MDmQr4" +
+ "wza08iX360ZN+BbSAnCK1YGa/7J7fhyydwxLJ7s5Eo0b6SUMY87FMc5XmkAk4xxL" +
+ "MLqS2HMtqsGBI5JQT0SgH0ghE6DjMWArBTZcD+swuzTi1/Cz5+Z9Es8xJ3MPvSZW" +
+ "pJi6VVB2eVMAqfHOj4ozHoVpvJypIVGRwWBzVRWom76R47utuRK6uKzoLiB1jwE5" +
+ "vwHpUQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBxjAd" +
+ "BgNVHQ4EFgQU5C9c1OMsB+/MOwl9OKG2D/XSwrUwDQYJKoZIhvcNAQELBQADggIB" +
+ "AGULYE/VrnA3K0ptgHrWlQoPfp5wGvScgsmy0wp9qE3b6n/4bLehBKb5w4Y3JVA9" +
+ "gjxoQ5xE2ssDtULZ3nKnGWmMN3qOBoRZCA6KjKs1860p09tm1ScUsajDJ15Tp1nI" +
+ "zfR0oP63+2bJx+JXM8fPKOJe245hj2rs1c3JXsGCe+UVrlGsotG+wR0PdrejaXJ8" +
+ "HbhBQHcbhgjsD1Gb6Egm4YxRKAtcVY3q9EKKWAGhbC1qvCh1iLNKo3FeGgm2r3EG" +
+ "L4cYJBb2fhSKltjISqCDhYq4tplOIeQSJJyJC8gfW/BnMU39lTjNgnSjjGPLQXGV" +
+ "+Ulb/CgNMJ3RhRJdBoLcpIm/EeLx6JLq/2Erxy7CxjaSOcD0UKa14+dzLSHVsXft" +
+ "HZuOy548X8m18KruSZsf5uAT3c7NqlXtr9YgOVUqSJykNAHTGi/BHB1dC2clKvxN" +
+ "ElfLWWrG9yaAd5TFW0+3wsaDIwRZL584AsFwwAD3KMo1oU/2zRvtm0E+VghsuD/Z" +
+ "IE1xaVGTPaL7ph/YgC9+0rGHieauT8SXz6Ryp3h0RtYMLFZOMTKM7xjmcbMZDwrO" +
+ "c+J/XjK9dbiCqlx5/B8P0xWaYYHzvE5/fafiPYzoGyFVUXquu0dFCCQrvjF/y0tC" +
+ "TPm4hQim3k1F+5NChcbeNggN+kq+VdlSqPhQEuOY+kNv"
+ };
+
+ //private static final Set<TrustAnchor> sTrustAnchors = buildCertSet();
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/OSUStatus.java b/packages/Osu/src/com/android/hotspot2/osu/OSUStatus.java
new file mode 100644
index 0000000..00f0634
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/OSUStatus.java
@@ -0,0 +1,5 @@
+package com.android.hotspot2.osu;
+
+public enum OSUStatus {
+ OK, ProvComplete, RemediationComplete, UpdateComplete, ExchangeComplete, Unknown, Error
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/PostDevDataResponse.java b/packages/Osu/src/com/android/hotspot2/osu/PostDevDataResponse.java
new file mode 100644
index 0000000..12b9997
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/PostDevDataResponse.java
@@ -0,0 +1,49 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+import com.android.hotspot2.osu.commands.OSUCommandData;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class PostDevDataResponse extends OSUResponse {
+ private final List<OSUCommand> mOSUCommands;
+
+ public PostDevDataResponse(XMLNode root) throws OMAException {
+ super(root, OSUMessageType.PostDevData);
+
+ if (getStatus() == OSUStatus.Error) {
+ mOSUCommands = null;
+ return;
+ }
+
+ mOSUCommands = new ArrayList<>();
+ for (XMLNode child : root.getChildren()) {
+ mOSUCommands.add(new OSUCommand(child));
+ }
+ }
+
+ public OSUCommandID getOSUCommand() {
+ return mOSUCommands.size() == 1 ? mOSUCommands.get(0).getOSUCommand() : null;
+ }
+
+ public ExecCommand getExecCommand() {
+ return mOSUCommands.size() == 1 ? mOSUCommands.get(0).getExecCommand() : null;
+ }
+
+ public OSUCommandData getCommandData() {
+ return mOSUCommands.size() == 1 ? mOSUCommands.get(0).getCommandData() : null;
+ }
+
+ public Collection<OSUCommand> getCommands() {
+ return Collections.unmodifiableCollection(mOSUCommands);
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + ", commands " + mOSUCommands;
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/RequestReason.java b/packages/Osu/src/com/android/hotspot2/osu/RequestReason.java
new file mode 100644
index 0000000..db222b4
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/RequestReason.java
@@ -0,0 +1,16 @@
+package com.android.hotspot2.osu;
+
+public enum RequestReason {
+ SubRegistration,
+ SubProvisioning,
+ SubRemediation,
+ InputComplete,
+ NoClientCert,
+ CertEnrollmentComplete,
+ CertEnrollmentFailed,
+ SubMetaDataUpdate,
+ PolicyUpdate,
+ NextCommand,
+ MOUpload,
+ Unspecified
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/ResponseFactory.java b/packages/Osu/src/com/android/hotspot2/osu/ResponseFactory.java
new file mode 100644
index 0000000..3e236a7
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/ResponseFactory.java
@@ -0,0 +1,8 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+
+public interface ResponseFactory {
+ public OSUResponse buildResponse(XMLNode root) throws OMAException;
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/SOAPBuilder.java b/packages/Osu/src/com/android/hotspot2/osu/SOAPBuilder.java
new file mode 100644
index 0000000..e2f91ea
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/SOAPBuilder.java
@@ -0,0 +1,188 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.MOTree;
+import com.android.hotspot2.omadm.OMAConstants;
+import com.android.hotspot2.omadm.XMLNode;
+
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+
+public class SOAPBuilder {
+ private static final String EnvelopeTag = "s12:Envelope";
+ private static final String BodyTag = "s12:Body";
+
+ private static final Map<String, String> sEnvelopeAttributes = new HashMap<>(2);
+ private static final Map<RequestReason, String> sRequestReasons =
+ new EnumMap<>(RequestReason.class);
+
+ static {
+ sEnvelopeAttributes.put("xmlns:s12", "http://www.w3.org/2003/05/soap-envelope");
+ sEnvelopeAttributes.put("xmlns:spp",
+ "http://www.wi-fi.org/specifications/hotspot2dot0/v1.0/spp");
+
+ sRequestReasons.put(RequestReason.SubRegistration, "Subscription registration");
+ sRequestReasons.put(RequestReason.SubProvisioning, "Subscription provisioning");
+ sRequestReasons.put(RequestReason.SubRemediation, "Subscription remediation");
+ sRequestReasons.put(RequestReason.InputComplete, "User input completed");
+ sRequestReasons.put(RequestReason.NoClientCert, "No acceptable client certificate");
+ sRequestReasons.put(RequestReason.CertEnrollmentComplete,
+ "Certificate enrollment completed");
+ sRequestReasons.put(RequestReason.CertEnrollmentFailed, "Certificate enrollment failed");
+ sRequestReasons.put(RequestReason.SubMetaDataUpdate, "Subscription metadata update");
+ sRequestReasons.put(RequestReason.PolicyUpdate, "Policy update");
+ sRequestReasons.put(RequestReason.NextCommand, "Retrieve next command");
+ sRequestReasons.put(RequestReason.MOUpload, "MO upload");
+ sRequestReasons.put(RequestReason.Unspecified, "Unspecified");
+ }
+
+ public static String buildPostDevDataResponse(RequestReason reason, String sessionID,
+ String redirURI, MOTree... mos) {
+ XMLNode envelope = buildEnvelope();
+ buildSppPostDevData(envelope.getChildren().get(0), sessionID, reason, redirURI, mos);
+ return envelope.toString();
+ }
+
+ public static String buildUpdateResponse(String sessionID, OSUError error) {
+ XMLNode envelope = buildEnvelope();
+ buildSppUpdateResponse(envelope.getChildren().get(0), sessionID, error);
+ return envelope.toString();
+ }
+
+ private static XMLNode buildEnvelope() {
+ XMLNode envelope = new XMLNode(null, EnvelopeTag, sEnvelopeAttributes);
+ envelope.addChild(new XMLNode(envelope, BodyTag, (Map<String, String>) null));
+ return envelope;
+ }
+
+ private static XMLNode buildSppPostDevData(XMLNode parent, String sessionID,
+ RequestReason reason, String redirURI,
+ MOTree... mos) {
+ Map<String, String> pddAttributes = new HashMap<>();
+ pddAttributes.put(OMAConstants.TAG_Version, OMAConstants.MOVersion);
+ pddAttributes.put("requestReason", sRequestReasons.get(reason));
+ if (sessionID != null) {
+ pddAttributes.put(OMAConstants.TAG_SessionID, sessionID);
+ }
+ if (redirURI != null) {
+ pddAttributes.put("redirectURI", redirURI);
+ }
+
+ XMLNode pddNode = new XMLNode(parent, OMAConstants.TAG_PostDevData, pddAttributes);
+
+ XMLNode vNode = new XMLNode(pddNode, OMAConstants.TAG_SupportedVersions,
+ (HashMap<String, String>) null);
+ vNode.setText("1.0");
+ pddNode.addChild(vNode);
+
+ XMLNode moNode = new XMLNode(pddNode, OMAConstants.TAG_SupportedMOs,
+ (HashMap<String, String>) null);
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for (String urn : OMAConstants.SupportedMO_URNs) {
+ if (first) {
+ first = false;
+ } else {
+ sb.append(' ');
+ }
+ sb.append(urn);
+ }
+ moNode.setText(sb.toString());
+ pddNode.addChild(moNode);
+
+ if (mos != null) {
+ for (MOTree moTree : mos) {
+ Map<String, String> map = null;
+ if (moTree.getUrn() != null) {
+ map = new HashMap<>(1);
+ map.put(OMAConstants.SppMOAttribute, moTree.getUrn());
+ }
+ moNode = new XMLNode(pddNode, OMAConstants.TAG_MOContainer, map);
+ moNode.setText(moTree.toXml());
+ pddNode.addChild(moNode);
+ }
+ }
+
+ parent.addChild(pddNode);
+ return pddNode;
+ }
+
+ private static XMLNode buildSppUpdateResponse(XMLNode parent, String sessionID,
+ OSUError error) {
+ Map<String, String> urAttributes = new HashMap<>();
+ urAttributes.put(OMAConstants.TAG_Version, OMAConstants.MOVersion);
+ if (sessionID != null) {
+ urAttributes.put(OMAConstants.TAG_SessionID, sessionID);
+ }
+ if (error == null) {
+ urAttributes.put(OMAConstants.TAG_Status, OMAConstants.mapStatus(OSUStatus.OK));
+ } else {
+ urAttributes.put(OMAConstants.TAG_Status, OMAConstants.mapStatus(OSUStatus.Error));
+ }
+
+ XMLNode urNode = new XMLNode(parent, OMAConstants.TAG_UpdateResponse, urAttributes);
+
+ if (error != null) {
+ Map<String, String> errorAttributes = new HashMap<>();
+ errorAttributes.put("errorCode", OMAConstants.mapError(error));
+ XMLNode errorNode = new XMLNode(urNode, OMAConstants.TAG_Error, errorAttributes);
+ urNode.addChild(errorNode);
+ }
+
+ parent.addChild(urNode);
+ return urNode;
+ }
+
+ /*
+ <xsd:element name="sppUpdateResponse">
+ <xsd:annotation>
+ <xsd:documentation>SOAP method used by SPP client to confirm installation of MO addition or update.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element ref="sppError" minOccurs="0"/>
+ <xsd:any namespace="##other" maxOccurs="unbounded" minOccurs="0"/>
+ </xsd:sequence>
+ <xsd:attribute ref="sppVersion" use="required"/>
+ <xsd:attribute ref="sppStatus" use="required"/>
+ <xsd:attribute ref="sessionID" use="required"/>
+ <xsd:anyAttribute namespace="##other"/>
+ </xsd:complexType>
+ </xsd:element>
+
+ <xsd:element name="sppError">
+ <xsd:annotation>
+ <xsd:documentation>Error response.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:attribute name="errorCode" use="required">
+ <xsd:simpleType>
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="SPP version not supported"/>
+ <xsd:enumeration value="One or more mandatory MOs not supported"/>
+ <xsd:enumeration value="Credentials cannot be provisioned at this time"/>
+ <xsd:enumeration value="Remediation cannot be completed at this time"/>
+ <xsd:enumeration value="Provisioning cannot be completed at this time"/>
+ <xsd:enumeration value="Continue to use existing certificate"/>
+ <xsd:enumeration value="Cookie invalid"/>
+ <xsd:enumeration value="No corresponding web-browser-connection Session ID"/>
+ <xsd:enumeration value="Permission denied"/>
+ <xsd:enumeration value="Command failed"/>
+ <xsd:enumeration value="MO addition or update failed"/>
+ <xsd:enumeration value="Device full"/>
+ <xsd:enumeration value="Bad management tree URI"/>
+ <xsd:enumeration value="Requested entity too large"/>
+ <xsd:enumeration value="Command not allowed"/>
+ <xsd:enumeration value="Command not executed due to user"/>
+ <xsd:enumeration value="Not found"/>
+ <xsd:enumeration value="Other"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ </xsd:attribute>
+ <xsd:anyAttribute namespace="##other"/>
+ </xsd:complexType>
+ </xsd:element>
+
+
+ */
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/SOAPParser.java b/packages/Osu/src/com/android/hotspot2/osu/SOAPParser.java
new file mode 100644
index 0000000..b848ba9
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/SOAPParser.java
@@ -0,0 +1,327 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+public class SOAPParser {
+
+ private static final String EnvelopeTag = "envelope";
+ private static final String BodyTag = "body";
+
+ private static final Map<String, ResponseFactory> sResponseMap = new HashMap<>();
+
+ static {
+ sResponseMap.put("spppostdevdataresponse", new ResponseFactory() {
+ @Override
+ public OSUResponse buildResponse(XMLNode root) throws OMAException {
+ return new PostDevDataResponse(root);
+ }
+ });
+ sResponseMap.put("sppexchangecomplete", new ResponseFactory() {
+ @Override
+ public OSUResponse buildResponse(XMLNode root) throws OMAException {
+ return new ExchangeCompleteResponse(root);
+ }
+ });
+ sResponseMap.put("getcertificate", new ResponseFactory() {
+ @Override
+ public OSUResponse buildResponse(XMLNode root) {
+ return null;
+ }
+ });
+ sResponseMap.put("spperror", new ResponseFactory() {
+ @Override
+ public OSUResponse buildResponse(XMLNode root) {
+ return null;
+ }
+ });
+ }
+
+ private final XMLNode mResponseNode;
+
+ public SOAPParser(InputStream in)
+ throws ParserConfigurationException, SAXException, IOException {
+ XMLNode root;
+
+ try {
+ XMLParser parser = new XMLParser(in);
+ root = parser.getRoot();
+ } finally {
+ in.close();
+ }
+
+ String[] nsn = root.getTag().split(":");
+ if (nsn.length > 2) {
+ throw new OMAException("Bad root tag syntax: '" + root.getTag() + "'");
+ } else if (!EnvelopeTag.equalsIgnoreCase(nsn[nsn.length - 1])) {
+ throw new OMAException("Expected envelope: '" + root.getTag() + "'");
+ }
+
+ String bodyTag = nsn.length > 1 ? (nsn[0] + ":" + BodyTag) : BodyTag;
+ XMLNode body = null;
+
+ for (XMLNode child : root.getChildren()) {
+ if (bodyTag.equalsIgnoreCase(child.getTag())) {
+ body = child;
+ break;
+ }
+ }
+
+ if (body == null || body.getChildren().isEmpty()) {
+ throw new OMAException("Missing SOAP body");
+ }
+
+ mResponseNode = body.getSoleChild();
+ }
+
+ public OSUResponse getResponse() throws OMAException {
+ ResponseFactory responseFactory = sResponseMap.get(mResponseNode.getStrippedTag());
+ if (responseFactory == null) {
+ throw new OMAException("Unknown response type: '"
+ + mResponseNode.getStrippedTag() + "'");
+ }
+ return responseFactory.buildResponse(mResponseNode);
+ }
+
+ public XMLNode getResponseNode() {
+ return mResponseNode;
+ }
+
+
+ /*
+ <xsd:element name="sppPostDevDataResponse">
+ <xsd:annotation>
+ <xsd:documentation>SOAP method response from SPP server.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:choice>
+ <xsd:element ref="sppError"/>
+ <xsd:element name="exec">
+ <xsd:annotation>
+ <xsd:documentation>Receipt of this element by a mobile device causes the following command to be executed.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:choice>
+ <xsd:element name="launchBrowserToURI" type="httpsURIType">
+ <xsd:annotation>
+ <xsd:documentation>When the mobile device receives this command, it launches its default browser to the URI contained in this element. The URI must use HTTPS as the protocol and must contain an FQDN.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element ref="getCertificate"/>
+ <xsd:element name="useClientCertTLS">
+ <xsd:annotation>
+ <xsd:documentation>Command to mobile to re-negotiate the TLS connection using a client certificate of the accepted type or Issuer to authenticate with the Subscription server.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="providerIssuerName" minOccurs="0"
+ maxOccurs="unbounded">
+ <xsd:complexType>
+ <xsd:attribute name="name" type="xsd:string">
+ <xsd:annotation>
+ <xsd:documentation>The issuer name of an acceptable provider-issued certificate. The text of this element is formatted in accordance with the Issuer Name field in RFC-3280. This element is present only when acceptProviderCerts is true.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:anyAttribute namespace="##other"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:any namespace="##other" minOccurs="0"
+ maxOccurs="unbounded"/>
+ </xsd:sequence>
+ <xsd:attribute name="acceptMfgCerts" type="xsd:boolean"
+ use="optional" default="false">
+ <xsd:annotation>
+ <xsd:documentation>When this boolean is true, IEEE 802.1ar manufacturing certificates are acceptable for mobile device authentication.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute name="acceptProviderCerts" type="xsd:boolean"
+ use="optional" default="true">
+ <xsd:annotation>
+ <xsd:documentation>When this boolean is true, X509v3 certificates issued by providers identified in the providerIssuerName child element(s) are acceptable for mobile device authentication.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:anyAttribute namespace="##other"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="uploadMO" maxOccurs="unbounded">
+ <xsd:annotation>
+ <xsd:documentation>Command to mobile to upload the MO named in the moURN attribute to the SPP server.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:attribute ref="moURN"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:any namespace="##other" maxOccurs="unbounded" minOccurs="0">
+ <xsd:annotation>
+ <xsd:documentation>Element to allow the addition of new commands in the future.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:any>
+ </xsd:choice>
+ <xsd:anyAttribute namespace="##other"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="addMO">
+ <xsd:annotation>
+ <xsd:documentation>This command causes an management object in the mobile devices management tree at the specified location to be added. If there is already a management object at that location, the object is replaced.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:simpleContent>
+ <xsd:extension base="xsd:string">
+ <xsd:attribute ref="managementTreeURI"/>
+ <xsd:attribute ref="moURN"/>
+ </xsd:extension>
+ </xsd:simpleContent>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element maxOccurs="unbounded" name="updateNode">
+ <xsd:annotation>
+ <xsd:documentation>This command causes the update of an interior node and its child nodes (if any) at the location specified in the management tree URI attribute. The content of this element is the MO node XML.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:simpleContent>
+ <xsd:extension base="xsd:string">
+ <xsd:attribute ref="managementTreeURI"/>
+ </xsd:extension>
+ </xsd:simpleContent>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="noMOUpdate">
+ <xsd:annotation>
+ <xsd:documentation>This response is used when there is no command to be executed nor update of any MO required.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:any namespace="##other" minOccurs="0" maxOccurs="unbounded">
+ <xsd:annotation>
+ <xsd:documentation>For vendor-specific extensions or future needs.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:any>
+ </xsd:choice>
+ <xsd:attribute ref="sppVersion" use="required"/>
+ <xsd:attribute ref="sppStatus" use="required"/>
+ <xsd:attribute ref="moreCommands" use="optional"/>
+ <xsd:attribute ref="sessionID" use="required"/>
+ <xsd:anyAttribute namespace="##other"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="sppUpdateResponse">
+ <xsd:annotation>
+ <xsd:documentation>SOAP method used by SPP client to confirm installation of MO addition or update.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element ref="sppError" minOccurs="0"/>
+ <xsd:any namespace="##other" maxOccurs="unbounded" minOccurs="0"/>
+ </xsd:sequence>
+ <xsd:attribute ref="sppVersion" use="required"/>
+ <xsd:attribute ref="sppStatus" use="required"/>
+ <xsd:attribute ref="sessionID" use="required"/>
+ <xsd:anyAttribute namespace="##other"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="sppExchangeComplete">
+ <xsd:annotation>
+ <xsd:documentation>SOAP method used by SPP server to end session.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element ref="sppError" minOccurs="0"/>
+ <xsd:any namespace="##other" maxOccurs="unbounded" minOccurs="0"/>
+ </xsd:sequence>
+ <xsd:attribute ref="sppVersion" use="required"/>
+ <xsd:attribute ref="sppStatus" use="required"/>
+ <xsd:attribute ref="sessionID" use="required"/>
+ <xsd:anyAttribute namespace="##other"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="getCertificate">
+ <xsd:annotation>
+ <xsd:documentation>Command to mobile to initiate certificate enrollment or re-enrollment and is a container for metadata to enable enrollment.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="enrollmentServerURI" type="httpsURIType">
+ <xsd:annotation>
+ <xsd:documentation>HTTPS URI of the server to be contacted to initiate certificate enrollment. The URI must contain an FQDN.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name="estUserID" minOccurs="0">
+ <xsd:annotation>
+ <xsd:documentation>Temporary userid used by an EST client to authenticate to the EST server using HTTP Digest authentication. This element must be used for initial certificate enrollment; its use is optional for certificate re-enrollment.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:simpleType>
+ <xsd:restriction base="xsd:string">
+ <xsd:maxLength value="255"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ </xsd:element>
+ <xsd:element name="estPassword" minOccurs="0">
+ <xsd:annotation>
+ <xsd:documentation>Temporary password used by an EST client to authenticate to the EST server using HTTP Digest authentication. This element must be used for initial certificate enrollment; its use is optional for certificate re-enrollment.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:simpleType>
+ <xsd:restriction base="xsd:base64Binary">
+ <xsd:maxLength value="340"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ </xsd:element>
+ <xsd:any namespace="##other" minOccurs="0" maxOccurs="unbounded">
+ <xsd:annotation>
+ <xsd:documentation>For vendor-specific extensions or future needs.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:any>
+ </xsd:sequence>
+ <xsd:attribute name="enrollmentProtocol" use="required">
+ <xsd:simpleType>
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="EST"/>
+ <xsd:enumeration value="Other"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ </xsd:attribute>
+ <xsd:anyAttribute namespace="##other"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="sppError">
+ <xsd:annotation>
+ <xsd:documentation>Error response.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:attribute name="errorCode" use="required">
+ <xsd:simpleType>
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="SPP version not supported"/>
+ <xsd:enumeration value="One or more mandatory MOs not supported"/>
+ <xsd:enumeration value="Credentials cannot be provisioned at this time"/>
+ <xsd:enumeration value="Remediation cannot be completed at this time"/>
+ <xsd:enumeration value="Provisioning cannot be completed at this time"/>
+ <xsd:enumeration value="Continue to use existing certificate"/>
+ <xsd:enumeration value="Cookie invalid"/>
+ <xsd:enumeration value="No corresponding web-browser-connection Session ID"/>
+ <xsd:enumeration value="Permission denied"/>
+ <xsd:enumeration value="Command failed"/>
+ <xsd:enumeration value="MO addition or update failed"/>
+ <xsd:enumeration value="Device full"/>
+ <xsd:enumeration value="Bad management tree URI"/>
+ <xsd:enumeration value="Requested entity too large"/>
+ <xsd:enumeration value="Command not allowed"/>
+ <xsd:enumeration value="Command not executed due to user"/>
+ <xsd:enumeration value="Not found"/>
+ <xsd:enumeration value="Other"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ </xsd:attribute>
+ <xsd:anyAttribute namespace="##other"/>
+ </xsd:complexType>
+ </xsd:element>
+
+ */
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/SPVerifier.java b/packages/Osu/src/com/android/hotspot2/osu/SPVerifier.java
new file mode 100644
index 0000000..c529053
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/SPVerifier.java
@@ -0,0 +1,329 @@
+package com.android.hotspot2.osu;
+
+import android.util.Log;
+
+import com.android.anqp.HSIconFileElement;
+import com.android.anqp.I18Name;
+import com.android.anqp.IconInfo;
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.asn1.Asn1Class;
+import com.android.hotspot2.asn1.Asn1Constructed;
+import com.android.hotspot2.asn1.Asn1Decoder;
+import com.android.hotspot2.asn1.Asn1Integer;
+import com.android.hotspot2.asn1.Asn1Object;
+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 java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class SPVerifier {
+ public static final int OtherName = 0;
+ public static final int DNSName = 2;
+
+ private final OSUInfo mOSUInfo;
+
+ public SPVerifier(OSUInfo osuInfo) {
+ mOSUInfo = osuInfo;
+ }
+
+ /*
+ SEQUENCE:
+ [Context 0]:
+ SEQUENCE:
+ [Context 0]: -- LogotypeData
+ SEQUENCE:
+ SEQUENCE:
+ SEQUENCE:
+ IA5String='image/png'
+ SEQUENCE:
+ SEQUENCE:
+ SEQUENCE:
+ OID=2.16.840.1.101.3.4.2.1
+ NULL
+ OCTET_STRING= cf aa 74 a8 ad af 85 82 06 c8 f5 b5 bf ee 45 72 8a ee ea bd 47 ab 50 d3 62 0c 92 c1 53 c3 4c 6b
+ SEQUENCE:
+ IA5String='http://www.r2-testbed.wi-fi.org/icon_orange_zxx.png'
+ SEQUENCE:
+ INTEGER=4184
+ INTEGER=-128
+ INTEGER=61
+ [Context 4]= 7a 78 78
+ [Context 0]: -- LogotypeData
+ SEQUENCE:
+ SEQUENCE: -- LogotypeImage
+ SEQUENCE: -- LogoTypeDetails
+ IA5String='image/png'
+ SEQUENCE:
+ SEQUENCE: -- HashAlgAndValue
+ SEQUENCE:
+ OID=2.16.840.1.101.3.4.2.1
+ NULL
+ OCTET_STRING= cb 35 5c ba 7a 21 59 df 8e 0a e1 d8 9f a4 81 9e 41 8f af 58 0c 08 d6 28 7f 66 22 98 13 57 95 8d
+ SEQUENCE:
+ IA5String='http://www.r2-testbed.wi-fi.org/icon_orange_eng.png'
+ SEQUENCE: -- LogotypeImageInfo
+ INTEGER=11635
+ INTEGER=-96
+ INTEGER=76
+ [Context 4]= 65 6e 67
+ */
+
+ private static class LogoTypeImage {
+ private final String mMimeType;
+ private final List<HashAlgAndValue> mHashes = new ArrayList<>();
+ private final List<String> mURIs = new ArrayList<>();
+ private final int mFileSize;
+ private final int mXsize;
+ private final int mYsize;
+ private final String mLanguage;
+
+ private LogoTypeImage(Asn1Constructed sequence) throws IOException {
+ Iterator<Asn1Object> children = sequence.getChildren().iterator();
+
+ Iterator<Asn1Object> logoTypeDetails =
+ castObject(children.next(), Asn1Constructed.class).getChildren().iterator();
+ mMimeType = castObject(logoTypeDetails.next(), Asn1String.class).getString();
+
+ Asn1Constructed hashes = castObject(logoTypeDetails.next(), Asn1Constructed.class);
+ for (Asn1Object hash : hashes.getChildren()) {
+ mHashes.add(new HashAlgAndValue(castObject(hash, Asn1Constructed.class)));
+ }
+ Asn1Constructed urls = castObject(logoTypeDetails.next(), Asn1Constructed.class);
+ for (Asn1Object url : urls.getChildren()) {
+ mURIs.add(castObject(url, Asn1String.class).getString());
+ }
+
+ boolean imageInfoSet = false;
+ int fileSize = -1;
+ int xSize = -1;
+ int ySize = -1;
+ String language = null;
+
+ if (children.hasNext()) {
+ Iterator<Asn1Object> imageInfo =
+ castObject(children.next(), Asn1Constructed.class).getChildren().iterator();
+
+ Asn1Object first = imageInfo.next();
+ if (first.getTag() == 0) {
+ first = imageInfo.next(); // Ignore optional LogotypeImageType
+ }
+
+ fileSize = (int) castObject(first, Asn1Integer.class).getValue();
+ xSize = (int) castObject(imageInfo.next(), Asn1Integer.class).getValue();
+ ySize = (int) castObject(imageInfo.next(), Asn1Integer.class).getValue();
+ imageInfoSet = true;
+
+ if (imageInfo.hasNext()) {
+ Asn1Object next = imageInfo.next();
+ if (next.getTag() != 4) {
+ next = imageInfo.hasNext() ? imageInfo.next() : null; // Skip resolution
+ }
+ if (next != null && next.getTag() == 4) {
+ language = new String(castObject(next, Asn1Octets.class).getOctets(),
+ StandardCharsets.US_ASCII);
+ }
+ }
+ }
+
+ if (imageInfoSet) {
+ mFileSize = complement(fileSize);
+ mXsize = complement(xSize);
+ mYsize = complement(ySize);
+ } else {
+ mFileSize = mXsize = mYsize = -1;
+ }
+ mLanguage = language;
+ }
+
+ private boolean verify(OSUInfo osuInfo) throws GeneralSecurityException, IOException {
+ IconInfo iconInfo = osuInfo.getIconInfo();
+ HSIconFileElement iconData = osuInfo.getIconFileElement();
+ if (!iconInfo.getIconType().equals(mMimeType) ||
+ !iconInfo.getLanguage().equals(mLanguage) ||
+ iconData.getIconData().length != mFileSize) {
+ return false;
+ }
+ for (HashAlgAndValue hash : mHashes) {
+ if (hash.getJCEName() != null) {
+ MessageDigest digest = MessageDigest.getInstance(hash.getJCEName());
+ byte[] computed = digest.digest(iconData.getIconData());
+ if (!Arrays.equals(computed, hash.getHash())) {
+ throw new IOException("Icon hash mismatch");
+ } else {
+ Log.d(OSUManager.TAG, "Icon verified with " + hash.getJCEName());
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "LogoTypeImage{" +
+ "MimeType='" + mMimeType + '\'' +
+ ", hashes=" + mHashes +
+ ", URIs=" + mURIs +
+ ", fileSize=" + mFileSize +
+ ", xSize=" + mXsize +
+ ", ySize=" + mYsize +
+ ", language='" + mLanguage + '\'' +
+ '}';
+ }
+ }
+
+ private static class HashAlgAndValue {
+ private final String mJCEName;
+ private final byte[] mHash;
+
+ private HashAlgAndValue(Asn1Constructed sequence) throws IOException {
+ if (sequence.getChildren().size() != 2) {
+ throw new IOException("Bad HashAlgAndValue");
+ }
+ Iterator<Asn1Object> children = sequence.getChildren().iterator();
+ mJCEName = OidMappings.getJCEName(getFirstInner(children.next(), Asn1Oid.class));
+ mHash = castObject(children.next(), Asn1Octets.class).getOctets();
+ }
+
+ public String getJCEName() {
+ return mJCEName;
+ }
+
+ public byte[] getHash() {
+ return mHash;
+ }
+
+ @Override
+ public String toString() {
+ return "HashAlgAndValue{" +
+ "JCEName='" + mJCEName + '\'' +
+ ", hash=" + Utils.toHex(mHash) +
+ '}';
+ }
+ }
+
+ private static int complement(int value) {
+ return value >= 0 ? value : (~value) + 1;
+ }
+
+ private static <T extends Asn1Object> T castObject(Asn1Object object, Class<T> klass)
+ throws IOException {
+ if (object.getClass() != klass) {
+ throw new IOException("Object is an " + object.getClass().getSimpleName() +
+ " expected an " + klass.getSimpleName());
+ }
+ return klass.cast(object);
+ }
+
+ private static <T extends Asn1Object> T getFirstInner(Asn1Object container, Class<T> klass)
+ throws IOException {
+ if (container.getClass() != Asn1Constructed.class) {
+ throw new IOException("Not a container");
+ }
+ Iterator<Asn1Object> children = container.getChildren().iterator();
+ if (!children.hasNext()) {
+ throw new IOException("No content");
+ }
+ return castObject(children.next(), klass);
+ }
+
+ public void verify(X509Certificate osuCert) throws IOException, GeneralSecurityException {
+ if (osuCert == null) {
+ throw new IOException("No OSU cert found");
+ }
+
+ checkName(castObject(getExtension(osuCert, OidMappings.IdCeSubjectAltName),
+ Asn1Constructed.class));
+
+ List<LogoTypeImage> logos = getImageData(getExtension(osuCert, OidMappings.IdPeLogotype));
+ Log.d(OSUManager.TAG, "Logos: " + logos);
+ for (LogoTypeImage logoTypeImage : logos) {
+ if (logoTypeImage.verify(mOSUInfo)) {
+ return;
+ }
+ }
+ throw new IOException("Failed to match icon against any cert logo");
+ }
+
+ private static List<LogoTypeImage> getImageData(Asn1Object logoExtension) throws IOException {
+ Asn1Constructed logo = castObject(logoExtension, Asn1Constructed.class);
+ Asn1Constructed communityLogo = castObject(logo.getChildren().iterator().next(),
+ Asn1Constructed.class);
+ if (communityLogo.getTag() != 0) {
+ throw new IOException("Expected tag [0] for communityLogos");
+ }
+
+ List<LogoTypeImage> images = new ArrayList<>();
+ Asn1Constructed communityLogoSeq = castObject(communityLogo.getChildren().iterator().next(),
+ Asn1Constructed.class);
+ for (Asn1Object logoTypeData : communityLogoSeq.getChildren()) {
+ if (logoTypeData.getTag() != 0) {
+ throw new IOException("Expected tag [0] for LogotypeData");
+ }
+ for (Asn1Object logoTypeImage : castObject(logoTypeData.getChildren().iterator().next(),
+ Asn1Constructed.class).getChildren()) {
+ // only read the image SEQUENCE and skip any audio [1] tags
+ if (logoTypeImage.getAsn1Class() == Asn1Class.Universal) {
+ images.add(new LogoTypeImage(castObject(logoTypeImage, Asn1Constructed.class)));
+ }
+ }
+ }
+ return images;
+ }
+
+ private void checkName(Asn1Constructed altName) throws IOException {
+ Map<String, I18Name> friendlyNames = new HashMap<>();
+ for (Asn1Object name : altName.getChildren()) {
+ if (name.getAsn1Class() == Asn1Class.Context && name.getTag() == OtherName) {
+ Asn1Constructed otherName = (Asn1Constructed) name;
+ Iterator<Asn1Object> children = otherName.getChildren().iterator();
+ if (children.hasNext()) {
+ Asn1Object oidObject = children.next();
+ if (OidMappings.sIdWfaHotspotFriendlyName.equals(oidObject) &&
+ children.hasNext()) {
+ Asn1Constructed value = castObject(children.next(), Asn1Constructed.class);
+ String text = castObject(value.getChildren().iterator().next(),
+ Asn1String.class).getString();
+ I18Name friendlyName = new I18Name(text);
+ friendlyNames.put(friendlyName.getLanguage(), friendlyName);
+ }
+ }
+ }
+ }
+ Log.d(OSUManager.TAG, "Friendly names: " + friendlyNames.values());
+ for (I18Name osuName : mOSUInfo.getOSUProvider().getNames()) {
+ I18Name friendlyName = friendlyNames.get(osuName.getLanguage());
+ if (!osuName.equals(friendlyName)) {
+ throw new IOException("Friendly name '" + osuName + " not in certificate");
+ }
+ }
+ }
+
+ private static Asn1Object getExtension(X509Certificate certificate, String extension)
+ throws GeneralSecurityException, IOException {
+ byte[] data = certificate.getExtensionValue(extension);
+ if (data == null) {
+ return null;
+ }
+ Asn1Octets octetString = (Asn1Octets) Asn1Decoder.decode(ByteBuffer.wrap(data)).
+ iterator().next();
+ Asn1Constructed sequence = castObject(Asn1Decoder.decode(
+ ByteBuffer.wrap(octetString.getOctets())).iterator().next(),
+ Asn1Constructed.class);
+ Log.d(OSUManager.TAG, "Extension " + extension + ": " + sequence);
+ return sequence;
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/UserInputListener.java b/packages/Osu/src/com/android/hotspot2/osu/UserInputListener.java
new file mode 100644
index 0000000..ca30e3d
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/UserInputListener.java
@@ -0,0 +1,46 @@
+package com.android.hotspot2.osu;
+
+import android.net.Network;
+
+import java.net.URL;
+
+public interface UserInputListener {
+ /**
+ * Launch an appropriate application to handle user input and HTTP exchanges to the target
+ * URL. Under normal circumstances this implies that a web-browser is started and pointed at
+ * the target URL from which it is supposed to perform an initial HTTP GET operation.
+ * This call must not block beyond the time it takes to launch the user agent, i.e. must return
+ * well before the HTTP exchange terminates.
+ * @param target A fully encoded URL to which to send an initial HTTP GET and then handle
+ * subsequent HTTP exchanges.
+ * @param endRedirect A URL to which the user agent will be redirected upon completion of
+ * the HTTP exchange. This parameter is for informational purposes only
+ * as the redirect to the URL is the responsibility of the remote server.
+ */
+ public void requestUserInput(URL target, Network network, URL endRedirect);
+
+ /**
+ * Notification that status of the OSU operation has changed. The implementation may choose to
+ * return a string that will be passed to the user agent. Please note that the string is
+ * passed as the payload of (the redirect) HTTP connection to the agent and must be formatted
+ * appropriately (e.g. as well formed HTML).
+ * Returning a null string on the initial status update of UserInputComplete or UserInputAborted
+ * will cause the local "redirect" web-server to terminate and any further strings returned will
+ * be ignored.
+ * If programmatic termination of the user agent is desired, it should be initiated from within
+ * the implementation of this method.
+ * @param status
+ * @param message
+ * @return
+ */
+ public String operationStatus(String spIdentity, OSUOperationStatus status, String message);
+
+ /**
+ * Notify the user that a de-authentication event is imminent.
+ * @param ess set to indicate that the de-authentication is for an ESS instead of a BSS
+ * @param delay delay the number of seconds that the user will have to wait before
+ * reassociating with the BSS or ESS.
+ * @param url a URL to which to redirect the user
+ */
+ public void deAuthNotification(String spIdentity, boolean ess, int delay, URL url);
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/WiFiKeyManager.java b/packages/Osu/src/com/android/hotspot2/osu/WiFiKeyManager.java
new file mode 100644
index 0000000..54a3c4d
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/WiFiKeyManager.java
@@ -0,0 +1,172 @@
+package com.android.hotspot2.osu;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.net.ssl.X509KeyManager;
+import javax.security.auth.x500.X500Principal;
+
+public class WiFiKeyManager implements X509KeyManager {
+ private final KeyStore mKeyStore;
+ private final Map<X500Principal, String[]> mAliases = new HashMap<>();
+
+ public WiFiKeyManager(KeyStore keyStore) throws IOException {
+ mKeyStore = keyStore;
+ }
+
+ public void enableClientAuth(List<String> issuerNames) throws GeneralSecurityException,
+ IOException {
+
+ Set<X500Principal> acceptedIssuers = new HashSet<>();
+ for (String issuerName : issuerNames) {
+ acceptedIssuers.add(new X500Principal(issuerName));
+ }
+
+ Enumeration<String> aliases = mKeyStore.aliases();
+ while (aliases.hasMoreElements()) {
+ String alias = aliases.nextElement();
+ Certificate cert = mKeyStore.getCertificate(alias);
+ if ((cert instanceof X509Certificate) && mKeyStore.getKey(alias, null) != null) {
+ X509Certificate x509Certificate = (X509Certificate) cert;
+ X500Principal issuer = x509Certificate.getIssuerX500Principal();
+ if (acceptedIssuers.contains(issuer)) {
+ mAliases.put(issuer, new String[]{alias, cert.getPublicKey().getAlgorithm()});
+ }
+ }
+ }
+
+ if (mAliases.isEmpty()) {
+ throw new IOException("No aliases match requested issuers: " + issuerNames);
+ }
+ }
+
+ private static class AliasEntry implements Comparable<AliasEntry> {
+ private final int mPreference;
+ private final String mAlias;
+
+ private AliasEntry(int preference, String alias) {
+ mPreference = preference;
+ mAlias = alias;
+ }
+
+ public int getPreference() {
+ return mPreference;
+ }
+
+ public String getAlias() {
+ return mAlias;
+ }
+
+ @Override
+ public int compareTo(AliasEntry other) {
+ return Integer.compare(getPreference(), other.getPreference());
+ }
+ }
+
+ @Override
+ public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
+
+ Map<String, Integer> keyPrefs = new HashMap<>(keyTypes.length);
+ int pref = 0;
+ for (String keyType : keyTypes) {
+ keyPrefs.put(keyType, pref++);
+ }
+
+ List<AliasEntry> aliases = new ArrayList<>();
+ if (issuers != null) {
+ for (Principal issuer : issuers) {
+ if (issuer instanceof X500Principal) {
+ String[] aliasAndKey = mAliases.get((X500Principal) issuer);
+ if (aliasAndKey != null) {
+ Integer preference = keyPrefs.get(aliasAndKey[1]);
+ if (preference != null) {
+ aliases.add(new AliasEntry(preference, aliasAndKey[0]));
+ }
+ }
+ }
+ }
+ } else {
+ for (String[] aliasAndKey : mAliases.values()) {
+ Integer preference = keyPrefs.get(aliasAndKey[1]);
+ if (preference != null) {
+ aliases.add(new AliasEntry(preference, aliasAndKey[0]));
+ }
+ }
+ }
+ Collections.sort(aliases);
+ return aliases.isEmpty() ? null : aliases.get(0).getAlias();
+ }
+
+ @Override
+ public String[] getClientAliases(String keyType, Principal[] issuers) {
+ List<String> aliases = new ArrayList<>();
+ if (issuers != null) {
+ for (Principal issuer : issuers) {
+ if (issuer instanceof X500Principal) {
+ String[] aliasAndKey = mAliases.get((X500Principal) issuer);
+ if (aliasAndKey != null) {
+ aliases.add(aliasAndKey[0]);
+ }
+ }
+ }
+ } else {
+ for (String[] aliasAndKey : mAliases.values()) {
+ aliases.add(aliasAndKey[0]);
+ }
+ }
+ return aliases.isEmpty() ? null : aliases.toArray(new String[aliases.size()]);
+ }
+
+ @Override
+ public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String[] getServerAliases(String keyType, Principal[] issuers) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias) {
+ try {
+ List<X509Certificate> certs = new ArrayList<>();
+ for (Certificate certificate : mKeyStore.getCertificateChain(alias)) {
+ if (certificate instanceof X509Certificate) {
+ certs.add((X509Certificate) certificate);
+ }
+ }
+ return certs.toArray(new X509Certificate[certs.size()]);
+ } catch (KeyStoreException kse) {
+ Log.w(OSUManager.TAG, "Failed to retrieve certificates: " + kse);
+ return null;
+ }
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias) {
+ try {
+ return (PrivateKey) mKeyStore.getKey(alias, null);
+ } catch (GeneralSecurityException gse) {
+ Log.w(OSUManager.TAG, "Failed to retrieve private key: " + gse);
+ return null;
+ }
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/XMLParser.java b/packages/Osu/src/com/android/hotspot2/osu/XMLParser.java
new file mode 100644
index 0000000..b23e555
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/XMLParser.java
@@ -0,0 +1,71 @@
+package com.android.hotspot2.osu;
+
+import com.android.hotspot2.omadm.XMLNode;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+public class XMLParser extends DefaultHandler {
+ private final SAXParser mParser;
+ private final InputSource mInputSource;
+
+ private XMLNode mRoot;
+ private XMLNode mCurrent;
+
+ public XMLParser(InputStream in) throws ParserConfigurationException, SAXException {
+ mParser = SAXParserFactory.newInstance().newSAXParser();
+ mInputSource = new InputSource(new BufferedReader(
+ new InputStreamReader(in, StandardCharsets.UTF_8)));
+ }
+
+ public XMLNode getRoot() throws SAXException, IOException {
+ mParser.parse(mInputSource, this);
+ return mRoot;
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attributes)
+ throws SAXException {
+ XMLNode parent = mCurrent;
+
+ mCurrent = new XMLNode(mCurrent, qName, attributes);
+ //System.out.println("Added " + mCurrent.getTag() + ", atts " + mCurrent.getAttributes());
+
+ if (mRoot == null)
+ mRoot = mCurrent;
+ else
+ parent.addChild(mCurrent);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ if (!qName.equals(mCurrent.getTag()))
+ throw new SAXException("End tag '" + qName + "' doesn't match current node: " +
+ mCurrent);
+
+ try {
+ mCurrent.close();
+ } catch (IOException ioe) {
+ throw new SAXException("Failed to close element", ioe);
+ }
+
+ mCurrent = mCurrent.getParent();
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ mCurrent.addText(ch, start, length);
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/commands/BrowserURI.java b/packages/Osu/src/com/android/hotspot2/osu/commands/BrowserURI.java
new file mode 100644
index 0000000..137dbc9
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/commands/BrowserURI.java
@@ -0,0 +1,32 @@
+package com.android.hotspot2.osu.commands;
+
+import com.android.hotspot2.omadm.XMLNode;
+
+/*
+ <spp:sppPostDevDataResponse xmlns:spp="http://www.wi-fi.org/specifications/hotspot2dot0/v1.0/spp"
+ spp:sessionID="D74A7B03005645DAA516191DEE77B94F" spp:sppStatus="OK"
+ spp:sppVersion="1.0">
+ <spp:exec>
+ <spp:launchBrowserToURI>
+ https://subscription-server.r2-testbed-rks.wi-fi.org:8443/web/ruckuswireles/home/-/onlinesignup/subscriberDetails?Credentials=USERNAME_PASSWORD&SessionID=D74A7B03005645DAA516191DEE77B94F&RedirectURI=http://127.0.0.1:12345/index.htm&UpdateMethod=SPP-ClientInitiated
+ </spp:launchBrowserToURI>
+ </spp:exec>
+ </spp:sppPostDevDataResponse>
+ */
+
+public class BrowserURI implements OSUCommandData {
+ private final String mURI;
+
+ public BrowserURI(XMLNode commandNode) {
+ mURI = commandNode.getText();
+ }
+
+ public String getURI() {
+ return mURI;
+ }
+
+ @Override
+ public String toString() {
+ return "URI: " + mURI;
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/commands/ClientCertInfo.java b/packages/Osu/src/com/android/hotspot2/osu/commands/ClientCertInfo.java
new file mode 100644
index 0000000..f877353
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/commands/ClientCertInfo.java
@@ -0,0 +1,94 @@
+package com.android.hotspot2.osu.commands;
+
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/*
+<xsd:element name="useClientCertTLS">
+ <xsd:annotation>
+ <xsd:documentation>Command to mobile to re-negotiate the TLS connection using a client certificate of the accepted type or Issuer to authenticate with the Subscription server.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="providerIssuerName" minOccurs="0"
+ maxOccurs="unbounded">
+ <xsd:complexType>
+ <xsd:attribute name="name" type="xsd:string">
+ <xsd:annotation>
+ <xsd:documentation>The issuer name of an acceptable provider-issued certificate. The text of this element is formatted in accordance with the Issuer Name field in RFC-3280. This element is present only when acceptProviderCerts is true.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:anyAttribute namespace="##other"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:any namespace="##other" minOccurs="0"
+ maxOccurs="unbounded"/>
+ </xsd:sequence>
+ <xsd:attribute name="acceptMfgCerts" type="xsd:boolean"
+ use="optional" default="false">
+ <xsd:annotation>
+ <xsd:documentation>When this boolean is true, IEEE 802.1ar manufacturing certificates are acceptable for mobile device authentication.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute name="acceptProviderCerts" type="xsd:boolean"
+ use="optional" default="true">
+ <xsd:annotation>
+ <xsd:documentation>When this boolean is true, X509v3 certificates issued by providers identified in the providerIssuerName child element(s) are acceptable for mobile device authentication.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:anyAttribute namespace="##other"/>
+ </xsd:complexType>
+</xsd:element>
+ */
+
+public class ClientCertInfo implements OSUCommandData {
+ private final boolean mAcceptMfgCerts;
+ private final boolean mAcceptProviderCerts;
+ /*
+ * The issuer name of an acceptable provider-issued certificate.
+ * The text of this element is formatted in accordance with the Issuer Name field in RFC-3280.
+ * This element is present only when acceptProviderCerts is true.
+ */
+ private final List<String> mIssuerNames;
+
+ public ClientCertInfo(XMLNode commandNode) throws OMAException {
+ mAcceptMfgCerts = Boolean.parseBoolean(commandNode.getAttributeValue("acceptMfgCerts"));
+ mAcceptProviderCerts =
+ Boolean.parseBoolean(commandNode.getAttributeValue("acceptProviderCerts"));
+
+ if (mAcceptMfgCerts) {
+ mIssuerNames = new ArrayList<>();
+ for (XMLNode node : commandNode.getChildren()) {
+ if (node.getStrippedTag().equals("providerIssuerName")) {
+ mIssuerNames.add(node.getAttributeValue("name"));
+ }
+ }
+ } else {
+ mIssuerNames = null;
+ }
+ }
+
+ public boolean doesAcceptMfgCerts() {
+ return mAcceptMfgCerts;
+ }
+
+ public boolean doesAcceptProviderCerts() {
+ return mAcceptProviderCerts;
+ }
+
+ public List<String> getIssuerNames() {
+ return mIssuerNames;
+ }
+
+ @Override
+ public String toString() {
+ return "ClientCertInfo{" +
+ "acceptMfgCerts=" + mAcceptMfgCerts +
+ ", acceptProviderCerts=" + mAcceptProviderCerts +
+ ", issuerNames=" + mIssuerNames +
+ '}';
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/commands/GetCertData.java b/packages/Osu/src/com/android/hotspot2/osu/commands/GetCertData.java
new file mode 100644
index 0000000..60a73fb
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/commands/GetCertData.java
@@ -0,0 +1,75 @@
+package com.android.hotspot2.osu.commands;
+
+import android.util.Base64;
+
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.XMLNode;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/*
+ <env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">
+ <env:Header/>
+ <env:Body>
+ <spp:sppPostDevDataResponse xmlns:spp="http://www.wi-fi.org/specifications/hotspot2dot0/v1.0/spp"
+ spp:sessionID="A40103ACEDE94C45BA127A41239BD60F" spp:sppStatus="OK"
+ spp:sppVersion="1.0">
+ <spp:exec>
+ <spp:getCertificate enrollmentProtocol="EST">
+ <spp:enrollmentServerURI>https://osu-server.r2-testbed-rks.wi-fi.org:9446/.well-known/est
+ </spp:enrollmentServerURI>
+ <spp:estUserID>a88c4830-aafd-420b-b790-c08f457a0fa3</spp:estUserID>
+ <spp:estPassword>cnVja3VzMTIzNA==</spp:estPassword>
+ </spp:getCertificate>
+ </spp:exec>
+ </spp:sppPostDevDataResponse>
+ </env:Body>
+ </env:Envelope>
+ */
+
+public class GetCertData implements OSUCommandData {
+ private final String mProtocol;
+ private final String mServer;
+ private final String mUserName;
+ private final byte[] mPassword;
+
+ public GetCertData(XMLNode commandNode) throws OMAException {
+ mProtocol = commandNode.getAttributeValue("enrollmentProtocol");
+
+ Map<String, String> values = new HashMap<>(3);
+ for (XMLNode node : commandNode.getChildren()) {
+ values.put(node.getStrippedTag(), node.getText());
+ }
+
+ mServer = values.get("enrollmentserveruri");
+ mUserName = values.get("estuserid");
+ mPassword = Base64.decode(values.get("estpassword"), Base64.DEFAULT);
+ }
+
+ public String getProtocol() {
+ return mProtocol;
+ }
+
+ public String getServer() {
+ return mServer;
+ }
+
+ public String getUserName() {
+ return mUserName;
+ }
+
+ public byte[] getPassword() {
+ return mPassword;
+ }
+
+ @Override
+ public String toString() {
+ return "GetCertData " +
+ "protocol='" + mProtocol + '\'' +
+ ", server='" + mServer + '\'' +
+ ", userName='" + mUserName + '\'' +
+ ", password='" + new String(mPassword, StandardCharsets.ISO_8859_1) + '\'';
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/commands/MOData.java b/packages/Osu/src/com/android/hotspot2/osu/commands/MOData.java
new file mode 100644
index 0000000..758c0cb
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/commands/MOData.java
@@ -0,0 +1,51 @@
+package com.android.hotspot2.osu.commands;
+
+import android.net.wifi.PasspointManagementObjectDefinition;
+
+import com.android.hotspot2.omadm.MOTree;
+import com.android.hotspot2.omadm.OMAConstants;
+import com.android.hotspot2.omadm.OMAParser;
+import com.android.hotspot2.omadm.XMLNode;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+
+public class MOData implements OSUCommandData {
+ private final String mBaseURI;
+ private final String mURN;
+ private final MOTree mMOTree;
+
+ public MOData(XMLNode root) {
+ mBaseURI = root.getAttributeValue("spp:managementTreeURI");
+ mURN = root.getAttributeValue("spp:moURN");
+ mMOTree = root.getMOTree();
+ }
+
+ public MOData(PasspointManagementObjectDefinition moDef) throws IOException, SAXException {
+ mBaseURI = ""; //moDef.getmBaseUri();
+ mURN = ""; // moDef.getmUrn();
+ /*
+ OMAParser omaParser = new OMAParser();
+ mMOTree = omaParser.parse(moDef.getmMoTree(), OMAConstants.PPS_URN);
+ */
+ mMOTree = null;
+ }
+
+ public String getBaseURI() {
+ return mBaseURI;
+ }
+
+ public String getURN() {
+ return mURN;
+ }
+
+ public MOTree getMOTree() {
+ return mMOTree;
+ }
+
+ @Override
+ public String toString() {
+ return "Base URI: " + mBaseURI + ", MO: " + mMOTree;
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/commands/MOURN.java b/packages/Osu/src/com/android/hotspot2/osu/commands/MOURN.java
new file mode 100644
index 0000000..46394ef
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/commands/MOURN.java
@@ -0,0 +1,33 @@
+package com.android.hotspot2.osu.commands;
+
+/*
+<xsd:element name="uploadMO" maxOccurs="unbounded">
+ <xsd:annotation>
+ <xsd:documentation>Command to mobile to upload the MO named in the moURN attribute to the SPP server.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:attribute ref="moURN"/>
+ </xsd:complexType>
+</xsd:element>
+ */
+
+import com.android.hotspot2.omadm.XMLNode;
+
+public class MOURN implements OSUCommandData {
+ private final String mURN;
+
+ public MOURN(XMLNode root) {
+ mURN = root.getAttributeValue("spp:moURN");
+ }
+
+ public String getURN() {
+ return mURN;
+ }
+
+ @Override
+ public String toString() {
+ return "MOURN{" +
+ "URN='" + mURN + '\'' +
+ '}';
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/commands/OSUCommandData.java b/packages/Osu/src/com/android/hotspot2/osu/commands/OSUCommandData.java
new file mode 100644
index 0000000..06f81bf
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/commands/OSUCommandData.java
@@ -0,0 +1,7 @@
+package com.android.hotspot2.osu.commands;
+
+/**
+ * Marker interface
+ */
+public interface OSUCommandData {
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/service/RedirectListener.java b/packages/Osu/src/com/android/hotspot2/osu/service/RedirectListener.java
new file mode 100644
index 0000000..ce26afd
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/service/RedirectListener.java
@@ -0,0 +1,202 @@
+package com.android.hotspot2.osu.service;
+
+import android.util.Log;
+
+import com.android.hotspot2.osu.OSUManager;
+import com.android.hotspot2.osu.OSUOperationStatus;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Random;
+
+public class RedirectListener extends Thread {
+ private static final long ThreadTimeout = 3000L;
+ private static final long UserTimeout = 3600000L;
+ private static final int MaxRetry = 5;
+ private static final String TAG = "OSULSN";
+
+ private static final String HTTPResponseHeader =
+ "HTTP/1.1 304 Not Modified\r\n" +
+ "Server: dummy\r\n" +
+ "Keep-Alive: timeout=500, max=5\r\n\r\n";
+
+ private static final String GoodBye =
+ "<html>" +
+ "<head><title>Goodbye</title></head>" +
+ "<body>" +
+ "<h3>Killing browser...</h3>" +
+ "</body>" +
+ "</html>\r\n";
+
+ private final OSUManager mOSUManager;
+ private final String mSpName;
+ private final ServerSocket mServerSocket;
+ private final String mPath;
+ private final URL mURL;
+ private final Object mLock = new Object();
+
+ private boolean mListening;
+ private OSUOperationStatus mUserStatus;
+ private volatile boolean mAborted;
+
+ public RedirectListener(OSUManager osuManager, String spName) throws IOException {
+ mOSUManager = osuManager;
+ mSpName = spName;
+ mServerSocket = new ServerSocket(0, 5, InetAddress.getLocalHost());
+ Random rnd = new Random(System.currentTimeMillis());
+ mPath = "rnd" + Integer.toString(Math.abs(rnd.nextInt()), Character.MAX_RADIX);
+ mURL = new URL("http", mServerSocket.getInetAddress().getHostAddress(),
+ mServerSocket.getLocalPort(), mPath);
+
+ Log.d(TAG, "Redirect URL: " + mURL);
+ setName("HS20-Redirect-Listener");
+ setDaemon(true);
+ }
+
+ public void startService() throws IOException {
+ start();
+ synchronized (mLock) {
+ long bail = System.currentTimeMillis() + ThreadTimeout;
+ long remainder = ThreadTimeout;
+ while (remainder > 0 && !mListening) {
+ try {
+ mLock.wait(remainder);
+ } catch (InterruptedException ie) {
+ /**/
+ }
+ if (mListening) {
+ break;
+ }
+ remainder = bail - System.currentTimeMillis();
+ }
+ if (!mListening) {
+ throw new IOException("Failed to start listener");
+ } else {
+ Log.d(TAG, "OSU Redirect listener running");
+ }
+ }
+ }
+
+ public boolean waitForUser() {
+ boolean success;
+ synchronized (mLock) {
+ long bail = System.currentTimeMillis() + UserTimeout;
+ long remainder = UserTimeout;
+ while (remainder > 0 && mUserStatus == null) {
+ try {
+ mLock.wait(remainder);
+ } catch (InterruptedException ie) {
+ /**/
+ }
+ if (mUserStatus != null) {
+ break;
+ }
+ remainder = bail - System.currentTimeMillis();
+ }
+ success = mUserStatus == OSUOperationStatus.UserInputComplete;
+ }
+ abort();
+ return success;
+ }
+
+ public void abort() {
+ try {
+ mAborted = true;
+ mServerSocket.close();
+ } catch (IOException ioe) {
+ /**/
+ }
+ }
+
+ public URL getURL() {
+ return mURL;
+ }
+
+ @Override
+ public void run() {
+ int count = 0;
+ synchronized (mLock) {
+ mListening = true;
+ mLock.notifyAll();
+ }
+
+ boolean terminate = false;
+
+ for (; ; ) {
+ count++;
+ try (Socket instance = mServerSocket.accept()) {
+ try (BufferedReader in = new BufferedReader(
+ new InputStreamReader(instance.getInputStream(), StandardCharsets.UTF_8))) {
+ boolean detected = false;
+ StringBuilder sb = new StringBuilder();
+ String s;
+ while ((s = in.readLine()) != null) {
+ sb.append(s).append('\n');
+ if (!detected && s.startsWith("GET")) {
+ String[] segments = s.split(" ");
+ if (segments.length == 3 &&
+ segments[2].startsWith("HTTP/") &&
+ segments[1].regionMatches(1, mPath, 0, mPath.length())) {
+ detected = true;
+ }
+ }
+ if (s.length() == 0) {
+ break;
+ }
+ }
+ Log.d(TAG, "Redirect receive: " + sb);
+ String response = null;
+ if (detected) {
+ response = status(OSUOperationStatus.UserInputComplete);
+ if (response == null) {
+ response = GoodBye;
+ terminate = true;
+ }
+ }
+ try (BufferedWriter out = new BufferedWriter(
+ new OutputStreamWriter(instance.getOutputStream(),
+ StandardCharsets.UTF_8))) {
+
+ out.write(HTTPResponseHeader);
+ if (response != null) {
+ out.write(response);
+ }
+ }
+ if (terminate) {
+ break;
+ } else if (count > MaxRetry) {
+ status(OSUOperationStatus.UserInputAborted);
+ break;
+ }
+ }
+ } catch (IOException ioe) {
+ if (mAborted) {
+ return;
+ } else if (count > MaxRetry) {
+ status(OSUOperationStatus.UserInputAborted);
+ break;
+ }
+ }
+ }
+ }
+
+ private String status(OSUOperationStatus status) {
+ Log.d(TAG, "User input status: " + status);
+ synchronized (mLock) {
+ mUserStatus = status;
+ mLock.notifyAll();
+ }
+ String message = (status == OSUOperationStatus.UserInputAborted) ?
+ "Browser closed" : null;
+
+ return mOSUManager.notifyUser(status, message, mSpName);
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/osu/service/SubscriptionTimer.java b/packages/Osu/src/com/android/hotspot2/osu/service/SubscriptionTimer.java
new file mode 100644
index 0000000..783d14b
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/osu/service/SubscriptionTimer.java
@@ -0,0 +1,108 @@
+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<HomeSP, UpdateAction> 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<HomeSP> 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/Credential.java b/packages/Osu/src/com/android/hotspot2/pps/Credential.java
new file mode 100644
index 0000000..15f0dcf
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/pps/Credential.java
@@ -0,0 +1,252 @@
+package com.android.hotspot2.pps;
+
+import android.text.TextUtils;
+import android.util.Base64;
+
+import com.android.anqp.eap.EAPMethod;
+import com.android.hotspot2.IMSIParameter;
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.omadm.OMAException;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+public class Credential {
+ public enum CertType {IEEE, x509v3}
+
+ public static final String CertTypeX509 = "x509v3";
+ public static final String CertTypeIEEE = "802.1ar";
+
+ private final long mCtime;
+ private final long mExpTime;
+ private final String mRealm;
+ private final boolean mCheckAAACert;
+
+ private final String mUserName;
+ private final String mPassword;
+ private final boolean mDisregardPassword;
+ private final boolean mMachineManaged;
+ private final String mSTokenApp;
+ private final boolean mShare;
+ private final EAPMethod mEAPMethod;
+
+ private final CertType mCertType;
+ private final byte[] mFingerPrint;
+
+ private final IMSIParameter mImsi;
+
+ public Credential(long ctime, long expTime, String realm, boolean checkAAACert,
+ EAPMethod eapMethod, String userName, String password,
+ boolean machineManaged, String stApp, boolean share) {
+ mCtime = ctime;
+ mExpTime = expTime;
+ mRealm = realm;
+ mCheckAAACert = checkAAACert;
+ mEAPMethod = eapMethod;
+ mUserName = userName;
+
+ if (!TextUtils.isEmpty(password)) {
+ byte[] pwOctets = Base64.decode(password, Base64.DEFAULT);
+ mPassword = new String(pwOctets, StandardCharsets.UTF_8);
+ } else {
+ mPassword = null;
+ }
+ mDisregardPassword = false;
+
+ mMachineManaged = machineManaged;
+ mSTokenApp = stApp;
+ mShare = share;
+
+ mCertType = null;
+ mFingerPrint = null;
+
+ mImsi = null;
+ }
+
+ public Credential(long ctime, long expTime, String realm, boolean checkAAACert,
+ EAPMethod eapMethod, Credential.CertType certType, byte[] fingerPrint) {
+ mCtime = ctime;
+ mExpTime = expTime;
+ mRealm = realm;
+ mCheckAAACert = checkAAACert;
+ mEAPMethod = eapMethod;
+ mCertType = certType;
+ mFingerPrint = fingerPrint;
+
+ mUserName = null;
+ mPassword = null;
+ mDisregardPassword = false;
+ mMachineManaged = false;
+ mSTokenApp = null;
+ mShare = false;
+
+ mImsi = null;
+ }
+
+ public Credential(long ctime, long expTime, String realm, boolean checkAAACert,
+ EAPMethod eapMethod, IMSIParameter imsi) {
+ mCtime = ctime;
+ mExpTime = expTime;
+ mRealm = realm;
+ mCheckAAACert = checkAAACert;
+ mEAPMethod = eapMethod;
+ mImsi = imsi;
+
+ mCertType = null;
+ mFingerPrint = null;
+
+ mUserName = null;
+ mPassword = null;
+ mDisregardPassword = false;
+ mMachineManaged = false;
+ mSTokenApp = null;
+ mShare = false;
+ }
+
+ public Credential(Credential other, String password) {
+ mCtime = other.mCtime;
+ mExpTime = other.mExpTime;
+ mRealm = other.mRealm;
+ mCheckAAACert = other.mCheckAAACert;
+ mUserName = other.mUserName;
+ mPassword = password;
+ mDisregardPassword = other.mDisregardPassword;
+ mMachineManaged = other.mMachineManaged;
+ mSTokenApp = other.mSTokenApp;
+ mShare = other.mShare;
+ mEAPMethod = other.mEAPMethod;
+ mCertType = other.mCertType;
+ mFingerPrint = other.mFingerPrint;
+ mImsi = other.mImsi;
+ }
+
+ public static CertType mapCertType(String certType) throws OMAException {
+ if (certType.equalsIgnoreCase(CertTypeX509)) {
+ return CertType.x509v3;
+ } else if (certType.equalsIgnoreCase(CertTypeIEEE)) {
+ return CertType.IEEE;
+ } else {
+ throw new OMAException("Invalid cert type: '" + certType + "'");
+ }
+ }
+
+ public EAPMethod getEAPMethod() {
+ return mEAPMethod;
+ }
+
+ public String getRealm() {
+ return mRealm;
+ }
+
+ public IMSIParameter getImsi() {
+ return mImsi;
+ }
+
+ public String getUserName() {
+ return mUserName;
+ }
+
+ public String getPassword() {
+ return mPassword;
+ }
+
+ public boolean hasDisregardPassword() {
+ return mDisregardPassword;
+ }
+
+ public CertType getCertType() {
+ return mCertType;
+ }
+
+ public byte[] getFingerPrint() {
+ return mFingerPrint;
+ }
+
+ public long getCtime() {
+ return mCtime;
+ }
+
+ public long getExpTime() {
+ return mExpTime;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Credential that = (Credential) o;
+
+ if (mCheckAAACert != that.mCheckAAACert) return false;
+ if (mCtime != that.mCtime) return false;
+ if (mExpTime != that.mExpTime) return false;
+ if (mMachineManaged != that.mMachineManaged) return false;
+ if (mShare != that.mShare) return false;
+ if (mCertType != that.mCertType) return false;
+ if (!mEAPMethod.equals(that.mEAPMethod)) return false;
+ if (!Arrays.equals(mFingerPrint, that.mFingerPrint)) return false;
+ if (!safeEquals(mImsi, that.mImsi)) {
+ return false;
+ }
+
+ if (!mDisregardPassword && !safeEquals(mPassword, that.mPassword)) {
+ return false;
+ }
+
+ if (!mRealm.equals(that.mRealm)) return false;
+ if (!safeEquals(mSTokenApp, that.mSTokenApp)) {
+ return false;
+ }
+ if (!safeEquals(mUserName, that.mUserName)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean safeEquals(Object s1, Object s2) {
+ if (s1 == null) {
+ return s2 == null;
+ } else {
+ return s2 != null && s1.equals(s2);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (mCtime ^ (mCtime >>> 32));
+ result = 31 * result + (int) (mExpTime ^ (mExpTime >>> 32));
+ result = 31 * result + mRealm.hashCode();
+ result = 31 * result + (mCheckAAACert ? 1 : 0);
+ result = 31 * result + (mUserName != null ? mUserName.hashCode() : 0);
+ result = 31 * result + (mPassword != null ? mPassword.hashCode() : 0);
+ result = 31 * result + (mMachineManaged ? 1 : 0);
+ result = 31 * result + (mSTokenApp != null ? mSTokenApp.hashCode() : 0);
+ result = 31 * result + (mShare ? 1 : 0);
+ result = 31 * result + mEAPMethod.hashCode();
+ result = 31 * result + (mCertType != null ? mCertType.hashCode() : 0);
+ result = 31 * result + (mFingerPrint != null ? Arrays.hashCode(mFingerPrint) : 0);
+ result = 31 * result + (mImsi != null ? mImsi.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Credential{" +
+ "mCtime=" + Utils.toUTCString(mCtime) +
+ ", mExpTime=" + Utils.toUTCString(mExpTime) +
+ ", mRealm='" + mRealm + '\'' +
+ ", mCheckAAACert=" + mCheckAAACert +
+ ", mUserName='" + mUserName + '\'' +
+ ", mPassword='" + mPassword + '\'' +
+ ", mDisregardPassword=" + mDisregardPassword +
+ ", mMachineManaged=" + mMachineManaged +
+ ", mSTokenApp='" + mSTokenApp + '\'' +
+ ", mShare=" + mShare +
+ ", mEAPMethod=" + mEAPMethod +
+ ", mCertType=" + mCertType +
+ ", mFingerPrint=" + Utils.toHexString(mFingerPrint) +
+ ", mImsi='" + mImsi + '\'' +
+ '}';
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/pps/DomainMatcher.java b/packages/Osu/src/com/android/hotspot2/pps/DomainMatcher.java
new file mode 100644
index 0000000..10768d6
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/pps/DomainMatcher.java
@@ -0,0 +1,149 @@
+package com.android.hotspot2.pps;
+
+import com.android.hotspot2.Utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class DomainMatcher {
+
+ public enum Match {None, Primary, Secondary}
+
+ private final Label mRoot;
+
+ private static class Label {
+ private final Map<String, Label> mSubDomains;
+ private final Match mMatch;
+
+ private Label(Match match) {
+ mMatch = match;
+ mSubDomains = match == Match.None ? new HashMap<String, Label>() : null;
+ }
+
+ private void addDomain(Iterator<String> labels, Match match) {
+ String labelName = labels.next();
+ if (labels.hasNext()) {
+ Label subLabel = new Label(Match.None);
+ mSubDomains.put(labelName, subLabel);
+ subLabel.addDomain(labels, match);
+ } else {
+ mSubDomains.put(labelName, new Label(match));
+ }
+ }
+
+ private Label getSubLabel(String labelString) {
+ return mSubDomains.get(labelString);
+ }
+
+ public Match getMatch() {
+ return mMatch;
+ }
+
+ private void toString(StringBuilder sb) {
+ if (mSubDomains != null) {
+ sb.append(".{");
+ for (Map.Entry<String, Label> entry : mSubDomains.entrySet()) {
+ sb.append(entry.getKey());
+ entry.getValue().toString(sb);
+ }
+ sb.append('}');
+ } else {
+ sb.append('=').append(mMatch);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ toString(sb);
+ return sb.toString();
+ }
+ }
+
+ public DomainMatcher(List<String> primary, List<List<String>> secondary) {
+ mRoot = new Label(Match.None);
+ for (List<String> secondaryLabel : secondary) {
+ mRoot.addDomain(secondaryLabel.iterator(), Match.Secondary);
+ }
+ // Primary overwrites secondary.
+ mRoot.addDomain(primary.iterator(), Match.Primary);
+ }
+
+ /**
+ * Check if domain is either a the same or a sub-domain of any of the domains in the domain tree
+ * in this matcher, i.e. all or or a sub-set of the labels in domain matches a path in the tree.
+ *
+ * @param domain Domain to be checked.
+ * @return None if domain is not a sub-domain, Primary if it matched one of the primary domains
+ * or Secondary if it matched on of the secondary domains.
+ */
+ public Match isSubDomain(List<String> domain) {
+
+ Label label = mRoot;
+ for (String labelString : domain) {
+ label = label.getSubLabel(labelString);
+ if (label == null) {
+ return Match.None;
+ } else if (label.getMatch() != Match.None) {
+ return label.getMatch();
+ }
+ }
+ return Match.None; // Domain is a super domain
+ }
+
+ public static boolean arg2SubdomainOfArg1(List<String> arg1, List<String> arg2) {
+ if (arg2.size() < arg1.size()) {
+ return false;
+ }
+
+ Iterator<String> l1 = arg1.iterator();
+ Iterator<String> l2 = arg2.iterator();
+
+ while (l1.hasNext()) {
+ if (!l1.next().equals(l2.next())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "Domain matcher " + mRoot;
+ }
+
+ private static final String[] TestDomains = {
+ "garbage.apple.com",
+ "apple.com",
+ "com",
+ "jan.android.google.com.",
+ "jan.android.google.com",
+ "android.google.com",
+ "google.com",
+ "jan.android.google.net.",
+ "jan.android.google.net",
+ "android.google.net",
+ "google.net",
+ "net.",
+ "."
+ };
+
+ public static void main(String[] args) {
+ DomainMatcher dm1 = new DomainMatcher(Utils.splitDomain("android.google.com"),
+ Collections.<List<String>>emptyList());
+ for (String domain : TestDomains) {
+ System.out.println(domain + ": " + dm1.isSubDomain(Utils.splitDomain(domain)));
+ }
+ List<List<String>> secondaries = new ArrayList<List<String>>();
+ secondaries.add(Utils.splitDomain("apple.com"));
+ secondaries.add(Utils.splitDomain("net"));
+ DomainMatcher dm2 = new DomainMatcher(Utils.splitDomain("android.google.com"), secondaries);
+ for (String domain : TestDomains) {
+ System.out.println(domain + ": " + dm2.isSubDomain(Utils.splitDomain(domain)));
+ }
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/pps/HomeSP.java b/packages/Osu/src/com/android/hotspot2/pps/HomeSP.java
new file mode 100644
index 0000000..cfbf9d1
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/pps/HomeSP.java
@@ -0,0 +1,211 @@
+package com.android.hotspot2.pps;
+
+import com.android.hotspot2.Utils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class HomeSP {
+ private final Map<String, Long> mSSIDs; // SSID, HESSID, [0,N]
+ private final String mFQDN;
+ private final DomainMatcher mDomainMatcher;
+ private final Set<String> mOtherHomePartners;
+ private final HashSet<Long> mRoamingConsortiums; // [0,N]
+ private final Set<Long> mMatchAnyOIs; // [0,N]
+ private final List<Long> mMatchAllOIs; // [0,N]
+
+ private final Credential mCredential;
+
+ // Informational:
+ private final String mFriendlyName; // [1]
+ private final String mIconURL; // [0,1]
+
+ private final Policy mPolicy;
+ private final int mCredentialPriority;
+ private final Map<String, String> mAAATrustRoots;
+ private final UpdateInfo mSubscriptionUpdate;
+ private final SubscriptionParameters mSubscriptionParameters;
+ private final int mUpdateIdentifier;
+
+ @Deprecated
+ public HomeSP(Map<String, Long> ssidMap,
+ /*@NotNull*/ String fqdn,
+ /*@NotNull*/ HashSet<Long> roamingConsortiums,
+ /*@NotNull*/ Set<String> otherHomePartners,
+ /*@NotNull*/ Set<Long> matchAnyOIs,
+ /*@NotNull*/ List<Long> matchAllOIs,
+ String friendlyName,
+ String iconURL,
+ Credential credential) {
+
+ mSSIDs = ssidMap;
+ List<List<String>> otherPartners = new ArrayList<>(otherHomePartners.size());
+ for (String otherPartner : otherHomePartners) {
+ otherPartners.add(Utils.splitDomain(otherPartner));
+ }
+ mOtherHomePartners = otherHomePartners;
+ mFQDN = fqdn;
+ mDomainMatcher = new DomainMatcher(Utils.splitDomain(fqdn), otherPartners);
+ mRoamingConsortiums = roamingConsortiums;
+ mMatchAnyOIs = matchAnyOIs;
+ mMatchAllOIs = matchAllOIs;
+ mFriendlyName = friendlyName;
+ mIconURL = iconURL;
+ mCredential = credential;
+
+ mPolicy = null;
+ mCredentialPriority = -1;
+ mAAATrustRoots = null;
+ mSubscriptionUpdate = null;
+ mSubscriptionParameters = null;
+ mUpdateIdentifier = -1;
+ }
+
+ public HomeSP(Map<String, Long> ssidMap,
+ /*@NotNull*/ String fqdn,
+ /*@NotNull*/ HashSet<Long> roamingConsortiums,
+ /*@NotNull*/ Set<String> otherHomePartners,
+ /*@NotNull*/ Set<Long> matchAnyOIs,
+ /*@NotNull*/ List<Long> matchAllOIs,
+ String friendlyName,
+ String iconURL,
+ Credential credential,
+
+ Policy policy,
+ int credentialPriority,
+ Map<String, String> AAATrustRoots,
+ UpdateInfo subscriptionUpdate,
+ SubscriptionParameters subscriptionParameters,
+ int updateIdentifier) {
+
+ mSSIDs = ssidMap;
+ List<List<String>> otherPartners = new ArrayList<>(otherHomePartners.size());
+ for (String otherPartner : otherHomePartners) {
+ otherPartners.add(Utils.splitDomain(otherPartner));
+ }
+ mOtherHomePartners = otherHomePartners;
+ mFQDN = fqdn;
+ mDomainMatcher = new DomainMatcher(Utils.splitDomain(fqdn), otherPartners);
+ mRoamingConsortiums = roamingConsortiums;
+ mMatchAnyOIs = matchAnyOIs;
+ mMatchAllOIs = matchAllOIs;
+ mFriendlyName = friendlyName;
+ mIconURL = iconURL;
+ mCredential = credential;
+
+ mPolicy = policy;
+ mCredentialPriority = credentialPriority;
+ mAAATrustRoots = AAATrustRoots;
+ mSubscriptionUpdate = subscriptionUpdate;
+ mSubscriptionParameters = subscriptionParameters;
+ mUpdateIdentifier = updateIdentifier;
+ }
+
+ public int getUpdateIdentifier() {
+ return mUpdateIdentifier;
+ }
+
+ public UpdateInfo getSubscriptionUpdate() {
+ return mSubscriptionUpdate;
+ }
+
+ public Policy getPolicy() {
+ return mPolicy;
+ }
+
+ private String imsiMatch(List<String> imsis, String mccMnc) {
+ if (mCredential.getImsi().matchesMccMnc(mccMnc)) {
+ for (String imsi : imsis) {
+ if (imsi.startsWith(mccMnc)) {
+ return imsi;
+ }
+ }
+ }
+ return null;
+ }
+
+ public String getFQDN() {
+ return mFQDN;
+ }
+
+ public String getFriendlyName() {
+ return mFriendlyName;
+ }
+
+ public HashSet<Long> getRoamingConsortiums() {
+ return mRoamingConsortiums;
+ }
+
+ public Credential getCredential() {
+ return mCredential;
+ }
+
+ public Map<String, Long> getSSIDs() {
+ return mSSIDs;
+ }
+
+ public Collection<String> getOtherHomePartners() {
+ return mOtherHomePartners;
+ }
+
+ public Set<Long> getMatchAnyOIs() {
+ return mMatchAnyOIs;
+ }
+
+ public List<Long> getMatchAllOIs() {
+ return mMatchAllOIs;
+ }
+
+ public String getIconURL() {
+ return mIconURL;
+ }
+
+ public boolean deepEquals(HomeSP other) {
+ return mFQDN.equals(other.mFQDN) &&
+ mSSIDs.equals(other.mSSIDs) &&
+ mOtherHomePartners.equals(other.mOtherHomePartners) &&
+ mRoamingConsortiums.equals(other.mRoamingConsortiums) &&
+ mMatchAnyOIs.equals(other.mMatchAnyOIs) &&
+ mMatchAllOIs.equals(other.mMatchAllOIs) &&
+ mFriendlyName.equals(other.mFriendlyName) &&
+ Utils.compare(mIconURL, other.mIconURL) == 0 &&
+ mCredential.equals(other.mCredential);
+ }
+
+ @Override
+ public boolean equals(Object thatObject) {
+ if (this == thatObject) {
+ return true;
+ } else if (thatObject == null || getClass() != thatObject.getClass()) {
+ return false;
+ }
+
+ HomeSP that = (HomeSP) thatObject;
+ return mFQDN.equals(that.mFQDN);
+ }
+
+ @Override
+ public int hashCode() {
+ return mFQDN.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "HomeSP{" +
+ "SSIDs=" + mSSIDs +
+ ", FQDN='" + mFQDN + '\'' +
+ ", DomainMatcher=" + mDomainMatcher +
+ ", RoamingConsortiums={" + Utils.roamingConsortiumsToString(mRoamingConsortiums) +
+ '}' +
+ ", MatchAnyOIs={" + Utils.roamingConsortiumsToString(mMatchAnyOIs) + '}' +
+ ", MatchAllOIs={" + Utils.roamingConsortiumsToString(mMatchAllOIs) + '}' +
+ ", Credential=" + mCredential +
+ ", FriendlyName='" + mFriendlyName + '\'' +
+ ", IconURL='" + mIconURL + '\'' +
+ '}';
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/pps/Policy.java b/packages/Osu/src/com/android/hotspot2/pps/Policy.java
new file mode 100644
index 0000000..5180436
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/pps/Policy.java
@@ -0,0 +1,195 @@
+package com.android.hotspot2.pps;
+
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.omadm.MOManager;
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.OMANode;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.android.hotspot2.omadm.MOManager.TAG_Country;
+import static com.android.hotspot2.omadm.MOManager.TAG_DLBandwidth;
+import static com.android.hotspot2.omadm.MOManager.TAG_FQDN_Match;
+import static com.android.hotspot2.omadm.MOManager.TAG_IPProtocol;
+import static com.android.hotspot2.omadm.MOManager.TAG_MaximumBSSLoadValue;
+import static com.android.hotspot2.omadm.MOManager.TAG_MinBackhaulThreshold;
+import static com.android.hotspot2.omadm.MOManager.TAG_NetworkType;
+import static com.android.hotspot2.omadm.MOManager.TAG_PolicyUpdate;
+import static com.android.hotspot2.omadm.MOManager.TAG_PortNumber;
+import static com.android.hotspot2.omadm.MOManager.TAG_PreferredRoamingPartnerList;
+import static com.android.hotspot2.omadm.MOManager.TAG_Priority;
+import static com.android.hotspot2.omadm.MOManager.TAG_RequiredProtoPortTuple;
+import static com.android.hotspot2.omadm.MOManager.TAG_SPExclusionList;
+import static com.android.hotspot2.omadm.MOManager.TAG_SSID;
+import static com.android.hotspot2.omadm.MOManager.TAG_ULBandwidth;
+
+public class Policy {
+ private final List<PreferredRoamingPartner> mPreferredRoamingPartners;
+ private final List<MinBackhaul> mMinBackhaulThresholds;
+ private final UpdateInfo mPolicyUpdate;
+ private final List<String> mSPExclusionList;
+ private final Map<Integer, List<Integer>> mRequiredProtos;
+ private final int mMaxBSSLoad;
+
+ public Policy(OMANode node) throws OMAException {
+
+ OMANode rpNode = node.getChild(TAG_PreferredRoamingPartnerList);
+ if (rpNode == null) {
+ mPreferredRoamingPartners = null;
+ } else {
+ mPreferredRoamingPartners = new ArrayList<>(rpNode.getChildren().size());
+ for (OMANode instance : rpNode.getChildren()) {
+ if (instance.isLeaf()) {
+ throw new OMAException("Not expecting leaf node in " +
+ TAG_PreferredRoamingPartnerList);
+ }
+ mPreferredRoamingPartners.add(new PreferredRoamingPartner(instance));
+ }
+ }
+
+ OMANode bhtNode = node.getChild(TAG_MinBackhaulThreshold);
+ if (bhtNode == null) {
+ mMinBackhaulThresholds = null;
+ } else {
+ mMinBackhaulThresholds = new ArrayList<>(bhtNode.getChildren().size());
+ for (OMANode instance : bhtNode.getChildren()) {
+ if (instance.isLeaf()) {
+ throw new OMAException("Not expecting leaf node in " +
+ TAG_MinBackhaulThreshold);
+ }
+ mMinBackhaulThresholds.add(new MinBackhaul(instance));
+ }
+ }
+
+ mPolicyUpdate = new UpdateInfo(node.getChild(TAG_PolicyUpdate));
+
+ OMANode sxNode = node.getChild(TAG_SPExclusionList);
+ if (sxNode == null) {
+ mSPExclusionList = null;
+ } else {
+ mSPExclusionList = new ArrayList<>(sxNode.getChildren().size());
+ for (OMANode instance : sxNode.getChildren()) {
+ if (instance.isLeaf()) {
+ throw new OMAException("Not expecting leaf node in " + TAG_SPExclusionList);
+ }
+ mSPExclusionList.add(MOManager.getString(instance, TAG_SSID));
+ }
+ }
+
+ OMANode rptNode = node.getChild(TAG_RequiredProtoPortTuple);
+ if (rptNode == null) {
+ mRequiredProtos = null;
+ } else {
+ mRequiredProtos = new HashMap<>(rptNode.getChildren().size());
+ for (OMANode instance : rptNode.getChildren()) {
+ if (instance.isLeaf()) {
+ throw new OMAException("Not expecting leaf node in " +
+ TAG_RequiredProtoPortTuple);
+ }
+ int protocol = (int) MOManager.getLong(instance, TAG_IPProtocol, null);
+ String[] portSegments = MOManager.getString(instance, TAG_PortNumber).split(",");
+ List<Integer> ports = new ArrayList<>(portSegments.length);
+ for (String portSegment : portSegments) {
+ try {
+ ports.add(Integer.parseInt(portSegment));
+ } catch (NumberFormatException nfe) {
+ throw new OMAException("Port is not a number: " + portSegment);
+ }
+ }
+ mRequiredProtos.put(protocol, ports);
+ }
+ }
+
+ mMaxBSSLoad = (int) MOManager.getLong(node, TAG_MaximumBSSLoadValue, Long.MAX_VALUE);
+ }
+
+ public List<PreferredRoamingPartner> getPreferredRoamingPartners() {
+ return mPreferredRoamingPartners;
+ }
+
+ public List<MinBackhaul> getMinBackhaulThresholds() {
+ return mMinBackhaulThresholds;
+ }
+
+ public UpdateInfo getPolicyUpdate() {
+ return mPolicyUpdate;
+ }
+
+ public List<String> getSPExclusionList() {
+ return mSPExclusionList;
+ }
+
+ public Map<Integer, List<Integer>> getRequiredProtos() {
+ return mRequiredProtos;
+ }
+
+ public int getMaxBSSLoad() {
+ return mMaxBSSLoad;
+ }
+
+ private static class PreferredRoamingPartner {
+ private final List<String> mDomain;
+ private final Boolean mIncludeSubDomains;
+ private final int mPriority;
+ private final String mCountry;
+
+ private PreferredRoamingPartner(OMANode node)
+ throws OMAException {
+
+ String[] segments = MOManager.getString(node, TAG_FQDN_Match).split(",");
+ if (segments.length != 2) {
+ throw new OMAException("Bad FQDN match string: " + TAG_FQDN_Match);
+ }
+ mDomain = Utils.splitDomain(segments[0]);
+ mIncludeSubDomains = MOManager.getSelection(TAG_FQDN_Match, segments[1]);
+ mPriority = (int) MOManager.getLong(node, TAG_Priority, null);
+ mCountry = MOManager.getString(node, TAG_Country);
+ }
+
+ @Override
+ public String toString() {
+ return "PreferredRoamingPartner{" +
+ "domain=" + mDomain +
+ ", includeSubDomains=" + mIncludeSubDomains +
+ ", priority=" + mPriority +
+ ", country='" + mCountry + '\'' +
+ '}';
+ }
+ }
+
+ private static class MinBackhaul {
+ private final Boolean mHome;
+ private final long mDL;
+ private final long mUL;
+
+ private MinBackhaul(OMANode node) throws OMAException {
+ mHome = MOManager.getSelection(node, TAG_NetworkType);
+ mDL = MOManager.getLong(node, TAG_DLBandwidth, Long.MAX_VALUE);
+ mUL = MOManager.getLong(node, TAG_ULBandwidth, Long.MAX_VALUE);
+ }
+
+ @Override
+ public String toString() {
+ return "MinBackhaul{" +
+ "home=" + mHome +
+ ", DL=" + mDL +
+ ", UL=" + mUL +
+ '}';
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Policy{" +
+ "preferredRoamingPartners=" + mPreferredRoamingPartners +
+ ", minBackhaulThresholds=" + mMinBackhaulThresholds +
+ ", policyUpdate=" + mPolicyUpdate +
+ ", SPExclusionList=" + mSPExclusionList +
+ ", requiredProtos=" + mRequiredProtos +
+ ", maxBSSLoad=" + mMaxBSSLoad +
+ '}';
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/pps/SubscriptionParameters.java b/packages/Osu/src/com/android/hotspot2/pps/SubscriptionParameters.java
new file mode 100644
index 0000000..e073ad7
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/pps/SubscriptionParameters.java
@@ -0,0 +1,81 @@
+package com.android.hotspot2.pps;
+
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.omadm.MOManager;
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.OMANode;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.android.hotspot2.omadm.MOManager.TAG_CreationDate;
+import static com.android.hotspot2.omadm.MOManager.TAG_DataLimit;
+import static com.android.hotspot2.omadm.MOManager.TAG_ExpirationDate;
+import static com.android.hotspot2.omadm.MOManager.TAG_StartDate;
+import static com.android.hotspot2.omadm.MOManager.TAG_TimeLimit;
+import static com.android.hotspot2.omadm.MOManager.TAG_TypeOfSubscription;
+import static com.android.hotspot2.omadm.MOManager.TAG_UsageLimits;
+import static com.android.hotspot2.omadm.MOManager.TAG_UsageTimePeriod;
+
+public class SubscriptionParameters {
+ private final long mCDate;
+ private final long mXDate;
+ private final String mType;
+ private final List<Limit> mLimits;
+
+ public SubscriptionParameters(OMANode node) throws OMAException {
+ mCDate = MOManager.getTime(node.getChild(TAG_CreationDate));
+ mXDate = MOManager.getTime(node.getChild(TAG_ExpirationDate));
+ mType = MOManager.getString(node.getChild(TAG_TypeOfSubscription));
+
+ OMANode ulNode = node.getChild(TAG_UsageLimits);
+ if (ulNode == null) {
+ mLimits = null;
+ } else {
+ mLimits = new ArrayList<>(ulNode.getChildren().size());
+ for (OMANode instance : ulNode.getChildren()) {
+ if (instance.isLeaf()) {
+ throw new OMAException("Not expecting leaf node in " +
+ TAG_UsageLimits);
+ }
+ mLimits.add(new Limit(instance));
+ }
+ }
+
+ }
+
+ private static class Limit {
+ private final long mDataLimit;
+ private final long mStartDate;
+ private final long mTimeLimit;
+ private final long mUsageTimePeriod;
+
+ private Limit(OMANode node) throws OMAException {
+ mDataLimit = MOManager.getLong(node, TAG_DataLimit, Long.MAX_VALUE);
+ mStartDate = MOManager.getTime(node.getChild(TAG_StartDate));
+ mTimeLimit = MOManager.getLong(node, TAG_TimeLimit, Long.MAX_VALUE) *
+ MOManager.IntervalFactor;
+ mUsageTimePeriod = MOManager.getLong(node, TAG_UsageTimePeriod, null);
+ }
+
+ @Override
+ public String toString() {
+ return "Limit{" +
+ "dataLimit=" + mDataLimit +
+ ", startDate=" + Utils.toUTCString(mStartDate) +
+ ", timeLimit=" + mTimeLimit +
+ ", usageTimePeriod=" + mUsageTimePeriod +
+ '}';
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "SubscriptionParameters{" +
+ "cDate=" + Utils.toUTCString(mCDate) +
+ ", xDate=" + Utils.toUTCString(mXDate) +
+ ", type='" + mType + '\'' +
+ ", limits=" + mLimits +
+ '}';
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/pps/UpdateInfo.java b/packages/Osu/src/com/android/hotspot2/pps/UpdateInfo.java
new file mode 100644
index 0000000..e32f6c3
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/pps/UpdateInfo.java
@@ -0,0 +1,103 @@
+package com.android.hotspot2.pps;
+
+import android.util.Base64;
+
+import com.android.hotspot2.Utils;
+import com.android.hotspot2.omadm.MOManager;
+import com.android.hotspot2.omadm.OMAException;
+import com.android.hotspot2.omadm.OMANode;
+
+import java.nio.charset.StandardCharsets;
+
+import static com.android.hotspot2.omadm.MOManager.TAG_CertSHA256Fingerprint;
+import static com.android.hotspot2.omadm.MOManager.TAG_CertURL;
+import static com.android.hotspot2.omadm.MOManager.TAG_Password;
+import static com.android.hotspot2.omadm.MOManager.TAG_Restriction;
+import static com.android.hotspot2.omadm.MOManager.TAG_TrustRoot;
+import static com.android.hotspot2.omadm.MOManager.TAG_URI;
+import static com.android.hotspot2.omadm.MOManager.TAG_UpdateInterval;
+import static com.android.hotspot2.omadm.MOManager.TAG_UpdateMethod;
+import static com.android.hotspot2.omadm.MOManager.TAG_Username;
+import static com.android.hotspot2.omadm.MOManager.TAG_UsernamePassword;
+
+public class UpdateInfo {
+ public enum UpdateRestriction {HomeSP, RoamingPartner, Unrestricted}
+
+ private final long mInterval;
+ private final boolean mSPPClientInitiated;
+ private final UpdateRestriction mUpdateRestriction;
+ private final String mURI;
+ private final String mUsername;
+ private final String mPassword;
+ private final String mCertURL;
+ private final String mCertFP;
+
+ public UpdateInfo(OMANode policyUpdate) throws OMAException {
+ mInterval = MOManager.getLong(policyUpdate, TAG_UpdateInterval, null) *
+ MOManager.IntervalFactor;
+ mSPPClientInitiated = MOManager.getSelection(policyUpdate, TAG_UpdateMethod);
+ mUpdateRestriction = MOManager.getSelection(policyUpdate, TAG_Restriction);
+ mURI = MOManager.getString(policyUpdate, TAG_URI);
+
+ OMANode unp = policyUpdate.getChild(TAG_UsernamePassword);
+ if (unp != null) {
+ mUsername = MOManager.getString(unp.getChild(TAG_Username));
+ String pw = MOManager.getString(unp.getChild(TAG_Password));
+ mPassword = new String(Base64.decode(pw.getBytes(StandardCharsets.US_ASCII),
+ Base64.DEFAULT), StandardCharsets.UTF_8);
+ } else {
+ mUsername = null;
+ mPassword = null;
+ }
+
+ OMANode trustRoot = MOManager.getChild(policyUpdate, TAG_TrustRoot);
+ mCertURL = MOManager.getString(trustRoot, TAG_CertURL);
+ mCertFP = MOManager.getString(trustRoot, TAG_CertSHA256Fingerprint);
+ }
+
+ public long getInterval() {
+ return mInterval;
+ }
+
+ public boolean isSPPClientInitiated() {
+ return mSPPClientInitiated;
+ }
+
+ public UpdateRestriction getUpdateRestriction() {
+ return mUpdateRestriction;
+ }
+
+ public String getURI() {
+ return mURI;
+ }
+
+ public String getUsername() {
+ return mUsername;
+ }
+
+ public String getPassword() {
+ return mPassword;
+ }
+
+ public String getCertURL() {
+ return mCertURL;
+ }
+
+ public String getCertFP() {
+ return mCertFP;
+ }
+
+ @Override
+ public String toString() {
+ return "UpdateInfo{" +
+ "interval=" + Utils.toHMS(mInterval) +
+ ", SPPClientInitiated=" + mSPPClientInitiated +
+ ", updateRestriction=" + mUpdateRestriction +
+ ", URI='" + mURI + '\'' +
+ ", username='" + mUsername + '\'' +
+ ", password=" + mPassword +
+ ", certURL='" + mCertURL + '\'' +
+ ", certFP='" + mCertFP + '\'' +
+ '}';
+ }
+}
diff --git a/packages/Osu/src/com/android/hotspot2/utils/HTTPMessage.java b/packages/Osu/src/com/android/hotspot2/utils/HTTPMessage.java
new file mode 100644
index 0000000..c675efd
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/utils/HTTPMessage.java
@@ -0,0 +1,36 @@
+package com.android.hotspot2.utils;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Map;
+
+public interface HTTPMessage {
+ public static final String HTTPVersion = "HTTP/1.1";
+ public static final String AgentHeader = "User-Agent";
+ public static final String AgentName = "Android HS Client";
+ public static final String HostHeader = "Host";
+ public static final String AcceptHeader = "Accept";
+ public static final String LengthHeader = "Content-Length";
+ public static final String ContentTypeHeader = "Content-Type";
+ public static final String ContentLengthHeader = "Content-Length";
+ public static final String ContentEncodingHeader = "Content-Transfer-Encoding";
+ public static final String AuthHeader = "WWW-Authenticate";
+ public static final String AuthorizationHeader = "Authorization";
+
+ public static final String ContentTypeSOAP = "application/soap+xml";
+
+ public static final int RX_BUFFER = 32768;
+ public static final String CRLF = "\r\n";
+ public static final int BODY_SEPARATOR = 0x0d0a0d0a;
+ public static final int BODY_SEPARATOR_LENGTH = 4;
+
+ public enum Method {GET, PUT, POST}
+
+ public Map<String, String> getHeaders();
+
+ public InputStream getPayloadStream();
+
+ public ByteBuffer getPayload();
+
+ public ByteBuffer getBinaryPayload();
+}
diff --git a/packages/Osu/src/com/android/hotspot2/utils/HTTPRequest.java b/packages/Osu/src/com/android/hotspot2/utils/HTTPRequest.java
new file mode 100644
index 0000000..e97c15a
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/utils/HTTPRequest.java
@@ -0,0 +1,307 @@
+package com.android.hotspot2.utils;
+
+import android.util.Base64;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class HTTPRequest implements HTTPMessage {
+ private static final Charset HeaderCharset = StandardCharsets.US_ASCII;
+ private static final int HTTPS_PORT = 443;
+
+ private final String mMethodLine;
+ private final Map<String, String> mHeaderFields;
+ private final byte[] mBody;
+
+ public HTTPRequest(Method method, URL url) {
+ this(null, null, method, url, null, false);
+ }
+
+ public HTTPRequest(String payload, Charset charset, Method method, URL url, String contentType,
+ boolean base64) {
+ mBody = payload != null ? payload.getBytes(charset) : null;
+
+ mHeaderFields = new LinkedHashMap<>();
+ mHeaderFields.put(AgentHeader, AgentName);
+ if (url.getPort() != HTTPS_PORT) {
+ mHeaderFields.put(HostHeader, url.getHost() + ':' + url.getPort());
+ } else {
+ mHeaderFields.put(HostHeader, url.getHost());
+ }
+ mHeaderFields.put(AcceptHeader, "*/*");
+ if (payload != null) {
+ if (base64) {
+ mHeaderFields.put(ContentTypeHeader, contentType);
+ mHeaderFields.put(ContentEncodingHeader, "base64");
+ } else {
+ mHeaderFields.put(ContentTypeHeader, contentType + "; charset=" +
+ charset.displayName().toLowerCase());
+ }
+ mHeaderFields.put(ContentLengthHeader, Integer.toString(mBody.length));
+ }
+
+ mMethodLine = method.name() + ' ' + url.getPath() + ' ' + HTTPVersion + CRLF;
+ }
+
+ public void doAuthenticate(HTTPResponse httpResponse, String userName, byte[] password,
+ URL url, int sequence) throws IOException, GeneralSecurityException {
+ mHeaderFields.put(HTTPMessage.AuthorizationHeader,
+ generateAuthAnswer(httpResponse, userName, password, url, sequence));
+ }
+
+ private static String generateAuthAnswer(HTTPResponse httpResponse, String userName,
+ byte[] password, URL url, int sequence)
+ throws IOException, GeneralSecurityException {
+
+ String authRequestLine = httpResponse.getHeader(HTTPMessage.AuthHeader);
+ if (authRequestLine == null) {
+ throw new IOException("Missing auth line");
+ }
+ String[] tokens = authRequestLine.split("[ ,]+");
+ //System.out.println("Tokens: " + Arrays.toString(tokens));
+ if (tokens.length < 3 || !tokens[0].equalsIgnoreCase("digest")) {
+ throw new IOException("Bad " + HTTPMessage.AuthHeader + ": '" + authRequestLine + "'");
+ }
+
+ Map<String, String> itemMap = new HashMap<>();
+ for (int n = 1; n < tokens.length; n++) {
+ String s = tokens[n];
+ int split = s.indexOf('=');
+ if (split < 0) {
+ continue;
+ }
+ itemMap.put(s.substring(0, split).trim().toLowerCase(),
+ unquote(s.substring(split + 1).trim()));
+ }
+
+ Set<String> qops = splitValue(itemMap.remove("qop"));
+ if (!qops.contains("auth")) {
+ throw new IOException("Unsupported quality of protection value(s): '" + qops + "'");
+ }
+ String algorithm = itemMap.remove("algorithm");
+ if (algorithm != null && !algorithm.equalsIgnoreCase("md5")) {
+ throw new IOException("Unsupported algorithm: '" + algorithm + "'");
+ }
+ String realm = itemMap.remove("realm");
+ String nonceText = itemMap.remove("nonce");
+ if (realm == null || nonceText == null) {
+ throw new IOException("realm and/or nonce missing: '" + authRequestLine + "'");
+ }
+ //System.out.println("Remaining tokens: " + itemMap);
+
+ byte[] cnonce = new byte[16];
+ SecureRandom prng = new SecureRandom();
+ prng.nextBytes(cnonce);
+
+ /*
+ * H(data) = MD5(data)
+ * KD(secret, data) = H(concat(secret, ":", data))
+ *
+ * A1 = unq(username-value) ":" unq(realm-value) ":" passwd
+ * A2 = Method ":" digest-uri-value
+ *
+ * response = KD ( H(A1), unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":"
+ * unq(qop-value) ":" H(A2) )
+ */
+
+ String nc = String.format("%08d", sequence);
+
+ /*
+ * This bears witness to the ingenuity of the emerging "web generation" and the authors of
+ * RFC-2617: Strings are treated as a sequence of octets in blind ignorance of character
+ * encoding, whereas octets strings apparently aren't "good enough" and expanded to
+ * "hex strings"...
+ * As a wild guess I apply UTF-8 below.
+ */
+ String passwordString = new String(password, StandardCharsets.UTF_8);
+ String cNonceString = bytesToHex(cnonce);
+
+ byte[] a1 = hash(userName, realm, passwordString);
+ byte[] a2 = hash("POST", url.getPath());
+ byte[] response = hash(a1, nonceText, nc, cNonceString, "auth", a2);
+
+ StringBuilder authLine = new StringBuilder();
+ authLine.append("Digest ")
+ .append("username=\"").append(userName).append("\", ")
+ .append("realm=\"").append(realm).append("\", ")
+ .append("nonce=\"").append(nonceText).append("\", ")
+ .append("uri=\"").append(url.getPath()).append("\", ")
+ .append("qop=\"auth\", ")
+ .append("nc=").append(nc).append(", ")
+ .append("cnonce=\"").append(cNonceString).append("\", ")
+ .append("response=\"").append(bytesToHex(response)).append('"');
+ String opaque = itemMap.get("opaque");
+ if (opaque != null) {
+ authLine.append(", \"").append(opaque).append('"');
+ }
+
+ return authLine.toString();
+ }
+
+ private static Set<String> splitValue(String value) {
+ Set<String> result = new HashSet<>();
+ if (value != null) {
+ for (String s : value.split(",")) {
+ result.add(s.trim());
+ }
+ }
+ return result;
+ }
+
+ private static byte[] hash(Object... objects) throws GeneralSecurityException {
+ MessageDigest hash = MessageDigest.getInstance("MD5");
+
+ //System.out.println("<Hash>");
+ boolean first = true;
+ for (Object object : objects) {
+ byte[] octets;
+ if (object.getClass() == String.class) {
+ //System.out.println("+= '" + object + "'");
+ octets = ((String) object).getBytes(StandardCharsets.UTF_8);
+ } else {
+ octets = bytesToHexBytes((byte[]) object);
+ //System.out.println("+= " + new String(octets, StandardCharsets.ISO_8859_1));
+ }
+ if (first) {
+ first = false;
+ } else {
+ hash.update((byte) ':');
+ }
+ hash.update(octets);
+ }
+ //System.out.println("</Hash>");
+ return hash.digest();
+ }
+
+ private static String unquote(String s) {
+ return s.startsWith("\"") ? s.substring(1, s.length() - 1) : s;
+ }
+
+ private static byte[] bytesToHexBytes(byte[] octets) {
+ return bytesToHex(octets).getBytes(StandardCharsets.ISO_8859_1);
+ }
+
+ private static String bytesToHex(byte[] octets) {
+ StringBuilder sb = new StringBuilder(octets.length * 2);
+ for (byte b : octets) {
+ sb.append(String.format("%02x", b & 0xff));
+ }
+ return sb.toString();
+ }
+
+ private byte[] buildHeader() {
+ StringBuilder header = new StringBuilder();
+ header.append(mMethodLine);
+ for (Map.Entry<String, String> entry : mHeaderFields.entrySet()) {
+ header.append(entry.getKey()).append(": ").append(entry.getValue()).append(CRLF);
+ }
+ header.append(CRLF);
+
+ //System.out.println("HTTP Request:");
+ StringBuilder sb2 = new StringBuilder();
+ sb2.append(header);
+ if (mBody != null) {
+ sb2.append(new String(mBody, StandardCharsets.ISO_8859_1));
+ }
+ //System.out.println(sb2);
+ //System.out.println("End HTTP Request.");
+
+ return header.toString().getBytes(HeaderCharset);
+ }
+
+ public void send(OutputStream out) throws IOException {
+ out.write(buildHeader());
+ if (mBody != null) {
+ out.write(mBody);
+ }
+ out.flush();
+ }
+
+ @Override
+ public Map<String, String> getHeaders() {
+ return Collections.unmodifiableMap(mHeaderFields);
+ }
+
+ @Override
+ public InputStream getPayloadStream() {
+ return mBody != null ? new ByteArrayInputStream(mBody) : null;
+ }
+
+ @Override
+ public ByteBuffer getPayload() {
+ return mBody != null ? ByteBuffer.wrap(mBody) : null;
+ }
+
+ @Override
+ public ByteBuffer getBinaryPayload() {
+ byte[] binary = Base64.decode(mBody, Base64.DEFAULT);
+ return ByteBuffer.wrap(binary);
+ }
+
+ public static void main(String[] args) throws GeneralSecurityException {
+ test("Mufasa", "testrealm@host.com", "Circle Of Life", "GET", "/dir/index.html",
+ "dcd98b7102dd2f0e8b11d0f600bfb0c093", "0a4f113b", "00000001", "auth",
+ "6629fae49393a05397450978507c4ef1");
+
+ // WWW-Authenticate: Digest realm="wi-fi.org", qop="auth",
+ // nonce="MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw=="
+ // Authorization: Digest
+ // username="1c7e1582-604d-4c00-b411-bb73735cbcb0"
+ // realm="wi-fi.org"
+ // nonce="MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw=="
+ // uri="/.well-known/est/simpleenroll"
+ // cnonce="NzA3NDk0"
+ // nc=00000001
+ // qop="auth"
+ // response="2c485d24076452e712b77f4e70776463"
+
+ String nonce = "MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw==";
+ String cnonce = "NzA3NDk0";
+ test("1c7e1582-604d-4c00-b411-bb73735cbcb0", "wi-fi.org", "ruckus1234", "POST",
+ "/.well-known/est/simpleenroll",
+ /*new String(Base64.getDecoder().decode(nonce), StandardCharsets.ISO_8859_1)*/
+ nonce,
+ /*new String(Base64.getDecoder().decode(cnonce), StandardCharsets.ISO_8859_1)*/
+ cnonce, "00000001", "auth", "2c485d24076452e712b77f4e70776463");
+ }
+
+ private static void test(String user, String realm, String password, String method, String path,
+ String nonce, String cnonce, String nc, String qop, String expect)
+ throws GeneralSecurityException {
+ byte[] a1 = hash(user, realm, password);
+ System.out.println("HA1: " + bytesToHex(a1));
+ byte[] a2 = hash(method, path);
+ System.out.println("HA2: " + bytesToHex(a2));
+ byte[] response = hash(a1, nonce, nc, cnonce, qop, a2);
+
+ StringBuilder authLine = new StringBuilder();
+ String responseString = bytesToHex(response);
+ authLine.append("Digest ")
+ .append("username=\"").append(user).append("\", ")
+ .append("realm=\"").append(realm).append("\", ")
+ .append("nonce=\"").append(nonce).append("\", ")
+ .append("uri=\"").append(path).append("\", ")
+ .append("qop=\"").append(qop).append("\", ")
+ .append("nc=").append(nc).append(", ")
+ .append("cnonce=\"").append(cnonce).append("\", ")
+ .append("response=\"").append(responseString).append('"');
+
+ System.out.println(authLine);
+ System.out.println("Success: " + responseString.equals(expect));
+ }
+}
\ No newline at end of file
diff --git a/packages/Osu/src/com/android/hotspot2/utils/HTTPResponse.java b/packages/Osu/src/com/android/hotspot2/utils/HTTPResponse.java
new file mode 100644
index 0000000..ba1b1671
--- /dev/null
+++ b/packages/Osu/src/com/android/hotspot2/utils/HTTPResponse.java
@@ -0,0 +1,185 @@
+package com.android.hotspot2.utils;
+
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.hotspot2.osu.OSUManager;
+
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class HTTPResponse implements HTTPMessage {
+ private final int mStatusCode;
+ private final Map<String, String> mHeaders = new LinkedHashMap<>();
+ private final ByteBuffer mBody;
+
+ private static final String csIndicator = "charset=";
+
+ public HTTPResponse(InputStream in) throws IOException {
+ int expected = Integer.MAX_VALUE;
+ int offset = 0;
+ int body = -1;
+ byte[] input = new byte[RX_BUFFER];
+
+ int statusCode = -1;
+ int bodyPattern = 0;
+
+ 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++) {
+ bodyPattern = (bodyPattern << 8) | (input[n] & 0xff);
+ if (bodyPattern == 0x0d0a0d0a) {
+ body = n + 1;
+ statusCode = parseHeader(input, body, mHeaders);
+ expected = calculateLength(body, mHeaders);
+ if (expected > input.length) {
+ input = Arrays.copyOf(input, expected);
+ }
+ break;
+ }
+ }
+ }
+ offset += amount;
+ if (offset < expected && offset == input.length) {
+ input = Arrays.copyOf(input, input.length * 2);
+ }
+ }
+ mStatusCode = statusCode;
+ mBody = ByteBuffer.wrap(input, body, expected - body);
+ }
+
+ private static int parseHeader(byte[] input, int body, Map<String, String> headers)
+ throws IOException {
+ String headerText = new String(input, 0, body - BODY_SEPARATOR_LENGTH,
+ StandardCharsets.ISO_8859_1);
+ //System.out.println("Received header: " + headerText);
+ Iterator<String> headerLines = Arrays.asList(headerText.split(CRLF)).iterator();
+ if (!headerLines.hasNext()) {
+ throw new IOException("Bad HTTP Request");
+ }
+
+ int statusCode;
+ String line0 = headerLines.next();
+ String[] status = line0.split(" ");
+ if (status.length != 3 || !"HTTP/1.1".equals(status[0])) {
+ throw new IOException("Bad HTTP Result: " + line0);
+ }
+ try {
+ statusCode = Integer.parseInt(status[1].trim());
+ } catch (NumberFormatException nfe) {
+ throw new IOException("Bad HTTP header line: '" + line0 + "'");
+ }
+
+ while (headerLines.hasNext()) {
+ String line = headerLines.next();
+ int keyEnd = line.indexOf(':');
+ if (keyEnd < 0) {
+ throw new IOException("Bad header line: '" + line + "'");
+ }
+ String key = line.substring(0, keyEnd).trim();
+ String value = line.substring(keyEnd + 1).trim();
+ headers.put(key, value);
+ }
+ return statusCode;
+ }
+
+ private static int calculateLength(int body, Map<String, String> headers) throws IOException {
+ String contentLength = headers.get(LengthHeader);
+ if (contentLength == null) {
+ throw new IOException("No " + LengthHeader);
+ }
+ try {
+ return body + Integer.parseInt(contentLength);
+ } catch (NumberFormatException nfe) {
+ throw new IOException("Bad " + LengthHeader + ": " + contentLength);
+ }
+ }
+
+ public int getStatusCode() {
+ return mStatusCode;
+ }
+
+ @Override
+ public Map<String, String> getHeaders() {
+ return Collections.unmodifiableMap(mHeaders);
+ }
+
+ public String getHeader(String key) {
+ return mHeaders.get(key);
+ }
+
+ @Override
+ public InputStream getPayloadStream() {
+ return new ByteArrayInputStream(mBody.array(), mBody.position(),
+ mBody.limit() - mBody.position());
+ }
+
+ @Override
+ public ByteBuffer getPayload() {
+ return mBody.duplicate();
+ }
+
+ @Override
+ public ByteBuffer getBinaryPayload() {
+ byte[] data = new byte[mBody.remaining()];
+ mBody.duplicate().get(data);
+ byte[] binary = Base64.decode(data, Base64.DEFAULT);
+ return ByteBuffer.wrap(binary);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Status: ").append(mStatusCode).append(CRLF);
+ for (Map.Entry<String, String> entry : mHeaders.entrySet()) {
+ sb.append(entry.getKey()).append(": ").append(entry.getValue()).append(CRLF);
+ }
+ sb.append(CRLF);
+ Charset charset;
+ try {
+ charset = Charset.forName(getCharset());
+ } catch (IllegalArgumentException iae) {
+ charset = StandardCharsets.ISO_8859_1;
+ }
+ sb.append(new String(mBody.array(), mBody.position(),
+ mBody.limit() - mBody.position(), charset));
+ return sb.toString();
+ }
+
+ public String getCharset() {
+ String contentType = mHeaders.get(ContentTypeHeader);
+ if (contentType == null) {
+ return null;
+ }
+ int csPos = contentType.indexOf(csIndicator);
+ return csPos < 0 ? null : contentType.substring(csPos + csIndicator.length()).trim();
+ }
+
+ private static boolean equals(byte[] b1, int offset, byte[] pattern) {
+ for (int n = 0; n < pattern.length; n++) {
+ if (b1[n + offset] != pattern[n]) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/services/core/java/com/android/server/LockSettingsService.java b/services/core/java/com/android/server/LockSettingsService.java
index 81607a9..ecba0a4 100644
--- a/services/core/java/com/android/server/LockSettingsService.java
+++ b/services/core/java/com/android/server/LockSettingsService.java
@@ -762,39 +762,24 @@
}
VerifyCredentialResponse response;
- boolean shouldReEnroll = false;;
- if (hasChallenge) {
- byte[] token = null;
- GateKeeperResponse gateKeeperResponse = getGateKeeperService()
- .verifyChallenge(userId, challenge, storedHash.hash, credential.getBytes());
- int responseCode = gateKeeperResponse.getResponseCode();
- if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
- response = new VerifyCredentialResponse(gateKeeperResponse.getTimeout());
- } else if (responseCode == GateKeeperResponse.RESPONSE_OK) {
- token = gateKeeperResponse.getPayload();
- if (token == null) {
- // something's wrong if there's no payload with a challenge
- Slog.e(TAG, "verifyChallenge response had no associated payload");
- response = VerifyCredentialResponse.ERROR;
- } else {
- shouldReEnroll = gateKeeperResponse.getShouldReEnroll();
- response = new VerifyCredentialResponse(token);
- }
- } else {
+ boolean shouldReEnroll = false;
+ GateKeeperResponse gateKeeperResponse = getGateKeeperService()
+ .verifyChallenge(userId, challenge, storedHash.hash, credential.getBytes());
+ int responseCode = gateKeeperResponse.getResponseCode();
+ if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
+ response = new VerifyCredentialResponse(gateKeeperResponse.getTimeout());
+ } else if (responseCode == GateKeeperResponse.RESPONSE_OK) {
+ byte[] token = gateKeeperResponse.getPayload();
+ if (token == null) {
+ // something's wrong if there's no payload with a challenge
+ Slog.e(TAG, "verifyChallenge response had no associated payload");
response = VerifyCredentialResponse.ERROR;
+ } else {
+ shouldReEnroll = gateKeeperResponse.getShouldReEnroll();
+ response = new VerifyCredentialResponse(token);
}
} else {
- GateKeeperResponse gateKeeperResponse = getGateKeeperService().verify(
- userId, storedHash.hash, credential.getBytes());
- int responseCode = gateKeeperResponse.getResponseCode();
- if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
- response = new VerifyCredentialResponse(gateKeeperResponse.getTimeout());
- } else if (responseCode == GateKeeperResponse.RESPONSE_OK) {
- shouldReEnroll = gateKeeperResponse.getShouldReEnroll();
- response = VerifyCredentialResponse.OK;
- } else {
- response = VerifyCredentialResponse.ERROR;
- }
+ response = VerifyCredentialResponse.ERROR;
}
if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index c021f4c..7aac9d7 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1304,6 +1304,7 @@
int mMemWatchDumpPid;
int mMemWatchDumpUid;
String mTrackAllocationApp = null;
+ String mNativeDebuggingApp = null;
final long[] mTmpLong = new long[2];
@@ -3485,6 +3486,13 @@
if ("1".equals(SystemProperties.get("debug.assert"))) {
debugFlags |= Zygote.DEBUG_ENABLE_ASSERT;
}
+ if (mNativeDebuggingApp != null && mNativeDebuggingApp.equals(app.processName)) {
+ // Enable all debug flags required by the native debugger.
+ debugFlags |= Zygote.DEBUG_ALWAYS_JIT; // Don't interpret anything
+ debugFlags |= Zygote.DEBUG_GENERATE_DEBUG_INFO; // Generate debug info
+ debugFlags |= Zygote.DEBUG_NATIVE_DEBUGGABLE; // Disbale optimizations
+ mNativeDebuggingApp = null;
+ }
String requiredAbi = (abiOverride != null) ? abiOverride : app.info.primaryCpuAbi;
if (requiredAbi == null) {
@@ -11284,6 +11292,16 @@
}
}
+ void setNativeDebuggingAppLocked(ApplicationInfo app, String processName) {
+ boolean isDebuggable = "1".equals(SystemProperties.get(SYSTEM_DEBUGGABLE, "0"));
+ if (!isDebuggable) {
+ if ((app.flags & ApplicationInfo.FLAG_DEBUGGABLE) == 0) {
+ throw new SecurityException("Process not debuggable: " + app.packageName);
+ }
+ }
+ mNativeDebuggingApp = processName;
+ }
+
@Override
public void setAlwaysFinish(boolean enabled) {
enforceCallingPermission(android.Manifest.permission.SET_ALWAYS_FINISH,
@@ -13977,6 +13995,15 @@
pw.println(" mProfileType=" + mProfileType);
}
}
+ if (mNativeDebuggingApp != null) {
+ if (dumpPackage == null || dumpPackage.equals(mNativeDebuggingApp)) {
+ if (needSep) {
+ pw.println();
+ needSep = false;
+ }
+ pw.println(" mNativeDebuggingApp=" + mNativeDebuggingApp);
+ }
+ }
if (dumpPackage == null) {
if (mAlwaysFinishActivities || mController != null) {
pw.println(" mAlwaysFinishActivities=" + mAlwaysFinishActivities
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 0b2ff65..f53e71a 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -1028,6 +1028,10 @@
mService.setDebugApp(aInfo.processName, true, false);
}
+ if ((startFlags & ActivityManager.START_FLAG_NATIVE_DEBUGGING) != 0) {
+ mService.setNativeDebuggingAppLocked(aInfo.applicationInfo, aInfo.processName);
+ }
+
if ((startFlags & ActivityManager.START_FLAG_TRACK_ALLOCATION) != 0) {
mService.setTrackAllocationApp(aInfo.applicationInfo, aInfo.processName);
}
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 97ef10b..28882de 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -81,12 +81,6 @@
Context mContext;
PowerManagerInternal mPowerManagerInternal;
- final int UPDATE_CPU = 0x01;
- final int UPDATE_WIFI = 0x02;
- final int UPDATE_RADIO = 0x04;
- final int UPDATE_BT = 0x08;
- final int UPDATE_ALL = UPDATE_CPU | UPDATE_WIFI | UPDATE_RADIO | UPDATE_BT;
-
class BatteryStatsHandler extends Handler implements BatteryStatsImpl.ExternalStatsSync {
public static final int MSG_SYNC_EXTERNAL_STATS = 1;
public static final int MSG_WRITE_TO_DISK = 2;
@@ -133,16 +127,9 @@
}
@Override
- public void scheduleSync(String reason) {
+ public void scheduleSync(String reason, int updateFlags) {
synchronized (this) {
- scheduleSyncLocked(reason, UPDATE_ALL);
- }
- }
-
- @Override
- public void scheduleWifiSync(String reason) {
- synchronized (this) {
- scheduleSyncLocked(reason, UPDATE_WIFI);
+ scheduleSyncLocked(reason, updateFlags);
}
}
@@ -194,7 +181,7 @@
public void shutdown() {
Slog.w("BatteryStats", "Writing battery stats before shutdown...");
- updateExternalStats("shutdown", UPDATE_ALL);
+ updateExternalStats("shutdown", BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL);
synchronized (mStats) {
mStats.shutdownLocked();
}
@@ -294,7 +281,7 @@
//Slog.i("foo", "SENDING BATTERY INFO:");
//mStats.dumpLocked(new LogPrinter(Log.INFO, "foo", Log.LOG_ID_SYSTEM));
Parcel out = Parcel.obtain();
- updateExternalStats("get-stats", UPDATE_ALL);
+ updateExternalStats("get-stats", BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL);
synchronized (mStats) {
mStats.writeToParcel(out, 0);
}
@@ -309,7 +296,7 @@
//Slog.i("foo", "SENDING BATTERY INFO:");
//mStats.dumpLocked(new LogPrinter(Log.INFO, "foo", Log.LOG_ID_SYSTEM));
Parcel out = Parcel.obtain();
- updateExternalStats("get-stats", UPDATE_ALL);
+ updateExternalStats("get-stats", BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL);
synchronized (mStats) {
mStats.writeToParcel(out, 0);
}
@@ -672,7 +659,8 @@
final String type = (powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH ||
powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_MEDIUM) ? "active"
: "inactive";
- mHandler.scheduleWifiSync("wifi-data: " + type);
+ mHandler.scheduleSync("wifi-data: " + type,
+ BatteryStatsImpl.ExternalStatsSync.UPDATE_WIFI);
}
mStats.noteWifiRadioPowerState(powerState, tsNanos);
}
@@ -860,13 +848,25 @@
@Override
public void noteBleScanStarted(WorkSource ws) {
enforceCallingPermission();
- Slog.d(TAG, "BLE scan started for " + ws);
+ synchronized (mStats) {
+ mStats.noteBluetoothScanStartedFromSourceLocked(ws);
+ }
}
@Override
public void noteBleScanStopped(WorkSource ws) {
enforceCallingPermission();
- Slog.d(TAG, "BLE scan stopped for " + ws);
+ synchronized (mStats) {
+ mStats.noteBluetoothScanStoppedFromSourceLocked(ws);
+ }
+ }
+
+ @Override
+ public void noteResetBleScan() {
+ enforceCallingPermission();
+ synchronized (mStats) {
+ mStats.noteResetBluetoothScanLocked();
+ }
}
public boolean isOnBattery() {
@@ -895,7 +895,7 @@
// Sync external stats first as the battery has changed states. If we don't sync
// immediately here, we may not collect the relevant data later.
- updateExternalStats("battery-state", UPDATE_ALL);
+ updateExternalStats("battery-state", BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL);
synchronized (mStats) {
mStats.setBatteryStateLocked(status, health, plugType, level, temp, volt);
}
@@ -1082,9 +1082,9 @@
pw.println("Battery stats reset.");
noOutput = true;
}
- updateExternalStats("dump", UPDATE_ALL);
+ updateExternalStats("dump", BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL);
} else if ("--write".equals(arg)) {
- updateExternalStats("dump", UPDATE_ALL);
+ updateExternalStats("dump", BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL);
synchronized (mStats) {
mStats.writeSyncLocked();
pw.println("Battery stats written.");
@@ -1148,7 +1148,7 @@
flags |= BatteryStats.DUMP_DEVICE_WIFI_ONLY;
}
// Fetch data from external sources and update the BatteryStatsImpl object with them.
- updateExternalStats("dump", UPDATE_ALL);
+ updateExternalStats("dump", BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -1358,8 +1358,10 @@
*
* @param reason The reason why this collection was requested. Useful for debugging.
* @param updateFlags Which external stats to update. Can be a combination of
- * {@link #UPDATE_CPU}, {@link #UPDATE_RADIO}, {@link #UPDATE_WIFI},
- * and {@link #UPDATE_BT}.
+ * {@link BatteryStatsImpl.ExternalStatsSync#UPDATE_CPU},
+ * {@link BatteryStatsImpl.ExternalStatsSync#UPDATE_RADIO},
+ * {@link BatteryStatsImpl.ExternalStatsSync#UPDATE_WIFI},
+ * and {@link BatteryStatsImpl.ExternalStatsSync#UPDATE_BT}.
*/
void updateExternalStats(final String reason, final int updateFlags) {
synchronized (mExternalStatsLock) {
@@ -1374,17 +1376,17 @@
}
WifiActivityEnergyInfo wifiEnergyInfo = null;
- if ((updateFlags & UPDATE_WIFI) != 0) {
+ if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_WIFI) != 0) {
wifiEnergyInfo = pullWifiEnergyInfoLocked();
}
ModemActivityInfo modemActivityInfo = null;
- if ((updateFlags & UPDATE_RADIO) != 0) {
+ if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_RADIO) != 0) {
modemActivityInfo = pullModemActivityInfoLocked();
}
BluetoothActivityEnergyInfo bluetoothEnergyInfo = null;
- if ((updateFlags & UPDATE_BT) != 0) {
+ if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_BT) != 0) {
// We only pull bluetooth stats when we have to, as we are not distributing its
// use amongst apps and the sampling frequency does not matter.
bluetoothEnergyInfo = pullBluetoothEnergyInfoLocked();
@@ -1398,20 +1400,20 @@
BatteryStats.HistoryItem.EVENT_COLLECT_EXTERNAL_STATS, reason, 0);
}
- if ((updateFlags & UPDATE_CPU) != 0) {
+ if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_CPU) != 0) {
mStats.updateCpuTimeLocked();
mStats.updateKernelWakelocksLocked();
}
- if ((updateFlags & UPDATE_RADIO) != 0) {
+ if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_RADIO) != 0) {
mStats.updateMobileRadioStateLocked(elapsedRealtime, modemActivityInfo);
}
- if ((updateFlags & UPDATE_WIFI) != 0) {
+ if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_WIFI) != 0) {
mStats.updateWifiStateLocked(wifiEnergyInfo);
}
- if ((updateFlags & UPDATE_BT) != 0) {
+ if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_BT) != 0) {
mStats.updateBluetoothStateLocked(bluetoothEnergyInfo);
}
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 2f916d9..880514c 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1260,8 +1260,11 @@
* it may obscure windows behind it.
*/
boolean isOpaqueDrawn() {
- return (mAttrs.format == PixelFormat.OPAQUE
- || mAttrs.type == TYPE_WALLPAPER)
+ // When there is keyguard, wallpaper could be placed over the secure app
+ // window but invisible. We need to check wallpaper visibility explicitly
+ // to determine if it's occluding apps.
+ return ((!mIsWallpaper && mAttrs.format == PixelFormat.OPAQUE)
+ || (mIsWallpaper && mWallpaperVisible))
&& isDrawnLw() && mWinAnimator.mAnimation == null
&& (mAppToken == null || mAppToken.mAppAnimator.animation == null);
}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
index 87c5ba0..849262e 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
@@ -17,7 +17,6 @@
package com.android.server.usage;
import android.app.usage.TimeSparseArray;
-import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.os.Build;
import android.util.AtomicFile;
@@ -45,7 +44,7 @@
private static final int CURRENT_VERSION = 3;
// Current version of the backup schema
- static final int BACKUP_STATE_VERSION = 1;
+ static final int BACKUP_VERSION = 1;
// Key under which the payload blob is stored
// same as UsageStatsBackupHelper.KEY_USAGE_STATS
@@ -536,6 +535,7 @@
* Update the stats in the database. They may not be written to disk immediately.
*/
public void putUsageStats(int intervalType, IntervalStats stats) throws IOException {
+ if (stats == null) return;
synchronized (mLock) {
if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
throw new IllegalArgumentException("Bad interval type " + intervalType);
@@ -555,36 +555,44 @@
/* Backup/Restore Code */
- protected byte[] getBackupPayload(String key){
+ byte[] getBackupPayload(String key) {
synchronized (mLock) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if (KEY_USAGE_STATS.equals(key)) {
prune(System.currentTimeMillis());
DataOutputStream out = new DataOutputStream(baos);
try {
- out.writeInt(BACKUP_STATE_VERSION);
+ out.writeInt(BACKUP_VERSION);
out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size());
- for(int i = 0; i<mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size(); i++){
- writeIntervalStatsToStream(out, mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].valueAt(i));
+ for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size();
+ i++) {
+ writeIntervalStatsToStream(out,
+ mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].valueAt(i));
}
out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size());
- for(int i = 0; i<mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size(); i++){
- writeIntervalStatsToStream(out, mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].valueAt(i));
+ for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size();
+ i++) {
+ writeIntervalStatsToStream(out,
+ mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].valueAt(i));
}
out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size());
- for(int i = 0; i<mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size(); i++){
- writeIntervalStatsToStream(out, mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].valueAt(i));
+ for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size();
+ i++) {
+ writeIntervalStatsToStream(out,
+ mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].valueAt(i));
}
out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size());
- for(int i = 0; i<mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size(); i++){
- writeIntervalStatsToStream(out, mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].valueAt(i));
+ for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size();
+ i++) {
+ writeIntervalStatsToStream(out,
+ mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].valueAt(i));
}
if (DEBUG) Slog.i(TAG, "Written " + baos.size() + " bytes of data");
- } catch (IOException ioe){
+ } catch (IOException ioe) {
Slog.d(TAG, "Failed to write data to output stream", ioe);
baos.reset();
}
@@ -594,55 +602,63 @@
}
- protected void applyRestoredPayload(String key, byte[] payload){
+ void applyRestoredPayload(String key, byte[] payload) {
synchronized (mLock) {
if (KEY_USAGE_STATS.equals(key)) {
// Read stats files for the current device configs
- IntervalStats dailyConfigSource = getLatestUsageStats(UsageStatsManager.INTERVAL_DAILY);
- IntervalStats weeklyConfigSource = getLatestUsageStats(UsageStatsManager.INTERVAL_WEEKLY);
- IntervalStats monthlyConfigSource = getLatestUsageStats(UsageStatsManager.INTERVAL_MONTHLY);
- IntervalStats yearlyConfigSource = getLatestUsageStats(UsageStatsManager.INTERVAL_YEARLY);
+ IntervalStats dailyConfigSource =
+ getLatestUsageStats(UsageStatsManager.INTERVAL_DAILY);
+ IntervalStats weeklyConfigSource =
+ getLatestUsageStats(UsageStatsManager.INTERVAL_WEEKLY);
+ IntervalStats monthlyConfigSource =
+ getLatestUsageStats(UsageStatsManager.INTERVAL_MONTHLY);
+ IntervalStats yearlyConfigSource =
+ getLatestUsageStats(UsageStatsManager.INTERVAL_YEARLY);
- // Delete all stats files
- for(int i = 0; i<mIntervalDirs.length; i++){
- deleteDirectoryContents(mIntervalDirs[i]);
- }
try {
DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
- int stateVersion = in.readInt();
+ int backupDataVersion = in.readInt();
+
+ // Can't handle this backup set
+ if (backupDataVersion < 1 || backupDataVersion > BACKUP_VERSION) return;
+
+ // Delete all stats files
+ // Do this after reading version and before actually restoring
+ for (int i = 0; i < mIntervalDirs.length; i++) {
+ deleteDirectoryContents(mIntervalDirs[i]);
+ }
int fileCount = in.readInt();
- for(int i = 0; i<fileCount; i++){
+ for (int i = 0; i < fileCount; i++) {
IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in));
stats = mergeStats(stats, dailyConfigSource);
putUsageStats(UsageStatsManager.INTERVAL_DAILY, stats);
}
fileCount = in.readInt();
- for(int i = 0; i<fileCount; i++){
+ for (int i = 0; i < fileCount; i++) {
IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in));
stats = mergeStats(stats, weeklyConfigSource);
putUsageStats(UsageStatsManager.INTERVAL_WEEKLY, stats);
}
fileCount = in.readInt();
- for(int i = 0; i<fileCount; i++){
+ for (int i = 0; i < fileCount; i++) {
IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in));
stats = mergeStats(stats, monthlyConfigSource);
putUsageStats(UsageStatsManager.INTERVAL_MONTHLY, stats);
}
fileCount = in.readInt();
- for(int i = 0; i<fileCount; i++){
+ for (int i = 0; i < fileCount; i++) {
IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in));
stats = mergeStats(stats, yearlyConfigSource);
putUsageStats(UsageStatsManager.INTERVAL_YEARLY, stats);
}
if (DEBUG) Slog.i(TAG, "Completed Restoring UsageStats");
- } catch (IOException ioe){
+ } catch (IOException ioe) {
Slog.d(TAG, "Failed to read data from input stream", ioe);
- }
- finally {
+ } finally {
indexFilesLocked();
}
}
@@ -654,13 +670,16 @@
* with the backed up usage statistics.
*/
private IntervalStats mergeStats(IntervalStats beingRestored, IntervalStats onDevice) {
+ if (onDevice == null) return beingRestored;
+ if (beingRestored == null) return null;
beingRestored.activeConfiguration = onDevice.activeConfiguration;
beingRestored.configurations.putAll(onDevice.configurations);
beingRestored.events = onDevice.events;
return beingRestored;
}
- private void writeIntervalStatsToStream(DataOutputStream out, AtomicFile statsFile) throws IOException{
+ private void writeIntervalStatsToStream(DataOutputStream out, AtomicFile statsFile)
+ throws IOException {
IntervalStats stats = new IntervalStats();
try {
UsageStatsXml.read(statsFile, stats);
@@ -716,7 +735,7 @@
return stats;
}
- private static void deleteDirectoryContents(File directory){
+ private static void deleteDirectoryContents(File directory) {
File[] files = directory.listFiles();
for (File file : files) {
deleteDirectory(file);
@@ -725,13 +744,15 @@
private static void deleteDirectory(File directory) {
File[] files = directory.listFiles();
- for (File file : files) {
- if (!file.isDirectory()) {
- file.delete();
- } else {
- deleteDirectory(file);
+ if (files != null) {
+ for (File file : files) {
+ if (!file.isDirectory()) {
+ file.delete();
+ } else {
+ deleteDirectory(file);
+ }
}
}
directory.delete();
}
-}
+}
\ No newline at end of file
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 4368b81..3ad7d34 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -591,6 +591,14 @@
@SystemApi
public static final String KEY_USE_RCS_PRESENCE_BOOL = "use_rcs_presence_bool";
+ /**
+ * The duration in seconds that platform call and message blocking is disabled after the user
+ * contacts emergency services. Platform considers values in the range 0 to 604800 (one week) as
+ * valid. See {@link android.provider.BlockedNumberContract#isBlocked(Context, String)}).
+ */
+ public static final String KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT =
+ "duration_blocking_disabled_after_emergency_int";
+
/** The default value for every variable. */
private final static PersistableBundle sDefaults;
@@ -660,6 +668,7 @@
"max_retries=3, 5000, 5000, 5000");
sDefaults.putLong(KEY_CARRIER_DATA_CALL_APN_DELAY_DEFAULT_LONG, 20000);
sDefaults.putLong(KEY_CARRIER_DATA_CALL_APN_DELAY_FASTER_LONG, 3000);
+ sDefaults.putInt(KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT, 7200);
sDefaults.putStringArray(KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY, null);
sDefaults.putStringArray(KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY, null);
diff --git a/telephony/java/com/android/ims/ImsCallForwardInfo.java b/telephony/java/com/android/ims/ImsCallForwardInfo.java
index 3f8fd19..eeee0fc 100644
--- a/telephony/java/com/android/ims/ImsCallForwardInfo.java
+++ b/telephony/java/com/android/ims/ImsCallForwardInfo.java
@@ -31,6 +31,8 @@
public int mStatus;
// 0x91: International, 0x81: Unknown
public int mToA;
+ // Service class
+ public int mServiceClass;
// Number (it will not include the "sip" or "tel" URI scheme)
public String mNumber;
// No reply timer for CF
@@ -55,13 +57,16 @@
out.writeInt(mToA);
out.writeString(mNumber);
out.writeInt(mTimeSeconds);
+ out.writeInt(mServiceClass);
}
@Override
public String toString() {
return super.toString() + ", Condition: " + mCondition
+ ", Status: " + ((mStatus == 0) ? "disabled" : "enabled")
- + ", ToA: " + mToA + ", Number=" + mNumber
+ + ", ToA: " + mToA
+ + ", Service Class: " + mServiceClass
+ + ", Number=" + mNumber
+ ", Time (seconds): " + mTimeSeconds;
}
@@ -71,6 +76,7 @@
mToA = in.readInt();
mNumber = in.readString();
mTimeSeconds = in.readInt();
+ mServiceClass = in.readInt();
}
public static final Creator<ImsCallForwardInfo> CREATOR =
diff --git a/telephony/java/com/android/ims/ImsReasonInfo.java b/telephony/java/com/android/ims/ImsReasonInfo.java
index 2769a2b..c909c6d 100644
--- a/telephony/java/com/android/ims/ImsReasonInfo.java
+++ b/telephony/java/com/android/ims/ImsReasonInfo.java
@@ -84,6 +84,8 @@
public static final int CODE_LOCAL_CALL_VOLTE_RETRY_REQUIRED = 147;
// IMS call is already terminated (in TERMINATED state)
public static final int CODE_LOCAL_CALL_TERMINATED = 148;
+ // Handover not feasible
+ public static final int CODE_LOCAL_HO_NOT_FEASIBLE = 149;
/**
* TIMEOUT (IMS -> Telephony)
@@ -153,6 +155,9 @@
public static final int CODE_SIP_USER_REJECTED = 361;
// Others
public static final int CODE_SIP_GLOBAL_ERROR = 362;
+ // Emergency failure
+ public static final int CODE_EMERGENCY_TEMP_FAILURE = 363;
+ public static final int CODE_EMERGENCY_PERM_FAILURE = 364;
/**
* MEDIA (IMS -> Telephony)
@@ -236,6 +241,14 @@
public static final int CODE_ANSWERED_ELSEWHERE = 1014;
/**
+ * Supplementary services (HOLD/RESUME) failure error codes.
+ * Values for Supplemetary services failure - Failed, Cancelled and Re-Invite collision.
+ */
+ public static final int CODE_SUPP_SVC_FAILED = 1201;
+ public static final int CODE_SUPP_SVC_CANCELLED = 1202;
+ public static final int CODE_SUPP_SVC_REINVITE_COLLISION = 1203;
+
+ /**
* Network string error messages.
* mExtraMessage may have these values.
*/
diff --git a/telephony/java/com/android/ims/ImsStreamMediaProfile.java b/telephony/java/com/android/ims/ImsStreamMediaProfile.java
index 5a99212..216cef5 100644
--- a/telephony/java/com/android/ims/ImsStreamMediaProfile.java
+++ b/telephony/java/com/android/ims/ImsStreamMediaProfile.java
@@ -51,6 +51,16 @@
public static final int AUDIO_QUALITY_GSM_EFR = 8;
public static final int AUDIO_QUALITY_GSM_FR = 9;
public static final int AUDIO_QUALITY_GSM_HR = 10;
+ public static final int AUDIO_QUALITY_G711U = 11;
+ public static final int AUDIO_QUALITY_G723 = 12;
+ public static final int AUDIO_QUALITY_G711A = 13;
+ public static final int AUDIO_QUALITY_G722 = 14;
+ public static final int AUDIO_QUALITY_G711AB = 15;
+ public static final int AUDIO_QUALITY_G729 = 16;
+ public static final int AUDIO_QUALITY_EVS_NB = 17;
+ public static final int AUDIO_QUALITY_EVS_WB = 18;
+ public static final int AUDIO_QUALITY_EVS_SWB = 19;
+ public static final int AUDIO_QUALITY_EVS_FB = 20;
/**
* Video information