Merge "Add media output group panel index"
diff --git a/api/current.txt b/api/current.txt
index 7a5753a..24478f4 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -56486,6 +56486,7 @@
method public int describeContents();
method @NonNull public android.util.Size getMaxSize();
method @NonNull public android.util.Size getMinSize();
+ method @Nullable public String getStyle();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.view.inline.InlinePresentationSpec> CREATOR;
}
@@ -56690,9 +56691,11 @@
public final class InlineSuggestionsRequest implements android.os.Parcelable {
method public int describeContents();
+ method @Nullable public android.os.Bundle getExtras();
method @NonNull public String getHostPackageName();
method public int getMaxSuggestionCount();
method @NonNull public java.util.List<android.view.inline.InlinePresentationSpec> getPresentationSpecs();
+ method @NonNull public android.os.LocaleList getSupportedLocales();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.InlineSuggestionsRequest> CREATOR;
field public static final int SUGGESTION_COUNT_UNLIMITED = 2147483647; // 0x7fffffff
@@ -56702,7 +56705,9 @@
ctor public InlineSuggestionsRequest.Builder(@NonNull java.util.List<android.view.inline.InlinePresentationSpec>);
method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder addPresentationSpecs(@NonNull android.view.inline.InlinePresentationSpec);
method @NonNull public android.view.inputmethod.InlineSuggestionsRequest build();
+ method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setExtras(@Nullable android.os.Bundle);
method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setMaxSuggestionCount(int);
+ method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setSupportedLocales(@NonNull android.os.LocaleList);
}
public final class InlineSuggestionsResponse implements android.os.Parcelable {
diff --git a/api/system-current.txt b/api/system-current.txt
index 773cd4d..5b6bd21 100755
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -14081,14 +14081,6 @@
}
-package android.view.inline {
-
- public final class InlinePresentationSpec implements android.os.Parcelable {
- method @Nullable public String getStyle();
- }
-
-}
-
package android.webkit {
public abstract class CookieManager {
diff --git a/cmds/statsd/src/StatsLogProcessor.cpp b/cmds/statsd/src/StatsLogProcessor.cpp
index 6e7f081..97e0a2e 100644
--- a/cmds/statsd/src/StatsLogProcessor.cpp
+++ b/cmds/statsd/src/StatsLogProcessor.cpp
@@ -248,6 +248,9 @@
#endif
event->updateValue(8 /*user id field id*/, userId, INT);
+ // If this event is a rollback event, then the following bits in the event
+ // are invalid and we will need to update them with the values we pulled
+ // from disk.
if (is_rollback) {
int bit = trainInfo.requiresStaging ? 1 : 0;
event->updateValue(3 /*requires staging field id*/, bit, INT);
@@ -294,19 +297,28 @@
if (!trainInfo->experimentIds.empty()) {
int64_t firstId = trainInfo->experimentIds.at(0);
+ auto& ids = trainInfo->experimentIds;
switch (trainInfo->status) {
case android::util::BINARY_PUSH_STATE_CHANGED__STATE__INSTALL_SUCCESS:
- trainInfo->experimentIds.push_back(firstId + 1);
+ if (find(ids.begin(), ids.end(), firstId + 1) == ids.end()) {
+ ids.push_back(firstId + 1);
+ }
break;
case android::util::BINARY_PUSH_STATE_CHANGED__STATE__INSTALLER_ROLLBACK_INITIATED:
- trainInfo->experimentIds.push_back(firstId + 2);
+ if (find(ids.begin(), ids.end(), firstId + 2) == ids.end()) {
+ ids.push_back(firstId + 2);
+ }
break;
case android::util::BINARY_PUSH_STATE_CHANGED__STATE__INSTALLER_ROLLBACK_SUCCESS:
- trainInfo->experimentIds.push_back(firstId + 3);
+ if (find(ids.begin(), ids.end(), firstId + 3) == ids.end()) {
+ ids.push_back(firstId + 3);
+ }
break;
}
}
+ // If this event is a rollback event, the following fields are invalid and
+ // need to be replaced by the fields stored to disk.
if (is_rollback) {
trainInfo->requiresStaging = trainInfoOnDisk.requiresStaging;
trainInfo->rollbackEnabled = trainInfoOnDisk.rollbackEnabled;
@@ -356,6 +368,7 @@
}
bool readTrainInfoSuccess = false;
InstallTrainInfo trainInfoOnDisk;
+ // We use the package name of the event as the train name.
readTrainInfoSuccess = StorageManager::readTrainInfo(packageNameIn, trainInfoOnDisk);
if (!readTrainInfoSuccess) {
@@ -365,13 +378,20 @@
if (trainInfoOnDisk.experimentIds.empty()) {
return vector<int64_t>();
}
+
+ int64_t firstId = trainInfoOnDisk.experimentIds[0];
+ auto& ids = trainInfoOnDisk.experimentIds;
switch (rollbackTypeIn) {
case android::util::WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE:
- trainInfoOnDisk.experimentIds.push_back(trainInfoOnDisk.experimentIds[0] + 4);
+ if (find(ids.begin(), ids.end(), firstId + 4) == ids.end()) {
+ ids.push_back(firstId + 4);
+ }
StorageManager::writeTrainInfo(trainInfoOnDisk);
break;
case android::util::WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS:
- trainInfoOnDisk.experimentIds.push_back(trainInfoOnDisk.experimentIds[0] + 5);
+ if (find(ids.begin(), ids.end(), firstId + 5) == ids.end()) {
+ ids.push_back(firstId + 5);
+ }
StorageManager::writeTrainInfo(trainInfoOnDisk);
break;
}
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/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index a305948..ea26ede 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -4720,19 +4720,19 @@
/**
* Returns the {@code uid} of the owner of a network connection.
*
- * @param protocol The protocol of the connection. Only {@code IPPROTO_TCP} and
- * {@code IPPROTO_UDP} currently supported.
+ * @param protocol The protocol of the connection. Only {@code IPPROTO_TCP} and {@code
+ * IPPROTO_UDP} currently supported.
* @param local The local {@link InetSocketAddress} of a connection.
* @param remote The remote {@link InetSocketAddress} of a connection.
- *
* @return {@code uid} if the connection is found and the app has permission to observe it
- * (e.g., if it is associated with the calling VPN app's tunnel) or
- * {@link android.os.Process#INVALID_UID} if the connection is not found.
- * Throws {@link SecurityException} if the caller is not the active VPN for the current user.
- * Throws {@link IllegalArgumentException} if an unsupported protocol is requested.
+ * (e.g., if it is associated with the calling VPN app's VpnService tunnel) or {@link
+ * android.os.Process#INVALID_UID} if the connection is not found.
+ * @throws {@link SecurityException} if the caller is not the active VpnService for the current
+ * user.
+ * @throws {@link IllegalArgumentException} if an unsupported protocol is requested.
*/
- public int getConnectionOwnerUid(int protocol, @NonNull InetSocketAddress local,
- @NonNull InetSocketAddress remote) {
+ public int getConnectionOwnerUid(
+ int protocol, @NonNull InetSocketAddress local, @NonNull InetSocketAddress remote) {
ConnectionInfo connectionInfo = new ConnectionInfo(protocol, local, remote);
try {
return mService.getConnectionOwnerUid(connectionInfo);
diff --git a/core/java/android/net/NetworkScore.java b/core/java/android/net/NetworkScore.java
index ae17378..d2e59eb 100644
--- a/core/java/android/net/NetworkScore.java
+++ b/core/java/android/net/NetworkScore.java
@@ -21,7 +21,6 @@
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.annotation.TestApi;
-import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@@ -87,7 +86,7 @@
/** toString */
public String toString() {
return "latency = " + latencyMs + " downlinkBandwidth = " + downlinkBandwidthKBps
- + "uplinkBandwidth = " + uplinkBandwidthKBps;
+ + " uplinkBandwidth = " + uplinkBandwidthKBps;
}
@NonNull
@@ -354,17 +353,27 @@
private Metrics mLinkLayerMetrics = new Metrics(Metrics.LATENCY_UNKNOWN,
Metrics.BANDWIDTH_UNKNOWN, Metrics.BANDWIDTH_UNKNOWN);
@NonNull
- private Metrics mEndToMetrics = new Metrics(Metrics.LATENCY_UNKNOWN,
+ private Metrics mEndToEndMetrics = new Metrics(Metrics.LATENCY_UNKNOWN,
Metrics.BANDWIDTH_UNKNOWN, Metrics.BANDWIDTH_UNKNOWN);
private int mSignalStrength = UNKNOWN_SIGNAL_STRENGTH;
private int mRange = RANGE_UNKNOWN;
private boolean mExiting = false;
private int mLegacyScore = 0;
- @NonNull private Bundle mExtensions = new Bundle();
/** Create a new builder. */
public Builder() { }
+ /** @hide */
+ public Builder(@NonNull final NetworkScore source) {
+ mPolicy = source.mPolicy;
+ mLinkLayerMetrics = source.mLinkLayerMetrics;
+ mEndToEndMetrics = source.mEndToEndMetrics;
+ mSignalStrength = source.mSignalStrength;
+ mRange = source.mRange;
+ mExiting = source.mExiting;
+ mLegacyScore = source.mLegacyScore;
+ }
+
/** Add a policy flag. */
@NonNull public Builder addPolicy(@Policy final int policy) {
mPolicy |= policy;
@@ -385,7 +394,7 @@
/** Set the end-to-end metrics. */
@NonNull public Builder setEndToEndMetrics(@NonNull final Metrics endToEndMetrics) {
- mEndToMetrics = endToEndMetrics;
+ mEndToEndMetrics = endToEndMetrics;
return this;
}
@@ -417,7 +426,7 @@
/** Build the NetworkScore object represented by this builder. */
@NonNull public NetworkScore build() {
- return new NetworkScore(mPolicy, mLinkLayerMetrics, mEndToMetrics,
+ return new NetworkScore(mPolicy, mLinkLayerMetrics, mEndToEndMetrics,
mSignalStrength, mRange, mExiting, mLegacyScore);
}
}
diff --git a/core/java/android/view/inline/InlinePresentationSpec.java b/core/java/android/view/inline/InlinePresentationSpec.java
index 406e599..8bda339 100644
--- a/core/java/android/view/inline/InlinePresentationSpec.java
+++ b/core/java/android/view/inline/InlinePresentationSpec.java
@@ -18,7 +18,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.SystemApi;
import android.os.Parcelable;
import android.util.Size;
@@ -53,15 +52,6 @@
return null;
}
- /**
- * @hide
- */
- @SystemApi
- public @Nullable String getStyle() {
- return mStyle;
- }
-
-
/** @hide */
@DataClass.Suppress({"setMaxSize", "setMinSize"})
abstract static class BaseBuilder {
@@ -114,6 +104,17 @@
return mMaxSize;
}
+ /**
+ * The fully qualified resource name of the UI style resource identifier, defaults to {@code
+ * null}.
+ *
+ * <p> The value can be obtained by calling {@code Resources#getResourceName(int)}.
+ */
+ @DataClass.Generated.Member
+ public @Nullable String getStyle() {
+ return mStyle;
+ }
+
@Override
@DataClass.Generated.Member
public String toString() {
@@ -283,10 +284,10 @@
}
@DataClass.Generated(
- time = 1581117017522L,
+ time = 1581736227796L,
codegenVersion = "1.0.14",
sourceFile = "frameworks/base/core/java/android/view/inline/InlinePresentationSpec.java",
- inputSignatures = "private final @android.annotation.NonNull android.util.Size mMinSize\nprivate final @android.annotation.NonNull android.util.Size mMaxSize\nprivate final @android.annotation.Nullable java.lang.String mStyle\nprivate static java.lang.String defaultStyle()\npublic @android.annotation.SystemApi @android.annotation.Nullable java.lang.String getStyle()\nclass InlinePresentationSpec extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genBuilder=true)\nclass BaseBuilder extends java.lang.Object implements []")
+ inputSignatures = "private final @android.annotation.NonNull android.util.Size mMinSize\nprivate final @android.annotation.NonNull android.util.Size mMaxSize\nprivate final @android.annotation.Nullable java.lang.String mStyle\nprivate static java.lang.String defaultStyle()\nclass InlinePresentationSpec extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genBuilder=true)\nclass BaseBuilder extends java.lang.Object implements []")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/view/inputmethod/InlineSuggestionsRequest.java b/core/java/android/view/inputmethod/InlineSuggestionsRequest.java
index be9370a..5700dda 100644
--- a/core/java/android/view/inputmethod/InlineSuggestionsRequest.java
+++ b/core/java/android/view/inputmethod/InlineSuggestionsRequest.java
@@ -19,7 +19,9 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
+import android.os.Bundle;
import android.os.IBinder;
+import android.os.LocaleList;
import android.os.Parcelable;
import android.view.inline.InlinePresentationSpec;
@@ -60,6 +62,13 @@
private @NonNull String mHostPackageName;
/**
+ * The IME provided locales for the request. If non-empty, the inline suggestions should
+ * return languages from the supported locales. If not provided, it'll default to system locale.
+ */
+ private @NonNull LocaleList mSupportedLocales;
+
+ // TODO(b/149609075): the generated code needs to be manually fixed due to the bug.
+ /**
* The host input token of the IME that made the request. This will be set by the system for
* safety reasons.
*
@@ -68,12 +77,9 @@
private @Nullable IBinder mHostInputToken;
/**
- * @hide
- * @see {@link #mHostPackageName}.
+ * The extras state propagated from the IME to pass extra data.
*/
- public void setHostPackageName(@NonNull String hostPackageName) {
- mHostPackageName = hostPackageName;
- }
+ private @Nullable Bundle mExtras;
/**
* @hide
@@ -95,10 +101,20 @@
return ActivityThread.currentPackageName();
}
+ private static LocaleList defaultSupportedLocales() {
+ return LocaleList.getDefault();
+ }
+
+ @Nullable
private static IBinder defaultHostInputToken() {
return null;
}
+ @Nullable
+ private static Bundle defaultExtras() {
+ return null;
+ }
+
/** @hide */
abstract static class BaseBuilder {
abstract Builder setPresentationSpecs(@NonNull List<InlinePresentationSpec> value);
@@ -128,7 +144,9 @@
int maxSuggestionCount,
@NonNull List<InlinePresentationSpec> presentationSpecs,
@NonNull String hostPackageName,
- @Nullable IBinder hostInputToken) {
+ @NonNull LocaleList supportedLocales,
+ @Nullable IBinder hostInputToken,
+ @Nullable Bundle extras) {
this.mMaxSuggestionCount = maxSuggestionCount;
this.mPresentationSpecs = presentationSpecs;
com.android.internal.util.AnnotationValidations.validate(
@@ -136,7 +154,11 @@
this.mHostPackageName = hostPackageName;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, mHostPackageName);
+ this.mSupportedLocales = supportedLocales;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSupportedLocales);
this.mHostInputToken = hostInputToken;
+ this.mExtras = extras;
onConstructed();
}
@@ -171,6 +193,15 @@
}
/**
+ * The IME provided locales for the request. If non-empty, the inline suggestions should
+ * return languages from the supported locales. If not provided, it'll default to system locale.
+ */
+ @DataClass.Generated.Member
+ public @NonNull LocaleList getSupportedLocales() {
+ return mSupportedLocales;
+ }
+
+ /**
* The host input token of the IME that made the request. This will be set by the system for
* safety reasons.
*
@@ -181,6 +212,14 @@
return mHostInputToken;
}
+ /**
+ * The extras state propagated from the IME to pass extra data.
+ */
+ @DataClass.Generated.Member
+ public @Nullable Bundle getExtras() {
+ return mExtras;
+ }
+
@Override
@DataClass.Generated.Member
public String toString() {
@@ -191,7 +230,9 @@
"maxSuggestionCount = " + mMaxSuggestionCount + ", " +
"presentationSpecs = " + mPresentationSpecs + ", " +
"hostPackageName = " + mHostPackageName + ", " +
- "hostInputToken = " + mHostInputToken +
+ "supportedLocales = " + mSupportedLocales + ", " +
+ "hostInputToken = " + mHostInputToken + ", " +
+ "extras = " + mExtras +
" }";
}
@@ -211,7 +252,9 @@
&& mMaxSuggestionCount == that.mMaxSuggestionCount
&& java.util.Objects.equals(mPresentationSpecs, that.mPresentationSpecs)
&& java.util.Objects.equals(mHostPackageName, that.mHostPackageName)
- && java.util.Objects.equals(mHostInputToken, that.mHostInputToken);
+ && java.util.Objects.equals(mSupportedLocales, that.mSupportedLocales)
+ && java.util.Objects.equals(mHostInputToken, that.mHostInputToken)
+ && java.util.Objects.equals(mExtras, that.mExtras);
}
@Override
@@ -224,7 +267,9 @@
_hash = 31 * _hash + mMaxSuggestionCount;
_hash = 31 * _hash + java.util.Objects.hashCode(mPresentationSpecs);
_hash = 31 * _hash + java.util.Objects.hashCode(mHostPackageName);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mSupportedLocales);
_hash = 31 * _hash + java.util.Objects.hashCode(mHostInputToken);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mExtras);
return _hash;
}
@@ -235,12 +280,15 @@
// void parcelFieldName(Parcel dest, int flags) { ... }
byte flg = 0;
- if (mHostInputToken != null) flg |= 0x8;
+ if (mHostInputToken != null) flg |= 0x10;
+ if (mExtras != null) flg |= 0x20;
dest.writeByte(flg);
dest.writeInt(mMaxSuggestionCount);
dest.writeParcelableList(mPresentationSpecs, flags);
dest.writeString(mHostPackageName);
+ dest.writeTypedObject(mSupportedLocales, flags);
if (mHostInputToken != null) dest.writeStrongBinder(mHostInputToken);
+ if (mExtras != null) dest.writeBundle(mExtras);
}
@Override
@@ -259,7 +307,9 @@
List<InlinePresentationSpec> presentationSpecs = new ArrayList<>();
in.readParcelableList(presentationSpecs, InlinePresentationSpec.class.getClassLoader());
String hostPackageName = in.readString();
- IBinder hostInputToken = (flg & 0x8) == 0 ? null : in.readStrongBinder();
+ LocaleList supportedLocales = (LocaleList) in.readTypedObject(LocaleList.CREATOR);
+ IBinder hostInputToken = (flg & 0x10) == 0 ? null : in.readStrongBinder();
+ Bundle extras = (flg & 0x20) == 0 ? null : in.readBundle();
this.mMaxSuggestionCount = maxSuggestionCount;
this.mPresentationSpecs = presentationSpecs;
@@ -268,7 +318,11 @@
this.mHostPackageName = hostPackageName;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, mHostPackageName);
+ this.mSupportedLocales = supportedLocales;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSupportedLocales);
this.mHostInputToken = hostInputToken;
+ this.mExtras = extras;
onConstructed();
}
@@ -297,7 +351,9 @@
private int mMaxSuggestionCount;
private @NonNull List<InlinePresentationSpec> mPresentationSpecs;
private @NonNull String mHostPackageName;
+ private @NonNull LocaleList mSupportedLocales;
private @Nullable IBinder mHostInputToken;
+ private @Nullable Bundle mExtras;
private long mBuilderFieldsSet = 0L;
@@ -368,6 +424,18 @@
}
/**
+ * The IME provided locales for the request. If non-empty, the inline suggestions should
+ * return languages from the supported locales. If not provided, it'll default to system locale.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setSupportedLocales(@NonNull LocaleList value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x8;
+ mSupportedLocales = value;
+ return this;
+ }
+
+ /**
* The host input token of the IME that made the request. This will be set by the system for
* safety reasons.
*
@@ -377,15 +445,26 @@
@Override
@NonNull Builder setHostInputToken(@Nullable IBinder value) {
checkNotUsed();
- mBuilderFieldsSet |= 0x8;
+ mBuilderFieldsSet |= 0x10;
mHostInputToken = value;
return this;
}
+ /**
+ * The extras state propagated from the IME to pass extra data.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setExtras(@Nullable Bundle value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x20;
+ mExtras = value;
+ return this;
+ }
+
/** Builds the instance. This builder should not be touched after calling this! */
public @NonNull InlineSuggestionsRequest build() {
checkNotUsed();
- mBuilderFieldsSet |= 0x10; // Mark builder used
+ mBuilderFieldsSet |= 0x40; // Mark builder used
if ((mBuilderFieldsSet & 0x1) == 0) {
mMaxSuggestionCount = defaultMaxSuggestionCount();
@@ -394,18 +473,26 @@
mHostPackageName = defaultHostPackageName();
}
if ((mBuilderFieldsSet & 0x8) == 0) {
+ mSupportedLocales = defaultSupportedLocales();
+ }
+ if ((mBuilderFieldsSet & 0x10) == 0) {
mHostInputToken = defaultHostInputToken();
}
+ if ((mBuilderFieldsSet & 0x20) == 0) {
+ mExtras = defaultExtras();
+ }
InlineSuggestionsRequest o = new InlineSuggestionsRequest(
mMaxSuggestionCount,
mPresentationSpecs,
mHostPackageName,
- mHostInputToken);
+ mSupportedLocales,
+ mHostInputToken,
+ mExtras);
return o;
}
private void checkNotUsed() {
- if ((mBuilderFieldsSet & 0x10) != 0) {
+ if ((mBuilderFieldsSet & 0x40) != 0) {
throw new IllegalStateException(
"This Builder should not be reused. Use a new Builder instance instead");
}
@@ -413,10 +500,10 @@
}
@DataClass.Generated(
- time = 1581555687721L,
+ time = 1581747892762L,
codegenVersion = "1.0.14",
sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestionsRequest.java",
- inputSignatures = "public static final int SUGGESTION_COUNT_UNLIMITED\nprivate final int mMaxSuggestionCount\nprivate final @android.annotation.NonNull java.util.List<android.view.inline.InlinePresentationSpec> mPresentationSpecs\nprivate @android.annotation.NonNull java.lang.String mHostPackageName\nprivate @android.annotation.Nullable android.os.IBinder mHostInputToken\npublic void setHostPackageName(java.lang.String)\npublic void setHostInputToken(android.os.IBinder)\nprivate void onConstructed()\nprivate static int defaultMaxSuggestionCount()\nprivate static java.lang.String defaultHostPackageName()\nprivate static android.os.IBinder defaultHostInputToken()\nclass InlineSuggestionsRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genBuilder=true)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setPresentationSpecs(java.util.List<android.view.inline.InlinePresentationSpec>)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nclass BaseBuilder extends java.lang.Object implements []")
+ inputSignatures = "public static final int SUGGESTION_COUNT_UNLIMITED\nprivate final int mMaxSuggestionCount\nprivate final @android.annotation.NonNull java.util.List<android.view.inline.InlinePresentationSpec> mPresentationSpecs\nprivate @android.annotation.NonNull java.lang.String mHostPackageName\nprivate @android.annotation.NonNull android.os.LocaleList mSupportedLocales\nprivate @android.annotation.Nullable android.os.IBinder mHostInputToken\nprivate @android.annotation.Nullable android.os.Bundle mExtras\npublic void setHostInputToken(android.os.IBinder)\nprivate void onConstructed()\nprivate static int defaultMaxSuggestionCount()\nprivate static java.lang.String defaultHostPackageName()\nprivate static android.os.LocaleList defaultSupportedLocales()\nprivate static @android.annotation.Nullable android.os.IBinder defaultHostInputToken()\nprivate static @android.annotation.Nullable android.os.Bundle defaultExtras()\nclass InlineSuggestionsRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genBuilder=true)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setPresentationSpecs(java.util.List<android.view.inline.InlinePresentationSpec>)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nclass BaseBuilder extends java.lang.Object implements []")
@Deprecated
private void __metadata() {}
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/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 60a30d3..0f36260 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -3817,8 +3817,9 @@
return avoidBadWifi();
}
-
private void rematchForAvoidBadWifiUpdate() {
+ ensureRunningOnConnectivityServiceThread();
+ mixInAllNetworkScores();
rematchAllNetworksAndRequests();
for (NetworkAgentInfo nai: mNetworkAgentInfos.values()) {
if (nai.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
@@ -7035,9 +7036,45 @@
}
}
+ /**
+ * Re-mixin all network scores.
+ * This is called when some global setting like avoidBadWifi has changed.
+ * TODO : remove this when all usages have been removed.
+ */
+ private void mixInAllNetworkScores() {
+ ensureRunningOnConnectivityServiceThread();
+ for (final NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
+ nai.setNetworkScore(mixInNetworkScore(nai, nai.getNetworkScore()));
+ }
+ }
+
+ /**
+ * Mix in the Connectivity-managed parts of the NetworkScore.
+ * @param nai The NAI this score applies to.
+ * @param sourceScore the score sent by the network agent, or the previous score of this NAI.
+ * @return A new score with the Connectivity-managed parts mixed in.
+ */
+ @NonNull
+ private NetworkScore mixInNetworkScore(@NonNull final NetworkAgentInfo nai,
+ @NonNull final NetworkScore sourceScore) {
+ final NetworkScore.Builder score = new NetworkScore.Builder(sourceScore);
+
+ // TODO : this should be done in Telephony. It should be handled per-network because
+ // it's a carrier-dependent config.
+ if (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
+ if (mMultinetworkPolicyTracker.getAvoidBadWifi()) {
+ score.clearPolicy(NetworkScore.POLICY_IGNORE_ON_WIFI);
+ } else {
+ score.addPolicy(NetworkScore.POLICY_IGNORE_ON_WIFI);
+ }
+ }
+
+ return score.build();
+ }
+
private void updateNetworkScore(NetworkAgentInfo nai, NetworkScore ns) {
if (VDBG || DDBG) log("updateNetworkScore for " + nai.toShortString() + " to " + ns);
- nai.setNetworkScore(ns);
+ nai.setNetworkScore(mixInNetworkScore(nai, ns));
rematchAllNetworksAndRequests();
sendUpdatedScoreToFactories(nai);
}
@@ -7519,6 +7556,13 @@
*/
public int getConnectionOwnerUid(ConnectionInfo connectionInfo) {
final Vpn vpn = enforceActiveVpnOrNetworkStackPermission();
+
+ // Only VpnService based VPNs should be able to get this information.
+ if (vpn != null && vpn.getActiveAppVpnType() != VpnManager.TYPE_VPN_SERVICE) {
+ throw new SecurityException(
+ "getConnectionOwnerUid() not allowed for non-VpnService VPNs");
+ }
+
if (connectionInfo.protocol != IPPROTO_TCP && connectionInfo.protocol != IPPROTO_UDP) {
throw new IllegalArgumentException("Unsupported protocol " + connectionInfo.protocol);
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 1da07ef..5df3e1f 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4866,7 +4866,7 @@
}
@GuardedBy("this")
- private final boolean attachApplicationLocked(IApplicationThread thread,
+ private boolean attachApplicationLocked(@NonNull IApplicationThread thread,
int pid, int callingUid, long startSeq) {
// Find the application record that is being attached... either via
@@ -5287,6 +5287,9 @@
@Override
public final void attachApplication(IApplicationThread thread, long startSeq) {
+ if (thread == null) {
+ throw new SecurityException("Invalid application interface");
+ }
synchronized (this) {
int callingPid = Binder.getCallingPid();
final int callingUid = Binder.getCallingUid();
diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java
index af47430..821653a 100644
--- a/services/core/java/com/android/server/compat/PlatformCompat.java
+++ b/services/core/java/com/android/server/compat/PlatformCompat.java
@@ -235,8 +235,8 @@
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- checkCompatChangeReadAndLogPermission();
if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, "platform_compat", pw)) return;
+ checkCompatChangeReadAndLogPermission();
mCompatConfig.dumpConfig(pw);
}
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 1a68f1b..77f4093 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -794,10 +794,10 @@
// ignore
}
mContext.unbindService(mConnection);
- mConnection = null;
+ cleanupVpnStateLocked();
} else if (mVpnRunner != null) {
+ // cleanupVpnStateLocked() is called from mVpnRunner.exit()
mVpnRunner.exit();
- mVpnRunner = null;
}
try {
@@ -1108,7 +1108,6 @@
*/
public synchronized ParcelFileDescriptor establish(VpnConfig config) {
// Check if the caller is already prepared.
- UserManager mgr = UserManager.get(mContext);
if (Binder.getCallingUid() != mOwnerUID) {
return null;
}
@@ -1122,10 +1121,7 @@
long token = Binder.clearCallingIdentity();
try {
// Restricted users are not allowed to create VPNs, they are tied to Owner
- UserInfo user = mgr.getUserInfo(mUserHandle);
- if (user.isRestricted()) {
- throw new SecurityException("Restricted users cannot establish VPNs");
- }
+ enforceNotRestrictedUser();
ResolveInfo info = AppGlobals.getPackageManager().resolveService(intent,
null, 0, mUserHandle);
@@ -1547,24 +1543,30 @@
public void interfaceRemoved(String interfaze) {
synchronized (Vpn.this) {
if (interfaze.equals(mInterface) && jniCheck(interfaze) == 0) {
- mStatusIntent = null;
- mNetworkCapabilities.setUids(null);
- mConfig = null;
- mInterface = null;
if (mConnection != null) {
mContext.unbindService(mConnection);
- mConnection = null;
- agentDisconnect();
+ cleanupVpnStateLocked();
} else if (mVpnRunner != null) {
- // agentDisconnect must be called from mVpnRunner.exit()
+ // cleanupVpnStateLocked() is called from mVpnRunner.exit()
mVpnRunner.exit();
- mVpnRunner = null;
}
}
}
}
};
+ private void cleanupVpnStateLocked() {
+ mStatusIntent = null;
+ mNetworkCapabilities.setUids(null);
+ mConfig = null;
+ mInterface = null;
+
+ // Unconditionally clear both VpnService and VpnRunner fields.
+ mVpnRunner = null;
+ mConnection = null;
+ agentDisconnect();
+ }
+
private void enforceControlPermission() {
mContext.enforceCallingPermission(Manifest.permission.CONTROL_VPN, "Unauthorized Caller");
}
@@ -1677,6 +1679,25 @@
}
/**
+ * Gets the currently running App-based VPN type
+ *
+ * @return the {@link VpnManager.VpnType}. {@link VpnManager.TYPE_VPN_NONE} if not running an
+ * app-based VPN. While VpnService-based VPNs are always app VPNs and LegacyVpn is always
+ * Settings-based, the Platform VPNs can be initiated by both apps and Settings.
+ */
+ public synchronized int getActiveAppVpnType() {
+ if (VpnConfig.LEGACY_VPN.equals(mPackage)) {
+ return VpnManager.TYPE_VPN_NONE;
+ }
+
+ if (mVpnRunner != null && mVpnRunner instanceof IkeV2VpnRunner) {
+ return VpnManager.TYPE_VPN_PLATFORM;
+ } else {
+ return VpnManager.TYPE_VPN_SERVICE;
+ }
+ }
+
+ /**
* @param uid The target uid.
*
* @return {@code true} if {@code uid} is included in one of the mBlockedUidsAsToldToNetd
@@ -1804,6 +1825,17 @@
throw new IllegalStateException("Unable to find IPv4 default gateway");
}
+ private void enforceNotRestrictedUser() {
+ Binder.withCleanCallingIdentity(() -> {
+ final UserManager mgr = UserManager.get(mContext);
+ final UserInfo user = mgr.getUserInfo(mUserHandle);
+
+ if (user.isRestricted()) {
+ throw new SecurityException("Restricted users cannot configure VPNs");
+ }
+ });
+ }
+
/**
* Start legacy VPN, controlling native daemons as needed. Creates a
* secondary thread to perform connection work, returning quickly.
@@ -2024,7 +2056,25 @@
public abstract void run();
- protected abstract void exit();
+ /**
+ * Disconnects the NetworkAgent and cleans up all state related to the VpnRunner.
+ *
+ * <p>All outer Vpn instance state is cleaned up in cleanupVpnStateLocked()
+ */
+ protected abstract void exitVpnRunner();
+
+ /**
+ * Triggers the cleanup of the VpnRunner, and additionally cleans up Vpn instance-wide state
+ *
+ * <p>This method ensures that simple calls to exit() will always clean up global state
+ * properly.
+ */
+ protected final void exit() {
+ synchronized (Vpn.this) {
+ exitVpnRunner();
+ cleanupVpnStateLocked();
+ }
+ }
}
interface IkeV2VpnRunnerCallback {
@@ -2354,17 +2404,6 @@
}
/**
- * Triggers cleanup of outer class' state
- *
- * <p>Can be called from any thread, as it does not mutate state in the Ikev2VpnRunner.
- */
- private void cleanupVpnState() {
- synchronized (Vpn.this) {
- agentDisconnect();
- }
- }
-
- /**
* Cleans up all Ikev2VpnRunner internal state
*
* <p>This method MUST always be called on the mExecutor thread in order to ensure
@@ -2383,10 +2422,7 @@
}
@Override
- public void exit() {
- // Cleanup outer class' state immediately, otherwise race conditions may ensue.
- cleanupVpnState();
-
+ public void exitVpnRunner() {
mExecutor.execute(() -> {
shutdownVpnRunner();
});
@@ -2485,10 +2521,9 @@
/** Tears down this LegacyVpn connection */
@Override
- public void exit() {
+ public void exitVpnRunner() {
// We assume that everything is reset after stopping the daemons.
interrupt();
- agentDisconnect();
try {
mContext.unregisterReceiver(mBroadcastReceiver);
} catch (IllegalArgumentException e) {}
@@ -2761,6 +2796,7 @@
checkNotNull(keyStore, "KeyStore missing");
verifyCallingUidAndPackage(packageName);
+ enforceNotRestrictedUser();
final byte[] encodedProfile = profile.encode();
if (encodedProfile.length > MAX_VPN_PROFILE_SIZE_BYTES) {
@@ -2796,6 +2832,7 @@
checkNotNull(keyStore, "KeyStore missing");
verifyCallingUidAndPackage(packageName);
+ enforceNotRestrictedUser();
Binder.withCleanCallingIdentity(
() -> {
@@ -2838,6 +2875,8 @@
checkNotNull(packageName, "No package name provided");
checkNotNull(keyStore, "KeyStore missing");
+ enforceNotRestrictedUser();
+
// Prepare VPN for startup
if (!prepare(packageName, null /* newPackage */, VpnManager.TYPE_VPN_PLATFORM)) {
throw new SecurityException("User consent not granted for package " + packageName);
@@ -2903,6 +2942,8 @@
public synchronized void stopVpnProfile(@NonNull String packageName) {
checkNotNull(packageName, "No package name provided");
+ enforceNotRestrictedUser();
+
// To stop the VPN profile, the caller must be the current prepared package and must be
// running an Ikev2VpnProfile.
if (!isCurrentPreparedPackage(packageName) && mVpnRunner instanceof IkeV2VpnRunner) {
diff --git a/services/core/java/com/android/server/media/BluetoothRouteProvider.java b/services/core/java/com/android/server/media/BluetoothRouteProvider.java
index 8379614..33e01bd 100644
--- a/services/core/java/com/android/server/media/BluetoothRouteProvider.java
+++ b/services/core/java/com/android/server/media/BluetoothRouteProvider.java
@@ -166,6 +166,20 @@
}
}
+ @Nullable
+ MediaRoute2Info getSelectedRoute() {
+ return (mSelectedRoute == null) ? null : mSelectedRoute.route;
+ }
+
+ @NonNull
+ List<MediaRoute2Info> getTransferableRoutes() {
+ List<MediaRoute2Info> routes = getAllBluetoothRoutes();
+ if (mSelectedRoute != null) {
+ routes.remove(mSelectedRoute.route);
+ }
+ return routes;
+ }
+
@NonNull
List<MediaRoute2Info> getAllBluetoothRoutes() {
ArrayList<MediaRoute2Info> routes = new ArrayList<>();
@@ -175,9 +189,12 @@
return routes;
}
- @Nullable
- String getSelectedRouteId() {
- return mSelectedRoute == null ? null : mSelectedRoute.route.getId();
+ boolean setSelectedRouteVolume(int volume) {
+ if (mSelectedRoute == null) return false;
+ mSelectedRoute.route = new MediaRoute2Info.Builder(mSelectedRoute.route)
+ .setVolume(volume)
+ .build();
+ return true;
}
private void notifyBluetoothRoutesUpdated() {
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
index efb9515..fe118e5 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
@@ -16,6 +16,7 @@
package com.android.server.media;
+import android.annotation.NonNull;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -171,7 +172,7 @@
};
public interface Callback {
- void onAddProviderService(MediaRoute2ProviderServiceProxy proxy);
- void onRemoveProviderService(MediaRoute2ProviderServiceProxy proxy);
+ void onAddProviderService(@NonNull MediaRoute2ProviderServiceProxy proxy);
+ void onRemoveProviderService(@NonNull MediaRoute2ProviderServiceProxy proxy);
}
}
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index a268720..3588916 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -87,6 +87,11 @@
mContext = context;
}
+ ////////////////////////////////////////////////////////////////
+ //// Calls from MediaRouter2
+ //// - Should not have @NonNull/@Nullable on any arguments
+ ////////////////////////////////////////////////////////////////
+
@NonNull
public List<MediaRoute2Info> getSystemRoutes() {
final int uid = Binder.getCallingUid();
@@ -135,9 +140,11 @@
}
}
- public void registerRouter2(@NonNull IMediaRouter2 router,
- @NonNull String packageName) {
+ public void registerRouter2(IMediaRouter2 router, String packageName) {
Objects.requireNonNull(router, "router must not be null");
+ if (TextUtils.isEmpty(packageName)) {
+ throw new IllegalArgumentException("packageName must not be empty");
+ }
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
@@ -155,7 +162,7 @@
}
}
- public void unregisterRouter2(@NonNull IMediaRouter2 router) {
+ public void unregisterRouter2(IMediaRouter2 router) {
Objects.requireNonNull(router, "router must not be null");
final long token = Binder.clearCallingIdentity();
@@ -168,33 +175,35 @@
}
}
- public void registerManager(@NonNull IMediaRouter2Manager manager,
- @NonNull String packageName) {
- Objects.requireNonNull(manager, "manager must not be null");
- //TODO: should check permission
- final boolean trusted = true;
-
- final int uid = Binder.getCallingUid();
- final int pid = Binder.getCallingPid();
- final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
+ public void setDiscoveryRequestWithRouter2(IMediaRouter2 router,
+ RouteDiscoveryPreference preference) {
+ Objects.requireNonNull(router, "router must not be null");
+ Objects.requireNonNull(preference, "preference must not be null");
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- registerManagerLocked(manager, uid, pid, packageName, userId, trusted);
+ RouterRecord routerRecord = mAllRouterRecords.get(router.asBinder());
+ if (routerRecord == null) {
+ Slog.w(TAG, "Ignoring updating discoveryRequest of null routerRecord.");
+ return;
+ }
+ setDiscoveryRequestWithRouter2Locked(routerRecord, preference);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
- public void unregisterManager(@NonNull IMediaRouter2Manager manager) {
- Objects.requireNonNull(manager, "manager must not be null");
+ public void setRouteVolumeWithRouter2(IMediaRouter2 router,
+ MediaRoute2Info route, int volume) {
+ Objects.requireNonNull(router, "router must not be null");
+ Objects.requireNonNull(route, "route must not be null");
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- unregisterManagerLocked(manager, false);
+ setRouteVolumeWithRouter2Locked(router, route, volume);
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -209,7 +218,7 @@
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- requestCreateSessionLocked(router, route, requestId, sessionHints);
+ requestCreateSessionWithRouter2Locked(router, route, requestId, sessionHints);
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -227,7 +236,7 @@
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- selectRouteLocked(router, uniqueSessionId, route);
+ selectRouteWithRouter2Locked(router, uniqueSessionId, route);
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -246,7 +255,7 @@
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- deselectRouteLocked(router, uniqueSessionId, route);
+ deselectRouteWithRouter2Locked(router, uniqueSessionId, route);
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -271,6 +280,21 @@
}
}
+ public void setSessionVolumeWithRouter2(IMediaRouter2 router, String uniqueSessionId,
+ int volume) {
+ Objects.requireNonNull(router, "router must not be null");
+ Objects.requireNonNull(uniqueSessionId, "uniqueSessionId must not be null");
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ setSessionVolumeWithRouter2Locked(router, uniqueSessionId, volume);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
public void releaseSessionWithRouter2(IMediaRouter2 router, String uniqueSessionId) {
Objects.requireNonNull(router, "router must not be null");
if (TextUtils.isEmpty(uniqueSessionId)) {
@@ -280,64 +304,60 @@
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- releaseSessionLocked(router, uniqueSessionId);
+ releaseSessionWithRouter2Locked(router, uniqueSessionId);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
- public void setDiscoveryRequestWithRouter2(IMediaRouter2 router,
- RouteDiscoveryPreference preference) {
- Objects.requireNonNull(router, "router must not be null");
- Objects.requireNonNull(preference, "preference must not be null");
+ ////////////////////////////////////////////////////////////////
+ //// Calls from MediaRouter2Manager
+ //// - Should not have @NonNull/@Nullable on any arguments
+ ////////////////////////////////////////////////////////////////
+ @NonNull
+ public List<RoutingSessionInfo> getActiveSessions(IMediaRouter2Manager manager) {
+ Objects.requireNonNull(manager, "manager must not be null");
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- RouterRecord routerRecord = mAllRouterRecords.get(router.asBinder());
- setDiscoveryRequestLocked(routerRecord, preference);
+ return getActiveSessionsLocked(manager);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
- public void setRouteVolumeWithRouter2(IMediaRouter2 router,
- MediaRoute2Info route, int volume) {
- Objects.requireNonNull(router, "router must not be null");
- Objects.requireNonNull(route, "route must not be null");
+ public void registerManager(IMediaRouter2Manager manager, String packageName) {
+ Objects.requireNonNull(manager, "manager must not be null");
+ if (TextUtils.isEmpty(packageName)) {
+ throw new IllegalArgumentException("packageName must not be empty");
+ }
+
+ final boolean trusted = true;
+
+ final int uid = Binder.getCallingUid();
+ final int pid = Binder.getCallingPid();
+ final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- setRouteVolumeLocked(router, route, volume);
+ registerManagerLocked(manager, uid, pid, packageName, userId, trusted);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
- public void setSessionVolumeWithRouter2(IMediaRouter2 router, String sessionId, int volume) {
- Objects.requireNonNull(router, "router must not be null");
- Objects.requireNonNull(sessionId, "sessionId must not be null");
+ public void unregisterManager(IMediaRouter2Manager manager) {
+ Objects.requireNonNull(manager, "manager must not be null");
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- setSessionVolumeLocked(router, sessionId, volume);
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- public void requestCreateSessionWithManager(IMediaRouter2Manager manager, String packageName,
- MediaRoute2Info route, int requestId) {
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mLock) {
- requestClientCreateSessionLocked(manager, packageName, route, requestId);
+ unregisterManagerLocked(manager, false);
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -352,7 +372,78 @@
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- setRouteVolumeLocked(manager, route, volume);
+ setRouteVolumeWithManagerLocked(manager, route, volume);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ public void requestCreateSessionWithManager(IMediaRouter2Manager manager, String packageName,
+ MediaRoute2Info route, int requestId) {
+ Objects.requireNonNull(manager, "manager must not be null");
+ if (TextUtils.isEmpty(packageName)) {
+ throw new IllegalArgumentException("packageName must not be empty");
+ }
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ requestCreateSessionWithManagerLocked(manager, packageName, route, requestId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ public void selectRouteWithManager(IMediaRouter2Manager manager, String uniqueSessionId,
+ MediaRoute2Info route) {
+ Objects.requireNonNull(manager, "manager must not be null");
+ if (TextUtils.isEmpty(uniqueSessionId)) {
+ throw new IllegalArgumentException("uniqueSessionId must not be empty");
+ }
+ Objects.requireNonNull(route, "route must not be null");
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ selectRouteWithManagerLocked(manager, uniqueSessionId, route);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ public void deselectRouteWithManager(IMediaRouter2Manager manager, String uniqueSessionId,
+ MediaRoute2Info route) {
+ Objects.requireNonNull(manager, "manager must not be null");
+ if (TextUtils.isEmpty(uniqueSessionId)) {
+ throw new IllegalArgumentException("uniqueSessionId must not be empty");
+ }
+ Objects.requireNonNull(route, "route must not be null");
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ deselectRouteWithManagerLocked(manager, uniqueSessionId, route);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ public void transferToRouteWithManager(IMediaRouter2Manager manager, String uniqueSessionId,
+ MediaRoute2Info route) {
+ Objects.requireNonNull(manager, "manager must not be null");
+ if (TextUtils.isEmpty(uniqueSessionId)) {
+ throw new IllegalArgumentException("uniqueSessionId must not be empty");
+ }
+ Objects.requireNonNull(route, "route must not be null");
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ transferToRouteWithManagerLocked(manager, uniqueSessionId, route);
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -360,73 +451,32 @@
}
public void setSessionVolumeWithManager(IMediaRouter2Manager manager,
- String sessionId, int volume) {
+ String uniqueSessionId, int volume) {
Objects.requireNonNull(manager, "manager must not be null");
- Objects.requireNonNull(sessionId, "sessionId must not be null");
+ if (TextUtils.isEmpty(uniqueSessionId)) {
+ throw new IllegalArgumentException("uniqueSessionId must not be empty");
+ }
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- setSessionVolumeLocked(manager, sessionId, volume);
+ setSessionVolumeWithManagerLocked(manager, uniqueSessionId, volume);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
- @NonNull
- public List<RoutingSessionInfo> getActiveSessions(IMediaRouter2Manager manager) {
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mLock) {
- return getActiveSessionsLocked(manager);
- }
- } finally {
- Binder.restoreCallingIdentity(token);
+ public void releaseSessionWithManager(IMediaRouter2Manager manager, String uniqueSessionId) {
+ Objects.requireNonNull(manager, "manager must not be null");
+ if (TextUtils.isEmpty(uniqueSessionId)) {
+ throw new IllegalArgumentException("uniqueSessionId must not be empty");
}
- }
- public void selectRouteWithManager(IMediaRouter2Manager manager, String sessionId,
- MediaRoute2Info route) {
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- selectRouteWithManagerLocked(manager, sessionId, route);
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- public void deselectRouteWithManager(IMediaRouter2Manager manager, String sessionId,
- MediaRoute2Info route) {
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mLock) {
- deselectRouteWithManagerLocked(manager, sessionId, route);
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- public void transferToRouteWithManager(IMediaRouter2Manager manager, String sessionId,
- MediaRoute2Info route) {
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mLock) {
- transferToRouteWithManagerLocked(manager, sessionId, route);
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- public void releaseSessionWithManager(IMediaRouter2Manager manager, String sessionId) {
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mLock) {
- releaseSessionWithManagerLocked(manager, sessionId);
+ releaseSessionWithManagerLocked(manager, uniqueSessionId);
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -457,247 +507,180 @@
}
}
- void routerDied(RouterRecord routerRecord) {
+ void routerDied(@NonNull RouterRecord routerRecord) {
synchronized (mLock) {
unregisterRouter2Locked(routerRecord.mRouter, true);
}
}
- void managerDied(ManagerRecord managerRecord) {
+ void managerDied(@NonNull ManagerRecord managerRecord) {
synchronized (mLock) {
unregisterManagerLocked(managerRecord.mManager, true);
}
}
- private void registerRouter2Locked(IMediaRouter2 router,
- int uid, int pid, String packageName, int userId, boolean trusted) {
+ ////////////////////////////////////////////////////////////////
+ //// ***Locked methods related to MediaRouter2
+ //// - Should have @NonNull/@Nullable on all arguments
+ ////////////////////////////////////////////////////////////////
+
+ private void registerRouter2Locked(@NonNull IMediaRouter2 router, int uid, int pid,
+ @NonNull String packageName, int userId, boolean trusted) {
final IBinder binder = router.asBinder();
- if (mAllRouterRecords.get(binder) == null) {
-
- UserRecord userRecord = getOrCreateUserRecordLocked(userId);
- RouterRecord routerRecord = new RouterRecord(userRecord, router, uid, pid,
- packageName, trusted);
- try {
- binder.linkToDeath(routerRecord, 0);
- } catch (RemoteException ex) {
- throw new RuntimeException("NediaRouter2 died prematurely.", ex);
- }
-
- userRecord.mRouterRecords.add(routerRecord);
- mAllRouterRecords.put(binder, routerRecord);
-
- userRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::notifyRoutesToRouter, userRecord.mHandler, router));
+ if (mAllRouterRecords.get(binder) != null) {
+ Slog.w(TAG, "Same router already exists. packageName=" + packageName);
+ return;
}
+
+ UserRecord userRecord = getOrCreateUserRecordLocked(userId);
+ RouterRecord routerRecord = new RouterRecord(
+ userRecord, router, uid, pid, packageName, trusted);
+ try {
+ binder.linkToDeath(routerRecord, 0);
+ } catch (RemoteException ex) {
+ throw new RuntimeException("MediaRouter2 died prematurely.", ex);
+ }
+
+ userRecord.mRouterRecords.add(routerRecord);
+ mAllRouterRecords.put(binder, routerRecord);
+
+ userRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::notifyRoutesToRouter, userRecord.mHandler, router));
}
- private void unregisterRouter2Locked(IMediaRouter2 router, boolean died) {
+ private void unregisterRouter2Locked(@NonNull IMediaRouter2 router, boolean died) {
RouterRecord routerRecord = mAllRouterRecords.remove(router.asBinder());
- if (routerRecord != null) {
- UserRecord userRecord = routerRecord.mUserRecord;
- userRecord.mRouterRecords.remove(routerRecord);
- //TODO: update discovery request
- routerRecord.dispose();
- disposeUserIfNeededLocked(userRecord); // since router removed from user
+ if (routerRecord == null) {
+ Slog.w(TAG, "Ignoring unregistering unknown router2");
+ return;
}
+
+ UserRecord userRecord = routerRecord.mUserRecord;
+ userRecord.mRouterRecords.remove(routerRecord);
+ //TODO: update discovery request
+ routerRecord.dispose();
+ disposeUserIfNeededLocked(userRecord); // since router removed from user
}
- private void requestCreateSessionLocked(@NonNull IMediaRouter2 router,
- @NonNull MediaRoute2Info route, long requestId, @Nullable Bundle sessionHints) {
- final IBinder binder = router.asBinder();
- final RouterRecord routerRecord = mAllRouterRecords.get(binder);
-
- // router id is not assigned yet
- if (toRouterOrManagerId(requestId) == 0) {
- requestId = toUniqueRequestId(routerRecord.mRouterId, toOriginalRequestId(requestId));
+ private void setDiscoveryRequestWithRouter2Locked(@NonNull RouterRecord routerRecord,
+ @NonNull RouteDiscoveryPreference discoveryRequest) {
+ if (routerRecord.mDiscoveryPreference.equals(discoveryRequest)) {
+ return;
}
-
- if (routerRecord != null) {
- routerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::requestCreateSessionOnHandler,
- routerRecord.mUserRecord.mHandler,
- routerRecord, route, requestId, sessionHints));
- }
+ routerRecord.mDiscoveryPreference = discoveryRequest;
+ routerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::notifyPreferredFeaturesChangedToManagers,
+ routerRecord.mUserRecord.mHandler, routerRecord));
+ routerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::updateDiscoveryPreference,
+ routerRecord.mUserRecord.mHandler));
}
- private void selectRouteLocked(@NonNull IMediaRouter2 router, String uniqueSessionId,
- @NonNull MediaRoute2Info route) {
- final IBinder binder = router.asBinder();
- final RouterRecord routerRecord = mAllRouterRecords.get(binder);
-
- if (routerRecord != null) {
- routerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::selectRouteOnHandler,
- routerRecord.mUserRecord.mHandler,
- routerRecord, uniqueSessionId, route));
- }
- }
-
- private void deselectRouteLocked(@NonNull IMediaRouter2 router, String uniqueSessionId,
- @NonNull MediaRoute2Info route) {
- final IBinder binder = router.asBinder();
- final RouterRecord routerRecord = mAllRouterRecords.get(binder);
-
- if (routerRecord != null) {
- routerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::deselectRouteOnHandler,
- routerRecord.mUserRecord.mHandler,
- routerRecord, uniqueSessionId, route));
- }
- }
-
- private void transferToRouteWithRouter2Locked(@NonNull IMediaRouter2 router,
- String uniqueSessionId, @NonNull MediaRoute2Info route) {
- final IBinder binder = router.asBinder();
- final RouterRecord routerRecord = mAllRouterRecords.get(binder);
-
- if (routerRecord != null) {
- routerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::transferToRouteOnHandler,
- routerRecord.mUserRecord.mHandler,
- routerRecord, uniqueSessionId, route));
- }
- }
-
- private void releaseSessionLocked(@NonNull IMediaRouter2 router, String uniqueSessionId) {
- final IBinder binder = router.asBinder();
- final RouterRecord routerRecord = mAllRouterRecords.get(binder);
-
- if (routerRecord != null) {
- routerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::releaseSessionOnHandler,
- routerRecord.mUserRecord.mHandler,
- routerRecord, uniqueSessionId));
- }
- }
-
- private void setDiscoveryRequestLocked(RouterRecord routerRecord,
- RouteDiscoveryPreference discoveryRequest) {
- if (routerRecord != null) {
- if (routerRecord.mDiscoveryPreference.equals(discoveryRequest)) {
- return;
- }
-
- routerRecord.mDiscoveryPreference = discoveryRequest;
- routerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::notifyPreferredFeaturesChangedToManagers,
- routerRecord.mUserRecord.mHandler, routerRecord));
- routerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::updateDiscoveryPreference,
- routerRecord.mUserRecord.mHandler));
- }
- }
-
- private void setRouteVolumeLocked(IMediaRouter2 router, MediaRoute2Info route,
- int volume) {
+ private void setRouteVolumeWithRouter2Locked(@NonNull IMediaRouter2 router,
+ @NonNull MediaRoute2Info route, int volume) {
final IBinder binder = router.asBinder();
RouterRecord routerRecord = mAllRouterRecords.get(binder);
if (routerRecord != null) {
routerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::setRouteVolume,
+ obtainMessage(UserHandler::setRouteVolumeOnHandler,
routerRecord.mUserRecord.mHandler, route, volume));
}
}
- private void setSessionVolumeLocked(IMediaRouter2 router, String sessionId,
- int volume) {
+ private void requestCreateSessionWithRouter2Locked(@NonNull IMediaRouter2 router,
+ @NonNull MediaRoute2Info route, int requestId, @Nullable Bundle sessionHints) {
+ final IBinder binder = router.asBinder();
+ final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+ if (routerRecord == null) {
+ return;
+ }
+
+ long uniqueRequestId = toUniqueRequestId(routerRecord.mRouterId, requestId);
+ routerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::requestCreateSessionOnHandler,
+ routerRecord.mUserRecord.mHandler,
+ routerRecord, route, uniqueRequestId, sessionHints));
+ }
+
+ private void selectRouteWithRouter2Locked(@NonNull IMediaRouter2 router,
+ @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+ final IBinder binder = router.asBinder();
+ final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+ if (routerRecord == null) {
+ return;
+ }
+
+ routerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::selectRouteOnHandler,
+ routerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId, route));
+ }
+
+ private void deselectRouteWithRouter2Locked(@NonNull IMediaRouter2 router,
+ @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+ final IBinder binder = router.asBinder();
+ final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+ if (routerRecord == null) {
+ return;
+ }
+
+ routerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::deselectRouteOnHandler,
+ routerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId, route));
+ }
+
+ private void transferToRouteWithRouter2Locked(@NonNull IMediaRouter2 router,
+ @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+ final IBinder binder = router.asBinder();
+ final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+ if (routerRecord == null) {
+ return;
+ }
+
+ routerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::transferToRouteOnHandler,
+ routerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId, route));
+ }
+
+ private void setSessionVolumeWithRouter2Locked(@NonNull IMediaRouter2 router,
+ @NonNull String uniqueSessionId, int volume) {
final IBinder binder = router.asBinder();
RouterRecord routerRecord = mAllRouterRecords.get(binder);
- if (routerRecord != null) {
- routerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::setSessionVolume,
- routerRecord.mUserRecord.mHandler, sessionId, volume));
+ if (routerRecord == null) {
+ return;
}
+
+ routerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::setSessionVolumeOnHandler,
+ routerRecord.mUserRecord.mHandler, uniqueSessionId, volume));
}
- private void registerManagerLocked(IMediaRouter2Manager manager,
- int uid, int pid, String packageName, int userId, boolean trusted) {
- final IBinder binder = manager.asBinder();
- ManagerRecord managerRecord = mAllManagerRecords.get(binder);
- if (managerRecord == null) {
- UserRecord userRecord = getOrCreateUserRecordLocked(userId);
- managerRecord = new ManagerRecord(userRecord, manager, uid, pid, packageName, trusted);
- try {
- binder.linkToDeath(managerRecord, 0);
- } catch (RemoteException ex) {
- throw new RuntimeException("Media router manager died prematurely.", ex);
- }
+ private void releaseSessionWithRouter2Locked(@NonNull IMediaRouter2 router,
+ @NonNull String uniqueSessionId) {
+ final IBinder binder = router.asBinder();
+ final RouterRecord routerRecord = mAllRouterRecords.get(binder);
- userRecord.mManagerRecords.add(managerRecord);
- mAllManagerRecords.put(binder, managerRecord);
-
- userRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::notifyRoutesToManager,
- userRecord.mHandler, manager));
-
- for (RouterRecord routerRecord : userRecord.mRouterRecords) {
- // TODO: Do not use notifyPreferredFeaturesChangedToManagers since it updates all
- // managers. Instead, Notify only to the manager that is currently being registered.
-
- // TODO: UserRecord <-> routerRecord, why do they reference each other?
- // How about removing mUserRecord from routerRecord?
- routerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::notifyPreferredFeaturesChangedToManagers,
- routerRecord.mUserRecord.mHandler, routerRecord));
- }
+ if (routerRecord == null) {
+ return;
}
+
+ routerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::releaseSessionOnHandler,
+ routerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId));
}
- private void unregisterManagerLocked(IMediaRouter2Manager manager, boolean died) {
- ManagerRecord managerRecord = mAllManagerRecords.remove(manager.asBinder());
- if (managerRecord != null) {
- UserRecord userRecord = managerRecord.mUserRecord;
- userRecord.mManagerRecords.remove(managerRecord);
- managerRecord.dispose();
- disposeUserIfNeededLocked(userRecord); // since manager removed from user
- }
- }
+ ////////////////////////////////////////////////////////////
+ //// ***Locked methods related to MediaRouter2Manager
+ //// - Should have @NonNull/@Nullable on all arguments
+ ////////////////////////////////////////////////////////////
- private void requestClientCreateSessionLocked(IMediaRouter2Manager manager,
- String packageName, MediaRoute2Info route, int requestId) {
- ManagerRecord managerRecord = mAllManagerRecords.get(manager.asBinder());
- if (managerRecord != null) {
- RouterRecord routerRecord =
- managerRecord.mUserRecord.findRouterRecordLocked(packageName);
- if (routerRecord == null) {
- Slog.w(TAG, "Ignoring session creation for unknown router.");
- }
- long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId);
- if (routerRecord != null && managerRecord.mTrusted) {
- //TODO: Use MediaRouter2's OnCreateSessionListener to send proper session hints.
- requestCreateSessionLocked(routerRecord.mRouter, route,
- uniqueRequestId, null /* sessionHints */);
- }
- }
- }
-
- private void setRouteVolumeLocked(IMediaRouter2Manager manager, MediaRoute2Info route,
- int volume) {
- final IBinder binder = manager.asBinder();
- ManagerRecord managerRecord = mAllManagerRecords.get(binder);
-
- if (managerRecord != null) {
- managerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::setRouteVolume,
- managerRecord.mUserRecord.mHandler, route, volume));
- }
- }
-
- private void setSessionVolumeLocked(IMediaRouter2Manager manager, String sessionId,
- int volume) {
- final IBinder binder = manager.asBinder();
- ManagerRecord managerRecord = mAllManagerRecords.get(binder);
-
- if (managerRecord != null) {
- managerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::setSessionVolume,
- managerRecord.mUserRecord.mHandler, sessionId, volume));
- }
- }
-
- private List<RoutingSessionInfo> getActiveSessionsLocked(IMediaRouter2Manager manager) {
+ private List<RoutingSessionInfo> getActiveSessionsLocked(
+ @NonNull IMediaRouter2Manager manager) {
final IBinder binder = manager.asBinder();
ManagerRecord managerRecord = mAllManagerRecords.get(binder);
@@ -713,6 +696,177 @@
return sessionInfos;
}
+ private void registerManagerLocked(@NonNull IMediaRouter2Manager manager,
+ int uid, int pid, @NonNull String packageName, int userId, boolean trusted) {
+ final IBinder binder = manager.asBinder();
+ ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+ if (managerRecord != null) {
+ Slog.w(TAG, "Same manager already exists. packageName=" + packageName);
+ return;
+ }
+
+ UserRecord userRecord = getOrCreateUserRecordLocked(userId);
+ managerRecord = new ManagerRecord(userRecord, manager, uid, pid, packageName, trusted);
+ try {
+ binder.linkToDeath(managerRecord, 0);
+ } catch (RemoteException ex) {
+ throw new RuntimeException("Media router manager died prematurely.", ex);
+ }
+
+ userRecord.mManagerRecords.add(managerRecord);
+ mAllManagerRecords.put(binder, managerRecord);
+
+ userRecord.mHandler.sendMessage(obtainMessage(UserHandler::notifyRoutesToManager,
+ userRecord.mHandler, manager));
+
+ for (RouterRecord routerRecord : userRecord.mRouterRecords) {
+ // TODO: Do not use notifyPreferredFeaturesChangedToManagers since it updates all
+ // managers. Instead, Notify only to the manager that is currently being registered.
+
+ // TODO: UserRecord <-> routerRecord, why do they reference each other?
+ // How about removing mUserRecord from routerRecord?
+ routerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::notifyPreferredFeaturesChangedToManagers,
+ routerRecord.mUserRecord.mHandler, routerRecord));
+ }
+ }
+
+ private void unregisterManagerLocked(@NonNull IMediaRouter2Manager manager, boolean died) {
+ ManagerRecord managerRecord = mAllManagerRecords.remove(manager.asBinder());
+ if (managerRecord == null) {
+ return;
+ }
+ UserRecord userRecord = managerRecord.mUserRecord;
+ userRecord.mManagerRecords.remove(managerRecord);
+ managerRecord.dispose();
+ disposeUserIfNeededLocked(userRecord); // since manager removed from user
+ }
+
+ private void setRouteVolumeWithManagerLocked(@NonNull IMediaRouter2Manager manager,
+ @NonNull MediaRoute2Info route, int volume) {
+ final IBinder binder = manager.asBinder();
+ ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+ if (managerRecord == null) {
+ return;
+ }
+ managerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::setRouteVolumeOnHandler,
+ managerRecord.mUserRecord.mHandler, route, volume));
+ }
+
+ private void requestCreateSessionWithManagerLocked(@NonNull IMediaRouter2Manager manager,
+ @NonNull String packageName, @NonNull MediaRoute2Info route, int requestId) {
+ ManagerRecord managerRecord = mAllManagerRecords.get(manager.asBinder());
+ if (managerRecord == null || !managerRecord.mTrusted) {
+ return;
+ }
+
+ RouterRecord routerRecord = managerRecord.mUserRecord.findRouterRecordLocked(packageName);
+ if (routerRecord == null) {
+ Slog.w(TAG, "Ignoring session creation for unknown router.");
+ return;
+ }
+
+ long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId);
+ //TODO: Use MediaRouter2's OnCreateSessionListener to send proper session hints.
+ routerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::requestCreateSessionOnHandler,
+ routerRecord.mUserRecord.mHandler,
+ routerRecord, route, uniqueRequestId, null /* sessionHints */));
+ }
+
+ private void selectRouteWithManagerLocked(@NonNull IMediaRouter2Manager manager,
+ @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+ final IBinder binder = manager.asBinder();
+ ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+ if (managerRecord == null) {
+ return;
+ }
+
+ // Can be null if the session is system's.
+ RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
+ .findRouterforSessionLocked(uniqueSessionId);
+
+ managerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::selectRouteOnHandler,
+ managerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId, route));
+ }
+
+ private void deselectRouteWithManagerLocked(@NonNull IMediaRouter2Manager manager,
+ @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+ final IBinder binder = manager.asBinder();
+ ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+ if (managerRecord == null) {
+ return;
+ }
+
+ // Can be null if the session is system's.
+ RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
+ .findRouterforSessionLocked(uniqueSessionId);
+
+ managerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::deselectRouteOnHandler,
+ managerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId, route));
+ }
+
+ private void transferToRouteWithManagerLocked(@NonNull IMediaRouter2Manager manager,
+ @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+ final IBinder binder = manager.asBinder();
+ ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+ if (managerRecord == null) {
+ return;
+ }
+
+ // Can be null if the session is system's.
+ RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
+ .findRouterforSessionLocked(uniqueSessionId);
+
+ managerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::transferToRouteOnHandler,
+ managerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId, route));
+ }
+
+ private void setSessionVolumeWithManagerLocked(@NonNull IMediaRouter2Manager manager,
+ @NonNull String uniqueSessionId, int volume) {
+ final IBinder binder = manager.asBinder();
+ ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+ if (managerRecord == null) {
+ return;
+ }
+
+ managerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::setSessionVolumeOnHandler,
+ managerRecord.mUserRecord.mHandler, uniqueSessionId, volume));
+ }
+
+ private void releaseSessionWithManagerLocked(@NonNull IMediaRouter2Manager manager,
+ @NonNull String uniqueSessionId) {
+ final IBinder binder = manager.asBinder();
+ ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+ if (managerRecord == null) {
+ return;
+ }
+
+ RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
+ .findRouterforSessionLocked(uniqueSessionId);
+
+ managerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(UserHandler::releaseSessionOnHandler,
+ managerRecord.mUserRecord.mHandler, routerRecord, uniqueSessionId));
+ }
+
+ ////////////////////////////////////////////////////////////
+ //// ***Locked methods used by both router2 and manager
+ //// - Should have @NonNull/@Nullable on all arguments
+ ////////////////////////////////////////////////////////////
+
private UserRecord getOrCreateUserRecordLocked(int userId) {
UserRecord userRecord = mUserRecords.get(userId);
if (userRecord == null) {
@@ -726,79 +880,7 @@
return userRecord;
}
- private void selectRouteWithManagerLocked(IMediaRouter2Manager manager, String sessionId,
- MediaRoute2Info route) {
- final IBinder binder = manager.asBinder();
- ManagerRecord managerRecord = mAllManagerRecords.get(binder);
-
- if (managerRecord == null) {
- Slog.w(TAG, "selectRouteWithManagerLocked: Ignoring unknown manager.");
- return;
- }
- RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
- .findRouterforSessionLocked(sessionId);
-
- managerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::selectRouteOnHandler,
- managerRecord.mUserRecord.mHandler,
- routerRecord, sessionId, route));
- }
-
- private void deselectRouteWithManagerLocked(IMediaRouter2Manager manager, String sessionId,
- MediaRoute2Info route) {
- final IBinder binder = manager.asBinder();
- ManagerRecord managerRecord = mAllManagerRecords.get(binder);
-
- if (managerRecord == null) {
- Slog.w(TAG, "deselectRouteWithManagerLocked: Ignoring unknown manager.");
- return;
- }
- RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
- .findRouterforSessionLocked(sessionId);
-
- managerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::deselectRouteOnHandler,
- managerRecord.mUserRecord.mHandler,
- routerRecord, sessionId, route));
- }
-
- private void transferToRouteWithManagerLocked(IMediaRouter2Manager manager, String sessionId,
- MediaRoute2Info route) {
- final IBinder binder = manager.asBinder();
- ManagerRecord managerRecord = mAllManagerRecords.get(binder);
-
- if (managerRecord == null) {
- Slog.w(TAG, "transferClientRouteLocked: Ignoring unknown manager.");
- return;
- }
- RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
- .findRouterforSessionLocked(sessionId);
-
- managerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::transferToRouteOnHandler,
- managerRecord.mUserRecord.mHandler,
- routerRecord, sessionId, route));
- }
-
- private void releaseSessionWithManagerLocked(IMediaRouter2Manager manager, String sessionId) {
- final IBinder binder = manager.asBinder();
- ManagerRecord managerRecord = mAllManagerRecords.get(binder);
-
- if (managerRecord == null) {
- Slog.w(TAG, "releaseSessionWithManagerLocked: Ignoring unknown manager.");
- return;
- }
-
- RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
- .findRouterforSessionLocked(sessionId);
-
- managerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::releaseSessionOnHandler,
- managerRecord.mUserRecord.mHandler,
- routerRecord, sessionId));
- }
-
- private void disposeUserIfNeededLocked(UserRecord userRecord) {
+ private void disposeUserIfNeededLocked(@NonNull UserRecord userRecord) {
// If there are no records left and the user is no longer current then go ahead
// and purge the user record and all of its associated state. If the user is current
// then leave it alone since we might be connected to a route or want to query
@@ -948,7 +1030,7 @@
private boolean mRunning;
- UserHandler(MediaRouter2ServiceImpl service, UserRecord userRecord) {
+ UserHandler(@NonNull MediaRouter2ServiceImpl service, @NonNull UserRecord userRecord) {
super(Looper.getMainLooper(), null, true);
mServiceRef = new WeakReference<>(service);
mUserRecord = userRecord;
@@ -974,14 +1056,14 @@
}
@Override
- public void onAddProviderService(MediaRoute2ProviderServiceProxy proxy) {
+ public void onAddProviderService(@NonNull MediaRoute2ProviderServiceProxy proxy) {
proxy.setCallback(this);
mMediaProviders.add(proxy);
proxy.updateDiscoveryPreference(mUserRecord.mCompositeDiscoveryPreference);
}
@Override
- public void onRemoveProviderService(MediaRoute2ProviderServiceProxy proxy) {
+ public void onRemoveProviderService(@NonNull MediaRoute2ProviderServiceProxy proxy) {
mMediaProviders.remove(proxy);
}
@@ -1012,19 +1094,19 @@
}
@Override
- public void onSessionReleased(MediaRoute2Provider provider,
- RoutingSessionInfo sessionInfo) {
+ public void onSessionReleased(@NonNull MediaRoute2Provider provider,
+ @NonNull RoutingSessionInfo sessionInfo) {
sendMessage(PooledLambda.obtainMessage(UserHandler::onSessionReleasedOnHandler,
this, provider, sessionInfo));
}
@Nullable
- public RouterRecord findRouterforSessionLocked(@NonNull String sessionId) {
- return mSessionToRouterMap.get(sessionId);
+ public RouterRecord findRouterforSessionLocked(@NonNull String uniqueSessionId) {
+ return mSessionToRouterMap.get(uniqueSessionId);
}
//TODO: notify session info updates
- private void onProviderStateChangedOnHandler(MediaRoute2Provider provider) {
+ private void onProviderStateChangedOnHandler(@NonNull MediaRoute2Provider provider) {
int providerIndex = getProviderInfoIndex(provider.getUniqueId());
MediaRoute2ProviderInfo providerInfo = provider.getProviderInfo();
MediaRoute2ProviderInfo prevInfo =
@@ -1096,7 +1178,7 @@
}
}
- private int getProviderInfoIndex(String providerId) {
+ private int getProviderInfoIndex(@NonNull String providerId) {
for (int i = 0; i < mLastProviderInfos.size(); i++) {
MediaRoute2ProviderInfo providerInfo = mLastProviderInfos.get(i);
if (TextUtils.equals(providerInfo.getUniqueId(), providerId)) {
@@ -1106,8 +1188,8 @@
return -1;
}
- private void requestCreateSessionOnHandler(RouterRecord routerRecord,
- MediaRoute2Info route, long requestId, @Nullable Bundle sessionHints) {
+ private void requestCreateSessionOnHandler(@NonNull RouterRecord routerRecord,
+ @NonNull MediaRoute2Info route, long requestId, @Nullable Bundle sessionHints) {
final MediaRoute2Provider provider = findProvider(route.getProviderId());
if (provider == null) {
@@ -1126,8 +1208,9 @@
requestId, sessionHints);
}
+ // routerRecord can be null if the session is system's.
private void selectRouteOnHandler(@Nullable RouterRecord routerRecord,
- String uniqueSessionId, MediaRoute2Info route) {
+ @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
if (!checkArgumentsForSessionControl(routerRecord, uniqueSessionId, route,
"selecting")) {
return;
@@ -1142,8 +1225,9 @@
provider.selectRoute(getOriginalId(uniqueSessionId), route.getOriginalId());
}
+ // routerRecord can be null if the session is system's.
private void deselectRouteOnHandler(@Nullable RouterRecord routerRecord,
- String uniqueSessionId, MediaRoute2Info route) {
+ @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
if (!checkArgumentsForSessionControl(routerRecord, uniqueSessionId, route,
"deselecting")) {
return;
@@ -1158,8 +1242,9 @@
provider.deselectRoute(getOriginalId(uniqueSessionId), route.getOriginalId());
}
- private void transferToRouteOnHandler(RouterRecord routerRecord,
- String uniqueSessionId, MediaRoute2Info route) {
+ // routerRecord can be null if the session is system's.
+ private void transferToRouteOnHandler(@Nullable RouterRecord routerRecord,
+ @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
if (!checkArgumentsForSessionControl(routerRecord, uniqueSessionId, route,
"transferring to")) {
return;
@@ -1171,17 +1256,12 @@
if (provider == null) {
return;
}
- provider.transferToRoute(getOriginalId(uniqueSessionId),
- route.getOriginalId());
+ provider.transferToRoute(getOriginalId(uniqueSessionId), route.getOriginalId());
}
private boolean checkArgumentsForSessionControl(@Nullable RouterRecord routerRecord,
- String uniqueSessionId, MediaRoute2Info route, @NonNull String description) {
- if (route == null) {
- Slog.w(TAG, "Ignoring " + description + " null route");
- return false;
- }
-
+ @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route,
+ @NonNull String description) {
final String providerId = route.getProviderId();
final MediaRoute2Provider provider = findProvider(providerId);
if (provider == null) {
@@ -1190,12 +1270,6 @@
return false;
}
- if (TextUtils.isEmpty(uniqueSessionId)) {
- Slog.w(TAG, "Ignoring " + description + " route with empty unique session ID. "
- + "route=" + route);
- return false;
- }
-
// Bypass checking router if it's the system session (routerRecord should be null)
if (TextUtils.equals(getProviderId(uniqueSessionId), mSystemProvider.getUniqueId())) {
return true;
@@ -1225,12 +1299,7 @@
}
private void releaseSessionOnHandler(@NonNull RouterRecord routerRecord,
- String uniqueSessionId) {
- if (TextUtils.isEmpty(uniqueSessionId)) {
- Slog.w(TAG, "Ignoring releasing session with empty unique session ID.");
- return;
- }
-
+ @NonNull String uniqueSessionId) {
final RouterRecord matchingRecord = mSessionToRouterMap.get(uniqueSessionId);
if (matchingRecord != routerRecord) {
Slog.w(TAG, "Ignoring releasing session from non-matching router."
@@ -1265,7 +1334,6 @@
private void onSessionCreatedOnHandler(@NonNull MediaRoute2Provider provider,
@NonNull RoutingSessionInfo sessionInfo, long requestId) {
-
notifySessionCreatedToManagers(getManagers(), sessionInfo);
if (requestId == MediaRoute2ProviderService.REQUEST_ID_UNKNOWN) {
@@ -1377,8 +1445,8 @@
notifySessionReleased(routerRecord, sessionInfo);
}
- private void notifySessionCreated(RouterRecord routerRecord,
- RoutingSessionInfo sessionInfo, int requestId) {
+ private void notifySessionCreated(@NonNull RouterRecord routerRecord,
+ @NonNull RoutingSessionInfo sessionInfo, int requestId) {
try {
routerRecord.mRouter.notifySessionCreated(sessionInfo, requestId);
} catch (RemoteException ex) {
@@ -1387,7 +1455,8 @@
}
}
- private void notifySessionCreationFailed(RouterRecord routerRecord, int requestId) {
+ private void notifySessionCreationFailed(@NonNull RouterRecord routerRecord,
+ int requestId) {
try {
routerRecord.mRouter.notifySessionCreated(/* sessionInfo= */ null, requestId);
} catch (RemoteException ex) {
@@ -1396,8 +1465,8 @@
}
}
- private void notifySessionInfoChanged(RouterRecord routerRecord,
- RoutingSessionInfo sessionInfo) {
+ private void notifySessionInfoChanged(@NonNull RouterRecord routerRecord,
+ @NonNull RoutingSessionInfo sessionInfo) {
try {
routerRecord.mRouter.notifySessionInfoChanged(sessionInfo);
} catch (RemoteException ex) {
@@ -1406,8 +1475,8 @@
}
}
- private void notifySessionReleased(RouterRecord routerRecord,
- RoutingSessionInfo sessionInfo) {
+ private void notifySessionReleased(@NonNull RouterRecord routerRecord,
+ @NonNull RoutingSessionInfo sessionInfo) {
try {
routerRecord.mRouter.notifySessionReleased(sessionInfo);
} catch (RemoteException ex) {
@@ -1416,21 +1485,21 @@
}
}
- private void setRouteVolume(MediaRoute2Info route, int volume) {
+ private void setRouteVolumeOnHandler(@NonNull MediaRoute2Info route, int volume) {
final MediaRoute2Provider provider = findProvider(route.getProviderId());
if (provider != null) {
provider.setRouteVolume(route.getOriginalId(), volume);
}
}
- private void setSessionVolume(String sessionId, int volume) {
- final MediaRoute2Provider provider = findProvider(getProviderId(sessionId));
+ private void setSessionVolumeOnHandler(@NonNull String uniqueSessionId, int volume) {
+ final MediaRoute2Provider provider = findProvider(getProviderId(uniqueSessionId));
if (provider == null) {
Slog.w(TAG, "setSessionVolume: couldn't find provider for session "
- + "id=" + sessionId);
+ + "id=" + uniqueSessionId);
return;
}
- provider.setSessionVolume(getOriginalId(sessionId), volume);
+ provider.setSessionVolume(getOriginalId(uniqueSessionId), volume);
}
private List<IMediaRouter2> getRouters() {
@@ -1461,7 +1530,7 @@
return managers;
}
- private void notifyRoutesToRouter(IMediaRouter2 router) {
+ private void notifyRoutesToRouter(@NonNull IMediaRouter2 router) {
List<MediaRoute2Info> routes = new ArrayList<>();
for (MediaRoute2ProviderInfo providerInfo : mLastProviderInfos) {
routes.addAll(providerInfo.getRoutes());
@@ -1476,8 +1545,8 @@
}
}
- private void notifyRoutesAddedToRouters(List<IMediaRouter2> routers,
- List<MediaRoute2Info> routes) {
+ private void notifyRoutesAddedToRouters(@NonNull List<IMediaRouter2> routers,
+ @NonNull List<MediaRoute2Info> routes) {
for (IMediaRouter2 router : routers) {
try {
router.notifyRoutesAdded(routes);
@@ -1487,8 +1556,8 @@
}
}
- private void notifyRoutesRemovedToRouters(List<IMediaRouter2> routers,
- List<MediaRoute2Info> routes) {
+ private void notifyRoutesRemovedToRouters(@NonNull List<IMediaRouter2> routers,
+ @NonNull List<MediaRoute2Info> routes) {
for (IMediaRouter2 router : routers) {
try {
router.notifyRoutesRemoved(routes);
@@ -1498,8 +1567,8 @@
}
}
- private void notifyRoutesChangedToRouters(List<IMediaRouter2> routers,
- List<MediaRoute2Info> routes) {
+ private void notifyRoutesChangedToRouters(@NonNull List<IMediaRouter2> routers,
+ @NonNull List<MediaRoute2Info> routes) {
for (IMediaRouter2 router : routers) {
try {
router.notifyRoutesChanged(routes);
@@ -1509,8 +1578,8 @@
}
}
- private void notifySessionInfoChangedToRouters(List<IMediaRouter2> routers,
- RoutingSessionInfo sessionInfo) {
+ private void notifySessionInfoChangedToRouters(@NonNull List<IMediaRouter2> routers,
+ @NonNull RoutingSessionInfo sessionInfo) {
for (IMediaRouter2 router : routers) {
try {
router.notifySessionInfoChanged(sessionInfo);
@@ -1520,7 +1589,7 @@
}
}
- private void notifyRoutesToManager(IMediaRouter2Manager manager) {
+ private void notifyRoutesToManager(@NonNull IMediaRouter2Manager manager) {
List<MediaRoute2Info> routes = new ArrayList<>();
for (MediaRoute2ProviderInfo providerInfo : mLastProviderInfos) {
routes.addAll(providerInfo.getRoutes());
@@ -1535,8 +1604,8 @@
}
}
- private void notifyRoutesAddedToManagers(List<IMediaRouter2Manager> managers,
- List<MediaRoute2Info> routes) {
+ private void notifyRoutesAddedToManagers(@NonNull List<IMediaRouter2Manager> managers,
+ @NonNull List<MediaRoute2Info> routes) {
for (IMediaRouter2Manager manager : managers) {
try {
manager.notifyRoutesAdded(routes);
@@ -1546,8 +1615,8 @@
}
}
- private void notifyRoutesRemovedToManagers(List<IMediaRouter2Manager> managers,
- List<MediaRoute2Info> routes) {
+ private void notifyRoutesRemovedToManagers(@NonNull List<IMediaRouter2Manager> managers,
+ @NonNull List<MediaRoute2Info> routes) {
for (IMediaRouter2Manager manager : managers) {
try {
manager.notifyRoutesRemoved(routes);
@@ -1557,8 +1626,8 @@
}
}
- private void notifyRoutesChangedToManagers(List<IMediaRouter2Manager> managers,
- List<MediaRoute2Info> routes) {
+ private void notifyRoutesChangedToManagers(@NonNull List<IMediaRouter2Manager> managers,
+ @NonNull List<MediaRoute2Info> routes) {
for (IMediaRouter2Manager manager : managers) {
try {
manager.notifyRoutesChanged(routes);
@@ -1568,8 +1637,8 @@
}
}
- private void notifySessionCreatedToManagers(List<IMediaRouter2Manager> managers,
- RoutingSessionInfo sessionInfo) {
+ private void notifySessionCreatedToManagers(@NonNull List<IMediaRouter2Manager> managers,
+ @NonNull RoutingSessionInfo sessionInfo) {
for (IMediaRouter2Manager manager : managers) {
try {
manager.notifySessionCreated(sessionInfo);
@@ -1580,7 +1649,8 @@
}
}
- private void notifySessionInfosChangedToManagers(List<IMediaRouter2Manager> managers) {
+ private void notifySessionInfosChangedToManagers(
+ @NonNull List<IMediaRouter2Manager> managers) {
for (IMediaRouter2Manager manager : managers) {
try {
manager.notifySessionsUpdated();
@@ -1591,7 +1661,7 @@
}
}
- private void notifyPreferredFeaturesChangedToManagers(RouterRecord routerRecord) {
+ private void notifyPreferredFeaturesChangedToManagers(@NonNull RouterRecord routerRecord) {
MediaRouter2ServiceImpl service = mServiceRef.get();
if (service == null) {
return;
@@ -1632,7 +1702,7 @@
}
}
- private MediaRoute2Provider findProvider(String providerId) {
+ private MediaRoute2Provider findProvider(@Nullable String providerId) {
for (MediaRoute2Provider provider : mMediaProviders) {
if (TextUtils.equals(provider.getUniqueId(), providerId)) {
return provider;
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index 777a8fe..8e38114 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -19,7 +19,6 @@
import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO;
import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO;
-import android.annotation.NonNull;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -42,15 +41,13 @@
import android.util.Log;
import com.android.internal.R;
-import com.android.internal.annotations.GuardedBy;
-import java.util.Collections;
-import java.util.List;
import java.util.Objects;
/**
* Provides routes for local playbacks such as phone speaker, wired headset, or Bluetooth speakers.
*/
+// TODO: check thread safety. We may need to use lock to protect variables.
class SystemMediaRoute2Provider extends MediaRoute2Provider {
private static final String TAG = "MR2SystemProvider";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -68,19 +65,16 @@
SystemMediaRoute2Provider.class.getPackageName$(),
SystemMediaRoute2Provider.class.getName());
- @GuardedBy("mLock")
private String mSelectedRouteId;
MediaRoute2Info mDefaultRoute;
- @NonNull List<MediaRoute2Info> mBluetoothRoutes = Collections.EMPTY_LIST;
final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo();
final IAudioRoutesObserver.Stub mAudioRoutesObserver = new IAudioRoutesObserver.Stub() {
@Override
public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) {
- mHandler.post(new Runnable() {
- @Override public void run() {
- updateAudioRoutes(newRoutes);
- }
+ mHandler.post(() -> {
+ updateDefaultRoute(newRoutes);
+ notifyProviderState();
});
}
};
@@ -97,11 +91,15 @@
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
mAudioService = IAudioService.Stub.asInterface(
ServiceManager.getService(Context.AUDIO_SERVICE));
+ AudioRoutesInfo newAudioRoutes = null;
+ try {
+ newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver);
+ } catch (RemoteException e) {
+ }
+ updateDefaultRoute(newAudioRoutes);
- initializeDefaultRoute();
mBtRouteProvider = BluetoothRouteProvider.getInstance(context, (routes) -> {
- mBluetoothRoutes = routes;
- publishRoutes();
+ publishProviderState();
boolean sessionInfoChanged;
synchronized (mLock) {
@@ -111,7 +109,15 @@
notifySessionInfoUpdated();
}
});
- initializeSessionInfo();
+
+ mHandler.post(() -> notifyProviderState());
+
+ //TODO: clean up this
+ // This is required because it is not instantiated in the main thread and
+ // BluetoothRoutesUpdatedListener can be called before here
+ synchronized (mLock) {
+ updateSessionInfosIfNeededLocked();
+ }
mContext.registerReceiver(new VolumeChangeReceiver(),
new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION));
@@ -164,65 +170,21 @@
// Do nothing since we don't support grouping volume yet.
}
- private void initializeDefaultRoute() {
- mDefaultRoute = new MediaRoute2Info.Builder(
- DEFAULT_ROUTE_ID,
- mContext.getResources().getText(R.string.default_audio_route_name).toString())
- .setVolumeHandling(mAudioManager.isVolumeFixed()
- ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED
- : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
- .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
- .setVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC))
- .addFeature(FEATURE_LIVE_AUDIO)
- .addFeature(FEATURE_LIVE_VIDEO)
- .build();
-
- AudioRoutesInfo newAudioRoutes = null;
- try {
- newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver);
- } catch (RemoteException e) {
- }
- if (newAudioRoutes != null) {
- // This will select the active BT route if there is one and the current
- // selected route is the default system route, or if there is no selected
- // route yet.
- updateAudioRoutes(newAudioRoutes);
- }
- }
-
- private void initializeSessionInfo() {
- mBluetoothRoutes = mBtRouteProvider.getAllBluetoothRoutes();
-
- MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder();
- builder.addRoute(mDefaultRoute);
- for (MediaRoute2Info route : mBluetoothRoutes) {
- builder.addRoute(route);
- }
- setProviderState(builder.build());
- mHandler.post(() -> notifyProviderState());
-
- //TODO: clean up this
- // This is required because it is not instantiated in the main thread and
- // BluetoothRoutesUpdatedListener can be called before this function
- synchronized (mLock) {
- updateSessionInfosIfNeededLocked();
- }
- }
-
- private void updateAudioRoutes(AudioRoutesInfo newRoutes) {
+ private void updateDefaultRoute(AudioRoutesInfo newRoutes) {
int name = R.string.default_audio_route_name;
- mCurAudioRoutesInfo.mainType = newRoutes.mainType;
- if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0
- || (newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
- name = com.android.internal.R.string.default_audio_route_name_headphones;
- } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
- name = com.android.internal.R.string.default_audio_route_name_dock_speakers;
- } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) {
- name = com.android.internal.R.string.default_audio_route_name_hdmi;
- } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) {
- name = com.android.internal.R.string.default_audio_route_name_usb;
+ if (newRoutes != null) {
+ mCurAudioRoutesInfo.mainType = newRoutes.mainType;
+ if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0
+ || (newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
+ name = com.android.internal.R.string.default_audio_route_name_headphones;
+ } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
+ name = com.android.internal.R.string.default_audio_route_name_dock_speakers;
+ } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) {
+ name = com.android.internal.R.string.default_audio_route_name_hdmi;
+ } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) {
+ name = com.android.internal.R.string.default_audio_route_name_usb;
+ }
}
-
mDefaultRoute = new MediaRoute2Info.Builder(
DEFAULT_ROUTE_ID, mContext.getResources().getText(name).toString())
.setVolumeHandling(mAudioManager.isVolumeFixed()
@@ -232,9 +194,20 @@
.setVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC))
.addFeature(FEATURE_LIVE_AUDIO)
.addFeature(FEATURE_LIVE_VIDEO)
+ .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED)
.build();
+ updateProviderState();
+ }
- publishRoutes();
+ private void updateProviderState() {
+ MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder();
+ builder.addRoute(mDefaultRoute);
+ if (mBtRouteProvider != null) {
+ for (MediaRoute2Info route : mBtRouteProvider.getAllBluetoothRoutes()) {
+ builder.addRoute(route);
+ }
+ }
+ setProviderState(builder.build());
}
/**
@@ -246,21 +219,21 @@
RoutingSessionInfo.Builder builder = new RoutingSessionInfo.Builder(
SYSTEM_SESSION_ID, "" /* clientPackageName */)
.setSystemSession(true);
- String activeBtDeviceAddress = mBtRouteProvider.getSelectedRouteId();
- mSelectedRouteId = TextUtils.isEmpty(activeBtDeviceAddress) ? mDefaultRoute.getId()
- : activeBtDeviceAddress;
- builder.addSelectedRoute(mSelectedRouteId);
- if (!TextUtils.isEmpty(activeBtDeviceAddress)) {
+ MediaRoute2Info selectedRoute = mBtRouteProvider.getSelectedRoute();
+ if (selectedRoute == null) {
+ selectedRoute = mDefaultRoute;
+ } else {
builder.addTransferableRoute(mDefaultRoute.getId());
}
+ mSelectedRouteId = selectedRoute.getId();
+ builder.addSelectedRoute(mSelectedRouteId);
- for (MediaRoute2Info route : mBluetoothRoutes) {
- if (!TextUtils.equals(mSelectedRouteId, route.getId())) {
- builder.addTransferableRoute(route.getId());
- }
+ for (MediaRoute2Info route : mBtRouteProvider.getTransferableRoutes()) {
+ builder.addTransferableRoute(route.getId());
}
+
RoutingSessionInfo newSessionInfo = builder.setProviderId(mUniqueId).build();
if (Objects.equals(oldSessionInfo, newSessionInfo)) {
return false;
@@ -271,13 +244,9 @@
}
}
- void publishRoutes() {
- MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder();
- builder.addRoute(mDefaultRoute);
- for (MediaRoute2Info route : mBluetoothRoutes) {
- builder.addRoute(route);
- }
- setAndNotifyProviderState(builder.build());
+ void publishProviderState() {
+ updateProviderState();
+ notifyProviderState();
}
void notifySessionInfoUpdated() {
@@ -306,24 +275,14 @@
AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0);
if (newVolume != oldVolume) {
- String activeBtDeviceAddress = mBtRouteProvider.getSelectedRouteId();
- if (!TextUtils.isEmpty(activeBtDeviceAddress)) {
- for (int i = mBluetoothRoutes.size() - 1; i >= 0; i--) {
- MediaRoute2Info route = mBluetoothRoutes.get(i);
- if (TextUtils.equals(activeBtDeviceAddress, route.getId())) {
- mBluetoothRoutes.set(i,
- new MediaRoute2Info.Builder(route)
- .setVolume(newVolume)
- .build());
- break;
- }
- }
- } else {
+ if (TextUtils.equals(mDefaultRoute.getId(), mSelectedRouteId)) {
mDefaultRoute = new MediaRoute2Info.Builder(mDefaultRoute)
.setVolume(newVolume)
.build();
+ } else {
+ mBtRouteProvider.setSelectedRouteVolume(newVolume);
}
- publishRoutes();
+ publishProviderState();
}
}
}
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/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index ab8e975..46596e35 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -2854,6 +2854,11 @@
boolean navigateUpTo(ActivityRecord srec, Intent destIntent, int resultCode,
Intent resultData) {
+ if (!srec.attachedToProcess()) {
+ // Nothing to do if the caller is not attached, because this method should be called
+ // from an alive activity.
+ return false;
+ }
final Task task = srec.getTask();
if (!mChildren.contains(task) || !task.hasChild(srec)) {
@@ -2915,14 +2920,14 @@
resultData = resultDataHolder[0];
if (parent != null && foundParentInTask) {
+ final int callingUid = srec.info.applicationInfo.uid;
final int parentLaunchMode = parent.info.launchMode;
final int destIntentFlags = destIntent.getFlags();
if (parentLaunchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE ||
parentLaunchMode == ActivityInfo.LAUNCH_SINGLE_TASK ||
parentLaunchMode == ActivityInfo.LAUNCH_SINGLE_TOP ||
(destIntentFlags & Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0) {
- parent.deliverNewIntentLocked(srec.info.applicationInfo.uid, destIntent,
- srec.packageName);
+ parent.deliverNewIntentLocked(callingUid, destIntent, srec.packageName);
} else {
try {
ActivityInfo aInfo = AppGlobals.getPackageManager().getActivityInfo(
@@ -2935,11 +2940,11 @@
.setActivityInfo(aInfo)
.setResultTo(parent.appToken)
.setCallingPid(-1)
- .setCallingUid(parent.launchedFromUid)
- .setCallingPackage(parent.launchedFromPackage)
+ .setCallingUid(callingUid)
+ .setCallingPackage(srec.packageName)
.setCallingFeatureId(parent.launchedFromFeatureId)
.setRealCallingPid(-1)
- .setRealCallingUid(parent.launchedFromUid)
+ .setRealCallingUid(callingUid)
.setComponentSpecified(true)
.execute();
foundParentInTask = res == ActivityManager.START_SUCCESS;
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/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index c7d00ec..03dc4c9 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -221,7 +221,9 @@
// has been sent to client by {@link android.app.IApplicationThread#bindApplication}.
// If this process is system server, it is fine because system is booting and a new
// configuration will update when display is ready.
- setLastReportedConfiguration(getConfiguration());
+ if (thread != null) {
+ setLastReportedConfiguration(getConfiguration());
+ }
}
}
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/ActivityStackTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
index 393d8b8..52fc3de 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
@@ -54,8 +54,12 @@
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import android.app.ActivityManager;
+import android.app.IApplicationThread;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
import android.os.UserHandle;
@@ -1109,6 +1113,37 @@
}
@Test
+ public void testNavigateUpTo() {
+ final ActivityStartController controller = mock(ActivityStartController.class);
+ final ActivityStarter starter = new ActivityStarter(controller,
+ mService, mService.mStackSupervisor, mock(ActivityStartInterceptor.class));
+ doReturn(controller).when(mService).getActivityStartController();
+ spyOn(starter);
+ doReturn(ActivityManager.START_SUCCESS).when(starter).execute();
+
+ final ActivityRecord firstActivity = new ActivityBuilder(mService).setTask(mTask).build();
+ final ActivityRecord secondActivity = new ActivityBuilder(mService).setTask(mTask)
+ .setUid(firstActivity.getUid() + 1).build();
+ doReturn(starter).when(controller).obtainStarter(eq(firstActivity.intent), anyString());
+
+ final IApplicationThread thread = secondActivity.app.getThread();
+ secondActivity.app.setThread(null);
+ // This should do nothing from a non-attached caller.
+ assertFalse(mStack.navigateUpTo(secondActivity /* source record */,
+ firstActivity.intent /* destIntent */, 0 /* resultCode */, null /* resultData */));
+
+ secondActivity.app.setThread(thread);
+ assertTrue(mStack.navigateUpTo(secondActivity /* source record */,
+ firstActivity.intent /* destIntent */, 0 /* resultCode */, null /* resultData */));
+ // The firstActivity uses default launch mode, so the activities between it and itself will
+ // be finished.
+ assertTrue(secondActivity.finishing);
+ assertTrue(firstActivity.finishing);
+ // The caller uid of the new activity should be the current real caller.
+ assertEquals(starter.mRequest.callingUid, secondActivity.getUid());
+ }
+
+ @Test
public void testResetTaskWithFinishingActivities() {
final ActivityRecord taskTop =
new ActivityBuilder(mService).setStack(mStack).setCreateTask(true).build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
index c93dc97..b6b3bc6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
@@ -238,6 +238,7 @@
aInfo.applicationInfo.uid = mUid;
aInfo.processName = mProcessName;
aInfo.packageName = mComponent.getPackageName();
+ aInfo.name = mComponent.getClassName();
if (mTargetActivity != null) {
aInfo.targetActivity = mTargetActivity;
}
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 a63d58d..3e10992 100644
--- a/tests/net/common/java/android/net/NetworkScoreTest.kt
+++ b/tests/net/common/java/android/net/NetworkScoreTest.kt
@@ -17,6 +17,9 @@
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
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
import com.android.testutils.assertParcelSane
@@ -39,19 +42,19 @@
assertEquals(TEST_SCORE, builder.build().getLegacyScore())
assertParcelSane(builder.build(), 7)
- builder.addPolicy(NetworkScore.POLICY_IGNORE_ON_WIFI)
- .addPolicy(NetworkScore.POLICY_DEFAULT_SUBSCRIPTION)
+ 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)
assertParcelSane(builder.build(), 7)
- builder.clearPolicy(NetworkScore.POLICY_IGNORE_ON_WIFI)
+ builder.clearPolicy(POLICY_IGNORE_ON_WIFI)
val ns = builder.build()
assertParcelSane(ns, 7)
- assertFalse(ns.hasPolicy(NetworkScore.POLICY_IGNORE_ON_WIFI))
- assertTrue(ns.hasPolicy(NetworkScore.POLICY_DEFAULT_SUBSCRIPTION))
+ assertFalse(ns.hasPolicy(POLICY_IGNORE_ON_WIFI))
+ assertTrue(ns.hasPolicy(POLICY_DEFAULT_SUBSCRIPTION))
val exitingNs = ns.withExiting(true)
assertNotEquals(ns.isExiting, exitingNs.isExiting)
@@ -73,4 +76,17 @@
assertTrue(builder1.build().equals(builder2.build()))
assertEquals(builder1.build().hashCode(), builder2.build().hashCode())
}
+
+ @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()
+ assertEquals(ns, NetworkScore.Builder(ns).build())
+ }
}
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index 2e59966..957683e 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -78,6 +78,7 @@
import static android.net.NetworkPolicyManager.RULE_REJECT_ALL;
import static android.net.NetworkPolicyManager.RULE_REJECT_METERED;
import static android.net.RouteInfo.RTN_UNREACHABLE;
+import static android.system.OsConstants.IPPROTO_TCP;
import static com.android.server.ConnectivityServiceTestUtilsKt.transportToLegacyType;
import static com.android.testutils.ConcurrentUtilsKt.await;
@@ -138,6 +139,7 @@
import android.content.res.Resources;
import android.location.LocationManager;
import android.net.CaptivePortalData;
+import android.net.ConnectionInfo;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.ConnectivityManager.PacketKeepalive;
@@ -153,6 +155,7 @@
import android.net.INetworkPolicyListener;
import android.net.INetworkPolicyManager;
import android.net.INetworkStatsService;
+import android.net.InetAddresses;
import android.net.InterfaceConfiguration;
import android.net.IpPrefix;
import android.net.IpSecManager;
@@ -176,6 +179,7 @@
import android.net.SocketKeepalive;
import android.net.UidRange;
import android.net.Uri;
+import android.net.VpnManager;
import android.net.metrics.IpConnectivityLog;
import android.net.shared.NetworkMonitorUtils;
import android.net.shared.PrivateDnsConfig;
@@ -272,6 +276,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
+import java.util.function.Supplier;
import kotlin.reflect.KClass;
@@ -445,15 +450,21 @@
return mPackageManager;
}
+ private int checkMockedPermission(String permission, Supplier<Integer> ifAbsent) {
+ final Integer granted = mMockedPermissions.get(permission);
+ return granted != null ? granted : ifAbsent.get();
+ }
+
@Override
public int checkPermission(String permission, int pid, int uid) {
- final Integer granted = mMockedPermissions.get(permission);
- if (granted == null) {
- // All non-mocked permissions should be held by the test or unnecessary: check as
- // normal to make sure the code does not rely on unexpected permissions.
- return super.checkPermission(permission, pid, uid);
- }
- return granted;
+ return checkMockedPermission(
+ permission, () -> super.checkPermission(permission, pid, uid));
+ }
+
+ @Override
+ public int checkCallingOrSelfPermission(String permission) {
+ return checkMockedPermission(
+ permission, () -> super.checkCallingOrSelfPermission(permission));
}
@Override
@@ -1002,6 +1013,7 @@
// Careful ! This is different from mNetworkAgent, because MockNetworkAgent does
// not inherit from NetworkAgent.
private TestNetworkAgentWrapper mMockNetworkAgent;
+ private int mVpnType = VpnManager.TYPE_VPN_SERVICE;
private VpnInfo mVpnInfo;
@@ -1022,6 +1034,10 @@
updateCapabilities(null /* defaultNetwork */);
}
+ public void setVpnType(int vpnType) {
+ mVpnType = vpnType;
+ }
+
@Override
public int getNetId() {
if (mMockNetworkAgent == null) {
@@ -1040,6 +1056,11 @@
return mConnected; // Similar trickery
}
+ @Override
+ public int getActiveAppVpnType() {
+ return mVpnType;
+ }
+
private void connect(boolean isAlwaysMetered) {
mNetworkCapabilities.set(mMockNetworkAgent.getNetworkCapabilities());
mConnected = true;
@@ -6429,6 +6450,90 @@
assertEquals(Process.INVALID_UID, newNc.getOwnerUid());
}
+ private void setupConnectionOwnerUid(int vpnOwnerUid, @VpnManager.VpnType int vpnType)
+ throws Exception {
+ final Set<UidRange> vpnRange = Collections.singleton(UidRange.createForUser(VPN_USER));
+ establishVpn(new LinkProperties(), vpnOwnerUid, vpnRange);
+ mMockVpn.setVpnType(vpnType);
+
+ final VpnInfo vpnInfo = new VpnInfo();
+ vpnInfo.ownerUid = vpnOwnerUid;
+ mMockVpn.setVpnInfo(vpnInfo);
+ }
+
+ private void setupConnectionOwnerUidAsVpnApp(int vpnOwnerUid, @VpnManager.VpnType int vpnType)
+ throws Exception {
+ setupConnectionOwnerUid(vpnOwnerUid, vpnType);
+
+ // Test as VPN app
+ mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED);
+ mServiceContext.setPermission(
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_DENIED);
+ }
+
+ private ConnectionInfo getTestConnectionInfo() throws Exception {
+ return new ConnectionInfo(
+ IPPROTO_TCP,
+ new InetSocketAddress(InetAddresses.parseNumericAddress("1.2.3.4"), 1234),
+ new InetSocketAddress(InetAddresses.parseNumericAddress("2.3.4.5"), 2345));
+ }
+
+ @Test
+ public void testGetConnectionOwnerUidPlatformVpn() throws Exception {
+ final int myUid = Process.myUid();
+ setupConnectionOwnerUidAsVpnApp(myUid, VpnManager.TYPE_VPN_PLATFORM);
+
+ try {
+ mService.getConnectionOwnerUid(getTestConnectionInfo());
+ fail("Expected SecurityException for non-VpnService app");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ @Test
+ public void testGetConnectionOwnerUidVpnServiceWrongUser() throws Exception {
+ final int myUid = Process.myUid();
+ setupConnectionOwnerUidAsVpnApp(myUid + 1, VpnManager.TYPE_VPN_SERVICE);
+
+ try {
+ mService.getConnectionOwnerUid(getTestConnectionInfo());
+ fail("Expected SecurityException for non-VpnService app");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ @Test
+ public void testGetConnectionOwnerUidVpnServiceDoesNotThrow() throws Exception {
+ final int myUid = Process.myUid();
+ setupConnectionOwnerUidAsVpnApp(myUid, VpnManager.TYPE_VPN_SERVICE);
+
+ // TODO: Test the returned UID
+ mService.getConnectionOwnerUid(getTestConnectionInfo());
+ }
+
+ @Test
+ public void testGetConnectionOwnerUidVpnServiceNetworkStackDoesNotThrow() throws Exception {
+ final int myUid = Process.myUid();
+ setupConnectionOwnerUid(myUid, VpnManager.TYPE_VPN_SERVICE);
+ mServiceContext.setPermission(
+ android.Manifest.permission.NETWORK_STACK, PERMISSION_GRANTED);
+
+ // TODO: Test the returned UID
+ mService.getConnectionOwnerUid(getTestConnectionInfo());
+ }
+
+ @Test
+ public void testGetConnectionOwnerUidVpnServiceMainlineNetworkStackDoesNotThrow()
+ throws Exception {
+ final int myUid = Process.myUid();
+ setupConnectionOwnerUid(myUid, VpnManager.TYPE_VPN_SERVICE);
+ mServiceContext.setPermission(
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_GRANTED);
+
+ // TODO: Test the returned UID
+ mService.getConnectionOwnerUid(getTestConnectionInfo());
+ }
+
private TestNetworkAgentWrapper establishVpn(
LinkProperties lp, int ownerUid, Set<UidRange> vpnRange) throws Exception {
final TestNetworkAgentWrapper
diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java
index eb78529..ac1c518 100644
--- a/tests/net/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/net/java/com/android/server/connectivity/VpnTest.java
@@ -656,8 +656,12 @@
}
private Vpn createVpnAndSetupUidChecks(int... grantedOps) throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
- setMockedUsers(primaryUser);
+ return createVpnAndSetupUidChecks(primaryUser, grantedOps);
+ }
+
+ private Vpn createVpnAndSetupUidChecks(UserInfo user, int... grantedOps) throws Exception {
+ final Vpn vpn = createVpn(user.id);
+ setMockedUsers(user);
when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt()))
.thenReturn(Process.myUid());
@@ -726,6 +730,19 @@
}
@Test
+ public void testProvisionVpnProfileRestrictedUser() throws Exception {
+ final Vpn vpn =
+ createVpnAndSetupUidChecks(
+ restrictedProfileA, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN);
+
+ try {
+ vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile, mKeyStore);
+ fail("Expected SecurityException due to restricted user");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ @Test
public void testDeleteVpnProfile() throws Exception {
final Vpn vpn = createVpnAndSetupUidChecks();
@@ -736,6 +753,19 @@
}
@Test
+ public void testDeleteVpnProfileRestrictedUser() throws Exception {
+ final Vpn vpn =
+ createVpnAndSetupUidChecks(
+ restrictedProfileA, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN);
+
+ try {
+ vpn.deleteVpnProfile(TEST_VPN_PKG, mKeyStore);
+ fail("Expected SecurityException due to restricted user");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ @Test
public void testGetVpnProfilePrivileged() throws Exception {
final Vpn vpn = createVpnAndSetupUidChecks();
@@ -820,6 +850,32 @@
}
@Test
+ public void testStartVpnProfileRestrictedUser() throws Exception {
+ final Vpn vpn =
+ createVpnAndSetupUidChecks(
+ restrictedProfileA, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN);
+
+ try {
+ vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore);
+ fail("Expected SecurityException due to restricted user");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ @Test
+ public void testStopVpnProfileRestrictedUser() throws Exception {
+ final Vpn vpn =
+ createVpnAndSetupUidChecks(
+ restrictedProfileA, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN);
+
+ try {
+ vpn.stopVpnProfile(TEST_VPN_PKG);
+ fail("Expected SecurityException due to restricted user");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ @Test
public void testSetPackageAuthorizationVpnService() throws Exception {
final Vpn vpn = createVpnAndSetupUidChecks();