From 2d4fa2c0fae8c2d79a25093d9f732a33c2f91dd4 Mon Sep 17 00:00:00 2001 From: junyulai Date: Tue, 31 Dec 2019 13:40:15 +0800 Subject: [PATCH] [SM05] Enable record mobile network stats by collapsed rat type Switch on the recording in device side. Metrics will be collected in follow-up patches which can be independently enabled/disabled. This change also fix the fail in NetworkStatsCollectionTest which caused by enabling this feature, where the rounding problem happened when records are distributed into smaller buckets and categorized into more NetworkIdentity. Test: atest FrameworksNetTests Bug: 129082217 Change-Id: If330e85330a4ff713dd420c98d42fa741eabd90a --- core/java/android/net/NetworkIdentity.java | 2 +- .../server/net/NetworkStatsService.java | 4 +- .../net/NetworkStatsCollectionTest.java | 90 +++++++-------- .../server/net/NetworkStatsServiceTest.java | 109 +++++++++++++++++- 4 files changed, 152 insertions(+), 53 deletions(-) diff --git a/core/java/android/net/NetworkIdentity.java b/core/java/android/net/NetworkIdentity.java index b67ad519b4373..2cf3531d2f37f 100644 --- a/core/java/android/net/NetworkIdentity.java +++ b/core/java/android/net/NetworkIdentity.java @@ -45,7 +45,7 @@ public class NetworkIdentity implements Comparable { * {@link #SUBTYPE_COMBINED}. */ // TODO: make this flag configurable through settings. See http://b/146415925 - public static final boolean COMBINE_SUBTYPE_ENABLED = true; + public static final boolean COMBINE_SUBTYPE_ENABLED = false; public static final int SUBTYPE_COMBINED = -1; diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/services/core/java/com/android/server/net/NetworkStatsService.java index 7c4624d25c9e8..ff4407aa12998 100644 --- a/services/core/java/com/android/server/net/NetworkStatsService.java +++ b/services/core/java/com/android/server/net/NetworkStatsService.java @@ -1209,7 +1209,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub { final int collapsedRatType = getCollapsedRatType(networkType); if (collapsedRatType == mLastCollapsedRatType) return; - if (LOGV) { + if (LOGD) { Log.d(TAG, "subtype changed for mobile: " + mLastCollapsedRatType + " -> " + collapsedRatType); } @@ -1217,7 +1217,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub { mHandler.removeMessages(MSG_UPDATE_IFACES); mLastCollapsedRatType = collapsedRatType; mHandler.sendMessageDelayed( - mHandler.obtainMessage(MSG_UPDATE_IFACES), SECOND_IN_MILLIS); + mHandler.obtainMessage(MSG_UPDATE_IFACES), mSettings.getPollDelay()); } } diff --git a/tests/net/java/com/android/server/net/NetworkStatsCollectionTest.java b/tests/net/java/com/android/server/net/NetworkStatsCollectionTest.java index 8f90f13ce7efc..551498f2c0cc5 100644 --- a/tests/net/java/com/android/server/net/NetworkStatsCollectionTest.java +++ b/tests/net/java/com/android/server/net/NetworkStatsCollectionTest.java @@ -319,33 +319,33 @@ public class NetworkStatsCollectionTest { assertEntry(18322, 75, 15031, 75, history.getValues(i++, null)); assertEntry(527798, 761, 78570, 652, history.getValues(i++, null)); assertEntry(527797, 760, 78570, 651, history.getValues(i++, null)); - assertEntry(10747, 50, 16838, 55, history.getValues(i++, null)); - assertEntry(10747, 49, 16838, 54, history.getValues(i++, null)); + assertEntry(10747, 50, 16839, 55, history.getValues(i++, null)); + assertEntry(10747, 49, 16837, 54, history.getValues(i++, null)); assertEntry(89191, 151, 18021, 140, history.getValues(i++, null)); assertEntry(89190, 150, 18020, 139, history.getValues(i++, null)); - assertEntry(3821, 22, 4525, 26, history.getValues(i++, null)); - assertEntry(3820, 22, 4524, 26, history.getValues(i++, null)); - assertEntry(91686, 159, 18575, 146, history.getValues(i++, null)); - assertEntry(91685, 159, 18575, 146, history.getValues(i++, null)); - assertEntry(8289, 35, 6863, 38, history.getValues(i++, null)); - assertEntry(8289, 35, 6863, 38, history.getValues(i++, null)); + assertEntry(3821, 23, 4525, 26, history.getValues(i++, null)); + assertEntry(3820, 21, 4524, 26, history.getValues(i++, null)); + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEntry(8289, 36, 6864, 39, history.getValues(i++, null)); + assertEntry(8289, 34, 6862, 37, history.getValues(i++, null)); assertEntry(113914, 174, 18364, 157, history.getValues(i++, null)); assertEntry(113913, 173, 18364, 157, history.getValues(i++, null)); - assertEntry(11378, 49, 9261, 49, history.getValues(i++, null)); - assertEntry(11377, 48, 9261, 49, history.getValues(i++, null)); - assertEntry(201765, 328, 41808, 291, history.getValues(i++, null)); - assertEntry(201765, 328, 41807, 290, history.getValues(i++, null)); - assertEntry(106106, 218, 39917, 201, history.getValues(i++, null)); - assertEntry(106105, 217, 39917, 201, history.getValues(i++, null)); + assertEntry(11378, 49, 9261, 50, history.getValues(i++, null)); + assertEntry(11377, 48, 9261, 48, history.getValues(i++, null)); + assertEntry(201766, 328, 41808, 291, history.getValues(i++, null)); + assertEntry(201764, 328, 41807, 290, history.getValues(i++, null)); + assertEntry(106106, 219, 39918, 202, history.getValues(i++, null)); + assertEntry(106105, 216, 39916, 200, history.getValues(i++, null)); assertEquals(history.size(), i); // Slice from middle should be untouched history = getHistory(collection, plan, TIME_B - HOUR_IN_MILLIS, TIME_B + HOUR_IN_MILLIS); i = 0; - assertEntry(3821, 22, 4525, 26, history.getValues(i++, null)); - assertEntry(3820, 22, 4524, 26, history.getValues(i++, null)); - assertEntry(91686, 159, 18575, 146, history.getValues(i++, null)); - assertEntry(91685, 159, 18575, 146, history.getValues(i++, null)); + assertEntry(3821, 23, 4525, 26, history.getValues(i++, null)); + assertEntry(3820, 21, 4524, 26, history.getValues(i++, null)); + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); assertEquals(history.size(), i); } @@ -373,25 +373,25 @@ public class NetworkStatsCollectionTest { assertEntry(527797, 760, 78570, 651, history.getValues(i++, null)); // Cycle point; start data normalization assertEntry(7507, 0, 11763, 0, history.getValues(i++, null)); - assertEntry(7507, 0, 11763, 0, history.getValues(i++, null)); + assertEntry(7507, 0, 11762, 0, history.getValues(i++, null)); assertEntry(62309, 0, 12589, 0, history.getValues(i++, null)); assertEntry(62309, 0, 12588, 0, history.getValues(i++, null)); assertEntry(2669, 0, 3161, 0, history.getValues(i++, null)); assertEntry(2668, 0, 3160, 0, history.getValues(i++, null)); // Anchor point; end data normalization - assertEntry(91686, 159, 18575, 146, history.getValues(i++, null)); - assertEntry(91685, 159, 18575, 146, history.getValues(i++, null)); - assertEntry(8289, 35, 6863, 38, history.getValues(i++, null)); - assertEntry(8289, 35, 6863, 38, history.getValues(i++, null)); + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEntry(8289, 36, 6864, 39, history.getValues(i++, null)); + assertEntry(8289, 34, 6862, 37, history.getValues(i++, null)); assertEntry(113914, 174, 18364, 157, history.getValues(i++, null)); assertEntry(113913, 173, 18364, 157, history.getValues(i++, null)); // Cycle point - assertEntry(11378, 49, 9261, 49, history.getValues(i++, null)); - assertEntry(11377, 48, 9261, 49, history.getValues(i++, null)); - assertEntry(201765, 328, 41808, 291, history.getValues(i++, null)); - assertEntry(201765, 328, 41807, 290, history.getValues(i++, null)); - assertEntry(106106, 218, 39917, 201, history.getValues(i++, null)); - assertEntry(106105, 217, 39917, 201, history.getValues(i++, null)); + assertEntry(11378, 49, 9261, 50, history.getValues(i++, null)); + assertEntry(11377, 48, 9261, 48, history.getValues(i++, null)); + assertEntry(201766, 328, 41808, 291, history.getValues(i++, null)); + assertEntry(201764, 328, 41807, 290, history.getValues(i++, null)); + assertEntry(106106, 219, 39918, 202, history.getValues(i++, null)); + assertEntry(106105, 216, 39916, 200, history.getValues(i++, null)); assertEquals(history.size(), i); // Slice from middle should be augmented @@ -399,8 +399,8 @@ public class NetworkStatsCollectionTest { TIME_B + HOUR_IN_MILLIS); i = 0; assertEntry(2669, 0, 3161, 0, history.getValues(i++, null)); assertEntry(2668, 0, 3160, 0, history.getValues(i++, null)); - assertEntry(91686, 159, 18575, 146, history.getValues(i++, null)); - assertEntry(91685, 159, 18575, 146, history.getValues(i++, null)); + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); assertEquals(history.size(), i); } @@ -427,34 +427,34 @@ public class NetworkStatsCollectionTest { assertEntry(527798, 761, 78570, 652, history.getValues(i++, null)); assertEntry(527797, 760, 78570, 651, history.getValues(i++, null)); // Cycle point; start data normalization - assertEntry(15015, 0, 23526, 0, history.getValues(i++, null)); - assertEntry(15015, 0, 23526, 0, history.getValues(i++, null)); + assertEntry(15015, 0, 23527, 0, history.getValues(i++, null)); + assertEntry(15015, 0, 23524, 0, history.getValues(i++, null)); assertEntry(124619, 0, 25179, 0, history.getValues(i++, null)); assertEntry(124618, 0, 25177, 0, history.getValues(i++, null)); assertEntry(5338, 0, 6322, 0, history.getValues(i++, null)); assertEntry(5337, 0, 6320, 0, history.getValues(i++, null)); // Anchor point; end data normalization - assertEntry(91686, 159, 18575, 146, history.getValues(i++, null)); - assertEntry(91685, 159, 18575, 146, history.getValues(i++, null)); - assertEntry(8289, 35, 6863, 38, history.getValues(i++, null)); - assertEntry(8289, 35, 6863, 38, history.getValues(i++, null)); + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEntry(8289, 36, 6864, 39, history.getValues(i++, null)); + assertEntry(8289, 34, 6862, 37, history.getValues(i++, null)); assertEntry(113914, 174, 18364, 157, history.getValues(i++, null)); assertEntry(113913, 173, 18364, 157, history.getValues(i++, null)); // Cycle point - assertEntry(11378, 49, 9261, 49, history.getValues(i++, null)); - assertEntry(11377, 48, 9261, 49, history.getValues(i++, null)); - assertEntry(201765, 328, 41808, 291, history.getValues(i++, null)); - assertEntry(201765, 328, 41807, 290, history.getValues(i++, null)); - assertEntry(106106, 218, 39917, 201, history.getValues(i++, null)); - assertEntry(106105, 217, 39917, 201, history.getValues(i++, null)); + assertEntry(11378, 49, 9261, 50, history.getValues(i++, null)); + assertEntry(11377, 48, 9261, 48, history.getValues(i++, null)); + assertEntry(201766, 328, 41808, 291, history.getValues(i++, null)); + assertEntry(201764, 328, 41807, 290, history.getValues(i++, null)); + assertEntry(106106, 219, 39918, 202, history.getValues(i++, null)); + assertEntry(106105, 216, 39916, 200, history.getValues(i++, null)); // Slice from middle should be augmented history = getHistory(collection, plan, TIME_B - HOUR_IN_MILLIS, TIME_B + HOUR_IN_MILLIS); i = 0; assertEntry(5338, 0, 6322, 0, history.getValues(i++, null)); assertEntry(5337, 0, 6320, 0, history.getValues(i++, null)); - assertEntry(91686, 159, 18575, 146, history.getValues(i++, null)); - assertEntry(91685, 159, 18575, 146, history.getValues(i++, null)); + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); assertEquals(history.size(), i); } } diff --git a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java index b346c923914ea..64c0221c5897c 100644 --- a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java +++ b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java @@ -42,6 +42,7 @@ import static android.net.NetworkStats.TAG_NONE; import static android.net.NetworkStats.UID_ALL; import static android.net.NetworkStatsHistory.FIELD_ALL; import static android.net.NetworkTemplate.buildTemplateMobileAll; +import static android.net.NetworkTemplate.buildTemplateMobileWithRatType; import static android.net.NetworkTemplate.buildTemplateWifiWildcard; import static android.net.TrafficStats.MB_IN_BYTES; import static android.net.TrafficStats.UID_REMOVED; @@ -60,11 +61,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.AlarmManager; import android.app.usage.NetworkStatsManager; import android.content.Context; @@ -92,6 +95,8 @@ import android.os.Message; import android.os.Messenger; import android.os.PowerManager; import android.os.SimpleClock; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; import android.telephony.TelephonyManager; import androidx.test.InstrumentationRegistry; @@ -163,11 +168,14 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { private @Mock NetworkStatsSettings mSettings; private @Mock IBinder mBinder; private @Mock AlarmManager mAlarmManager; + private @Mock TelephonyManager mTelephonyManager; private HandlerThread mHandlerThread; private NetworkStatsService mService; private INetworkStatsSession mSession; private INetworkManagementEventObserver mNetworkObserver; + @Nullable + private PhoneStateListener mPhoneStateListener; private final Clock mClock = new SimpleClock(ZoneOffset.UTC) { @Override @@ -195,7 +203,7 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { mHandlerThread = new HandlerThread("HandlerThread"); final NetworkStatsService.Dependencies deps = makeDependencies(); mService = new NetworkStatsService(mServiceContext, mNetManager, mAlarmManager, wakeLock, - mClock, mServiceContext.getSystemService(TelephonyManager.class), mSettings, + mClock, mTelephonyManager, mSettings, mStatsFactory, new NetworkStatsObservers(), mStatsDir, getBaseDir(mStatsDir), deps); mElapsedRealtime = 0L; @@ -216,6 +224,12 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { ArgumentCaptor.forClass(INetworkManagementEventObserver.class); verify(mNetManager).registerObserver(networkObserver.capture()); mNetworkObserver = networkObserver.getValue(); + + // Capture the phone state listener that created by service. + final ArgumentCaptor phoneStateListenerCaptor = + ArgumentCaptor.forClass(PhoneStateListener.class); + verify(mTelephonyManager).listen(phoneStateListenerCaptor.capture(), anyInt()); + mPhoneStateListener = phoneStateListenerCaptor.getValue(); } @NonNull @@ -534,7 +548,7 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { } @Test - public void testUid3g4gCombinedByTemplate() throws Exception { + public void testUid3gWimaxCombinedByTemplate() throws Exception { // pretend that network comes online expectDefaultSettings(); NetworkState[] states = new NetworkState[] {buildMobile3gState(IMSI_1)}; @@ -558,10 +572,10 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { assertUidTotal(sTemplateImsi1, UID_RED, 1024L, 8L, 1024L, 8L, 5); - // now switch over to 4g network + // now switch over to wimax network incrementCurrentTime(HOUR_IN_MILLIS); expectDefaultSettings(); - states = new NetworkState[] {buildMobile4gState(TEST_IFACE2)}; + states = new NetworkState[] {buildWimaxState(TEST_IFACE2)}; expectNetworkStatsSummary(buildEmptyStats()); expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1024L, 8L, 1024L, 8L, 0L) @@ -588,6 +602,89 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { assertUidTotal(sTemplateImsi1, UID_RED, 1536L, 12L, 1280L, 10L, 10); } + @Test + public void testMobileStatsByRatType() throws Exception { + final NetworkTemplate template3g = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS); + final NetworkTemplate template4g = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_LTE); + final NetworkTemplate template5g = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_NR); + final NetworkState[] states = new NetworkState[]{buildMobile3gState(IMSI_1)}; + + // 3G network comes online. + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS); + mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states), + new VpnInfo[0]); + + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 12L, 18L, 14L, 1L, 0L))); + forcePollAndWaitForIdle(); + + // Verify 3g templates gets stats. + assertUidTotal(sTemplateImsi1, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(template4g, UID_RED, 0L, 0L, 0L, 0L, 0); + assertUidTotal(template5g, UID_RED, 0L, 0L, 0L, 0L, 0); + + // 4G network comes online. + incrementCurrentTime(MINUTE_IN_MILLIS); + setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_LTE); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + // Append more traffic on existing 3g stats entry. + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 16L, 22L, 17L, 2L, 0L)) + // Add entry that is new on 4g. + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, + 33L, 27L, 8L, 10L, 1L))); + forcePollAndWaitForIdle(); + + // Verify ALL_MOBILE template gets all. 3g template counters do not increase. + assertUidTotal(sTemplateImsi1, UID_RED, 49L, 49L, 25L, 12L, 1); + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + // Verify 4g template counts appended stats on existing entry and newly created entry. + assertUidTotal(template4g, UID_RED, 4L + 33L, 4L + 27L, 3L + 8L, 1L + 10L, 1); + // Verify 5g template doesn't get anything since no traffic is generated on 5g. + assertUidTotal(template5g, UID_RED, 0L, 0L, 0L, 0L, 0); + + // 5g network comes online. + incrementCurrentTime(MINUTE_IN_MILLIS); + setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_NR); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + // Existing stats remains. + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 16L, 22L, 17L, 2L, 0L)) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, + 33L, 27L, 8L, 10L, 1L)) + // Add some traffic on 5g. + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 5L, 13L, 31L, 9L, 2L))); + forcePollAndWaitForIdle(); + + // Verify ALL_MOBILE template gets all. + assertUidTotal(sTemplateImsi1, UID_RED, 54L, 62L, 56L, 21L, 3); + // 3g/4g template counters do not increase. + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(template4g, UID_RED, 4L + 33L, 4L + 27L, 3L + 8L, 1L + 10L, 1); + // Verify 5g template gets the 5g count. + assertUidTotal(template5g, UID_RED, 5L, 13L, 31L, 9L, 2); + } + + // TODO: support per IMSI state + private void setMobileRatTypeAndWaitForIdle(int ratType) { + final ServiceState mockSs = mock(ServiceState.class); + when(mockSs.getDataNetworkType()).thenReturn(ratType); + mPhoneStateListener.onServiceStateChanged(mockSs); + + HandlerUtilsKt.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + } + @Test public void testSummaryForAllUid() throws Exception { // pretend that network comes online @@ -1242,6 +1339,7 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { final NetworkCapabilities capabilities = new NetworkCapabilities(); capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, !isMetered); capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true); + capabilities.addTransportType(NetworkCapabilities.TRANSPORT_WIFI); return new NetworkState(info, prop, capabilities, WIFI_NETWORK, null, TEST_SSID); } @@ -1259,10 +1357,11 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { final NetworkCapabilities capabilities = new NetworkCapabilities(); capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, false); capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, !isRoaming); + capabilities.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); return new NetworkState(info, prop, capabilities, MOBILE_NETWORK, subscriberId, null); } - private static NetworkState buildMobile4gState(String iface) { + private static NetworkState buildWimaxState(@NonNull String iface) { final NetworkInfo info = new NetworkInfo(TYPE_WIMAX, 0, null, null); info.setDetailedState(DetailedState.CONNECTED, null, null); final LinkProperties prop = new LinkProperties();