Merge "Document more about volume provider ID."
diff --git a/cmds/statsd/src/StatsService.cpp b/cmds/statsd/src/StatsService.cpp
index 7087c68..7ab6c71 100644
--- a/cmds/statsd/src/StatsService.cpp
+++ b/cmds/statsd/src/StatsService.cpp
@@ -280,7 +280,7 @@
status_t StatsService::handleShellCommand(int in, int out, int err, const char** argv,
uint32_t argc) {
uid_t uid = AIBinder_getCallingUid();
- if (uid != AID_ROOT || uid != AID_SHELL) {
+ if (uid != AID_ROOT && uid != AID_SHELL) {
return PERMISSION_DENIED;
}
diff --git a/core/java/android/net/NetworkScore.java b/core/java/android/net/NetworkScore.java
index d2e59eb..d207e79 100644
--- a/core/java/android/net/NetworkScore.java
+++ b/core/java/android/net/NetworkScore.java
@@ -83,6 +83,65 @@
uplinkBandwidthKBps = uplinkBandwidth;
}
+ /**
+ * Evaluate whether a metrics codes for faster network is faster than another.
+ *
+ * This is a simple comparison of expected speeds. If either of the tested attributes
+ * are unknown, this returns zero. This implementation just assumes downlink bandwidth
+ * is more important than uplink bandwidth, which is more important than latency. This
+ * is not a very good way of evaluating network speed, but it's a start.
+ * TODO : do something more representative of how fast the network feels
+ *
+ * @param other the Metrics to evaluate against
+ * @return a negative integer, zero, or a positive integer as this metrics is worse than,
+ * equally good as (or unknown), or better than the passed Metrics.
+ * @see #compareToPreferringKnown(Metrics)
+ * @hide
+ */
+ // Can't implement Comparable<Metrics> because this is @hide.
+ public int compareTo(@NonNull final Metrics other) {
+ if (downlinkBandwidthKBps != BANDWIDTH_UNKNOWN
+ && other.downlinkBandwidthKBps != BANDWIDTH_UNKNOWN) {
+ if (downlinkBandwidthKBps > other.downlinkBandwidthKBps) return 1;
+ if (downlinkBandwidthKBps < other.downlinkBandwidthKBps) return -1;
+ }
+ if (uplinkBandwidthKBps != BANDWIDTH_UNKNOWN
+ && other.uplinkBandwidthKBps != BANDWIDTH_UNKNOWN) {
+ if (uplinkBandwidthKBps > other.uplinkBandwidthKBps) return 1;
+ if (uplinkBandwidthKBps < other.uplinkBandwidthKBps) return -1;
+ }
+ if (latencyMs != LATENCY_UNKNOWN && other.latencyMs != LATENCY_UNKNOWN) {
+ // Latency : lower is better
+ if (latencyMs > other.latencyMs) return -1;
+ if (latencyMs < other.latencyMs) return 1;
+ }
+ return 0;
+ }
+
+ /**
+ * Evaluate whether a metrics codes for faster network is faster than another.
+ *
+ * This is a simple comparison of expected speeds. If either of the tested attributes
+ * are unknown, this prefers the known attributes. This implementation just assumes
+ * downlink bandwidth is more important than uplink bandwidth, which is more important than
+ * latency. This is not a very good way of evaluating network speed, but it's a start.
+ * TODO : do something more representative of how fast the network feels
+ *
+ * @param other the Metrics to evaluate against
+ * @return a negative integer, zero, or a positive integer as this metrics is worse than,
+ * equally good as (or unknown), or better than the passed Metrics.
+ * @see #compareTo(Metrics)
+ * @hide
+ */
+ public int compareToPreferringKnown(@NonNull final Metrics other) {
+ if (downlinkBandwidthKBps > other.downlinkBandwidthKBps) return 1;
+ if (downlinkBandwidthKBps < other.downlinkBandwidthKBps) return -1;
+ if (uplinkBandwidthKBps > other.uplinkBandwidthKBps) return 1;
+ if (uplinkBandwidthKBps < other.uplinkBandwidthKBps) return -1;
+ // Latency : lower is better
+ return -Integer.compare(latencyMs, other.latencyMs);
+ }
+
/** toString */
public String toString() {
return "latency = " + latencyMs + " downlinkBandwidth = " + downlinkBandwidthKBps
@@ -346,6 +405,33 @@
return mLegacyScore;
}
+ /**
+ * Use the metrics to evaluate whether a network is faster than another.
+ *
+ * This is purely based on the metrics, and explicitly ignores policy or exiting. It's
+ * provided to get a decision on two networks when policy can not decide, or to evaluate
+ * how a network is expected to compare to another if it should validate.
+ *
+ * @param other the score to evaluate against
+ * @return whether this network is probably faster than the other
+ * @hide
+ */
+ public boolean probablyFasterThan(@NonNull final NetworkScore other) {
+ if (mLegacyScore > other.mLegacyScore) return true;
+ final int atEndToEnd = mEndToEndMetrics.compareTo(other.mEndToEndMetrics);
+ if (atEndToEnd > 0) return true;
+ if (atEndToEnd < 0) return false;
+ final int atLinkLayer = mLinkLayerMetrics.compareTo(other.mLinkLayerMetrics);
+ if (atLinkLayer > 0) return true;
+ if (atLinkLayer < 0) return false;
+ final int atEndToEndPreferringKnown =
+ mEndToEndMetrics.compareToPreferringKnown(other.mEndToEndMetrics);
+ if (atEndToEndPreferringKnown > 0) return true;
+ if (atEndToEndPreferringKnown < 0) return false;
+ // If this returns 0, neither is "probably faster" so just return false.
+ return mLinkLayerMetrics.compareToPreferringKnown(other.mLinkLayerMetrics) > 0;
+ }
+
/** Builder for NetworkScore. */
public static class Builder {
private int mPolicy = 0;
diff --git a/core/java/com/android/internal/util/FunctionalUtils.java b/core/java/com/android/internal/util/FunctionalUtils.java
index 3c97917..720f4fe 100644
--- a/core/java/com/android/internal/util/FunctionalUtils.java
+++ b/core/java/com/android/internal/util/FunctionalUtils.java
@@ -16,12 +16,16 @@
package com.android.internal.util;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.os.RemoteException;
import android.util.ExceptionUtils;
+import java.util.Collection;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.function.Predicate;
import java.util.function.Supplier;
/**
@@ -218,4 +222,20 @@
}
}
}
+
+ /**
+ * Find the first element in the list that matches the predicate.
+ *
+ * The standard Java way of doing this is to use streams, which is very expensive.
+ *
+ * @return the first matching element, or null if none.
+ */
+ @Nullable
+ public static <T> T findFirst(@NonNull final Collection<T> haystack,
+ @NonNull final Predicate<T> p) {
+ for (final T needle : haystack) {
+ if (p.test(needle)) return needle;
+ }
+ return null;
+ }
}
diff --git a/core/proto/android/app/settings_enums.proto b/core/proto/android/app/settings_enums.proto
index bf17c55..0aaf41e 100644
--- a/core/proto/android/app/settings_enums.proto
+++ b/core/proto/android/app/settings_enums.proto
@@ -2622,4 +2622,9 @@
// CATEGORY: SETTINGS
// OS: R
NOTIFICATION_CONVERSATION_LIST_SETTINGS = 1834;
+
+ // Panel for Media Output Group operation
+ // CATEGORY: SETTINGS
+ // OS: R
+ PANEL_MEDIA_OUTPUT_GROUP = 1835;
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 5ff88ac..f69e4f5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -193,6 +193,34 @@
}
/**
+ * Get the MediaDevice list that has been selected to current media.
+ *
+ * @return list of MediaDevice
+ */
+ List<MediaDevice> getSelectedMediaDevice() {
+ final List<MediaDevice> deviceList = new ArrayList<>();
+ if (TextUtils.isEmpty(mPackageName)) {
+ Log.w(TAG, "getSelectedMediaDevice() package name is null or empty!");
+ return deviceList;
+ }
+
+ final RoutingSessionInfo info = getRoutingSessionInfo();
+ if (info != null) {
+ for (MediaRoute2Info route : mRouterManager.getControllerForSession(info)
+ .getSelectedRoutes()) {
+ deviceList.add(new InfoMediaDevice(mContext, mRouterManager,
+ route, mPackageName));
+ }
+ return deviceList;
+ }
+
+ Log.w(TAG, "getSelectedMediaDevice() cannot found selectable MediaDevice from : "
+ + mPackageName);
+
+ return deviceList;
+ }
+
+ /**
* Adjust the volume of {@link android.media.RoutingSessionInfo}.
*
* @param volume the value of volume
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index fc373a5..617da6e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -282,6 +282,15 @@
}
/**
+ * Get the MediaDevice list that has been selected to current media.
+ *
+ * @return list of MediaDevice
+ */
+ public List<MediaDevice> getSelectedMediaDevice() {
+ return mInfoMediaManager.getSelectedMediaDevice();
+ }
+
+ /**
* Adjust the volume of session.
*
* @param volume the value of volume
diff --git a/packages/Tethering/Android.bp b/packages/Tethering/Android.bp
index fa3926c..3111ab7 100644
--- a/packages/Tethering/Android.bp
+++ b/packages/Tethering/Android.bp
@@ -17,6 +17,7 @@
java_defaults {
name: "TetheringAndroidLibraryDefaults",
// TODO (b/146757305): change to module API once available
+ // TODO (b/148190005): change to module-libs-api-stubs-current once it is ready.
sdk_version: "core_platform",
srcs: [
"src/**/*.java",
@@ -34,7 +35,12 @@
"net-utils-framework-common",
],
libs: [
+ // Order matters: framework-tethering needs to be before the system stubs, otherwise
+ // hidden fields in the framework-tethering classes (which are also used to generate stubs)
+ // will not be found.
"framework-tethering",
+ "android_system_stubs_current",
+ "framework-res",
"unsupportedappusage",
"android_system_stubs_current",
"framework-res",
@@ -86,6 +92,7 @@
java_defaults {
name: "TetheringAppDefaults",
// TODO (b/146757305): change to module API once available
+ // TODO (b/148190005): change to module-libs-api-stubs-current once it is ready.
sdk_version: "core_platform",
privileged: true,
// Build system doesn't track transitive dependeicies for jni_libs, list all the dependencies
@@ -99,6 +106,9 @@
"res",
],
libs: [
+ // Order matters: framework-tethering needs to be before the system stubs, otherwise
+ // hidden fields in the framework-tethering classes (which are also used to generate stubs)
+ // will not be found.
"framework-tethering",
"android_system_stubs_current",
"framework-res",
diff --git a/packages/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl b/packages/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
index 5febe73..8be7964 100644
--- a/packages/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
+++ b/packages/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
@@ -1,16 +1,16 @@
-/**
- * Copyright (c) 2019, The Android Open Source Project
+/*
+ * Copyright (C) 2020 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
+ * 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 perNmissions and
+ * See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net;
diff --git a/packages/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl b/packages/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
index 28a810d..a554193 100644
--- a/packages/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
+++ b/packages/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl
@@ -17,6 +17,7 @@
package android.net;
import android.net.Network;
+import android.net.TetheredClient;
import android.net.TetheringConfigurationParcel;
import android.net.TetheringCallbackStartedParcel;
import android.net.TetherStatesParcel;
@@ -33,4 +34,5 @@
void onUpstreamChanged(in Network network);
void onConfigurationChanged(in TetheringConfigurationParcel config);
void onTetherStatesChanged(in TetherStatesParcel states);
+ void onTetherClientsChanged(in List<TetheredClient> clients);
}
diff --git a/packages/Tethering/common/TetheringLib/src/android/net/TetheredClient.java b/packages/Tethering/common/TetheringLib/src/android/net/TetheredClient.java
index 779aa3b..8b8b9e5 100644
--- a/packages/Tethering/common/TetheringLib/src/android/net/TetheredClient.java
+++ b/packages/Tethering/common/TetheringLib/src/android/net/TetheredClient.java
@@ -191,6 +191,15 @@
return new AddressInfo[size];
}
};
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "AddressInfo {"
+ + mAddress
+ + (mHostname != null ? ", hostname " + mHostname : "")
+ + "}";
+ }
}
@Override
@@ -212,4 +221,13 @@
return new TetheredClient[size];
}
};
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "TetheredClient {hwAddr " + mMacAddress
+ + ", addresses " + mAddresses
+ + ", tetheringType " + mTetheringType
+ + "}";
+ }
}
diff --git a/packages/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl b/packages/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl
index 14ee2d3..c064aa4 100644
--- a/packages/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl
+++ b/packages/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl
@@ -17,6 +17,7 @@
package android.net;
import android.net.Network;
+import android.net.TetheredClient;
import android.net.TetheringConfigurationParcel;
import android.net.TetherStatesParcel;
@@ -29,4 +30,5 @@
Network upstreamNetwork;
TetheringConfigurationParcel config;
TetherStatesParcel states;
+ List<TetheredClient> tetheredClients;
}
\ No newline at end of file
diff --git a/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 6a9f010..bfa962a 100644
--- a/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -375,6 +375,9 @@
mTetherStatesParcel = states;
}
+ @Override
+ public void onTetherClientsChanged(List<TetheredClient> clients) { }
+
public void waitForStarted() {
mWaitForCallback.block(DEFAULT_TIMEOUT_MS);
throwIfPermissionFailure(mError);
@@ -921,6 +924,7 @@
sendRegexpsChanged(parcel.config);
maybeSendTetherableIfacesChangedCallback(parcel.states);
maybeSendTetheredIfacesChangedCallback(parcel.states);
+ callback.onClientsChanged(parcel.tetheredClients);
});
}
@@ -951,6 +955,11 @@
maybeSendTetheredIfacesChangedCallback(states);
});
}
+
+ @Override
+ public void onTetherClientsChanged(final List<TetheredClient> clients) {
+ executor.execute(() -> callback.onClientsChanged(clients));
+ }
};
getConnector(c -> c.registerTetheringEventCallback(remoteCallback, callerPkg));
mTetheringEventCallbacks.put(callback, remoteCallback);
diff --git a/packages/Tethering/src/android/net/ip/IpServer.java b/packages/Tethering/src/android/net/ip/IpServer.java
index 2653b6d..b4d49c0 100644
--- a/packages/Tethering/src/android/net/ip/IpServer.java
+++ b/packages/Tethering/src/android/net/ip/IpServer.java
@@ -19,6 +19,7 @@
import static android.net.InetAddresses.parseNumericAddress;
import static android.net.RouteInfo.RTN_UNICAST;
import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
+import static android.net.shared.Inet4AddressUtils.intToInet4AddressHTH;
import static android.net.util.NetworkConstants.FF;
import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
import static android.net.util.NetworkConstants.asByte;
@@ -29,11 +30,15 @@
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
+import android.net.MacAddress;
import android.net.RouteInfo;
+import android.net.TetheredClient;
import android.net.TetheringManager;
+import android.net.dhcp.DhcpLeaseParcelable;
import android.net.dhcp.DhcpServerCallbacks;
import android.net.dhcp.DhcpServingParamsParcel;
import android.net.dhcp.DhcpServingParamsParcelExt;
+import android.net.dhcp.IDhcpLeaseCallbacks;
import android.net.dhcp.IDhcpServer;
import android.net.ip.RouterAdvertisementDaemon.RaParams;
import android.net.shared.NetdUtils;
@@ -48,6 +53,8 @@
import android.util.Log;
import android.util.SparseArray;
+import androidx.annotation.NonNull;
+
import com.android.internal.util.MessageUtils;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
@@ -57,7 +64,10 @@
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
@@ -130,6 +140,11 @@
* @param newLp the new LinkProperties to report
*/
public void updateLinkProperties(IpServer who, LinkProperties newLp) { }
+
+ /**
+ * Notify that the DHCP leases changed in one of the IpServers.
+ */
+ public void dhcpLeasesChanged() { }
}
/** Capture IpServer dependencies, for injection. */
@@ -205,6 +220,8 @@
private IDhcpServer mDhcpServer;
private RaParams mLastRaParams;
private LinkAddress mIpv4Address;
+ @NonNull
+ private List<TetheredClient> mDhcpLeases = Collections.emptyList();
public IpServer(
String ifaceName, Looper looper, int interfaceType, SharedLog log,
@@ -262,6 +279,14 @@
return new LinkProperties(mLinkProperties);
}
+ /**
+ * Get the latest list of DHCP leases that was reported. Must be called on the IpServer looper
+ * thread.
+ */
+ public List<TetheredClient> getAllLeases() {
+ return Collections.unmodifiableList(mDhcpLeases);
+ }
+
/** Stop this IpServer. After this is called this IpServer should not be used any more. */
public void stop() {
sendMessage(CMD_INTERFACE_DOWN);
@@ -334,7 +359,7 @@
mDhcpServer = server;
try {
- mDhcpServer.start(new OnHandlerStatusCallback() {
+ mDhcpServer.startWithCallbacks(new OnHandlerStatusCallback() {
@Override
public void callback(int startStatusCode) {
if (startStatusCode != STATUS_SUCCESS) {
@@ -342,7 +367,7 @@
handleError();
}
}
- });
+ }, new DhcpLeaseCallback());
} catch (RemoteException e) {
throw new IllegalStateException(e);
}
@@ -355,6 +380,48 @@
}
}
+ private class DhcpLeaseCallback extends IDhcpLeaseCallbacks.Stub {
+ @Override
+ public void onLeasesChanged(List<DhcpLeaseParcelable> leaseParcelables) {
+ final ArrayList<TetheredClient> leases = new ArrayList<>();
+ for (DhcpLeaseParcelable lease : leaseParcelables) {
+ final LinkAddress address = new LinkAddress(
+ intToInet4AddressHTH(lease.netAddr), lease.prefixLength);
+
+ final MacAddress macAddress;
+ try {
+ macAddress = MacAddress.fromBytes(lease.hwAddr);
+ } catch (IllegalArgumentException e) {
+ Log.wtf(TAG, "Invalid address received from DhcpServer: "
+ + Arrays.toString(lease.hwAddr));
+ return;
+ }
+
+ final TetheredClient.AddressInfo addressInfo = new TetheredClient.AddressInfo(
+ address, lease.hostname, lease.expTime);
+ leases.add(new TetheredClient(
+ macAddress,
+ Collections.singletonList(addressInfo),
+ mInterfaceType));
+ }
+
+ getHandler().post(() -> {
+ mDhcpLeases = leases;
+ mCallback.dhcpLeasesChanged();
+ });
+ }
+
+ @Override
+ public int getInterfaceVersion() {
+ return this.VERSION;
+ }
+
+ @Override
+ public String getInterfaceHash() throws RemoteException {
+ return this.HASH;
+ }
+ }
+
private boolean startDhcp(Inet4Address addr, int prefixLen) {
if (mUsingLegacyDhcp) {
return true;
@@ -388,6 +455,8 @@
mLastError = TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
// Not much more we can do here
}
+ mDhcpLeases.clear();
+ getHandler().post(mCallback::dhcpLeasesChanged);
}
});
mDhcpServer = null;
diff --git a/packages/Tethering/src/com/android/server/connectivity/tethering/ConnectedClientsTracker.java b/packages/Tethering/src/com/android/server/connectivity/tethering/ConnectedClientsTracker.java
new file mode 100644
index 0000000..cdd1a5d
--- /dev/null
+++ b/packages/Tethering/src/com/android/server/connectivity/tethering/ConnectedClientsTracker.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2020 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 com.android.server.connectivity.tethering;
+
+import static android.net.TetheringManager.TETHERING_WIFI;
+
+import android.net.MacAddress;
+import android.net.TetheredClient;
+import android.net.TetheredClient.AddressInfo;
+import android.net.ip.IpServer;
+import android.net.wifi.WifiClient;
+import android.os.SystemClock;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Tracker for clients connected to downstreams.
+ *
+ * <p>This class is not thread safe, it is intended to be used only from the tethering handler
+ * thread.
+ */
+public class ConnectedClientsTracker {
+ private final Clock mClock;
+
+ @NonNull
+ private List<WifiClient> mLastWifiClients = Collections.emptyList();
+ @NonNull
+ private List<TetheredClient> mLastTetheredClients = Collections.emptyList();
+
+ @VisibleForTesting
+ static class Clock {
+ public long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+ }
+
+ public ConnectedClientsTracker() {
+ this(new Clock());
+ }
+
+ @VisibleForTesting
+ ConnectedClientsTracker(Clock clock) {
+ mClock = clock;
+ }
+
+ /**
+ * Update the tracker with new connected clients.
+ *
+ * <p>The new list can be obtained through {@link #getLastTetheredClients()}.
+ * @param ipServers The IpServers used to assign addresses to clients.
+ * @param wifiClients The list of L2-connected WiFi clients. Null for no change since last
+ * update.
+ * @return True if the list of clients changed since the last calculation.
+ */
+ public boolean updateConnectedClients(
+ Iterable<IpServer> ipServers, @Nullable List<WifiClient> wifiClients) {
+ final long now = mClock.elapsedRealtime();
+
+ if (wifiClients != null) {
+ mLastWifiClients = wifiClients;
+ }
+ final Set<MacAddress> wifiClientMacs = getClientMacs(mLastWifiClients);
+
+ // Build the list of non-expired leases from all IpServers, grouped by mac address
+ final Map<MacAddress, TetheredClient> clientsMap = new HashMap<>();
+ for (IpServer server : ipServers) {
+ for (TetheredClient client : server.getAllLeases()) {
+ if (client.getTetheringType() == TETHERING_WIFI
+ && !wifiClientMacs.contains(client.getMacAddress())) {
+ // Skip leases of WiFi clients that are not (or no longer) L2-connected
+ continue;
+ }
+ final TetheredClient prunedClient = pruneExpired(client, now);
+ if (prunedClient == null) continue; // All addresses expired
+
+ addLease(clientsMap, prunedClient);
+ }
+ }
+
+ // TODO: add IPv6 addresses from netlink
+
+ // Add connected WiFi clients that do not have any known address
+ for (MacAddress client : wifiClientMacs) {
+ if (clientsMap.containsKey(client)) continue;
+ clientsMap.put(client, new TetheredClient(
+ client, Collections.emptyList() /* addresses */, TETHERING_WIFI));
+ }
+
+ final HashSet<TetheredClient> clients = new HashSet<>(clientsMap.values());
+ final boolean clientsChanged = clients.size() != mLastTetheredClients.size()
+ || !clients.containsAll(mLastTetheredClients);
+ mLastTetheredClients = Collections.unmodifiableList(new ArrayList<>(clients));
+ return clientsChanged;
+ }
+
+ private static void addLease(Map<MacAddress, TetheredClient> clientsMap, TetheredClient lease) {
+ final TetheredClient aggregateClient = clientsMap.getOrDefault(
+ lease.getMacAddress(), lease);
+ if (aggregateClient == lease) {
+ // This is the first lease with this mac address
+ clientsMap.put(lease.getMacAddress(), lease);
+ return;
+ }
+
+ // Only add the address info; this assumes that the tethering type is the same when the mac
+ // address is the same. If a client is connected through different tethering types with the
+ // same mac address, connected clients callbacks will report all of its addresses under only
+ // one of these tethering types. This keeps the API simple considering that such a scenario
+ // would really be a rare edge case.
+ clientsMap.put(lease.getMacAddress(), aggregateClient.addAddresses(lease));
+ }
+
+ /**
+ * Get the last list of tethered clients, as calculated in {@link #updateConnectedClients}.
+ *
+ * <p>The returned list is immutable.
+ */
+ @NonNull
+ public List<TetheredClient> getLastTetheredClients() {
+ return mLastTetheredClients;
+ }
+
+ private static boolean hasExpiredAddress(List<AddressInfo> addresses, long now) {
+ for (AddressInfo info : addresses) {
+ if (info.getExpirationTime() <= now) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ private static TetheredClient pruneExpired(TetheredClient client, long now) {
+ final List<AddressInfo> addresses = client.getAddresses();
+ if (addresses.size() == 0) return null;
+ if (!hasExpiredAddress(addresses, now)) return client;
+
+ final ArrayList<AddressInfo> newAddrs = new ArrayList<>(addresses.size() - 1);
+ for (AddressInfo info : addresses) {
+ if (info.getExpirationTime() > now) {
+ newAddrs.add(info);
+ }
+ }
+
+ if (newAddrs.size() == 0) {
+ return null;
+ }
+ return new TetheredClient(client.getMacAddress(), newAddrs, client.getTetheringType());
+ }
+
+ @NonNull
+ private static Set<MacAddress> getClientMacs(@NonNull List<WifiClient> clients) {
+ final Set<MacAddress> macs = new HashSet<>(clients.size());
+ for (WifiClient c : clients) {
+ macs.add(c.getMacAddress());
+ }
+ return macs;
+ }
+}
diff --git a/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java b/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
index 64c16e4..6261def 100644
--- a/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
+++ b/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
@@ -24,6 +24,7 @@
import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED;
import static android.net.TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY;
import static android.net.TetheringManager.EXTRA_ACTIVE_TETHER;
@@ -79,6 +80,7 @@
import android.net.Network;
import android.net.NetworkInfo;
import android.net.TetherStatesParcel;
+import android.net.TetheredClient;
import android.net.TetheringCallbackStartedParcel;
import android.net.TetheringConfigurationParcel;
import android.net.TetheringRequestParcel;
@@ -89,6 +91,7 @@
import android.net.util.PrefixUtils;
import android.net.util.SharedLog;
import android.net.util.VersionedBroadcastListener;
+import android.net.wifi.WifiClient;
import android.net.wifi.WifiManager;
import android.net.wifi.p2p.WifiP2pGroup;
import android.net.wifi.p2p.WifiP2pInfo;
@@ -128,8 +131,10 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.HashSet;
+import java.util.Collections;
import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
@@ -145,6 +150,10 @@
private static final boolean DBG = false;
private static final boolean VDBG = false;
+ // TODO: add the below permissions to @SystemApi
+ private static final String PERMISSION_NETWORK_SETTINGS = "android.permission.NETWORK_SETTINGS";
+ private static final String PERMISSION_NETWORK_STACK = "android.permission.NETWORK_STACK";
+
private static final Class[] sMessageClasses = {
Tethering.class, TetherMasterSM.class, IpServer.class
};
@@ -176,6 +185,17 @@
}
}
+ /**
+ * Cookie added when registering {@link android.net.TetheringManager.TetheringEventCallback}.
+ */
+ private static class CallbackCookie {
+ public final boolean hasListClientsPermission;
+
+ private CallbackCookie(boolean hasListClientsPermission) {
+ this.hasListClientsPermission = hasListClientsPermission;
+ }
+ }
+
private final SharedLog mLog = new SharedLog(TAG);
private final RemoteCallbackList<ITetheringEventCallback> mTetheringEventCallbacks =
new RemoteCallbackList<>();
@@ -191,7 +211,8 @@
private final UpstreamNetworkMonitor mUpstreamNetworkMonitor;
// TODO: Figure out how to merge this and other downstream-tracking objects
// into a single coherent structure.
- private final HashSet<IpServer> mForwardedDownstreams;
+ // Use LinkedHashSet for predictable ordering order for ConnectedClientsTracker.
+ private final LinkedHashSet<IpServer> mForwardedDownstreams;
private final VersionedBroadcastListener mCarrierConfigChange;
private final TetheringDependencies mDeps;
private final EntitlementManager mEntitlementMgr;
@@ -200,6 +221,7 @@
private final NetdCallback mNetdCallback;
private final UserRestrictionActionListener mTetheringRestriction;
private final ActiveDataSubIdListener mActiveDataSubIdListener;
+ private final ConnectedClientsTracker mConnectedClientsTracker;
private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID;
// All the usage of mTetheringEventCallback should run in the same thread.
private ITetheringEventCallback mTetheringEventCallback = null;
@@ -234,6 +256,7 @@
mPublicSync = new Object();
mTetherStates = new ArrayMap<>();
+ mConnectedClientsTracker = new ConnectedClientsTracker();
mTetherMasterSM = new TetherMasterSM("TetherMaster", mLooper, deps);
mTetherMasterSM.start();
@@ -246,7 +269,7 @@
statsManager, mLog);
mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mTetherMasterSM, mLog,
TetherMasterSM.EVENT_UPSTREAM_CALLBACK);
- mForwardedDownstreams = new HashSet<>();
+ mForwardedDownstreams = new LinkedHashSet<>();
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_CARRIER_CONFIG_CHANGED);
@@ -291,6 +314,9 @@
startStateMachineUpdaters(mHandler);
startTrackDefaultNetwork();
+ getWifiManager().registerSoftApCallback(
+ mHandler::post /* executor */,
+ new TetheringSoftApCallback());
}
private void startStateMachineUpdaters(Handler handler) {
@@ -385,6 +411,24 @@
}
}
+ private class TetheringSoftApCallback implements WifiManager.SoftApCallback {
+ // TODO: Remove onStateChanged override when this method has default on
+ // WifiManager#SoftApCallback interface.
+ // Wifi listener for state change of the soft AP
+ @Override
+ public void onStateChanged(final int state, final int failureReason) {
+ // Nothing
+ }
+
+ // Called by wifi when the number of soft AP clients changed.
+ @Override
+ public void onConnectedClientsChanged(final List<WifiClient> clients) {
+ if (mConnectedClientsTracker.updateConnectedClients(mForwardedDownstreams, clients)) {
+ reportTetherClientsChanged(mConnectedClientsTracker.getLastTetheredClients());
+ }
+ }
+ }
+
void interfaceStatusChanged(String iface, boolean up) {
// Never called directly: only called from interfaceLinkStateChanged.
// See NetlinkHandler.cpp: notifyInterfaceChanged.
@@ -1938,14 +1982,21 @@
/** Register tethering event callback */
void registerTetheringEventCallback(ITetheringEventCallback callback) {
+ final boolean hasListPermission =
+ hasCallingPermission(PERMISSION_NETWORK_SETTINGS)
+ || hasCallingPermission(PERMISSION_MAINLINE_NETWORK_STACK)
+ || hasCallingPermission(PERMISSION_NETWORK_STACK);
mHandler.post(() -> {
- mTetheringEventCallbacks.register(callback);
+ mTetheringEventCallbacks.register(callback, new CallbackCookie(hasListPermission));
final TetheringCallbackStartedParcel parcel = new TetheringCallbackStartedParcel();
parcel.tetheringSupported = mDeps.isTetheringSupported();
parcel.upstreamNetwork = mTetherUpstream;
parcel.config = mConfig.toStableParcelable();
parcel.states =
mTetherStatesParcel != null ? mTetherStatesParcel : emptyTetherStatesParcel();
+ parcel.tetheredClients = hasListPermission
+ ? mConnectedClientsTracker.getLastTetheredClients()
+ : Collections.emptyList();
try {
callback.onCallbackStarted(parcel);
} catch (RemoteException e) {
@@ -1965,6 +2016,10 @@
return parcel;
}
+ private boolean hasCallingPermission(@NonNull String permission) {
+ return mContext.checkCallingPermission(permission) == PERMISSION_GRANTED;
+ }
+
/** Unregister tethering event callback */
void unregisterTetheringEventCallback(ITetheringEventCallback callback) {
mHandler.post(() -> {
@@ -2018,6 +2073,24 @@
}
}
+ private void reportTetherClientsChanged(List<TetheredClient> clients) {
+ final int length = mTetheringEventCallbacks.beginBroadcast();
+ try {
+ for (int i = 0; i < length; i++) {
+ try {
+ final CallbackCookie cookie =
+ (CallbackCookie) mTetheringEventCallbacks.getBroadcastCookie(i);
+ if (!cookie.hasListClientsPermission) continue;
+ mTetheringEventCallbacks.getBroadcastItem(i).onTetherClientsChanged(clients);
+ } catch (RemoteException e) {
+ // Not really very much to do here.
+ }
+ }
+ } finally {
+ mTetheringEventCallbacks.finishBroadcast();
+ }
+ }
+
void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args) {
// Binder.java closes the resource for us.
@SuppressWarnings("resource")
@@ -2109,6 +2182,14 @@
public void updateLinkProperties(IpServer who, LinkProperties newLp) {
notifyLinkPropertiesChanged(who, newLp);
}
+
+ @Override
+ public void dhcpLeasesChanged() {
+ if (mConnectedClientsTracker.updateConnectedClients(
+ mForwardedDownstreams, null /* wifiClients */)) {
+ reportTetherClientsChanged(mConnectedClientsTracker.getLastTetheredClients());
+ }
+ }
};
}
diff --git a/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index f29ad78..acedfab 100644
--- a/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -469,7 +469,8 @@
private void assertDhcpStarted(IpPrefix expectedPrefix) throws Exception {
verify(mDependencies, times(1)).makeDhcpServer(eq(IFACE_NAME), any(), any());
- verify(mDhcpServer, timeout(MAKE_DHCPSERVER_TIMEOUT_MS).times(1)).start(any());
+ verify(mDhcpServer, timeout(MAKE_DHCPSERVER_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
final DhcpServingParamsParcel params = mDhcpParamsCaptor.getValue();
// Last address byte is random
assertTrue(expectedPrefix.contains(intToInet4AddressHTH(params.serverAddr)));
diff --git a/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/ConnectedClientsTrackerTest.kt b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/ConnectedClientsTrackerTest.kt
new file mode 100644
index 0000000..56f3e21
--- /dev/null
+++ b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/ConnectedClientsTrackerTest.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2020 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 com.android.server.connectivity.tethering
+
+import android.net.LinkAddress
+import android.net.MacAddress
+import android.net.TetheredClient
+import android.net.TetheredClient.AddressInfo
+import android.net.TetheringManager.TETHERING_USB
+import android.net.TetheringManager.TETHERING_WIFI
+import android.net.ip.IpServer
+import android.net.wifi.WifiClient
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ConnectedClientsTrackerTest {
+
+ private val server1 = mock(IpServer::class.java)
+ private val server2 = mock(IpServer::class.java)
+ private val servers = listOf(server1, server2)
+
+ private val clock = TestClock(1324L)
+
+ private val client1Addr = MacAddress.fromString("01:23:45:67:89:0A")
+ private val client1 = TetheredClient(client1Addr, listOf(
+ AddressInfo(LinkAddress("192.168.43.44/32"), null /* hostname */, clock.time + 20)),
+ TETHERING_WIFI)
+ private val wifiClient1 = makeWifiClient(client1Addr)
+ private val client2Addr = MacAddress.fromString("02:34:56:78:90:AB")
+ private val client2Exp30AddrInfo = AddressInfo(
+ LinkAddress("192.168.43.45/32"), "my_hostname", clock.time + 30)
+ private val client2 = TetheredClient(client2Addr, listOf(
+ client2Exp30AddrInfo,
+ AddressInfo(LinkAddress("2001:db8:12::34/72"), "other_hostname", clock.time + 10)),
+ TETHERING_WIFI)
+ private val wifiClient2 = makeWifiClient(client2Addr)
+ private val client3Addr = MacAddress.fromString("03:45:67:89:0A:BC")
+ private val client3 = TetheredClient(client3Addr,
+ listOf(AddressInfo(LinkAddress("2001:db8:34::34/72"), "other_other_hostname",
+ clock.time + 10)),
+ TETHERING_USB)
+
+ @Test
+ fun testUpdateConnectedClients() {
+ doReturn(emptyList<TetheredClient>()).`when`(server1).allLeases
+ doReturn(emptyList<TetheredClient>()).`when`(server2).allLeases
+
+ val tracker = ConnectedClientsTracker(clock)
+ assertFalse(tracker.updateConnectedClients(servers, null))
+
+ // Obtain a lease for client 1
+ doReturn(listOf(client1)).`when`(server1).allLeases
+ assertSameClients(listOf(client1), assertNewClients(tracker, servers, listOf(wifiClient1)))
+
+ // Client 2 L2-connected, no lease yet
+ val client2WithoutAddr = TetheredClient(client2Addr, emptyList(), TETHERING_WIFI)
+ assertSameClients(listOf(client1, client2WithoutAddr),
+ assertNewClients(tracker, servers, listOf(wifiClient1, wifiClient2)))
+
+ // Client 2 lease obtained
+ doReturn(listOf(client1, client2)).`when`(server1).allLeases
+ assertSameClients(listOf(client1, client2), assertNewClients(tracker, servers, null))
+
+ // Client 3 lease obtained
+ doReturn(listOf(client3)).`when`(server2).allLeases
+ assertSameClients(listOf(client1, client2, client3),
+ assertNewClients(tracker, servers, null))
+
+ // Client 2 L2-disconnected
+ assertSameClients(listOf(client1, client3),
+ assertNewClients(tracker, servers, listOf(wifiClient1)))
+
+ // Client 1 L2-disconnected
+ assertSameClients(listOf(client3), assertNewClients(tracker, servers, emptyList()))
+
+ // Client 1 comes back
+ assertSameClients(listOf(client1, client3),
+ assertNewClients(tracker, servers, listOf(wifiClient1)))
+
+ // Leases lost, client 1 still L2-connected
+ doReturn(emptyList<TetheredClient>()).`when`(server1).allLeases
+ doReturn(emptyList<TetheredClient>()).`when`(server2).allLeases
+ assertSameClients(listOf(TetheredClient(client1Addr, emptyList(), TETHERING_WIFI)),
+ assertNewClients(tracker, servers, null))
+ }
+
+ @Test
+ fun testUpdateConnectedClients_LeaseExpiration() {
+ val tracker = ConnectedClientsTracker(clock)
+ doReturn(listOf(client1, client2)).`when`(server1).allLeases
+ doReturn(listOf(client3)).`when`(server2).allLeases
+ assertSameClients(listOf(client1, client2, client3), assertNewClients(
+ tracker, servers, listOf(wifiClient1, wifiClient2)))
+
+ clock.time += 20
+ // Client 3 has no remaining lease: removed
+ val expectedClients = listOf(
+ // Client 1 has no remaining lease but is L2-connected
+ TetheredClient(client1Addr, emptyList(), TETHERING_WIFI),
+ // Client 2 has some expired leases
+ TetheredClient(
+ client2Addr,
+ // Only the "t + 30" address is left, the "t + 10" address expired
+ listOf(client2Exp30AddrInfo),
+ TETHERING_WIFI))
+ assertSameClients(expectedClients, assertNewClients(tracker, servers, null))
+ }
+
+ private fun assertNewClients(
+ tracker: ConnectedClientsTracker,
+ ipServers: Iterable<IpServer>,
+ wifiClients: List<WifiClient>?
+ ): List<TetheredClient> {
+ assertTrue(tracker.updateConnectedClients(ipServers, wifiClients))
+ return tracker.lastTetheredClients
+ }
+
+ private fun assertSameClients(expected: List<TetheredClient>, actual: List<TetheredClient>) {
+ val expectedSet = HashSet(expected)
+ assertEquals(expected.size, expectedSet.size)
+ assertEquals(expectedSet, HashSet(actual))
+ }
+
+ private fun makeWifiClient(macAddr: MacAddress): WifiClient {
+ // Use a mock WifiClient as the constructor is not part of the WiFi module exported API.
+ return mock(WifiClient::class.java).apply { doReturn(macAddr).`when`(this).macAddress }
+ }
+
+ private class TestClock(var time: Long) : ConnectedClientsTracker.Clock() {
+ override fun elapsedRealtime(): Long {
+ return time
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
index 6d49e20..8e5aaf2 100644
--- a/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
+++ b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
@@ -88,6 +88,7 @@
import android.net.NetworkRequest;
import android.net.RouteInfo;
import android.net.TetherStatesParcel;
+import android.net.TetheredClient;
import android.net.TetheringCallbackStartedParcel;
import android.net.TetheringConfigurationParcel;
import android.net.TetheringRequestParcel;
@@ -142,6 +143,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import java.util.Vector;
@RunWith(AndroidJUnit4.class)
@@ -470,6 +472,7 @@
ArgumentCaptor.forClass(PhoneStateListener.class);
verify(mTelephonyManager).listen(phoneListenerCaptor.capture(),
eq(PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE));
+ verify(mWifiManager).registerSoftApCallback(any(), any());
mPhoneStateListener = phoneListenerCaptor.getValue();
}
@@ -728,7 +731,8 @@
sendIPv6TetherUpdates(upstreamState);
verify(mRouterAdvertisementDaemon, never()).buildNewRa(any(), notNull());
- verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
}
@Test
@@ -764,7 +768,8 @@
verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
verify(mRouterAdvertisementDaemon, times(1)).start();
- verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
sendIPv6TetherUpdates(upstreamState);
verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
@@ -778,7 +783,8 @@
verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
- verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
@@ -794,7 +800,8 @@
runUsbTethering(upstreamState);
verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
- verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
// Then 464xlat comes up
@@ -817,7 +824,8 @@
verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
// DHCP not restarted on downstream (still times(1))
- verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
}
@Test
@@ -847,7 +855,8 @@
public void workingNcmTethering() throws Exception {
runNcmTethering();
- verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
}
@Test
@@ -1171,6 +1180,11 @@
}
@Override
+ public void onTetherClientsChanged(List<TetheredClient> clients) {
+ // TODO: check this
+ }
+
+ @Override
public void onCallbackStarted(TetheringCallbackStartedParcel parcel) {
mActualUpstreams.add(parcel.upstreamNetwork);
mTetheringConfigs.add(parcel.config);
diff --git a/services/core/java/com/android/server/connectivity/NetworkRanker.java b/services/core/java/com/android/server/connectivity/NetworkRanker.java
index 1ae7dc5..c536ab2 100644
--- a/services/core/java/com/android/server/connectivity/NetworkRanker.java
+++ b/services/core/java/com/android/server/connectivity/NetworkRanker.java
@@ -16,8 +16,13 @@
package com.android.server.connectivity;
+import static android.net.NetworkScore.POLICY_IGNORE_ON_WIFI;
+
+import static com.android.internal.util.FunctionalUtils.findFirst;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import java.util.ArrayList;
@@ -37,15 +42,33 @@
@NonNull final Collection<NetworkAgentInfo> nais) {
final ArrayList<NetworkAgentInfo> candidates = new ArrayList<>(nais);
candidates.removeIf(nai -> !nai.satisfies(request));
+ // Enforce policy.
+ filterBadWifiAvoidancePolicy(candidates);
NetworkAgentInfo bestNetwork = null;
int bestScore = Integer.MIN_VALUE;
for (final NetworkAgentInfo nai : candidates) {
- if (nai.getCurrentScore() > bestScore) {
+ final int score = nai.getCurrentScore();
+ if (score > bestScore) {
bestNetwork = nai;
- bestScore = nai.getCurrentScore();
+ bestScore = score;
}
}
return bestNetwork;
}
+
+ // If some network with wifi transport is present, drop all networks with POLICY_IGNORE_ON_WIFI.
+ private void filterBadWifiAvoidancePolicy(
+ @NonNull final ArrayList<NetworkAgentInfo> candidates) {
+ final NetworkAgentInfo wifi = findFirst(candidates,
+ nai -> nai.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
+ && nai.everValidated
+ // Horrible hack : there is old UI that will let a user say they want to
+ // override the policy only for this network only at this time and it
+ // feeds into the following member. This old UI should probably be removed
+ // but for now keep backward compatibility.
+ && !nai.avoidUnvalidated);
+ if (null == wifi) return; // No wifi : this policy doesn't apply
+ candidates.removeIf(nai -> nai.getNetworkScore().hasPolicy(POLICY_IGNORE_ON_WIFI));
+ }
}
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index 8e38114..05867ba 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -62,7 +62,7 @@
private final BluetoothRouteProvider mBtRouteProvider;
private static ComponentName sComponentName = new ComponentName(
- SystemMediaRoute2Provider.class.getPackageName$(),
+ SystemMediaRoute2Provider.class.getPackage().getName(),
SystemMediaRoute2Provider.class.getName());
private String mSelectedRouteId;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index b3b8159..b61a2ce 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -6224,6 +6224,13 @@
return mRemoteAnimationDefinition;
}
+ @Override
+ void applyFixedRotationTransform(DisplayInfo info, DisplayFrames displayFrames,
+ Configuration config) {
+ super.applyFixedRotationTransform(info, displayFrames, config);
+ ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */);
+ }
+
void setRequestedOrientation(int requestedOrientation) {
setOrientation(requestedOrientation, mayFreezeScreenLocked());
mAtmService.getTaskChangeNotificationController().notifyActivityRequestedOrientationChanged(
@@ -6462,11 +6469,20 @@
@Override
void resolveOverrideConfiguration(Configuration newParentConfiguration) {
- Configuration resolvedConfig = getResolvedOverrideConfiguration();
+ super.resolveOverrideConfiguration(newParentConfiguration);
+ final Configuration resolvedConfig = getResolvedOverrideConfiguration();
+ if (isFixedRotationTransforming()) {
+ // The resolved configuration is applied with rotated display configuration. If this
+ // activity matches its parent (the following resolving procedures are no-op), then it
+ // can use the resolved configuration directly. Otherwise (e.g. fixed aspect ratio),
+ // the rotated configuration is used as parent configuration to compute the actual
+ // resolved configuration. It is like putting the activity in a rotated container.
+ mTmpConfig.setTo(resolvedConfig);
+ newParentConfiguration = mTmpConfig;
+ }
if (mCompatDisplayInsets != null) {
resolveSizeCompatModeConfiguration(newParentConfiguration);
} else {
- super.resolveOverrideConfiguration(newParentConfiguration);
// We ignore activities' requested orientation in multi-window modes. Task level may
// take them into consideration when calculating bounds.
if (getParent() != null && getParent().inMultiWindowMode()) {
@@ -6495,7 +6511,6 @@
* inheriting the parent bounds.
*/
private void resolveSizeCompatModeConfiguration(Configuration newParentConfiguration) {
- super.resolveOverrideConfiguration(newParentConfiguration);
final Configuration resolvedConfig = getResolvedOverrideConfiguration();
final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds();
@@ -6669,28 +6684,26 @@
mTmpPrevBounds.set(getBounds());
super.onConfigurationChanged(newParentConfig);
- final Rect overrideBounds = getResolvedOverrideBounds();
- if (task != null && !overrideBounds.isEmpty()
- // If the changes come from change-listener, the incoming parent configuration is
- // still the old one. Make sure their orientations are the same to reduce computing
- // the compatibility bounds for the intermediate state.
- && (task.getConfiguration().orientation == newParentConfig.orientation)) {
- final Rect taskBounds = task.getBounds();
- // Since we only center the activity horizontally, if only the fixed height is smaller
- // than its container, the override bounds don't need to take effect.
- if ((overrideBounds.width() != taskBounds.width()
- || overrideBounds.height() > taskBounds.height())) {
- calculateCompatBoundsTransformation(newParentConfig);
- updateSurfacePosition();
- } else if (mSizeCompatBounds != null) {
+ if (shouldUseSizeCompatMode()) {
+ final Rect overrideBounds = getResolvedOverrideBounds();
+ if (task != null && !overrideBounds.isEmpty()) {
+ final Rect taskBounds = task.getBounds();
+ // Since we only center the activity horizontally, if only the fixed height is
+ // smaller than its container, the override bounds don't need to take effect.
+ if ((overrideBounds.width() != taskBounds.width()
+ || overrideBounds.height() > taskBounds.height())) {
+ calculateCompatBoundsTransformation(newParentConfig);
+ updateSurfacePosition();
+ } else if (mSizeCompatBounds != null) {
+ mSizeCompatBounds = null;
+ mSizeCompatScale = 1f;
+ updateSurfacePosition();
+ }
+ } else if (overrideBounds.isEmpty()) {
mSizeCompatBounds = null;
mSizeCompatScale = 1f;
updateSurfacePosition();
}
- } else if (overrideBounds.isEmpty()) {
- mSizeCompatBounds = null;
- mSizeCompatScale = 1f;
- updateSurfacePosition();
}
final int newWinMode = getWindowingMode();
@@ -7710,6 +7723,12 @@
return;
}
win.getAnimationFrames(outFrame, outInsets, outStableInsets, outSurfaceInsets);
+ if (isFixedRotationTransforming()) {
+ // This activity has been rotated but the display is still in old rotation. Because the
+ // animation applies in display space coordinates, the rotated animation frames need to
+ // be unrotated to avoid being cropped.
+ unrotateAnimationFrames(outFrame, outInsets, outStableInsets, outSurfaceInsets);
+ }
}
void setPictureInPictureParams(PictureInPictureParams p) {
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index 014cb76..8cf0881 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -528,6 +528,12 @@
}
}
+ private void notifyAppTransitionTimeoutLocked() {
+ for (int i = 0; i < mListeners.size(); i++) {
+ mListeners.get(i).onAppTransitionTimeoutLocked();
+ }
+ }
+
private int notifyAppTransitionStartingLocked(int transit, long duration,
long statusBarAnimationStartTime, long statusBarAnimationDuration) {
int redoLayout = 0;
@@ -2300,6 +2306,7 @@
if (dc == null) {
return;
}
+ notifyAppTransitionTimeoutLocked();
if (isTransitionSet() || !dc.mOpeningApps.isEmpty() || !dc.mClosingApps.isEmpty()
|| !dc.mChangingApps.isEmpty()) {
ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index 9bd380a..33dd9cf 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -113,13 +113,6 @@
* @see #mFullConfiguration
*/
public void onConfigurationChanged(Configuration newParentConfig) {
- onConfigurationChanged(newParentConfig, true /*forwardToChildren*/);
- }
-
- // TODO(root-unify): Consolidate with onConfigurationChanged() method above once unification is
- // done. This is only currently need during the process of unification where we don't want
- // configuration forwarded to a child from both parents.
- public void onConfigurationChanged(Configuration newParentConfig, boolean forwardToChildren) {
mResolvedTmpConfig.setTo(mResolvedOverrideConfiguration);
resolveOverrideConfiguration(newParentConfig);
mFullConfiguration.setTo(newParentConfig);
@@ -141,11 +134,9 @@
mChangeListeners.get(i).onMergedOverrideConfigurationChanged(
mMergedOverrideConfiguration);
}
- if (forwardToChildren) {
- for (int i = getChildCount() - 1; i >= 0; --i) {
- final ConfigurationContainer child = getChildAt(i);
- child.onConfigurationChanged(mFullConfiguration);
- }
+ for (int i = getChildCount() - 1; i >= 0; --i) {
+ final ConfigurationContainer child = getChildAt(i);
+ child.onConfigurationChanged(mFullConfiguration);
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 8a46771..1fbaadc 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -259,6 +259,7 @@
implements WindowManagerPolicy.DisplayContentInfo {
private static final String TAG = TAG_WITH_CLASS_NAME ? "DisplayContent" : TAG_WM;
private static final String TAG_STACK = TAG + POSTFIX_STACK;
+ private static final int NO_ROTATION = -1;
/** The default scaling mode that scales content automatically. */
static final int FORCE_SCALING_MODE_AUTO = 0;
@@ -515,6 +516,13 @@
*/
ActivityRecord mFocusedApp = null;
+ /**
+ * The launching activity which is using fixed rotation transformation.
+ *
+ * @see #handleTopActivityLaunchingInDifferentOrientation
+ */
+ ActivityRecord mFixedRotationLaunchingApp;
+
/** Windows added since {@link #mCurrentFocus} was set to null. Used for ANR blaming. */
final ArrayList<WindowState> mWinAddedSinceNullFocus = new ArrayList<>();
@@ -1308,6 +1316,9 @@
if (mDisplayRotation.isWaitingForRemoteRotation()) {
return;
}
+ // Clear the record because the display will sync to current rotation.
+ mFixedRotationLaunchingApp = null;
+
final boolean configUpdated = updateDisplayOverrideConfigurationLocked();
if (configUpdated) {
return;
@@ -1367,7 +1378,7 @@
* {@link #sendNewConfiguration} if the method returns {@code true}.
*/
boolean updateOrientation() {
- return mDisplayRotation.updateOrientation(getOrientation(), false /* forceUpdate */);
+ return updateOrientation(false /* forceUpdate */);
}
/**
@@ -1390,7 +1401,7 @@
}
Configuration config = null;
- if (mDisplayRotation.updateOrientation(getOrientation(), forceUpdate)) {
+ if (updateOrientation(forceUpdate)) {
// If we changed the orientation but mOrientationChangeComplete is already true,
// we used seamless rotation, and we don't need to freeze the screen.
if (freezeDisplayToken != null && !mWmService.mRoot.mOrientationChangeComplete) {
@@ -1421,6 +1432,126 @@
return config;
}
+ private boolean updateOrientation(boolean forceUpdate) {
+ final int orientation = getOrientation();
+ // The last orientation source is valid only after getOrientation.
+ final WindowContainer orientationSource = getLastOrientationSource();
+ final ActivityRecord r =
+ orientationSource != null ? orientationSource.asActivityRecord() : null;
+ // Currently there is no use case from non-activity.
+ if (r != null && handleTopActivityLaunchingInDifferentOrientation(r)) {
+ mFixedRotationLaunchingApp = r;
+ // Display orientation should be deferred until the top fixed rotation is finished.
+ return false;
+ }
+ return mDisplayRotation.updateOrientation(orientation, forceUpdate);
+ }
+
+ /** @return a valid rotation if the activity can use different orientation than the display. */
+ @Surface.Rotation
+ private int rotationForActivityInDifferentOrientation(@NonNull ActivityRecord r) {
+ if (!mWmService.mIsFixedRotationTransformEnabled) {
+ return NO_ROTATION;
+ }
+ if (r.inMultiWindowMode()
+ || r.getRequestedConfigurationOrientation() == getConfiguration().orientation) {
+ return NO_ROTATION;
+ }
+ final int currentRotation = getRotation();
+ final int rotation = mDisplayRotation.rotationForOrientation(r.getRequestedOrientation(),
+ currentRotation);
+ if (rotation == currentRotation) {
+ return NO_ROTATION;
+ }
+ return rotation;
+ }
+
+ /**
+ * We need to keep display rotation fixed for a while when the activity in different orientation
+ * is launching until the launch animation is done to avoid showing the previous activity
+ * inadvertently in a wrong orientation.
+ *
+ * @return {@code true} if the fixed rotation is started.
+ */
+ private boolean handleTopActivityLaunchingInDifferentOrientation(@NonNull ActivityRecord r) {
+ if (!mWmService.mIsFixedRotationTransformEnabled) {
+ return false;
+ }
+ if (r.isFinishingFixedRotationTransform()) {
+ return false;
+ }
+ if (r.hasFixedRotationTransform()) {
+ // It has been set and not yet finished.
+ return true;
+ }
+ if (!mAppTransition.isTransitionSet()) {
+ // Apply normal rotation animation in case of the activity set different requested
+ // orientation without activity switch.
+ return false;
+ }
+ if (!mOpeningApps.contains(r)
+ // Without screen rotation, the rotation behavior of non-top visible activities is
+ // undefined. So the fixed rotated activity needs to cover the screen.
+ && r.findMainWindow() != mDisplayPolicy.getTopFullscreenOpaqueWindow()) {
+ return false;
+ }
+ final int rotation = rotationForActivityInDifferentOrientation(r);
+ if (rotation == NO_ROTATION) {
+ return false;
+ }
+ if (!r.getParent().matchParentBounds()) {
+ // Because the fixed rotated configuration applies to activity directly, if its parent
+ // has it own policy for bounds, the activity bounds based on parent is unknown.
+ return false;
+ }
+
+ startFixedRotationTransform(r, rotation);
+ mAppTransition.registerListenerLocked(new WindowManagerInternal.AppTransitionListener() {
+ void done() {
+ r.clearFixedRotationTransform();
+ mAppTransition.unregisterListener(this);
+ }
+
+ @Override
+ public void onAppTransitionFinishedLocked(IBinder token) {
+ if (token == r.token) {
+ done();
+ }
+ }
+
+ @Override
+ public void onAppTransitionCancelledLocked(int transit) {
+ done();
+ }
+
+ @Override
+ public void onAppTransitionTimeoutLocked() {
+ done();
+ }
+ });
+ return true;
+ }
+
+ /** @return {@code true} if the display orientation will be changed. */
+ boolean continueUpdateOrientationForDiffOrienLaunchingApp(WindowToken token) {
+ if (token != mFixedRotationLaunchingApp) {
+ return false;
+ }
+ if (updateOrientation()) {
+ sendNewConfiguration();
+ return true;
+ }
+ return false;
+ }
+
+ private void startFixedRotationTransform(WindowToken token, int rotation) {
+ mTmpConfiguration.unset();
+ final DisplayInfo info = computeScreenConfiguration(mTmpConfiguration, rotation);
+ final WmDisplayCutout cutout = calculateDisplayCutoutForRotation(rotation);
+ final DisplayFrames displayFrames = new DisplayFrames(mDisplayId, info, cutout);
+ token.applyFixedRotationTransform(info, displayFrames, mTmpConfiguration);
+ }
+
/**
* Update rotation of the display.
*
@@ -1622,6 +1753,63 @@
}
/**
+ * Compute display info and configuration according to the given rotation without changing
+ * current display.
+ */
+ DisplayInfo computeScreenConfiguration(Configuration outConfig, int rotation) {
+ final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
+ final int dw = rotated ? mBaseDisplayHeight : mBaseDisplayWidth;
+ final int dh = rotated ? mBaseDisplayWidth : mBaseDisplayHeight;
+ outConfig.windowConfiguration.getBounds().set(0, 0, dw, dh);
+
+ final int uiMode = getConfiguration().uiMode;
+ final DisplayCutout displayCutout =
+ calculateDisplayCutoutForRotation(rotation).getDisplayCutout();
+ computeScreenAppConfiguration(outConfig, dw, dh, rotation, uiMode, displayCutout);
+
+ final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo);
+ displayInfo.rotation = rotation;
+ displayInfo.logicalWidth = dw;
+ displayInfo.logicalHeight = dh;
+ final Rect appBounds = outConfig.windowConfiguration.getAppBounds();
+ displayInfo.appWidth = appBounds.width();
+ displayInfo.appHeight = appBounds.height();
+ displayInfo.displayCutout = displayCutout.isEmpty() ? null : displayCutout;
+ computeSizeRangesAndScreenLayout(displayInfo, rotated, uiMode, dw, dh,
+ mDisplayMetrics.density, outConfig);
+ return displayInfo;
+ }
+
+ /** Compute configuration related to application without changing current display. */
+ private void computeScreenAppConfiguration(Configuration outConfig, int dw, int dh,
+ int rotation, int uiMode, DisplayCutout displayCutout) {
+ final int appWidth = mDisplayPolicy.getNonDecorDisplayWidth(dw, dh, rotation, uiMode,
+ displayCutout);
+ final int appHeight = mDisplayPolicy.getNonDecorDisplayHeight(dw, dh, rotation, uiMode,
+ displayCutout);
+ mDisplayPolicy.getNonDecorInsetsLw(rotation, dw, dh, displayCutout, mTmpRect);
+ final int leftInset = mTmpRect.left;
+ final int topInset = mTmpRect.top;
+ // AppBounds at the root level should mirror the app screen size.
+ outConfig.windowConfiguration.setAppBounds(leftInset /* left */, topInset /* top */,
+ leftInset + appWidth /* right */, topInset + appHeight /* bottom */);
+ outConfig.windowConfiguration.setRotation(rotation);
+ outConfig.orientation = (dw <= dh) ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
+
+ final float density = mDisplayMetrics.density;
+ outConfig.screenWidthDp = (int) (mDisplayPolicy.getConfigDisplayWidth(dw, dh, rotation,
+ uiMode, displayCutout) / density);
+ outConfig.screenHeightDp = (int) (mDisplayPolicy.getConfigDisplayHeight(dw, dh, rotation,
+ uiMode, displayCutout) / density);
+ outConfig.compatScreenWidthDp = (int) (outConfig.screenWidthDp / mCompatibleScreenScale);
+ outConfig.compatScreenHeightDp = (int) (outConfig.screenHeightDp / mCompatibleScreenScale);
+
+ final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
+ outConfig.compatSmallestScreenWidthDp = computeCompatSmallestWidth(rotated, uiMode, dw,
+ dh, displayCutout);
+ }
+
+ /**
* Compute display configuration based on display properties and policy settings.
* Do not call if mDisplayReady == false.
*/
@@ -1629,42 +1817,19 @@
final DisplayInfo displayInfo = updateDisplayAndOrientation(config.uiMode, config);
calculateBounds(displayInfo, mTmpBounds);
config.windowConfiguration.setBounds(mTmpBounds);
+ config.windowConfiguration.setWindowingMode(getWindowingMode());
+ config.windowConfiguration.setDisplayWindowingMode(getWindowingMode());
final int dw = displayInfo.logicalWidth;
final int dh = displayInfo.logicalHeight;
- config.orientation = (dw <= dh) ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
- config.windowConfiguration.setWindowingMode(getWindowingMode());
- config.windowConfiguration.setDisplayWindowingMode(getWindowingMode());
- config.windowConfiguration.setRotation(displayInfo.rotation);
-
- final float density = mDisplayMetrics.density;
- config.screenWidthDp =
- (int)(mDisplayPolicy.getConfigDisplayWidth(dw, dh, displayInfo.rotation,
- config.uiMode, displayInfo.displayCutout) / density);
- config.screenHeightDp =
- (int)(mDisplayPolicy.getConfigDisplayHeight(dw, dh, displayInfo.rotation,
- config.uiMode, displayInfo.displayCutout) / density);
-
- mDisplayPolicy.getNonDecorInsetsLw(displayInfo.rotation, dw, dh,
- displayInfo.displayCutout, mTmpRect);
- final int leftInset = mTmpRect.left;
- final int topInset = mTmpRect.top;
- // appBounds at the root level should mirror the app screen size.
- config.windowConfiguration.setAppBounds(leftInset /* left */, topInset /* top */,
- leftInset + displayInfo.appWidth /* right */,
- topInset + displayInfo.appHeight /* bottom */);
- final boolean rotated = (displayInfo.rotation == Surface.ROTATION_90
- || displayInfo.rotation == Surface.ROTATION_270);
+ computeScreenAppConfiguration(config, dw, dh, displayInfo.rotation, config.uiMode,
+ displayInfo.displayCutout);
config.screenLayout = (config.screenLayout & ~Configuration.SCREENLAYOUT_ROUND_MASK)
| ((displayInfo.flags & Display.FLAG_ROUND) != 0
? Configuration.SCREENLAYOUT_ROUND_YES
: Configuration.SCREENLAYOUT_ROUND_NO);
- config.compatScreenWidthDp = (int)(config.screenWidthDp / mCompatibleScreenScale);
- config.compatScreenHeightDp = (int)(config.screenHeightDp / mCompatibleScreenScale);
- config.compatSmallestScreenWidthDp = computeCompatSmallestWidth(rotated, config.uiMode, dw,
- dh, displayInfo.displayCutout);
config.densityDpi = displayInfo.logicalDensityDpi;
config.colorMode =
@@ -2111,19 +2276,19 @@
/**
* In the general case, the orientation is computed from the above app windows first. If none of
* the above app windows specify orientation, the orientation is computed from the child window
- * container, e.g. {@link AppWindowToken#getOrientation(int)}.
+ * container, e.g. {@link ActivityRecord#getOrientation(int)}.
*/
@ScreenOrientation
@Override
int getOrientation() {
- final WindowManagerPolicy policy = mWmService.mPolicy;
+ mLastOrientationSource = null;
if (mIgnoreRotationForApps) {
return SCREEN_ORIENTATION_USER;
}
if (mWmService.mDisplayFrozen) {
- if (policy.isKeyguardLocked()) {
+ if (mWmService.mPolicy.isKeyguardLocked()) {
// Use the last orientation the while the display is frozen with the keyguard
// locked. This could be the keyguard forced orientation or from a SHOW_WHEN_LOCKED
// window. We don't want to check the show when locked window directly though as
@@ -2135,7 +2300,9 @@
return getLastOrientation();
}
}
- return mRootDisplayArea.getOrientation();
+ final int rootOrientation = mRootDisplayArea.getOrientation();
+ mLastOrientationSource = mRootDisplayArea.getLastOrientationSource();
+ return rootOrientation;
}
void updateDisplayInfo() {
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 3302445..a96e3a61 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1430,7 +1430,7 @@
}
}
- private void simulateLayoutDecorWindow(WindowState win, DisplayFrames displayFrames, int uiMode,
+ private void simulateLayoutDecorWindow(WindowState win, DisplayFrames displayFrames,
InsetsState insetsState, WindowFrames simulatedWindowFrames, Runnable layout) {
win.setSimulatedWindowFrames(simulatedWindowFrames);
try {
@@ -1454,7 +1454,7 @@
final WindowFrames simulatedWindowFrames = new WindowFrames();
if (mNavigationBar != null) {
simulateLayoutDecorWindow(
- mNavigationBar, displayFrames, uiMode, insetsState, simulatedWindowFrames,
+ mNavigationBar, displayFrames, insetsState, simulatedWindowFrames,
() -> layoutNavigationBar(displayFrames, uiMode, mLastNavVisible,
mLastNavTranslucent, mLastNavAllowedHidden,
mLastNotificationShadeForcesShowingNavigation,
@@ -1462,7 +1462,7 @@
}
if (mStatusBar != null) {
simulateLayoutDecorWindow(
- mStatusBar, displayFrames, uiMode, insetsState, simulatedWindowFrames,
+ mStatusBar, displayFrames, insetsState, simulatedWindowFrames,
() -> layoutStatusBar(displayFrames, mLastSystemUiFlags,
false /* isRealLayout */));
}
@@ -1536,7 +1536,7 @@
if (updateSysUiVisibility) {
updateSystemUiVisibilityLw();
}
- layoutScreenDecorWindows(displayFrames, null /* transientFrames */);
+ layoutScreenDecorWindows(displayFrames, null /* simulatedFrames */);
postAdjustDisplayFrames(displayFrames);
mLastNavVisible = navVisible;
mLastNavTranslucent = navTranslucent;
@@ -1939,6 +1939,7 @@
final int requestedSysUiFl = PolicyControl.getSystemUiVisibility(null, attrs);
final int sysUiFl = requestedSysUiFl | getImpliedSysUiFlagsForLayout(attrs);
+ displayFrames = win.getDisplayFrames(displayFrames);
final WindowFrames windowFrames = win.getWindowFrames();
sTmpLastParentFrame.set(windowFrames.mParentFrame);
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index e71371a..1a0dcb9 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -484,9 +484,11 @@
prepareNormalRotationAnimation();
}
- // The display is frozen now, give a remote handler (system ui) some time to reposition
- // things.
- startRemoteRotation(oldRotation, mRotation);
+ // TODO(b/147469351): Remove the restriction.
+ if (mDisplayContent.mFixedRotationLaunchingApp == null) {
+ // Give a remote handler (system ui) some time to reposition things.
+ startRemoteRotation(oldRotation, mRotation);
+ }
return true;
}
@@ -551,7 +553,15 @@
@VisibleForTesting
boolean shouldRotateSeamlessly(int oldRotation, int newRotation, boolean forceUpdate) {
final WindowState w = mDisplayPolicy.getTopFullscreenOpaqueWindow();
- if (w == null || w != mDisplayContent.mCurrentFocus) {
+ if (w == null) {
+ return false;
+ }
+ // Display doesn't need to be frozen because application has been started in correct
+ // rotation already, so the rest of the windows can use seamless rotation.
+ if (w.mToken.hasFixedRotationTransform()) {
+ return true;
+ }
+ if (w != mDisplayContent.mCurrentFocus) {
return false;
}
// We only enable seamless rotation if the top window has requested it and is in the
diff --git a/services/core/java/com/android/server/wm/SeamlessRotator.java b/services/core/java/com/android/server/wm/SeamlessRotator.java
index c621c48..024da88 100644
--- a/services/core/java/com/android/server/wm/SeamlessRotator.java
+++ b/services/core/java/com/android/server/wm/SeamlessRotator.java
@@ -20,6 +20,7 @@
import static android.view.Surface.ROTATION_90;
import android.graphics.Matrix;
+import android.graphics.Rect;
import android.os.IBinder;
import android.view.DisplayInfo;
import android.view.Surface.Rotation;
@@ -27,6 +28,7 @@
import android.view.SurfaceControl.Transaction;
import com.android.server.wm.utils.CoordinateTransforms;
+import com.android.server.wm.utils.InsetUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
@@ -45,34 +47,51 @@
private final float[] mFloat9 = new float[9];
private final int mOldRotation;
private final int mNewRotation;
+ private final int mRotationDelta;
+ private final int mW;
+ private final int mH;
public SeamlessRotator(@Rotation int oldRotation, @Rotation int newRotation, DisplayInfo info) {
mOldRotation = oldRotation;
mNewRotation = newRotation;
+ mRotationDelta = DisplayContent.deltaRotation(oldRotation, newRotation);
final boolean flipped = info.rotation == ROTATION_90 || info.rotation == ROTATION_270;
- final int h = flipped ? info.logicalWidth : info.logicalHeight;
- final int w = flipped ? info.logicalHeight : info.logicalWidth;
+ mH = flipped ? info.logicalWidth : info.logicalHeight;
+ mW = flipped ? info.logicalHeight : info.logicalWidth;
final Matrix tmp = new Matrix();
- CoordinateTransforms.transformLogicalToPhysicalCoordinates(oldRotation, w, h, mTransform);
- CoordinateTransforms.transformPhysicalToLogicalCoordinates(newRotation, w, h, tmp);
+ CoordinateTransforms.transformLogicalToPhysicalCoordinates(oldRotation, mW, mH, mTransform);
+ CoordinateTransforms.transformPhysicalToLogicalCoordinates(newRotation, mW, mH, tmp);
mTransform.postConcat(tmp);
}
/**
- * Applies a transform to the {@link WindowState} surface that undoes the effect of the global
- * display rotation.
+ * Applies a transform to the {@link WindowContainer} surface that undoes the effect of the
+ * global display rotation.
*/
- public void unrotate(Transaction transaction, WindowState win) {
+ public void unrotate(Transaction transaction, WindowContainer win) {
transaction.setMatrix(win.getSurfaceControl(), mTransform, mFloat9);
-
// WindowState sets the position of the window so transform the position and update it.
final float[] winSurfacePos = {win.mLastSurfacePosition.x, win.mLastSurfacePosition.y};
mTransform.mapPoints(winSurfacePos);
transaction.setPosition(win.getSurfaceControl(), winSurfacePos[0], winSurfacePos[1]);
}
+ /** Rotates the frame from {@link #mNewRotation} to {@link #mOldRotation}. */
+ void unrotateFrame(Rect inOut) {
+ if (mRotationDelta == ROTATION_90) {
+ inOut.set(inOut.top, mH - inOut.right, inOut.bottom, mH - inOut.left);
+ } else if (mRotationDelta == ROTATION_270) {
+ inOut.set(mW - inOut.bottom, inOut.left, mW - inOut.top, inOut.right);
+ }
+ }
+
+ /** Rotates the insets from {@link #mNewRotation} to {@link #mOldRotation}. */
+ void unrotateInsets(Rect inOut) {
+ InsetUtils.rotateInsets(inOut, mRotationDelta);
+ }
+
/**
* Returns the rotation of the display before it started rotating.
*
@@ -95,10 +114,8 @@
* it.
*/
public void finish(WindowState win, boolean timeout) {
- mTransform.reset();
final Transaction t = win.getPendingTransaction();
- t.setMatrix(win.mSurfaceControl, mTransform, mFloat9);
- t.setPosition(win.mSurfaceControl, win.mLastSurfacePosition.x, win.mLastSurfacePosition.y);
+ finish(t, win);
if (win.mWinAnimator.mSurfaceController != null && !timeout) {
t.deferTransactionUntil(win.mSurfaceControl,
win.getDeferTransactionBarrier(), win.getFrameNumber());
@@ -107,6 +124,13 @@
}
}
+ /** Removes the transform and restore to the original last position. */
+ void finish(Transaction t, WindowContainer win) {
+ mTransform.reset();
+ t.setMatrix(win.mSurfaceControl, mTransform, mFloat9);
+ t.setPosition(win.mSurfaceControl, win.mLastSurfacePosition.x, win.mLastSurfacePosition.y);
+ }
+
public void dump(PrintWriter pw) {
pw.print("{old="); pw.print(mOldRotation); pw.print(", new="); pw.print(mNewRotation);
pw.print("}");
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 294c36a..da996dc 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -141,6 +141,12 @@
@ActivityInfo.ScreenOrientation
protected int mOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
+ /**
+ * The window container which decides its orientation since the last time
+ * {@link #getOrientation(int) was called.
+ */
+ protected WindowContainer mLastOrientationSource;
+
private final Pools.SynchronizedPool<ForAllWindowsConsumerWrapper> mConsumerWrapperPool =
new Pools.SynchronizedPool<>(3);
@@ -1061,6 +1067,7 @@
* @return The orientation as specified by this branch or the window hierarchy.
*/
int getOrientation(int candidate) {
+ mLastOrientationSource = null;
if (!fillsParent()) {
// Ignore containers that don't completely fill their parents.
return SCREEN_ORIENTATION_UNSET;
@@ -1072,6 +1079,7 @@
// if none of the children have a better candidate for the orientation.
if (mOrientation != SCREEN_ORIENTATION_UNSET
&& mOrientation != SCREEN_ORIENTATION_UNSPECIFIED) {
+ mLastOrientationSource = this;
return mOrientation;
}
@@ -1087,6 +1095,7 @@
// can find one. Else return SCREEN_ORIENTATION_BEHIND so the caller can choose to
// look behind this container.
candidate = orientation;
+ mLastOrientationSource = wc;
continue;
}
@@ -1100,6 +1109,7 @@
ProtoLog.v(WM_DEBUG_ORIENTATION, "%s is requesting orientation %d (%s)",
wc.toString(), orientation,
ActivityInfo.screenOrientationToString(orientation));
+ mLastOrientationSource = wc;
return orientation;
}
}
@@ -1108,6 +1118,22 @@
}
/**
+ * @return The deepest source which decides the orientation of this window container since the
+ * last time {@link #getOrientation(int) was called.
+ */
+ @Nullable
+ WindowContainer getLastOrientationSource() {
+ final WindowContainer source = mLastOrientationSource;
+ if (source != null && source != this) {
+ final WindowContainer nextSource = source.getLastOrientationSource();
+ if (nextSource != null) {
+ return nextSource;
+ }
+ }
+ return source;
+ }
+
+ /**
* Returns true if this container is opaque and fills all the space made available by its parent
* container.
*
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 59eee9c..240f566 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -120,6 +120,11 @@
public void onAppTransitionCancelledLocked(int transit) {}
/**
+ * Called when an app transition is timed out.
+ */
+ public void onAppTransitionTimeoutLocked() {}
+
+ /**
* Called when an app transition gets started
*
* @param transit transition type indicating what kind of transition gets run, must be one
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index e01e8d22..e1f713e 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -412,6 +412,10 @@
private static final int ANIMATION_COMPLETED_TIMEOUT_MS = 5000;
+ // TODO(b/143053092): Remove the settings if it becomes stable.
+ private static final String FIXED_ROTATION_TRANSFORM_SETTING_NAME = "fixed_rotation_transform";
+ boolean mIsFixedRotationTransformEnabled;
+
final WindowManagerConstants mConstants;
final WindowTracing mWindowTracing;
@@ -738,6 +742,8 @@
DEVELOPMENT_ENABLE_SIZECOMPAT_FREEFORM);
private final Uri mRenderShadowsInCompositorUri = Settings.Global.getUriFor(
DEVELOPMENT_RENDER_SHADOWS_IN_COMPOSITOR);
+ private final Uri mFixedRotationTransformUri = Settings.Global.getUriFor(
+ FIXED_ROTATION_TRANSFORM_SETTING_NAME);
public SettingsObserver() {
super(new Handler());
@@ -762,6 +768,8 @@
UserHandle.USER_ALL);
resolver.registerContentObserver(mRenderShadowsInCompositorUri, false, this,
UserHandle.USER_ALL);
+ resolver.registerContentObserver(mFixedRotationTransformUri, false, this,
+ UserHandle.USER_ALL);
}
@Override
@@ -805,6 +813,11 @@
return;
}
+ if (mFixedRotationTransformUri.equals(uri)) {
+ updateFixedRotationTransform();
+ return;
+ }
+
@UpdateAnimationScaleMode
final int mode;
if (mWindowAnimationScaleUri.equals(uri)) {
@@ -821,6 +834,12 @@
mH.sendMessage(m);
}
+ void loadSettings() {
+ updateSystemUiSettings();
+ updatePointerLocation();
+ updateFixedRotationTransform();
+ }
+
void updateSystemUiSettings() {
boolean changed;
synchronized (mGlobalLock) {
@@ -884,6 +903,11 @@
mAtmService.mSizeCompatFreeform = sizeCompatFreeform;
}
+
+ void updateFixedRotationTransform() {
+ mIsFixedRotationTransformEnabled = Settings.Global.getInt(mContext.getContentResolver(),
+ FIXED_ROTATION_TRANSFORM_SETTING_NAME, 0) != 0;
+ }
}
private void setShadowRenderer() {
@@ -1651,7 +1675,7 @@
outFrame, outContentInsets, outStableInsets, outDisplayCutout)) {
res |= WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_SYSTEM_BARS;
}
- outInsetsState.set(displayContent.getInsetsPolicy().getInsetsForDispatch(win),
+ outInsetsState.set(win.getInsetsState(),
win.mClient instanceof IWindow.Stub /* copySource */);
if (mInTouchMode) {
@@ -2389,7 +2413,7 @@
outStableInsets);
outCutout.set(win.getWmDisplayCutout().getDisplayCutout());
outBackdropFrame.set(win.getBackdropFrame(win.getFrameLw()));
- outInsetsState.set(displayContent.getInsetsPolicy().getInsetsForDispatch(win),
+ outInsetsState.set(win.getInsetsState(),
win.mClient instanceof IWindow.Stub /* copySource */);
if (DEBUG) {
Slog.v(TAG_WM, "Relayout given client " + client.asBinder()
@@ -4574,8 +4598,7 @@
mTaskSnapshotController.systemReady();
mHasWideColorGamutSupport = queryWideColorGamutSupport();
mHasHdrSupport = queryHdrSupport();
- UiThread.getHandler().post(mSettingsObserver::updateSystemUiSettings);
- UiThread.getHandler().post(mSettingsObserver::updatePointerLocation);
+ UiThread.getHandler().post(mSettingsObserver::loadSettings);
IVrManager vrManager = IVrManager.Stub.asInterface(
ServiceManager.getService(Context.VR_SERVICE));
if (vrManager != null) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 46081c5..78d6b9631 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -696,6 +696,11 @@
return;
}
+ if (mToken.hasFixedRotationTransform()) {
+ // The transform of its surface is handled by fixed rotation.
+ return;
+ }
+
if (mPendingSeamlessRotate != null) {
oldRotation = mPendingSeamlessRotate.getOldRotation();
}
@@ -1000,6 +1005,7 @@
final boolean isFullscreenAndFillsDisplay = !inMultiWindowMode() && matchesDisplayBounds();
final boolean windowsAreFloating = task != null && task.isFloating();
final DisplayContent dc = getDisplayContent();
+ final DisplayInfo displayInfo = getDisplayInfo();
final WindowFrames windowFrames = getLayoutingWindowFrames();
mInsetFrame.set(getBounds());
@@ -1086,7 +1092,7 @@
layoutXDiff = mInsetFrame.left - windowFrames.mContainingFrame.left;
layoutYDiff = mInsetFrame.top - windowFrames.mContainingFrame.top;
layoutContainingFrame = mInsetFrame;
- mTmpRect.set(0, 0, dc.getDisplayInfo().logicalWidth, dc.getDisplayInfo().logicalHeight);
+ mTmpRect.set(0, 0, displayInfo.logicalWidth, displayInfo.logicalHeight);
subtractInsets(windowFrames.mDisplayFrame, layoutContainingFrame, layoutDisplayFrame,
mTmpRect);
if (!layoutInParentFrame()) {
@@ -1161,9 +1167,8 @@
windowFrames.mDisplayFrame);
windowFrames.calculateDockedDividerInsets(c.getDisplayCutout().getSafeInsets());
} else {
- getDisplayContent().getBounds(mTmpRect);
- windowFrames.calculateInsets(
- windowsAreFloating, isFullscreenAndFillsDisplay, mTmpRect);
+ windowFrames.calculateInsets(windowsAreFloating, isFullscreenAndFillsDisplay,
+ getDisplayFrames(dc.mDisplayFrames).mUnrestricted);
}
windowFrames.setDisplayCutout(
@@ -1186,12 +1191,8 @@
if (mIsWallpaper && (fw != windowFrames.mFrame.width()
|| fh != windowFrames.mFrame.height())) {
- final DisplayContent displayContent = getDisplayContent();
- if (displayContent != null) {
- final DisplayInfo displayInfo = displayContent.getDisplayInfo();
- getDisplayContent().mWallpaperController.updateWallpaperOffset(this,
- displayInfo.logicalWidth, displayInfo.logicalHeight, false);
- }
+ dc.mWallpaperController.updateWallpaperOffset(this,
+ displayInfo.logicalWidth, displayInfo.logicalHeight, false /* sync */);
}
// Calculate relative frame
@@ -1467,9 +1468,28 @@
}
}
+ DisplayFrames getDisplayFrames(DisplayFrames originalFrames) {
+ final DisplayFrames diplayFrames = mToken.getFixedRotationTransformDisplayFrames();
+ if (diplayFrames != null) {
+ return diplayFrames;
+ }
+ return originalFrames;
+ }
+
DisplayInfo getDisplayInfo() {
- final DisplayContent displayContent = getDisplayContent();
- return displayContent != null ? displayContent.getDisplayInfo() : null;
+ final DisplayInfo displayInfo = mToken.getFixedRotationTransformDisplayInfo();
+ if (displayInfo != null) {
+ return displayInfo;
+ }
+ return getDisplayContent().getDisplayInfo();
+ }
+
+ InsetsState getInsetsState() {
+ final InsetsState insetsState = mToken.getFixedRotationTransformInsetsState();
+ if (insetsState != null) {
+ return insetsState;
+ }
+ return getDisplayContent().getInsetsPolicy().getInsetsForDispatch(this);
}
@Override
@@ -1990,6 +2010,11 @@
}
private boolean matchesDisplayBounds() {
+ final Rect displayBounds = mToken.getFixedRotationTransformDisplayBounds();
+ if (displayBounds != null) {
+ // If the rotated display bounds are available, the window bounds are also rotated.
+ return displayBounds.equals(getBounds());
+ }
return getDisplayContent().getBounds().equals(getBounds());
}
@@ -4786,7 +4811,7 @@
if (!displayContent.isDefaultDisplay && !displayContent.supportsSystemDecorations()) {
// On a different display there is no system decor. Crop the window
// by the screen boundaries.
- final DisplayInfo displayInfo = displayContent.getDisplayInfo();
+ final DisplayInfo displayInfo = getDisplayInfo();
policyCrop.set(0, 0, mWindowFrames.mCompatFrame.width(),
mWindowFrames.mCompatFrame.height());
policyCrop.intersect(-mWindowFrames.mCompatFrame.left, -mWindowFrames.mCompatFrame.top,
@@ -5012,7 +5037,7 @@
return;
}
- final DisplayInfo displayInfo = getDisplayContent().getDisplayInfo();
+ final DisplayInfo displayInfo = getDisplayInfo();
anim.initialize(mWindowFrames.mFrame.width(), mWindowFrames.mFrame.height(),
displayInfo.appWidth, displayInfo.appHeight);
anim.restrictDuration(MAX_ANIMATION_DURATION);
@@ -5550,7 +5575,7 @@
}
/**
- * If the transient frame is set, the computed result won't be used in real layout. So this
+ * If the simulated frame is set, the computed result won't be used in real layout. So this
* frames must be cleared when the simulated computation is done.
*/
void setSimulatedWindowFrames(WindowFrames windowFrames) {
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index 43cd66d..1180566 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -36,15 +36,20 @@
import static com.android.server.wm.WindowTokenProto.WINDOW_CONTAINER;
import android.annotation.CallSuper;
+import android.content.res.Configuration;
+import android.graphics.Rect;
import android.os.Debug;
import android.os.IBinder;
import android.util.proto.ProtoOutputStream;
+import android.view.DisplayInfo;
+import android.view.InsetsState;
import android.view.SurfaceControl;
import com.android.server.policy.WindowManagerPolicy;
import com.android.server.protolog.common.ProtoLog;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.Comparator;
/**
@@ -84,6 +89,57 @@
/** The owner has {@link android.Manifest.permission#MANAGE_APP_TOKENS} */
final boolean mOwnerCanManageAppTokens;
+ private FixedRotationTransformState mFixedRotationTransformState;
+
+ /**
+ * Used to fix the transform of the token to be rotated to a rotation different than it's
+ * display. The window frames and surfaces corresponding to this token will be layouted and
+ * rotated by the given rotated display info, frames and insets.
+ */
+ private static class FixedRotationTransformState {
+ final DisplayInfo mDisplayInfo;
+ final DisplayFrames mDisplayFrames;
+ final InsetsState mInsetsState;
+ final Configuration mRotatedOverrideConfiguration;
+ final SeamlessRotator mRotator;
+ final ArrayList<WindowContainer<?>> mRotatedContainers = new ArrayList<>();
+ boolean mIsTransforming = true;
+
+ FixedRotationTransformState(DisplayInfo rotatedDisplayInfo,
+ DisplayFrames rotatedDisplayFrames, InsetsState rotatedInsetsState,
+ Configuration rotatedConfig, int currentRotation) {
+ mDisplayInfo = rotatedDisplayInfo;
+ mDisplayFrames = rotatedDisplayFrames;
+ mInsetsState = rotatedInsetsState;
+ mRotatedOverrideConfiguration = rotatedConfig;
+ // This will use unrotate as rotate, so the new and old rotation are inverted.
+ mRotator = new SeamlessRotator(rotatedDisplayInfo.rotation, currentRotation,
+ rotatedDisplayInfo);
+ }
+
+ /**
+ * Transforms the window container from the next rotation to the current rotation for
+ * showing the window in a display with different rotation.
+ */
+ void transform(WindowContainer<?> container) {
+ mRotator.unrotate(container.getPendingTransaction(), container);
+ if (!mRotatedContainers.contains(container)) {
+ mRotatedContainers.add(container);
+ }
+ }
+
+ /**
+ * Resets the transformation of the window containers which have been rotated. This should
+ * be called when the window has the same rotation as display.
+ */
+ void resetTransform() {
+ for (int i = mRotatedContainers.size() - 1; i >= 0; i--) {
+ final WindowContainer<?> c = mRotatedContainers.get(i);
+ mRotator.finish(c.getPendingTransaction(), c);
+ }
+ }
+ }
+
/**
* Compares two child window of this token and returns -1 if the first is lesser than the
* second in terms of z-order and 1 otherwise.
@@ -274,6 +330,107 @@
return builder;
}
+ boolean hasFixedRotationTransform() {
+ return mFixedRotationTransformState != null;
+ }
+
+ boolean isFinishingFixedRotationTransform() {
+ return mFixedRotationTransformState != null
+ && !mFixedRotationTransformState.mIsTransforming;
+ }
+
+ boolean isFixedRotationTransforming() {
+ return mFixedRotationTransformState != null
+ && mFixedRotationTransformState.mIsTransforming;
+ }
+
+ DisplayInfo getFixedRotationTransformDisplayInfo() {
+ return isFixedRotationTransforming() ? mFixedRotationTransformState.mDisplayInfo : null;
+ }
+
+ DisplayFrames getFixedRotationTransformDisplayFrames() {
+ return isFixedRotationTransforming() ? mFixedRotationTransformState.mDisplayFrames : null;
+ }
+
+ Rect getFixedRotationTransformDisplayBounds() {
+ return isFixedRotationTransforming()
+ ? mFixedRotationTransformState.mRotatedOverrideConfiguration.windowConfiguration
+ .getBounds()
+ : null;
+ }
+
+ InsetsState getFixedRotationTransformInsetsState() {
+ return isFixedRotationTransforming() ? mFixedRotationTransformState.mInsetsState : null;
+ }
+
+ /** Applies the rotated layout environment to this token in the simulated rotated display. */
+ void applyFixedRotationTransform(DisplayInfo info, DisplayFrames displayFrames,
+ Configuration config) {
+ if (mFixedRotationTransformState != null) {
+ return;
+ }
+ final InsetsState insetsState = new InsetsState();
+ mDisplayContent.getDisplayPolicy().simulateLayoutDisplay(displayFrames, insetsState,
+ mDisplayContent.getConfiguration().uiMode);
+ mFixedRotationTransformState = new FixedRotationTransformState(info, displayFrames,
+ insetsState, new Configuration(config), mDisplayContent.getRotation());
+ onConfigurationChanged(getParent().getConfiguration());
+ }
+
+ /** Clears the transformation and continue updating the orientation change of display. */
+ void clearFixedRotationTransform() {
+ if (mFixedRotationTransformState == null) {
+ return;
+ }
+ mFixedRotationTransformState.resetTransform();
+ // Clear the flag so if the display will be updated to the same orientation, the transform
+ // won't take effect. The state is cleared at the end, because it is used to indicate that
+ // other windows can use seamless rotation when applying rotation to display.
+ mFixedRotationTransformState.mIsTransforming = false;
+ final boolean changed =
+ mDisplayContent.continueUpdateOrientationForDiffOrienLaunchingApp(this);
+ // If it is not the launching app or the display is not rotated, make sure the merged
+ // override configuration is restored from parent.
+ if (!changed) {
+ onMergedOverrideConfigurationChanged();
+ }
+ mFixedRotationTransformState = null;
+ }
+
+ @Override
+ void resolveOverrideConfiguration(Configuration newParentConfig) {
+ super.resolveOverrideConfiguration(newParentConfig);
+ if (isFixedRotationTransforming()) {
+ // Apply the rotated configuration to current resolved configuration, so the merged
+ // override configuration can update to the same state.
+ getResolvedOverrideConfiguration().updateFrom(
+ mFixedRotationTransformState.mRotatedOverrideConfiguration);
+ }
+ }
+
+ @Override
+ void updateSurfacePosition() {
+ super.updateSurfacePosition();
+ if (isFixedRotationTransforming()) {
+ // The window is layouted in a simulated rotated display but the real display hasn't
+ // rotated, so here transforms its surface to fit in the real display.
+ mFixedRotationTransformState.transform(this);
+ }
+ }
+
+ /**
+ * Converts the rotated animation frames and insets back to display space for local animation.
+ * It should only be called when {@link #hasFixedRotationTransform} is true.
+ */
+ void unrotateAnimationFrames(Rect outFrame, Rect outInsets, Rect outStableInsets,
+ Rect outSurfaceInsets) {
+ final SeamlessRotator rotator = mFixedRotationTransformState.mRotator;
+ rotator.unrotateFrame(outFrame);
+ rotator.unrotateInsets(outInsets);
+ rotator.unrotateInsets(outStableInsets);
+ rotator.unrotateInsets(outSurfaceInsets);
+ }
+
@CallSuper
@Override
public void dumpDebug(ProtoOutputStream proto, long fieldId,
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 3b6816a..1a8f2a6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -995,6 +995,35 @@
}
@Test
+ public void testApplyTopFixedRotationTransform() {
+ mWm.mIsFixedRotationTransformEnabled = true;
+ final Configuration config90 = new Configuration();
+ mDisplayContent.getDisplayRotation().setRotation(ROTATION_90);
+ mDisplayContent.computeScreenConfiguration(config90);
+ mDisplayContent.onRequestedOverrideConfigurationChanged(config90);
+
+ final Configuration config = new Configuration();
+ mDisplayContent.getDisplayRotation().setRotation(Surface.ROTATION_0);
+ mDisplayContent.computeScreenConfiguration(config);
+ mDisplayContent.onRequestedOverrideConfigurationChanged(config);
+
+ final ActivityRecord app = mAppWindow.mActivityRecord;
+ mDisplayContent.prepareAppTransition(WindowManager.TRANSIT_ACTIVITY_OPEN,
+ false /* alwaysKeepCurrent */);
+ mDisplayContent.mOpeningApps.add(app);
+ app.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+
+ assertTrue(app.isFixedRotationTransforming());
+ assertEquals(config.orientation, mDisplayContent.getConfiguration().orientation);
+ assertEquals(config90.orientation, app.getConfiguration().orientation);
+
+ mDisplayContent.mAppTransition.notifyAppTransitionFinishedLocked(app.token);
+
+ assertFalse(app.hasFixedRotationTransform());
+ assertEquals(config90.orientation, mDisplayContent.getConfiguration().orientation);
+ }
+
+ @Test
public void testRemoteRotation() {
DisplayContent dc = createNewDisplay();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
index da807d8..cf7411e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
@@ -638,7 +638,7 @@
mBuilder.build();
final WindowState win = mock(WindowState.class);
- win.mActivityRecord = mock(ActivityRecord.class);
+ win.mToken = win.mActivityRecord = mock(ActivityRecord.class);
final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
attrs.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
diff --git a/tests/net/common/java/android/net/NetworkScoreTest.kt b/tests/net/common/java/android/net/NetworkScoreTest.kt
index 3e10992..a7e33b3 100644
--- a/tests/net/common/java/android/net/NetworkScoreTest.kt
+++ b/tests/net/common/java/android/net/NetworkScoreTest.kt
@@ -16,7 +16,6 @@
package android.net
-import android.net.NetworkScore.Metrics.BANDWIDTH_UNKNOWN
import android.net.NetworkScore.POLICY_DEFAULT_SUBSCRIPTION
import android.net.NetworkScore.POLICY_IGNORE_ON_WIFI
import android.net.NetworkScore.RANGE_MEDIUM
@@ -35,20 +34,24 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
class NetworkScoreTest {
+ private fun makeScoreBuilder() = NetworkScore.Builder()
+ .setLegacyScore(TEST_SCORE)
+ .addPolicy(POLICY_IGNORE_ON_WIFI)
+ .addPolicy(POLICY_DEFAULT_SUBSCRIPTION)
+ .setExiting(false)
+ .setEndToEndMetrics(NetworkScore.Metrics(145 /* latency */,
+ 2500 /* downlinkBandwidth */, 1430 /* uplinkBandwidth */))
+ .setRange(RANGE_MEDIUM)
+ .setSignalStrength(400)
+
@Test
fun testParcelNetworkScore() {
val defaultCap = NetworkCapabilities()
- val builder = NetworkScore.Builder().setLegacyScore(TEST_SCORE)
- assertEquals(TEST_SCORE, builder.build().getLegacyScore())
- assertParcelSane(builder.build(), 7)
+ val legacyBuilder = NetworkScore.Builder().setLegacyScore(TEST_SCORE)
+ assertEquals(TEST_SCORE, legacyBuilder.build().getLegacyScore())
+ assertParcelSane(legacyBuilder.build(), 7)
- builder.addPolicy(POLICY_IGNORE_ON_WIFI)
- .addPolicy(POLICY_DEFAULT_SUBSCRIPTION)
- .setLinkLayerMetrics(NetworkScore.Metrics(44 /* latency */,
- 380 /* downlinkBandwidth */, BANDWIDTH_UNKNOWN /* uplinkBandwidth */))
- .setEndToEndMetrics(NetworkScore.Metrics(11 /* latency */,
- BANDWIDTH_UNKNOWN /* downlinkBandwidth */, 100_000 /* uplinkBandwidth */))
- .setRange(NetworkScore.RANGE_MEDIUM)
+ val builder = makeScoreBuilder()
assertParcelSane(builder.build(), 7)
builder.clearPolicy(POLICY_IGNORE_ON_WIFI)
val ns = builder.build()
@@ -79,14 +82,7 @@
@Test
fun testBuilderEquals() {
- val ns = NetworkScore.Builder()
- .addPolicy(POLICY_IGNORE_ON_WIFI)
- .addPolicy(POLICY_DEFAULT_SUBSCRIPTION)
- .setExiting(true)
- .setEndToEndMetrics(NetworkScore.Metrics(145, 2500, 1430))
- .setRange(RANGE_MEDIUM)
- .setSignalStrength(400)
- .build()
+ val ns = makeScoreBuilder().build()
assertEquals(ns, NetworkScore.Builder(ns).build())
}
}
diff --git a/tests/net/java/com/android/server/connectivity/NetworkRankerTest.kt b/tests/net/java/com/android/server/connectivity/NetworkRankerTest.kt
index 86c9116..a6b371a 100644
--- a/tests/net/java/com/android/server/connectivity/NetworkRankerTest.kt
+++ b/tests/net/java/com/android/server/connectivity/NetworkRankerTest.kt
@@ -16,6 +16,7 @@
package com.android.server.connectivity
+import android.net.NetworkCapabilities
import android.net.NetworkRequest
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
@@ -35,6 +36,7 @@
private fun makeNai(satisfy: Boolean, score: Int) = mock(NetworkAgentInfo::class.java).also {
doReturn(satisfy).`when`(it).satisfies(any())
doReturn(score).`when`(it).currentScore
+ it.networkCapabilities = NetworkCapabilities()
}
@Test